When doing practices like continuous delivery and continuous integration, testing becomes a more important part of the work because it’s unreasonable to regression test all possible effects of a release manually. When doing five releases on a workday, you don’t get to do a full manual regression testing on every release as that would easily take up one person’s time for a day. Instead, you want your code to be nicely covered with the right tests for ensuring the application work as expected.
What are the different kinds of tests?
There are different ways to test in your application and you should always ask yourself: “What kind of test will cover this feature” and then choose that test.
The testing pyramid
You might have heard about the testing pyramid and the saying that 80 % of the tests should be unit tests, 15 % is integration tests and 5 % is UI tests. These percentages might vary depending on who you ask but the ratio should be around the same. Note also, that we get static typing in Angular apps, which can be seen as part of the unit tests coverage, as I don’t recommend we unit test every component with 100% code coverage, but instead focus on the essential areas: services, pure functions, and container components.
When going up the testing pyramid, tests become more expensive because; they are harder to create, they are harder to maintain/more flakey because they test on fragile interfaces like the UI. On the top of the pyramid is manual testing, which still has its place, even in the modern ages of automatization and DevOps, because validating a new feature for the first time requires a human to decide if it is passing the user acceptance criteria (UAT). In Angular context, there are the following types of test: isolated and shallow unit testing, integration tests between components and UI/E2E tests, which can be functional and visual regression testing.
Isolated unit testing
Isolated tests are used to test a unit in isolation. The unit might contain some business logic that needs to be tested in isolation. When writing isolated unit tests all external dependencies should be mocked out, eg using Spies.
Here is an example of a service with an isolated test:
Shallow unit testing
Shallow testing is when you test a component with a template but you don’t render child component by setting schema to NO_ERRORS_SCHEMA. This will ignore unknown tags in the template, making us not needing to import the child components. An example of a shallow tested component:
Alternatively, you can use the method overrideTemplate on the configureTestingModule to override the template to one that fits the test.
Integration testing
Integration testing is when you test two or more components together. This makes sense for parts of the applications where the integration between component is important to test, such as a smart/container component. With integration testing, you simply import the implementation of the dependencies of the unit under test even though you still should mock out HTTPClient. An example of integration testing is:
End to end testing
End to end testing means that you run the complete application together, including the backend, using a browser automatization API, such as Selenium. Protractor, which is built on top of Selenium, ships with an Angular CLI project and is what we will be working with here.
Page object
Out of the box a new Angular CLI project ships with the page object pattern, which is a method of separating the stable and the fragile part of the end to end testing into a .po file (fragile part) and the .e2e-spec file (stable part).
End to end test spec
The test specification should use the page object for accessing dom elements. Protractor comes with built-in integrations for Angular such as waitForAngular, which will wait for an Angular page to be ready before doing something.
A word about fragile E2E tests
If you have had any experience with automatic E2E testing you know that they are fragile by nature and breaks easily.
Here are some techniques to make them more stable:
- Retry expects with a reasonable fallback and retry count, eg. 1 second retry of up to 5 retries
- Automatically rerun tests that have a fragile dependency, such as a fragile dev API
- Make page objects select using id attributes since classes are more prone to changes
A pragmatic standpoint on writing Angular tests
Writing tests for Angular/front end apps is not the same as writing tests for back end systems. Why? With back end testing you don’t have a fragile and slow GUI to interact with but just plain server code. When you are testing an Angular app it is hard to automatically prove that it looks and works correctly, as the requirements are softer than clearly defined business logic.
For this reason, I recommend NOT to have a 100% percent code coverage as not all tests give an equal return of investment and we want to focus our effort on the code that is closest to the use cases and business logic which is going to yield the highest ROI.
I recommend prioritizing your tests effort like this:
- Services – Sandboxes, business logic, NgRx effects (100% coverage)
- Pure functions – Pipes, NgRx reducers, helpers (100% coverage)
- Container component – Integration tests for happy paths (mock out services but render all components)
Having these tests in place will yield you the highest ROI. When starting to write unit tests for presentation components, the ROI is getting less and less because the tests are likely not going to save you if your component looks wrong.
What about presentation component?
Presentation components are, as the name implies, about the UI of the component. It is hard to automatically verify changes in the UI. I have tried a couple of solutions already such as plugins for Selenium and SaaS solutions and all of them had been full of flakiness and false positives.
Using Jest’s snapshot tests might be the best option here to test presentation components, but still, manual testing will be needed to make sure everything looks and works correctly.
Using Spectator for less boilerplate
We can get rid of a lot of the boilerplate when writing tests and also get some mock helpers by using Spectator. Spectator wraps the Angular testing methods and provides easy configuration for setting up tests and interacting with the testing API
Look how simple it can be done to create this integration test:
Pretty neat, huh?
Keeping unit tests fast by running them separately
Running unit tests should be fast, so you get the fastest feedback cycle possible when doing TDD style development. The bottleneck for Angular test executing is usually component template rendering. For this reason, I recommend postfixing integration test files with integration.spec.ts for distinguishing between unit tests (.spec.ts postfix). I use this for creating NPM scripts for running different kinds of test: all tests, unit tests only and integration tests only.
NPM scripts for running different types of tests
For creating an easy facade for executing the test scripts, these are written as npm scripts so they can be executed with npm run test, npm run test:unit and npm run test:integration with additional variations for running in watchmode and without sourcemaps.
Setup config for unit tests
For running unit test you want to run all test that ends with .spec and exclude all .integration.spec.ts:
Setup config for integration tests
For running integration test I like to use the convention *.integration.spec.ts. Ensuring that only files ending with integration.spec.ts will be run with this configuration:
Setting up test configs in Angular.json
As of Angular 6’s Angular.json file, you can specify test configurations using the test.configurations property. We create one for all, unit and integration tests including variations for watch mode:
Discovering slow tests
You can use Karma Verbose Reporter to measure the execution speed for each test and postfix them with integration.spec.ts if they are too slow so they won’t impact unit test, that always should be running fast.
Wrapping up
In this post, we discussed the different ways of testing an Angular app: isolated, shallow, integration, end to end and visual regression testing.
We looked at a pragmatic approach to testing to focus on services, pure functions, and container components to ensure the highest ROI when testing.
We also saw how to keep unit tests fast by using a separate postfix for integration test spec files, setting up NPM scripts for running different types of tests and how to discover slow tests that should be moved to integration tests.
Do you want to become an Angular architect? Check out Angular Architect Accelerator.