Saturday, March 22, 2014

Refactoring using Page Objects in Tests


I am still looking for a good reason to hate Page Objects.

Page Objects are a pattern of testing that wraps an object around a page such that any action that a user might take (be it happy path or error generating) is represented as a method. Page Objects also expose getter methods to extract state information from the page for testing. Any action a user takes is represented by a method. Any test expectations should be made against getters. It sounds wonderful.

So why do I hate it? Mostly I hate indirection or anything that begins to suggest cleverness in testing. I like all my tests procedural and right up front. I would much rather copy and paste test code because, when the test inevitably fails, I hate tracing through clever testing code—it's bad enough tracing through clever application code.

All that said, I have found very little reason to dislike Page Objects when testing Polymer elements. They even seem to work quite nicely in both the JavaScript and Dart versions Polymer. Darn it.

So tonight, I am going to put Page Objects to a bit more stringent a test—I am going to refactor my Dart Polymer element. When I started on my pizza builder <x-pizza> component in JavaScript, I made the mistake of refactoring first and testing second. I did not make that same mistake with the Dart version. I wrote my tests last night and tonight I refactor.

The refactoring goes back to how the different toppings are added to the pizza builder:



Among some very excellent advice that I received on Patterns in Polymer was that the model in the Model Driven View code of <x-pizza> was overly complex. It was suggested that instead of placing the model in <x-pizza> itself, I should put the model into <x-pizza-topping> elements that are then created to be private child elements of <x-pizza>.

So let's see how that works with the Dart version of <x-pizza> and, more importantly, how Page Objects adapt to the change.

As I found with the JavaScript version of this Polymer element, the changes do make life easier for me. Mostly, it involved replacing a lot of template code in the element that looks like:
<polymer-element name="x-pizza">
  <template>
    <h2>Build Your Pizza</h2>
    <pre id="state">{{pizzaState}}</pre>
    <!-- first & second half toppings done here .. -->
    <p id="wholeToppings">
      <select class="form-control" value="{{currentWhole}}">
        <option>Choose an ingredient...</option>
        <option value="{{ingredient}}" template repeat="{{ingredient in ingredients}}">
          {{ingredient}}
        </option>
      </select>
      <button on-click="{{addWhole}}" type="button" class="btn btn-default">
        Add Whole Topping
      </button>
    </p>
  </template>
  <script type="application/dart" src="x_pizza.dart"></script>
</polymer-element>
With custom, private Polymer elements:
<link rel="import" href="x-pizza-toppings.html">
<polymer-element name="x-pizza">
  <template>
    <h2>Build Your Pizza</h2>
    <pre id="state">{{pizzaState}}</pre>
    <!-- first & second half toppings done here .. -->
    <x-pizza-toppings id="wholeToppings"
                      name="Whole Toppings"
                      ingredients="{{ingredients}}"></x-pizza-toppings>
    <p class=help-block>
      Build Your Pizza!
    </p>
  </template>
  <script type="application/dart" src="x_pizza.dart"></script>
</polymer-element>
Nice. That is easier to read and thus easier to maintain. It is a win all around. Except my Page Objects based test now fails:
FAIL: [adding toppings] updates the pizza state accordingly
  Expected: '{"firstHalfToppings":[],"secondHalfToppings":[],"wholeToppings":["green peppers"]}'
    Actual: '{"firstHalfToppings":[],"secondHalfToppings":[],"wholeToppings":[]}'
     Which: is different.
  Expected: ... oppings":["green pep ...
    Actual: ... oppings":[]}
                          ^
   Differ at offset 65
And here is where Page Objects really seems to help. To fix this, I only need update the addWholeTopping() method in my Page Objects wrapper around <x-pizza>. Instead of querying for the drop-down and button to add toppings, I can use Polymer's automatic node-finding:
class XPizzaComponent {
  PolymerElement el;
  XPizzaComponent(this.el);

  // Extract information from the page
  String get currentPizzaStateDisplay => el.$['state'].text;

  // Interact with the page
  XPizzaComponent addWholeTopping(String topping) {
    var toppings = el.$['wholeToppings'],
        select = toppings.$['ingredients'],
        button = toppings.$['add'];

    // Choosing an item from the drop-down and clicking the button
    // stay the same...

    return this;
  }
}
With that, I have my tests all passing again. I am not sure that qualifies as a real-life test of Page Objects because I only have one interaction method and one query method (which didn't change). Still, I am quite happy with the results here. I am very close to considering myself a Page Objects convert—at least with Polymer code.



Day #11

1 comment:

  1. You can misuse any pattern. Page Objects can be good or bad depending on how you apply it... I think the way you use it is great and makes for more DRYness and flexibility ;)

    ReplyDelete