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.

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 to this 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!