Why Your Angular Tests Probably Smell
Common pitfalls encountered when testing Angular projects
Testing an Angular application is inherently straightforward. Angular frameworks are built from the ground up so their abstractions are easy to test. Default loose architecture, splitting logic into components, directives, services, modules or pipes — all of these significantly help us in the testing process itself. If we follow proven patterns and produce truly testable code, testing shouldn’t be a nuisance — it should be something that facilitates reliable code development.
You’ll never walk alone
Moreover, Angular does not leave us alone on the battlefield — it equips us with a powerful set of tools that facilitate and accelerate writing tests.
As Angular users, it’s our responsibility to learn these tools thoroughly. In this Angular Testing Series of articles, I would like to present the typical and correct use of built-in unit testing mechanism in Angular, based on prepared examples.
Bad testing habits
Bad habits often come from temptation. I’ve noticed that we (developers) also often give in to one temptation: copying a code for unit testing. This technique has two faces:
- We can stick to a consistent way in testing and accelerate our productivity and bringing value.
Or, if we do not know what we’re actually copying:
- We can contribute to smearing smelly pieces of code among all the tests in the projects.
In this article, I would like to focus on those smelly pieces of code in unit tests that I have encountered in various Angular projects.
Component Testing
Let’s assume this is our component under test:
We would like to verify whether Full Names is rendered into the tag. Let’s look at the following test:
When we run jest everything works:
But something smells — can you tell what it is?
Let’s take a look at the last test and see what’s going on there. ngOnInit is called, change detection is simulated in fixture, and assertion is made when the fixture template is ready. Apparently, everything is OK, but this is a distortion of the truth and a bad simulation of the component’s life cycle. In this example, everything will work out as expected. However, the method ngOnInit is called twice. The change detection is also performed one more time.
How should we handle it?
What should we do to make our test reflect reality is to get rid of the ngOnInit call? Instantiated fixture:ComponentFixture has appropriate responsibility and when we call on it detectChanges method, we can trust that the individual component lifecycle methods will be executed automatically.
In the above gist, I changed one more thing. The async function was replaced with waitForAsync. Why? Because in Angular 10, the async function is finally marked as deprecated and we can expect that in Angular 12 we won’t see it at all.
Don’t care about rendering in tests
Quite often we find and write component tests where it is not important to check if actual values are generated in the DOM. Whether this is a good or bad approach depends only on your test findings and objectives, but I would like to mention another common mistake related to this type of test.
Now, we want to verify only if the field title has the expected value assigned. It seems that everything is fine, and the test itself is deterministic, but something stinks about the solution presented.
As mentioned previously, the fixture reflects the template with the generated DOM model. In the above test, we don’t check anything in DOM. Is it worth using resources to create fixture, indeed? This is a typical example of an isolation test. If all tests gathered in the spec collection are isolated, then the entire spec should be implemented, like this:
In isolated tests, we have to remember to call appropriate lifecycle methods like ngOnInit
Isolated tests are significantly faster than shallow tests with ComponentFixture. Even for such simple tests, the difference in resource usage and total test time is significant.
Asynchronous Services and Events
In Angular’s project development, we deal with asynchronous operations most of the time. XHR requests, handling of render/browser events, debouncing of inputs entered by users are all asynchronous tasks, so tests should handle them appropriately.
Let’s analyze the sample scenario with a straightforward service which fetches data as XHRs:
The above service is used by the smart domain component FullNameListComponent.
We would like to test fullNames$ stream. This time, we use ComponentFixture, due to the fact that it’s a convenient way of providing stubs to Angular DI.
Two basic tests can look like this:
The tests are passed. Is it OK? Obviously, not! To find out why I slightly modify the second test:
it('should fullNames be triggered on name change', () => {
const expectedFullNames = *FAKE_NAMES*;
component.fullNames$.subscribe((fullNames) => {
expect(fullNames).toEqual(**null**);
});
component.name = 'John';
});
I modified assertation to expect fullNames as null, which is incorrect. The test is still passing. What’s wrong?
fakeAsync for help
Aside from the fact that so-called subscribe-assert is not the best method we can use for stream testing, we often forget about one thing. If the stream runs asynchronously, the test should take this fact into account. Fortunately, Angular provides us with useful tooling: a special fakeAsync zone.
it('should fullNames be triggered on name change', fakeAsync(() => {
const expectedFullNames = ***FAKE_NAMES***;
component.fullNames$.subscribe((fullNames) => {
expect(fullNames).toEqual(null);
});
component.name = 'John';
tick();
}));
Now, the test runner is able to identify that assertion is not correct:
I don’t want to explain exactly whatfakeAsync is here, because that’s not the purpose of this article. However, we must always remember that if we deal with asynchrony, then we should also take it into account in tests.
There is another smelly thing in the above test method. Let’s look at our stub:
Here we simulate returning Observable with fake data. It’s a popular approach, but is it correct? Not at all.
I refer here to a very good article from Netanel Basal. He explains well why of is not an appropriate function to use to fake an API response.
In short, of function provides synchronous data, while the response to an XHR is asynchronous and treated as a macrotask. Faking asynchronous data with synchronous invocation is cheating and stinks. It increases code coverage, but the quality of such tests is questionable.
As providing an appropriate scheduler to the of function is deprecated, I recommend the following approach:
import { ***asyncScheduler***, Observable, scheduled } from 'rxjs';
getFullNames(name: string): Observable<FullName[]> {
return scheduled([***FAKE_NAMES***], ***asyncScheduler***);
}
Summary
How to avoid such mishaps in tests:
Try to follow Red/Green/Refactor technique. If you don’t use TDD methodology, try to fail your tests on purpose.
See and learn about two features that Angular gives us: waitForAsync and fakeAsync. Be aware of the differences between them.
Don’t use of for faking data. In most cases, you don’t notice any differences. However, there is a framework hacking. Trust the provided fakeAsync zone to simulate synchronous execution.
All examples are gathered in the repository on my Github.
I would be grateful also if you tell me what your consternation with Angular testing is. I will be happy to prepare a pragmatic solution and assist you.
Top comments (0)