TDD in Angular - Dependency Injection and Mocking

In our last article here, we went through the basic structure of an Angular Unit Test and went on to test services. In this article, I want to show how to connect your service to a component and how to properly test this from a Test-Driven Development perspective.

Code for this article can be found here

Let's get started!

Creation of an inventory component

Let's say we want to create a component that takes our inventoryCount from the Inventory Service and displays it, as well as increase and decrease the count. This means that InventoryService is a dependency of InventoryComponent. In Angular, we inject dependencies through the constructor.

Therefore, we'll need to inject our InventoryService through the constructor of our InventoryComponent to have access to the methods.

I know there are better ways to update a count in a service and bind it to a component (such as using an Observable). This is just to illustrate a concept.

Whenever we bring in dependencies into components, we should always make sure that those services are tested first so that they behave as expected. Our InventoryService was tested in the previous article so it's safe for us to use it now.

The logic for this component is beyond simple but there's still a key concept of testing that it covers. We don't need to re-test the service code in this component, but we do need to make sure that it is called when needed.

Let's focus on the component test and run through what the auto-generate code means. Remember we can focus on a test suite by using fdescribe (focused describe) and focus on a single test using fit (focused it).

We see that an instance of the component is created and a fixture is set up to house the component instance. This also gives us access to component life cycle methods and a DOM that we can use during our unit tests. You can read more about fixtures here.

TestBed.createComponent(InventoryComponent) instantiates the component, which means that the constructor code is immediately executed along with all the component life cycle hooks implemented by that component. fixture.detectChanges() is responsible for any updates made to the component. It syncs any component variables bound to the DOM. On the first time that it is is run, it runs ngOnChanges() and ngOnInit() (Thanks @LayZeeDK for the correction! :heart:). You can read more about ngOnChanges and ngOnInit on the docs.

If the component has any dependencies, those are instantiated as well, meaning that their constructor functions are immediately executed. This breaks our concept of unit testing since multiple pieces of code are being brought into this one unit test suite. These dependencies need to be mocked.

Mocking Dependencies

Typically when mocking a dependency, a dummy class is provided with many of the same methods as the original. These methods do not provide functionality, but they may just return predictable values that we can use for testing purposes.

For example, you may want to mock network calls, return a known value and see if your components and services behave as they should. You may want to willingly return errors from mock services to see if your application handles errors gracefully. You can even mock Angular features such as the Router.

All this is necessary to isolate the piece of code to be tested. Otherwise, when a test fails we won't know if a dependency or the code in question caused it, which leads to many wasted hours and a poorly designed codebase.

Let's create a MockInventoryService and supply that in place of our InventoryService in the component unit test. We know that the service is already tested, so if any tests fail, the bad code has to be in our component.

Notice how our incrementCount and decrementCount are basically No-ops. Because the logic of this service is so simple, we just want to test if these functions are going to be called in our component. If the methods of the mock service are called in the unit test then it is safe to assume that the actual methods of the real service are called in the component during normal execution.

We need to tell our component unit test to replace the injected InventoryService with the MockInventoryService. This is done in the providers array in the module setup of the component test as follows:

Now, whenever incrementCount is called in the component during the unit test, the method from the mock service will be invoked instead.

Writing our tests

In order for us to tell when a method has been called on a service or not, we need to spy on that method. Jasmine can tell us when a function has been invoked, what the parameters were and what the return value was. This is useful for us to test our component.

When increment() is called in the component, we expect that incrementCount() is called in the service. Similarly, when decrement() is called in the component, we expect that decrementCount() is called in the service. Let's set up our Jasmine spies and write our tests.

We set up our spies at the very beginning of our test suite and instantiated them after we got a hold of the service from TestBed.inject.

expect(incrementSpy).toHaveBeenCalled() tests whether or not the function being spied upon was called during the test.

Jasmine Spies Tests Passing

Conclusion

In this article, we covered the following:

  • How to inject dependencies into components
  • The auto-generated unit test of a component
  • Producing a mock service
  • Providing the mock service to the component
  • Spying on functions inside that service.

Hopefully this article was useful to you. There are lots more to learn about mocking and test strategies in Angular and I aim to cover them all. Thanks a lot for reading!