Unit testing is a very important topic in the world of software development. It gives us feedback about the design of our code (difficulty to test a code means bad design). But most importantly, it gives us confidence that we can refactor our application, or add new features to it, without breaking existing functionalities.
It is very rare that the units we are testing do not have dependencies (or collaborators as called in the literature). Therefore, most of the time, we are required to provide test doubles for immediate dependencies of the System Under Test (SUT), in other words, the class we are testing. There are many ways to provide those test doubles, depending on the testing framework we are using. Also many different terms are involved; there are many schools of thought about how things should be done.
In Angular, the default testing framework is Jasmine. It comes out of the box when generating an Angular project using the Angular CLI.
In this article, I will show you the differences between the various kinds of test doubles, and how to use them when unit testing Angular applications using Jasmine.
The Example Used in This Article
- We use the
NgFordirective to loop over an array of
- To fetch the list of teams, we inject the
- We use the
ngOnInitlifecycle hook to invoke the service's
getTeamsmethod. This method returns an Observable of
TeamListComponent is what we are going to test; it is our SUT. It depends on
TeamService is the collaborator.
Before diving into the actual implementation of the tests, let's go over some important theory.
Solitary vs Sociable Tests
In an article called Unit Test, Martin Fowler puts the emphasize on the distinction between the two types of unit testing: solitary and sociable unit testing.
Sociable unit tests
Sociable unit tests are unit tests that use real instances of their dependencies. When using this kind of testing, we assume the correctness of your SUT's dependencies.
- This service returns hard-coded data. So it doesn't need to be doubled.
When testing a system that depends on this implementation of
TeamService, we don't need to use a test double. This example is not realistic, because we hard-coded the data. But it illustrates well the case where we can use a real instance of the collaborator.
It is not always possible, nor is it a good idea, to make all your unit tests sociable. If the collaboration between the SUT and its dependencies is awkward, then test doubles should be used instead.
Solitary unit tests
Solitary unit tests are unit tests that do not use real instances of their dependencies. For example, when you are testing an Angular service that depends on the
HttpClient service, you don't want to hit the real back end in your unit tests.
- Inject the Angular's
- Make a GET request to the back end.
When testing a class that injects this service, using the real
HttpClient instance would make your tests brittle and non-deterministic, as their result would depend on the responses of your back end.
Sociable or Solitary in Angular ?
In Angular, I note that many people find it difficult to use real instances of dependencies. This is mainly due to a lack of separation of concerns. It is not rare to see an Angular service, let's say
AuthService, that inject
ToasterService, and the list goes on.
Instead we should thrive to create services that do one thing and do it well. Paradigms like Redux, and libraries like ngrx, could surely help. But that is a topic for another day.
My rule of thumb is to use the real instances if I don't need to import many modules, or provide many services, when setting up the tests. And of course, if the collaborator is stable, i.e. it is not accessing file systems or making network calls.
The Differences Between Stubs, Spies and Mocks
Stubs provide canned answers to calls made during the test, usually not responding at all to anything outside what's programmed in for the test.
So a stub is a function that replaces a real implementation of an existing function. Stubbing is, generally, an operation local to a test. We use stubs if we want to:
- control individual method behavior for a specific test case,
- prevent a method from making side effects like communicating with the outside world using Angular's
- We create an instance of our collaborator by invoking its constructor. We pass it the
undefinedprimitive to fill the parameters list because otherwise the TypeScript compiler would yell at us. We don't care about the actual value passed to the service's constructor.
undefinedis called a dummy in the literature.
- We stub the
getTeamsmethod with a function that just returns an
- We act on the SUT by invoking the
- We finish off by asserting that the
teams$property contains the correct
Spies are stubs that also record some information based on how they were called. One form of this might be an email service that records how many messages it was sent.
First of all, a spy is a stub. So what we said previously about stubs also applies to spies. The added benefit of spies is that they allow to watch how the function is used. We use spies when we want to track:
- if a function has been called by the SUT,
- how many times it has been called,
- which arguments were passed.
- Spy on the collaborator's
getTeamsmethod. The default behavior of Jasmine spies is not to call the original function. This is tantamount to transforming the
getTeamsinto a method with an empty body.
- We act on the SUT by invoking the
- We check that the method
getTeamsof the collaborator have been called by the SUT.
Mocks are pre-programmed with expectations which form a specification of the calls they are expected to receive. They can throw an exception if they receive a call they don't expect and are checked during verification to ensure they got all the calls they were expecting.
Mocks are different than stubs and spies in the sense that it is the mock itself verifies that it has been used correctly by the SUT. Therefore mocks are often tightly coupled to implementation details, thus making your code harder to refactor. We will want to use mock if we want to test the interaction of our SUT with a collaborator that communicate with the outside world.
- We create a mock object by calling
sinon.mockand passing it the collaborator we want to mock. For this example, I used Sinon.JS, because we can't create mocks in Jasmine. Such a concept just doesn't exist.
- Next, we setup expectations on the mock. We are expecting that the method
getTeamswill be called exactly once in the lifespan of this test.
- Then, we exercise the SUT by invoking the
- Finally, the mock itself verifies that expectations we set on it are met. If not
mock.verify()will throw an exception and fails our test.
Most of time, you will want to use mocks when testing HTTP request. That's why Angular provides out-of-the-box a way to mock the
HttpClient with the use of the
Summary of the Differences Between Stubs, Spies and Mocks
To summarize this section, I will say that mocks and spies insist on behavior (which methods were called and how) while stubs put the emphasis on state (what is the result of calling those methods?). The difference between spies and mocks is that with mocks the verification is automatic and done by the mock itself while you have to manually verify your spies.
With that said, should we use stubs, spies or mocks ? This question has led to the dichotomy between Classicism and Mockism. Classicists use real instances when they can while mockists always use mocks. Classicism goes along with sociable testing and Mockism with solitary testing. If you want to learn more about the subject, check out Martin Fowler's article called Mocks Aren't Stubs where he delves on the opposition between the two schools of thought.
In a Nutshell
Thank you very much and see you soon.
If you enjoyed this article, follow @ahasall on Twitter for more content like this.