Better Programming

Advice for programmers.

Follow publication

How to Write Useful Tests for Angular Components

Easily achieve almost 100% code coverage

Michael Seemann
Better Programming
Published in
7 min readMar 28, 2022

--

Photo by Markus Spiske on Unsplash

After years of development with different languages and different frameworks in different projects, I thought I have seen everything. But the only natural constant in our business is, that you are all the way long are faced with something you haven’t seen before. One of my latest projects still moves my mind and I’d like to share my thoughts with you. It’s about the way we write component tests for our Angular application.

As you may know you can generate a new component with angular cli and angular cli generates a spec file for us next to our component. If you look into it you’ll see that there is a TestBed that is used to create a test harness for the component. But if we compare the different approaches how we can write component tests we see that there is a really simple way called Component Class Testing (https://angular.io/guide/testing-components-basics). And a far more elaborated testing strategy that is described in https://angular.io/guide/testing-components-scenarios. Only if I have a look at the amount of documentation in both cases, I feel a little bit in the domain of “Fast and Slow thinking” by Daniel Kahneman. But what falls in the category of slow thinking today can be fast thinking tomorrow. It’s a question of practicing — and motivation.

Enough philosophized. Let’s have a look at some code. Suppose we want to write a custom form control that should behave like a very basic select box (simplified for the purpose of this article). There are options the user can select from and the selected value should be reflected in a FormControl. Here is the component TypeScript code that implements the desired ControlValueAccessor:

and here is the corresponding html template

Now we want to test our code so that we reach 100% test coverage. Here is the complete spec file:

And here is the result of the test coverage report:

Statements   : 100% ( 32/32 )
Branches : 100% ( 2/2 )
Functions : 100% ( 14/14 )
Lines : 100% ( 29/29 )

Great! We have 100% code coverage. On every line, on every branch on every function. How could this be reached so easy?

  • First of all you may noticed that the Component Class Testing approach is used. The components constructor is called directly. The only parameter should be an NgControl instance. Because we haven’t such an object we just provide a mock.
  • The template is completely ignored.
  • There are a lot of functions and properties that are private in the component. To avoid compiler errors the component must be casted to any so we can access these functions and properties during compilation.
  • Every function that is called is tested with a spy — e.g. the test checks if the function is called.
  • Don’t care if you are calling functions (for example registerOnTouch) that the framework is supposed to call — remember we want to reach 100% test coverage.

The implementation of the test is straight forward: start at the top, create a test for every function and check with a spy that the methods are called that are present in your functions under test. To mimic real testing just check some properties that are changed in the function. If the function or property is private — don’t care — just cast your component to any and you can do anything. It will compile. At runtime private did not exist.

Do you have a problem with this? You are welcome — me too.

What really was done here?

The tests are testing the way the component was implemented and not the desired behavior. Let’s checkout what happens if we change something at the implementation level:

  • If the template code is changed the test still pass. Lastly the complete template code can be removed and the test will pass — because it is not part of the test. So every change can break the component behavior.
  • If somebody removes the line in the constructor (ngControl.valueAccessor = this;). The component is no longer usable as a control. But the test still passes.
  • If the updateSelection function is removed from the code and a setter is used to ensure that the selectedOption and the viewModel are in sync, the test will not pass. This wouldn’t change the behavior of the component but the implementation. (4 test fail because of runtime errors)
  • If the implementation of the optionClicked function is changed and it is ensured that every time onChange is called the Output selectionChanged emits the value the tests will not pass. Also this wouldn’t change the behavior but the way the component is implemented. (2 tests fail because the callback function is no longer the one that is passed in the registerOnChange function and emit is no longer called because of the spy on the onChange function)
  • If the name of the onChange function is changed the test will break. The IDE can not ensure the function is renamed in the test too because there is a cast to any. (3 tests fail because of runtime errors)

If you want to play around by yourself and find new ways to break the tests without changing the behavior: here is the code https://github.com/mseemann/testing-with-100-percent-testcoverage.

So what is the reason for this problem? The tests are bound to the implementation. If you want to, or need to change the implementation your test will break. Now you have to adapt your tests. But the result is that the tests are completely useless — they doesn’t prevent us from breaking the desired behavior. These kind of tests do ensure that the implementation can not be changed without changing or fixing the test code. Even worse: because of the overwhelming use of any the compiler can not help and the tests will fail with runtime errors. If one is familiar with the Angular form control api this may be easy in this example. But if things are more complicated you have to put a huge amount of effort to fix these errors.

So let’s make it crystal clear:

The desired behavior is the invariant of our code — the implementation is not. Valuable tests are testing the invariant. There is absolutely no justification to do the opposite.

How can we do better?

If you don’t want to write tests that interact with your template. Then just do not write any component test at all. Just extract business logic in your components code into pure functions. And test these functions. But don’t write tests that ensure your implementation is not changed. This is useless and results in exponentially increasing maintenance costs.

Write tests that are testing the behavior and not the implementation. That requires you to interact with your components' inputs and outputs. What are the components’ inputs and outputs? For sure these are the @Input and @Output properties and events. But in fact, the real input and output are what is rendered in your templates and what the user can do with your template.

Here is an example of how the behavior of the components can be tested:

Just to emphasize a few things:

  • A special TestComponent is used to interact with the component under test. This is also a great documentation because the user of the component can see how the component should be used.
  • The amount of test cases is only 5 not 10.
  • Every test checks the desired behavior of the component and not the implementation.
  • The template is rendered — and breaking changes in the template are breaking the test.
  • There is absolutely no any keyword in the complete code that interacts with the component under test (In fact there are some hidden types in the angular test API).

Let’s have a look at the coverage report:

Statements   : 100% ( 30/30 )
Branches : 100% ( 2/2 )
Functions : 84.61% ( 11/13 )
Lines : 100% ( 27/27 )

Nearly the same as before. I have not included the useless test (component as any).onTouched(). This code is only present to ensure that the onTouched member can never be null or undefined. So I can avoid checking that the onTouched member is set every time I need to call it. These are the two missing functions in the coverage report — who cares.

By the way! Every change I have listed above can be made without breaking the test because the behavior of the component did not change. This version is also available at github: https://github.com/mseemann/testing-with-100-percent-testcoverage/tree/not-100-percent-but-valuable-test.

Can we do even better?

Yes for sure.

  • An option component can be introduced and after that, some css queries in the tests can be removed too. After that, someone can change the style of the component without potentially breaking the tests.
  • The page object pattern can be used to avoid redundant code fragments — so the tests can even get more DRY and less brittle.
  • We can provide test harnesses for our components. You may have a look at this article on how to use them.
  • If you are concerned about performance. It is possible to switch from karma to jest — the jsdom may be a little bit faster than the real dom in a browser and tests can be run in parallel. If you want to go one step further you can switch to nrwl nx and run only tests that are effected by code changes.

Conclusion

I confess — I am addicted to a green badge too. But as you could see the way to achieve the green badge could be very different. In the future, I will think twice before I write again a test case just to reach 100% code coverage. If I am in doubt that the test is really testing behavior I will better omit the code before I start testing the implementation.

Sure it is more effort to write the right tests but only at the time, the code is created first. On every required change, you’ll get back a huge amount from this effort and your tests are valuable because they are testing the desired behavior.

By the way, this whole problem would never arise if the test is written before the implementation is done — an aspect I need to think about.

Sign up to discover human stories that deepen your understanding of the world.

--

--

Michael Seemann
Michael Seemann

Written by Michael Seemann

I am a freelance full stack developer and creator of angular-mdl; a component lib that is based on material design light.

Responses (1)

Write a response