How to write a ‘Good’ unit test in Angular...

February 20, 2018

Within any community of creators there arises this nebulous concept of “Best Practices”. It is not always called by that name, but it exists overtly or subversively. Within the Angular community and the larger JavaScript community it can be very hard to know what the ‘best practices’ truly are. I propose that ‘best practices’ are much like any type of advice. They are not the law, they should not be something that excludes or divides in an of them selves. Within this post I intend to give my view of what a ‘good’ Angular unit test is. My definition of good, comes from two things. What I have read; e.g. ‘Best Practices’, and what I have experienced. Those two aren't mutually exclusive of course. Sometimes we do things, simply because they are ‘Best Practice’ and sometimes we do them because we have experienced a truth.

NOTE: Through out my posts I try to highlight the important bits. You will know what is important because it will look like this.

TIP 0: This is an important tip that you can take away from the post.

Okay enough philosophy, lets get started.

What is a Unit?

A unit is a small part of a software system. You may be thinking, “wait, what does small mean?”. Small is up to you really, but I think its pretty intuitive. In my opinion the smallest atomic unit of a software system is a variable. How do we test variables. Well, variables don’t change unless we write code to change them, or use them so poorly that they are getting changed without our knowledge. So in order to get the correct target for a unit, lets look at the main mechanism with which we introduce changes to variables. Can you guess? …..

Times up! The answer is Functions. Even if they are called methods, or procedures, they are the things that change small groups of variables. Their inputs are functions or variables. Their outputs are the same, functions and variables. So, here is my proposal. Are you ready for it?

TIP 1: Units are functions!

Whoa! Pretty profound right? So your unit tests should be testing functions specifically. You may be asking several questions right now. Questions like: what if those functions happen to be attached to a class? What if they are pure functions? What if they call out to a database and have what we call ‘Side Effects’? And what if they are a function on a class, that happens to be an angular component? Don’t worry, we can handle this. We are software engineers after all. We have different unit testing strategies for each type of function.

Strategy

If you get anything at all out of this post, let it be this. Pure functions are the easiest and most reliable functions to test. A pure function is something that only operates on its inputs to produce its outputs. Meaning, in JavaScript, it does not access this or rely on closures not created by the function itself to get state from a variable. Here are two examples. Once simple example and then another complex.

I’m not sure if the math I put here has any significance, I just think its fun. Breaking down the example we can see that what is a very simple function. And its very simple to test, although not incredibly useful. The way to test what is to make sure you know the edge cases. For example putting in 0s, negatives, nulls and so forth. Writing these tests up front will force you to handle all the edge case that the function needs to handle, within the function. I say this because I see many programmers testing functions that call functions, and they only handle the edge cases before they call the inner function. What usually happens later is a refactor comes through and uses the inner function somewhere else without handling all the edge cases.

TIP 2: When testing pure functions, handle all the edge cases for that “Unit/function” within the “Unit/function”.

We can test what easily and it will cause us to change the function like so. Below is a fiddle of some tests and refactor to for what.

Because we wrote tests with edge-cases in mind we can get instant feedback on refactoring the function to pass tests. As you can see, testing pure functions is pretty simple and it allows you to focus your programming effort to one small world and handle all the edge cases effectively for that one little unit. I must say; most of your software should be pure functions. If most of your software is pure functions then most of your software is easy to test. By virtue of that , you end up with safer programs.

TIP 3: Most of your software can be pure functions.

Lets dig into the more complex pure function whatRunner. This function may not look like a pure function since it calls another function, but it is. Because what is a pure function, the purity of whatRunner stays in tact. Especially since we arend doing any scope or context sharing. An example of scope sharing would be if whatRunner called what like this what.call(this, a, b). This would make what depend on the context of whatrunner.

Lets write some tests for whatRunner , and remember we are trying to focus on the edge cases of just whatRunner .

Okay now we have two pure functions being tested. One thing to notice is we aren’t re-testing what we are trying to scope our test to only whatRunner does. In English that is , put stuff in an array based on a loop. For most cases the tests above for these two functions are sufficient, but we can go farther. Instead of actually calling what we can stub it. Stubbing is basically overriding something and making it do/return what you want. Almost all the testing libraries allow you to stub something. Another name for stubbing is “mocking”. There may be some small differences between the two, but its inconsequential for this post. For example we could re write the test as follows.

In Jasmine, the stubbing system is called a spy. Its something that can track invocations, and stub functionality. What we did above was take what out of the equation. The assumption is that whatRunner as a unit of code, should perform the same, regardless of how what behaves. This is of course not always the case. For example, perhaps some change was made to what and it can now throw an exception or something. That would change the behavior of whatRunner because it would fail as well. Yet another assumption in unit testing however is that the changes to what would need to pass through the tests on that unit. If we can uphold those assumptions namely Isolated tests, and Test every refactor, then we can start to have very valuable unit testing. There are assumptions in all testing paradigms, especially when you get into specific framework domains such as Angular components.

Angular?

I hope at this point you are wondering how this all relates to Angular. The key realization is that the above assumptions and tips are effective beyond your framework choices. That being said it it necessary to dive into how these tips are applied to Angular. Again, we will list the Angular primitives. Services, Directives and Pipes. Wait, what about components? They are just directives, with some fancy template stuff. We will focus on services and components.

Services

A service in angular is just a class, or a closure if you aren’t using TypeScript or Es6. The class has functions attached, called methods and a fancy dependency injection system on the constructor function. A service as a whole certain makes up one group of functionality, but its the individual methods, that form the units of computation that we want to target while unit testing. Services can get complicated when they have lots of dependencies, this makes testing complex as well. You will end up creating lots of stubs or perhaps coupling units together and creating brittle tests. Lets look at a services that is composed of many small pure functions to achieve what it needs. Here is a little plunkr to run the code.

Angular + Typescript + Jasmine - Plunker

Here is the gist.

As you can see I have separated the code into two sections. The pure functions and the impure functions. The impure bit really just allows for configuration, leveraging the the dependency injection system. The great part about doing this is that you can write 90% of your tests without having to worry about the dependency injection system. This truth is shown by the fact that the static methods don’t need an instantiated SecretService to run. You can simply call SecretService.*. Then when you what to test the encode public function you can just ensure its calling the private methods the right way and returning a result.

TIP 4: Angular service functions can be static.

It is worth mentioning that there are different types of testing strategies. I have focused on the ‘Isolated’ strategy. Which tries very hard to keep the units of functionality isolated from one another. The pure function strategy makes this easier. When you need to test more complex domains, that do need to couple units together it is important to have a organized approach to doing so. For example, perhaps you have a service that does more things in the impure section. The behavior of these functions will differ, depending on some thing external to the ‘unit’. In the above case, the state of the containing service matters. The encode function above relies on a instance variable coming in from the dependency injection. Perhaps in a more complex example you have Boolean coming in from a feature flag system. In order to test the impure parts of the service you will actually need to instantiate a copy. You can do this in two ways.

  1. Using the TestBed system and have the mocked dependencies brought in through the TestBed NgModules.
  2. Using plain old Javascript and Typescript to instantiate the services with their dependencies using new.

This concept is very well documented and I’ll link to it here.

Angular Docs

Using number two from the above list lets your tests remain simple, in terms of not needing the angular TestBed helpers. I’ll take this one step further by saying you can create test helpers that will create instances of your services with different ‘configurations’. The goal is to treat ‘context’ as a parameter to your test. What I mean by that is if you think about the call and bind properties of Javascript functions, the ‘context’ is the first argument. Using the helpers I talked about you can test specific methods of your Service by passing it as the context to the function. Here is an example.

The key here is the use of call on the encode method. You can also test the encode method on svc and that’s totally fine, its basically the same thing. I just think the call way is a little more explicit on what is being tested. Boiling this down into a simple statement we can say.

TIP 5: Treat instances as a argument to the function you want to test.

As your services start to get more complicated you may find it harder to create the instances of the Services using the new keyword instead of TestBed. At this point go for it. There is nothing wrong with using the TestBed, I’m just saying that it should be brought in when its needed. Another situation that makes TestBed very useful is when you need to depend on builtin angular services like HttpClient.

At this point I hope you can see the theme, test your Angular Service ‘units’ in isolation by making them pure or by managing context. There are so many reasons to do this, but the chief of all of them is to create tests that will continue to be useful after refactoring. You want tests to fail because the code is broken and stay green when the ‘unit’ still behaves as expected.

Components

So how do we apply the ‘isolation’ and ‘managing context’ principles to Components. Well isolation is achieved by the same strategy as services. You can create static methods and test them outside of the instance, then you can test their usage inside the component instance by creating methods that call the static function with the instance context. Managing context is a little harder to do. My advice on this is to separate your components into two categories. Smart components and Dumb components. Smart components have dependencies, handle events, communicate with services , etc… Dumb components could be the pure function equivalence. With dumb components you can use the Input annotation as if they are arguments, and treat Output annotations as the return value. This is well documented withing the ‘testing a component within a host component` part of the angular docs. Because it is so well documented, I don’t think I can do better than the official docs linked below.

Angular Docs

The thing to realize is that Dumb components are just long lived functions. Smart components however are not. You will need to use the above strategy to try to separate pure from impure, manage the context of the component by creating substantiation helpers, and sometimes, you will need to mock dependent services.

Summary

My goal is to empower you to test your code. The strategy above is one I follow to make testing more enjoyable and safe. Reviewing the tips above we find a neat list to keep in mind while you test. The list is as follows:

  1. Units are functions
  2. Handle all the edge cases for that “Unit/function” within the “Unit/function”.
  3. Most of your software can be pure functions.
  4. Service and Component methods can be static
  5. Treat instances as a argument to the function you want to test.

I hope this helps you next time you start to test your angular code.