Angular testing made easy
Disclaimer: This article is about Angular, as opposed to AngularJS. This means it applies for Angular 2.x, 4.x, and hopefully future versions.
Update 08/15/2017
A more up-to-date version of the code from this article is available here:
The differences with this article are explained in this talk:
Now, if you’re still interested in the original article, feel free to go on.
If you’re here, you have probably already read the Angular documentation on testing, and maybe have written a few tests for your own project. If you have not, I can only suggest you do so, because this article will require some prior knowledge of Angular testing.
So at that point, you know how many hoops you need to jump through to test even the simplest of components and directives. TestBed
configuration, compiling the components, creating the one you need, getting access to the directives instances, cleaning up, … And if you are working on a real-life project, you probably have many spec files across your code base, all needing the same boilerplate. So what’s the most efficient way to centralize that boilerplate, so that your actual spec files can focus on the unit tests themselves?
This article will show you a neat solution to this problem, using two little-known features: Jasmine’s user context and Typescript’s ability to type this
. We’ll round it up by relying on some “advanced” parameterized typing to make sure everything is truly reusable.
Let’s open your new toys
Jasmine’s user context
Did you know that Jasmine binds all the functions declared in it
, beforeEach
andafterEach
to the same object when running a test? This object allows you to share information across the different phases of your test, and the object gets properly destroyed at the end of each test to avoid memory leaks.
So what does that look like? Here is a simple example:
It creates a fresh new Dog object for each of the two tests, and properly tears down the object after each one. Pretty simple, you could admittedly do the same with a local variable at the beginning of your describe. There are two main advantages to this approach over local variables: it helps fight the memory leaks that keep creeping up on large projects’ tests, and it also offers a very easy way to have your beforeEach
and it
in two different files, which is much harder to set up with local variables.
CAUTION: We are using a plain
function() {}
here, rather than the very useful() => {}
arrow notation from Typescript. This is on purpose, because arrow notation would prevent us from accessing the user context Jasmine assigns tothis
when calling our functions.
Typing a function’s context
The previous example is fine, but all the nice tools you get from using Typescript instead of Javascript disappear when you work with an abstract this
that comes from Jasmine. It is automatically typed as any
, so you will not get any form of autocompletion, type checking or smart navigation in your IDE.
Thankfully, recent versions of Typescript (starting at Typescript 2.0) now allow you to type this
as if it were an argument of the function:
Doing so will give you back of the advantages of Typescript’s strict typing, including compilation errors when accessing unknown properties or autocompletion in your favorite IDE:
Time to put them together
Let’s apply this to testing a component in Angular, for instance to check if a two-way binding works properly. One of the recommended patterns (and the one we use the most, by far, in Clarity) is to use a “test host” component which includes the actual component to test in its template:
This example makes a few assumptions to keep it simple: the templates are all inline or inlined at build time so we don’t need to callTestBed.compileComponents()
, MyUserComponent
doesn’t depend on other components, … Feel free to adapt to your specific use case.
Introducing the TestContext
If we want to extract all the boilerplate into a separate file, we need to decide what the type of the user context passed around by Jasmine should be. Since the configuration, component creation, element querying and teardown can all be tied to just the act of “creating a component”, we’ll expose a single create
method that will handle all this. Then of course, we’ll want access to all the queried native elements and component instances, so we’ll need to expose these too. Using type parameters to make sure it can work on any directive to test and any test host wrapping it, we obtain this:
You might notice the optional providers
argument: this will let us declare providers in the testing module configuration if we need any. Also note that the userComponent
from before will now be testedDirective
, the reason being that we might sometimes be testing attribute directives instead of components, so the more “general” denomination makes more sense.
Making it available in tests
We have the interface, we can now type the user context. But just typing it won’t do much if don’t actually implement the create
method somewhere… Let’s expose a function that will add it to the user context in a beforeEach
that should be called before anything else:
We make sure the create
method is implemented in our beforeEach
, and we make sure created fixtures are destroyed after each test. Suddenly, most sneaky memory leaks that would happen because of one spec forgetting to clean up are handled!
What the create method does is more or less what our initial beforeEach
setup did, from configuring the testing module to querying created elements. The one difference is that we’re getting the tested directive through the injector, rather than componentInstance
, once again to be able handle attribute directives and components at the same time.
The final spec file
Let’s remove all the boilerplate from the user component’s spec file and use our brand new context:
It’s now entirely comprised of the unit tests, no distractions or verbose setup to go through. And since TestContext
is parameterized, this pattern can now be used for free in every single other spec file in the project. Writing new tests is now easy, as promised. You don’t have to go through all this again, just call setup()
and you’re ready to go!
No more boilerplate in all spec files, no need to remember teardown to avoid memory leaks, and a single place to update when the testing API of Angular changes. That’s a win!
To improve this further, you could easily:
- include the
setup()
call at the root of the tests to have it always be available, - allow passing extra directives to declare or modules to import, in case your component depends on other ones,
- add additional shortcuts to the
TestContext
interface, - throw explicit errors when trying to test with a host component that does not contain the directive,
- …
In our case, for instance, we have a shortcut to get providers from the tested element’s injector. That’s a great way to test communication between our components. Go crazy, and feel free to comment with your own awesome helpers!