Developer’s Guide to Unit Testing in Angular — Part 3 (Resolving Dependencies)

Gurseerat Kaur
KhojChakra
Published in
6 min readSep 14, 2020

--

Explore how to write test cases for components and services with dependencies.

The whole idea of unit testing is to test each piece of code in an isolated system. But what if that piece of code is dependant on any service or component. What do we do then? Let’s dive into this question in this article. Points covered here are:

  1. Component’s dependency on another component
  2. Service’s dependency in a component or on another service
  3. Windows Property

If you are new to unit testing and want to learn how to set up your unit testing environment and write test cases for components, read our first guide here: Developer’s Guide to Unit Testing in Angular — Part 1.

If you want to learn how to write test cases for services, pipes, and directives, read our second guide here: Developer’s Guide to Unit Testing in Angular — Part 2 (Services, Pipes, Directives).

Component’s Dependency on another Component

In Angular, a component can have a dependency on a child component. Let’s create a parent component, that calls a child component inside it with a condition.

parent.component.html

<app-child *ngIf="showChild"></app-child>

parent.component.ts

export class ParentComponent implements OnInit {  @ViewChild(ChildComponent, {static: false}) childComponent: ChildComponent;  showChild: boolean;  constructor() { }ngOnInit() {
this.childComponent.getData();
}
}

Now, when we run the command ng test in the terminal, Karma will open the browser window and start running the test cases. But, we’ll get the error like in the image below.

Here, as you can see, we’re getting the error ‘app-child’ is not a known element. To make this element known, we can’t call the actual child component because this beats the whole purpose of “isolated system”.

Instead, we’ll create a mock component in our spec.ts file or we can create a mock component file and then import it inside the spec.ts file.

parent.component.spec.ts

import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ParentComponent } from './parent.component';
import { ChildComponent } from '../child/child.component';
import { Component } from '@angular/core';
import { By } from '@angular/platform-browser';
@Component({
selector: 'app-child',
template: '<p>Mock Child Component</p>',
providers: [{
provide: ChildComponent,
useClass: MockChildComponent
}]
})
class MockChildComponent {
getData() { }
}
describe('ParentComponent', () => {
let component: ParentComponent;
let fixture: ComponentFixture<ParentComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
ParentComponent,
MockChildComponent
]
}).compileComponents();
fixture = TestBed.createComponent(ParentComponent);
component = fixture.componentInstance;
component.childComponent = <ChildComponent>TestBed.createComponent(MockChildComponent).componentInstance;
fixture.detectChanges();
}));
});

Points to note here:

  1. We’ve created a mock child component here with the same selector name as the original child component. This will resolve the ‘app-child’ unknown element error.
  2. In the Mock child component definition, we’ll add providers to inject our dependency for the Child component. In useClass, we can define the class to be used instead of child component class. This is mostly added for assertions where we check the functions of the child component.
  3. We’ve added the getData function in the mock child component as we’ll be testing it later on. There is no need to add the complete function as the return value of the function is tested in child.component.spec.ts (remember the isolated system of unit testing).
  4. Don’t forget to add the mock component in the Testbed declarations.
  5. We’ve defined component.childComponent as an instance of the Mock child component which is of type ChildComponent. Adding the type for the instance as ChildComponent is important over here or we’ll run into an error ‘Property ‘ngOnInit’ is missing in type ‘MockChildComponent’ but required in type ‘ChildComponent’’.

Let’s add some assertions now:

it('should check that if showChild in component is false, child component is not displayed', () => {
component.showChild = false;
fixture.detectChanges();
const mockComponentEl = fixture.debugElement.query(By.directive(MockChildComponent));
expect(mockComponentEl).toBeNull();
});
it('should check that if showChild in component is true, child component is displayed', () => {
component.showChild = true;
fixture.detectChanges();
const mockComponentEl = fixture.debugElement.query(By.directive(MockChildComponent));
expect(mockComponentEl).toBeTruthy();
});
it('should check getData function of child component is called', () => {
component.showChild = true;
fixture.detectChanges();
spyOn(component.childComponent, 'getData');
component.ngOnInit();
expect(component.childComponent.getData).toHaveBeenCalled();
});

Since we’ve added *ngIf on the child component, we’ll assert that when showChild is false, the mock child component is null.

We’ll use spyOn() function of Jasmine library to check whether the getData() function is triggered when we call ngOnInit() of the parent component. We can also use toHaveBeenCalledTimes(1).

Note: Assign value true to showChild before using the spyOn(), otherwise the childComponent will be null as we’ve used *ngIf.

Service’s dependency in a component or on another Service

Services are mostly used for HTTP requests and are then imported in a component to fetch the result. Many time developers create core services which are later used in other services as well. The setup for these services or components are almost similar and thus we’ll look into resolving service dependency in a component here.

users.component.ts

ngOnInit() {
this.userService.getUsers().subscribe(data => {
this.users = data;
});
}

user.component.spec.ts

import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing';import { UserComponent } from './user.component';
import { of, Observable } from 'rxjs';
import { UsersService } from '../users.service';
class mockUserService {
getUsers() : Observable<any> {
const mockData = { "name": "Gurseerat" }
return of(mockData);
}
}
describe('UserComponent', () => {
let component: UserComponent;
let fixture: ComponentFixture<UserComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ UserComponent ],
providers: [{
provide: UsersService,
useClass: mockUserService
}]
}).compileComponents();
fixture = TestBed.createComponent(UserComponent);
component = fixture.componentInstance;
fixture.detectChanges();
}));
it('Service injected via inject(...) and TestBed.get(...) should be the same instance', inject([UsersService], (service: UsersService) => {
service.getUsers().subscribe(data => {
expect(component.users).toEqual(data);
})
}));
});

Similar to what we’ve done in resolving dependency in components for child component, we’ve created a mock user service class here which is then added in the TestBed configurations to be injected when user service is called.

We’re using the inject() function here for dependency injection of the user service. Alternately, we can also create a TestBed for the service and use it the same way without using the inject() function.

Note: The name and type of functions in the mock services should be the same as the original service.

let userService: UserService;beforeEach(() => {
userService = TestBed.get(UserService);
});

Windows Property

A window property is any data assigned to a window. E.g

window['name'] = 'Gurseerat';

If we are using windows properties in the application, we would want to test it too. In this case, we’ll mock our windows data. To do this, we’ll first define windows in app.module.ts, so that it is available globally in the application.

app.module.ts

@NgModule({
declarations: [ ... ],
imports: [ ... ],
providers: [{
provide: 'Window',
useValue: window
}],
})
export class AppModule {
}

Next, add it in the constructor of the component and/or service, where the windows property is being used.

app.component.ts

import { Inject } from '@angular/core';constructor( @Inject('Window') private window: Window )

Now, we’ll create a mock window object in our spec.ts file and use it instead of the actual window property so that our test cases are isolated.

app.component.spec.ts

let mockWindow: Window = <any>{
name: "Gurseerat"
}
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AppComponent ],
providers: [{
provide: 'Window',
useFactory: (() => {
return mockWindow;
})
}]
}).compileComponents();
}));
it('Windows data to be equal to mock window object', () => {
expect(window['name']).toBe(mockWindow.name)
});

The useFactory field tells the application that the provider is a factory function whose implementation is Window. Once we’ve created the mock for the windows data, we can add our assertions.

This brings us to the end of our series for unit testing in Angular. I’ve tried to cover many important topics here. In case you have any queries, drop a comment. Additionally, you can also find me on LinkedIn.

Happy Coding! 😃

References

  1. https://angular.io/api
  2. https://codecraft.tv/courses/angular/unit-testing/dependency-injection/

--

--

Gurseerat Kaur
KhojChakra

Front End Developer | Part time coder, part time learner | Chicken Biryani fan