Good Angular Unit Tests
How do you know that your Angular unit tests are good? It’s important to be able to answer this question correctly. If you’re wrong, you’ll have false confidence and will be deceiving yourself. If you’re right, you’ll be able to sleep well at night knowing your code is unlikely to come back and haunt you.
In Angular it is critical that we test our templates. The template is the behavior that the user will notice, and often covers most of the scenarios in the component file as well. And our Angular templates contain significant logic that we do not want to skip testing. In fact, DOM testing belongs specifically in our unit tests per the Angular Testing FAQ.
Test behavior, not code.
Your code shouldn’t be doing anything that isn’t necessary for the defined behavior of the component. So, starting with the behavior should cover all of the scenarios needed.
But behavior can be complicated. We need to understand the facets of behavior that should be tested. If you’re using Gherkin syntax (Given, When, Then) in your user stories and acceptance criteria then you have a good start as to what tests should be needed.
From a higher level, you can think about three main areas that you should be sure to cover.
- Desired Behavior
This is the most obvious. We want to test our happy-path, default values and state, etc. - Expected Undesirable Behavior
There are certain error scenarios that we want to handle in a specific way. We’ve foreseen them and coded for them, so we should test that these work as expected. - Boundaries and Extents
Null, undefined, 0’s, off-by-ones. If you’re dealing with arrays in particular, be sure to test that empty arrays, single values, and multiple values work as expected. Dr. Cem Kaner said “Pay attention to zeros. If there is a zero, someone will divide by it.”
For Angular in particular, you should be sure to test that your *ngIf
's and your *ngFor
's are behaving correctly based upon the data set in the component. But, wait, isn’t that testing Angular itself? No. And that brings us to the broader guidance.
You should be testing that you are binding the right data to the right object in the template.
*ngIf
, *ngFor
, (click)="onClick($event)"
, {{ something }}
are all ways to bind in Angular. And we need to test that all of these do what is expected according to the three main areas listed above. Does the DOM reflect that an element was removed due to an *ngIf
evaluating to false? Can we emulate a click from a child and ensure that the component’s onClick
handler was called? Did we render the right text based on state?
You’ll notice that I left one out: [childInput]="componentProperty"
. Usually, in order to test this, we would need to mock the child component. And that would be okay and we would be able to verify it. It might represent more work than we really want to do though. It may be better to blur the line between unit test and integration test and simply use the actual child component in our test. Your situation will determine what is best for you. But we do need to test this in some fashion.
“But,” you may say, “why do we need to write tests for bindings? If they got misspelled or something then the compiler will usually catch those.” One would think that, but it’s not true. Here is a good example…
Say you have a child component of the component under test that has:
@Input items: [];
@Output click: EventEmitter<boolean>;
In your component template you have:
<child [item]="items" (clicked)="onClick($event)"></child>
It might surprise you to find that ng build
will miss both of the spelling mistakes in your template. ng build --prod
will catch that [item]
doesn’t exist on the child, but it won’t catch that clicked
doesn’t! The only way you’ll find this is in an integration or e2e test! Way too late!
Good unit tests are one of the keys to us having high confidence in being able to consistently release stable code. It lets us get our beauty sleep. And I don’t know about you, but the code I write when I’m sleepy isn’t pretty!