DEV Community

Bearded JavaScripter
Bearded JavaScripter

Posted on • Edited on

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! ❤️). 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!

Top comments (17)

Collapse
 
stealthmusic profile image
Jan Wedel • Edited

I am struggling with TDD in Angular ever since I have started and I was really hoping to find some answers here.

In the backend (Java), TDD just comes naturally.

But in the UI, there is more than services and component logic. There is layouting and a mixture of those. So, I often ask myself where do I start to write tests?

I am just developing a complex master/detail component and I was fiddling with choosing the right display material components, wiring data bindings etc until I wrote the first test.

This actually made me feel really uncomfortable, actually kind of dirty.

I would love to start with Cypress tests, then probably layouting, then Unit tests and then implement logic but just doesn’t work. I tried.

I am wondering how you Or others do it?

One last bit: Using TestBed integration tests slows down the test speed significantly (from 20md each test to 3-4seconds) for a complex component. That is inacceptable. So we refactored all jasmine tests to be pure unit tests of components and to all DOM testing in Cypress.

Collapse
 
qarunqb profile image
Bearded JavaScripter

Bear in mind, I have a lot more articles coming up that definitely touch and explain more on the topics in this thread. It's just a matter of getting them out but I'm really glad to see that all these points come up so I know what to focus on in my articles and what to prioritize.

Really grateful for all your comments :D ❤️

Collapse
 
stealthmusic profile image
Jan Wedel

Looking forward to that!

Collapse
 
qarunqb profile image
Bearded JavaScripter

As per the Angular Style Guide, we try our best to keep the application logic in services and anything to do with UI in components. So you can test the majority of your application logic through testing services and, instead of accessing the DOM from jasmine/jest, you can leave that to cypress.

Collapse
 
stealthmusic profile image
Jan Wedel

Yeah, mostly yes. But you’d still have mappings from backend to component models, validation logic, event emitters and stuff you want to test.

But my question was more like: Can you build a whole angular application test-driven? Does it even make sense?

It just feels bad not to TDD everything to me but it doesn’t work when ever I try.

Thread Thread
 
qarunqb profile image
Bearded JavaScripter

When you say mappings from backend to component models, validation logic, event emitters, what do you mean?

Do you mean testing http requests, validation of data submitted to backend routes and testing Observables, events received from the backend?

Angular allows you to test those things using an HttpInterceptor, so you can create a mock backend. In terms of unit testing, you'd want to isolate your front end from your back end. You can connect them when it's time for an e2e tests, most of my articles cover Unit Testing for now, e2e is definitely something I have to get into more.

If your back end is already built, you can streamline your mock backend to mimic how data is supposed to be returned, throw in a few errors just to see how your front end handles it.

I could be misunderstanding what you're referring to though.

Thread Thread
 
stealthmusic profile image
Jan Wedel

I mean things like:

  • mapping a backend model (which is returned by an angular service) to a component model that contains all and only the information in appropriate data format that it can be used in templates.
  • Form validation logic In complex non-html-formS

- Cross-component event that need to be handled correctly

Thread Thread
 
qarunqb profile image
Bearded JavaScripter

I think I'm getting confused here. When you say backend, do you mean your services or your actual back end server? You can share interfaces and type formats throughout your entire application. If your server is also in TypeScript, you can share your types and interfaces with both front end and back end. If not then you'll need to manually create your interface types within your application.

Angular Http calls from a service can return the data model and format required by the calling code

Very simplified example below

interface User {
   name: string;
   id: number;
   age: number;
}

this.http.get<User[]>('/api/users')

In this way, the data returned by your back end should be of that User format. TypeScript doesn't strictly check that, but the type annotations are there for your IDE. If there are fields that absolutely must be there, then you might wanna write functions to make sure that those fields are there, which is also a good form of unit testing as well

With respect to the second point, I'm not sure what you mean, but ReactiveForms in Angular are made with TDD first in mind since they are created in the TypeScript before the HTML is initialized. Can you tell me more about non-html-forms?

With respect to testing events between components directly related, check out this article here.

For components that are connected via a service, your unit tests should only test the events (RxJS or otherwise) in that service only. Bringing in other components to test the events from a service isn't unit testing anymore, it's more integration testing.

I'm really sorry if I'm not answering any questions you might have :/

Thread Thread
 
qarunqb profile image
Bearded JavaScripter • Edited

Also, with respect to one of your previous questions, yes it is possible to build an entire Angular app using TDD, it helps to promote a nice structure in your application.

However, aiming for 100% test coverage is difficult for pretty much any codebase, so I would personally advise testing Business Logic and core requirements first and then the rest of the tests can come after.

Keep in mind that some business logic could be UI related as well, so that's where Cypress e2e tests would come in, not necessary unit tests

Thread Thread
 
stealthmusic profile image
Jan Wedel

When you say backend, do you mean your services or your actual back end server?

By backend, I mean the backend server. By backend model, I mean a typescript class/interface that maps to the data model from the backend.

By component model, I mean a data structure that holds all the state/fields of the component which is data binded from the parent component and represents the UI better.

So in your example, User would be a backend model. We don't have a backend-for-frontend architecture but multiple backends. For example, one businiess entitiy that references a user by ID. Then we need to load the appropriate user as well to display it name in the ui.

The frontend model e.g. contains selected chip options for a autocompletable chip list rather that an array of User entites etc.

With respect to the second point, I'm not sure what you mean, but ReactiveForms in Angular are made with TDD first in mind since they are created in the TypeScript before the HTML is initialized.

I know about reactive forms but haven't tried them. Maybe I should have a look on them. We use template driven forms but we don't use the HTML submit. Instead, we call the backend with a POST/PUT when the form is "saved". Then, we need some cross-field validation logic that also can/should be unit tested to cover all the edge cases.

I hope that clears my comment up a bit.

However, aiming for 100% test coverage is difficult for pretty much any codebase, so I would personally advise testing Business Logic and core requirements first and then the rest of the tests can come after.

Noone should pointlessly aim for 100% coverage. Why I really meant is writing tests before writing any line of component code, including template and code.

When I tried that, I really didn't know how to start. I first needed to play around with material components, learn how they work, what there interfaces are... So I couldn't really write a Cypress test before because I din't know what HTML element I should select for expectations...

Thread Thread
 
qarunqb profile image
Bearded JavaScripter

Thanks a lot for the clarifications.

With respect to the forms, it's really up to the developers on how they wanna handle the validation. Once you decide that, the unit tests will follow where your validation is.

So for example:

<form action = '/create-user' method = 'POST'>
...
</form>

There really isn't much unit testing that could be done on the front end in terms of validation. An Angular HttpInterceptor can technically still catch the request before it goes off to the server, so it can grab the body and perform validation checks there, but you can also do the validation checks on the server and return the appropriate response based on the form value.

With respect to a ReactiveForms declaration inside a component as follows:

form: FormGroup;

constructor(private fb: FormBuilder) {
  this.form = this.fb.group({
    name: this.fb.control('', Validators.required),
    email: this.fb.control('', [Validators.required, Validators.email]) 
  });
}

This form can be unit tested and validated entirely on the client side to ease Validation on the server side. You can find many of the Validators here. And you can even stitch together custom validators to consider weird cases as well. Angular also comes with Sanitization for user input as well, though I have to do some more research on it.

Thread Thread
 
layzee profile image
Lars Gyrup Brink Nielsen • Edited

@stealthmusic

When I tried that, I really didn't know how to start. I first needed to play around with material components, learn how they work, what there interfaces are... So I couldn't really write a Cypress test before because I din't know what HTML element I should select for expectations...

This is one of the issues that component harnesses address. You'd have to implement a HarnessEnvironment for Cypress though.

mapping a backend model (which is returned by an angular service) to a component model that contains all and only the information in appropriate data format that it can be used in templates.

There are many ways to approach this. I prefer creating input properties with data types that are the most convenient for presentational components and then doing mapping either in container components or services used by container components.

We can then easily test the mapping in isolated unit tests and there's less business logic in our presentational components.

Collapse
 
layzee profile image
Lars Gyrup Brink Nielsen • Edited

The first call to ComponentFixture#detectChanges runs ngOnChanges and ngOnInit.

Collapse
 
qarunqb profile image
Bearded JavaScripter

Thank you for the correction! Will edit this now

Collapse
 
bodote profile image
bodote

I think MockInventoryService is not necessary here. You can make a spy without it.
Your tests would do just fine, if you use the real service, but using the spyOn()'s .and.returnValue() method, or any other of spyOn()'s methods, to make the spy do what you need for your test.
That would save you a lot of lines of code.

Collapse
 
bravemaster619 profile image
bravemaster619

Angular TDD is really annoying when you have to inject every dependency to TestBed for each component.

Collapse
 
qarunqb profile image
Bearded JavaScripter

Yep it can get annoying at times. At that point, there's usually a compromise to be made between the number of dependencies for a component and what the conponent can do in terms of features.

There are articles that say to keep the dependencies per component less that 3, but sometimes it's inevitable