Clean Up Your Fronted Tests: Part 4

Using Page Objects to manage complexity

Stawiarski Jakub
TechTalks@Vattenfall
4 min readDec 23, 2020

--

This article is a part of a cycle. All parts are:

  1. Why we focus on integration tests for UI components.
  2. Tips & Tricks.
  3. Reusable testing modules.
  4. Using Page Objects to manage complexity.

Credits to Paweł Nowakowski, co-creator of the series.

Photo by Negative Space from Pexels

Issues with raising complexity

As the complexity of features you are implementing grows, so will your test files. More and more cases need to be covered, which is fine. What is worse with the number of cases the file will also fill up with a lot of “plumbing” code. You will need more and more references to the DOM elements to simulate user behavior and assert results. This may clutter your test files and make them hard to read.

If you decide to go with the integration tests route you may
also need to build more complex testing modules and pass them toTestBed#configureTestingModule(..). Heavier modules will probably mean there might be more moving parts and dependencies like services you need to observe. This will again make your initialization heavier, and the files will again become larger and harder to read.

It may also be an issue if you reach a critical mass and decide to split tests
for a component into separate files. You will now need to move the initialization logic somewhere to avoid duplicating it.

Solving the mess

One pattern that could help with splitting up and managing the complexity is the page object. It’s not a new pattern at all, as it is used with multiple testing technologies already. Martin Fowler has a great write up on it. The Angular guide mentions this pattern as well.

The example shown in the Angular guide would help to move out all the DOM queries out of the testing files. This can be a great improvement already. The queries can be quite verbose, even in simpler cases.

It does not solve the issue of initialization, as the page object accepts an instance of a ComponentFixture. You still need to configure a testing module and call the TestBed.createComponent(..) somewhere.

A solution to that could be to use a static factory method. Such a method could configure the testing module, create the component to test and return a Page object that allows the tests to interact with it.

Example

Say we are implementing a simple component called (quite verbosely) the number selector. It should be something simple, like this:

Of course, this is a very simple component, so implementing a page object is probably overkill. Let’s still do it, as even in this case you should ask questions like:

  • Should there be any initial value?
  • Should the value go below zero?
  • Should it go below some specific minimal value?
  • Can the user click the value and type it instead of clicking the buttons?

The more of those you will need to implement, the more test cases you will need.

First, let’s create a method that will configure the testing module and create the page object.

Note, that the method has been marked as async. That is because the TestBed#compileComponents() is asynchronous too, and we may need to wait for its completion. If you are using Ivy, you may not need to call this method, though.

If you need to spy on some calls to shared services or provide some additional initialization, this method would be a good place to do it.

Once this method is in place we can initialize our testing environments in the test file by simply calling this method.

With such simple initialization, separating tests for validation or keyboard input into their own files will be a breeze. For now, let’s focus on the basics. With properly named helper methods we can test the basic functionality in a quick and readable way.

Good practices when designing page objects

You may have noticed that we did not explicitly trigger the change detection in the test. The page object exposes methods for all interactions with the page.
Such methods can call change detection instead. This removes even more chatter from the test files and makes them more readable.

In more complex cases you may also want to move the code to wait for or simulate asynchronous operations to the page object. This can be useful when the component needs to trigger routing, for example. One way to do it is to use the fakeAsync function and to immediately call the returned function.

The page object can become big itself if you are not careful. Try to provide an interface that is both readable and easy to maintain. Sometimes it’s worth to provide a single method to click any button based on the label, instead of a separate method for each button.

The page object can throw helpful error messages when something unexpected happens. One example is that an expected button has not been found, or that there were more buttons than expected.

You may consider extracting such helpers somewhere out of your page object to not repeat such logic. Maybe you need an abstract class for all your page objects to inherit? Maybe a simple function that you can import will do?

The factory method can accept parameters. This might be handy when you need to test that the component behaves differently under certain conditions.
In our example, this could be used if we had to provide minimal value.

Live demo

--

--