An Opinionated Approach to Testing Angular
Georgi Parlakov
Posted on July 22, 2019
Informed by a few years of writing, reading and depending on tests.
Originally posted at https://medium.com/ng-gotchas/an-opinionated-approach-to-testing-angular-4cf14ef7463f
First, let me lay out the basic points and then we’ll jump into details.
Disclaimer: I will not try to convince you that unit testing is a good idea. If you don’t think it is, go read something else. The opinions expressed here may seem arbitrary and just plain wrong if you are not relying on unit tests for your everyday work.
1. No logic in the templates — just call an event handler public method. We can trust Angular to do its part (that is already tested — example) which is to call us. And we do our part — test only our code.
2. Include all dependencies as injectable constructor params. Document, Date.now(), requestAnimationFrame etc.
3. All properties and methods used by the template are *public *. This might be obvious to you but it was not for me initially. And the language service wasn’t there to tell me otherwise.
4. In each test have a setup function that sets up all required state and infrastructure for the test.
5. In the setup function use the builder pattern to set up the dependencies and construct the class under test.
6. Use Jest (jest.io) [Jest’s snapshot feature (this one is marginally useful)]
7. For button clicking and input typing i.e. what the user uses the UI for, create a few UI (aka end-to-end) tests which actually use the user interface
All right — now for some details:
1. No logic in the templates — just call the event handler public method — this way we can trust Angular to do its part (which is already tested example). We test only our code. Consider the following two versions of the same template:
The top one looks like a smart and readable choice when writing the template but actually requires for your test to use the template , which needs to get compiled etc. It also requires any person reading the code to have both the my.component.html and my.component.ts open in order to follow the logic.
The bottom one allows for the test to instantiate the component class (no compiling of the template and so on) and simply call the public method onHereButtonClick(). The name of the handler is a convention combining the element name and the event name — onHereButtonClick — when here button is clicked do something — the logic.
This way the my.component.ts holds the logic and not the template and a person reading it can follow the logic that much easier.
…trusting that the DOM and/or Angular will call my code and when I call them they will do their job…
1.1. That allows us to only test the class without compiling the whole component with its .css and .html. Much faster, easier and no need to keep all the .ts and .html and .spec.ts open in order to write the tests
- Include all dependencies as injectable constructor params. Document, Date.now(), requestAnimationFrame etc. Any direct calls to browser APIs like Date.now() or requestAnimationFrame or setTimeout become hard to test. But, if we wrap those in services the tests become trivial.
The document is already provided by the framework: https://github.com/angular/angular/blob/2b44be984e0932bab83fcea015fc5b4ee2d3b151/packages/common/src/dom_tokens.ts#L11-L19
2.1. Use the AAA — arrange act assertpattern/convention. It allows for the person reading the code to discern what happens where.
Any similar convention —ex When Then Should — would do the trick as long as the whole team is on board of course.
All properties and methods used by the template are public . This might be obvious to you but it was not for me initially. And the language service wasn’t there to tell me otherwise. Since the method needs to be used by the template, which is ‘external’ to the class, it needs to be public. That’s how I think about it. Also in the test that allows us to decide what we need to test — any public method.
In each test have a setup function that sets up all required state and infrastructure for the test.
Instead of relying on the TestBed we shift its responsibilities to a setup function. We call that function in each test (or in beforeEach hook) and it returns to us an object that allows for specifying what is relevant to this test and constructing the class-under-test. In case of changes in the dependencies or the public API of the class (due to everchanging requirements), we’ll be able to update the test in a single place — the setup function.
4.1 Use autoSpy. That’s a function that can create an object with the methods of the class mocked automatically.
It allows the returned object to get used like:expect(o.method).hasBeenCalledWith('arg1'); and o.method.mockReturnValue(10).
This is a simple implementation. If a service needs to return a complex object you need to take the mock and add that behavior. See the example in the next section (5.) withTempUser method on the builder object.
A similar idea is implemented in the jasmine-auto-spies.
- In the setup function use the builder pattern to set up the dependencies and construct the class under test. The builder pattern allows for dot-chain declaring of each particular test’s needs.
In the following example, we need to have a temporary user on our site which only stores their email in their localStorage . Only tested one of the methods for conciseness.
In the builder we have the withTempUser method that allows us to specify that there is a temp user with a certain email. That takes the messy mock code and:
- moves it in one place, out of the test for improved test readability and maintainability
- preserves the intention for the person reading so they can see what we wanted to have happen, instead of trying to guess by reading the implementation
For each business logic case , we can add a similar method that does a piece of dependency or state set up.
5.1. That (the setup function implementing the builder pattern) also allows for some automation. Basically, a tool that can read the class-under-test and create a test for it following these guidelines. And update that test when the class-under-test changes. See SCURI.
6. Use Jest. It has (almost) identical API to Jasmine so migrating existing tests and testing skills is trivial. Jest has a bit better errors test than jasmine, like showing you exactly what is the difference between the expected and the actual value in a failed test. It does not depend on starting a server and running a browser. Instead, it uses the JsDOMlibrary and everything runs in a node.js process. It parallelizes the tests using all available CPU cores. It supports mocking js modules so that your tests see the mocked ../my-service.js instead of the actual ../my-service.js Don’t rely on this for all unit tests, but this feature saved our butts on a few occasions.
Finally — for our use case at propy.com— we could not start Chrome on our build machine and that was the main turning point to switch to our Angular unit tests using Jest.
Switching to Jest is even simpler when using this Jest Schematic.
Finally, Jest has a nice console UI when running in jest --watch mode. Lets you filter test by file name, test name, status failed or just changed i.e. understands git changes and runs only the currently changed files — which makes a lot of sense.
Using Jest’s snapshot feature proves to be marginally useful and a major pain to maintain. For the 6 months we used it we had maybe 1 or 2 instances that it may be caught something. That is — during a code review, I noticed that after changing something in a component a whole section disappeared from its rendered HTML (which is what we did a snapshot on). I am now not doing snapshots on the new components I write.
See this great article for a stronger opinion on that https://medium.com/@tomgold_48918/why-i-stopped-using-snapshot-testing-with-jest-3279fe41ffb2
- For testing the actual UI , like button clicking and input typing (what the user experiences) create UI (aka end-to-end) tests that actually exercise the UI as a user would. I use Puppeteer ( https://pptr.dev/) because it’s developer friendly, but have also used Protractor ( https://www.protractortest.org/#/) and heard good things about Cypress ( https://www.cypress.io/)
Bonus thoughts:
Unit tests that do button clicking and event dispatching I find expensive to write and very low return on investment ( ROI ). I tend to not write any of those. Instead, I control the instantiation of the class-under-test (component, directive, service) via the setup builder then invoke the public methods and examine the results, trusting that the DOM and/or Angular will call my code what I declared I wanted it called and when I call them— they will do their job.
They are expensive to write because I need to uncover the underlying DOM calls, usually hidden by Angular (e.g. dispatchEvent, addClass, focus) all the while Angular actually provides that functionality and has tested it thoroughly.
Hey, thanks for reading and happy to discuss — how do you do your testing?
Posted on July 22, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.