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.
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.
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.
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.
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.
- Using the TestBed system and have the mocked dependencies brought in through the TestBed NgModules.
This concept is very well documented and I’ll link to it here.
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.
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.
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.
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:
- Units are functions
- Handle all the edge cases for that “Unit/function” within the “Unit/function”.
- Most of your software can be pure functions.
- Service and Component methods can be static
- 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.