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.
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 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.
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
Visual regression testing
For ensuring that the app’s design stays tight and don’t rotten by time UI regression testing tools can be helpful to diff a new feature with the previous application.
I would recommend looking at Screenster (no affiliate, just a good tool) as they have a really easy to use platform for recording or coding visual regression tests.
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.
In this post, we discussed the different ways of testing an Angular app: isolated, shallow, integration, end to end and visual regression 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.