Unit Testing and Mocking Angular Components and Child Components
In this article we shall be Unit Testing and Mocking Components and Child Components in Angular Application.
Today in this article, we will cover below aspects,
We shall be using angular unit-testing best practices which we learned in our last article,
Please see here GitHub link for complete example,
GitHub : https://github.com/thecodebuzz/angular-8-getting-started-ivy
We shall be using the same sample and going to test the HeroesComponent in this article,
- Open heroes.component.ts file
import { Component, OnInit } from '@angular/core';
import { Hero } from '../hero';
import { HeroService } from '../hero.service';
@Component({
selector: 'app-heroes',
templateUrl: './heroes.component.html',
styleUrls: ['./heroes.component.css']
})
export class HeroesComponent implements OnInit {
heroes: Hero[];
constructor(private heroService: HeroService) { }
ngOnInit() {
this.getHeroes();
}
getHeroes(): void {
this.heroService.getHeroes()
.subscribe(heroes => this.heroes = heroes);
}
add(name: string): void {
name = name.trim();
var strength = 11
if (!name) { return; }
this.heroService.addHero({ name, strength } as Hero)
.subscribe(hero => {
this.heroes.push(hero);
});
}
delete(hero: Hero): void {
this.heroes = this.heroes.filter(h => h !== hero);
this.heroService.deleteHero(hero).subscribe();
}
}
- This component is relatively simple, it does have one dependency, the HeroService, and it does have a few methods: OnInit, getHeroes, add, and then finally, it has a delete method.
- We’re just going to test the delete method.
- Let’s start by creating the test file, heroes.component.spec.ts.
- Paste the below code in the test file:
Mocking to Isolate Code
- In order to construct our HeroesComponent correctly, we need to pass in an object that looks like the HeroService.
- Again, if we look at the parameter list the HeroesComponent constructor, it expects a HeroService.
- This is where Jasmine comes to the rescue, by helping us create a mock object.
- We define a variable called mockHeroService, and I’m going to initialize that mockHeroService, and set it to a call to the global jasmine.createSpyObj.
- This creates a mock object that we can control.
- We can tell it what methods it has, what those methods should return when they’re called, and we can ask it what methods were called in a test.
- When we create a spy object like this we do have to pass in an array of method names.
- If the object has no methods we leave it blank, but in this case, in our HeroesComponent we are using the addHero method, the deleteHero method, and the getHeroes method.
- This call will create an object that has three methods, getHeroes, addHero, and deleteHero.
- We pass it into our HeroesComponent constructor.
- The deleteHero method returns an observable, which we subscribe to, so we need our mock object to return an observable when deleteHero is called.
- We want an observable, and the simplest way to create an observable, is to call the ‘of’ method and pass in a true.We’re going to import that from rxjs.
- Save the changes and the test passes.
Let’s write another test to check if deleteHero method of mockHeroService is called when we call the delete method of the HeroesComponent.
- Paste in the below code in heroes.component.spec.ts after the first test.
import { HeroesComponent } from "./heroes.component"; import { ConstantPool } from "@angular/compiler"; import { of } from "rxjs"; describe('HeroesComponent', ()=>{ let component : HeroesComponent; let HEROES; let mockHeroService; beforeEach(()=>{ HEROES =[ {id:1, name:'IronMan', strength:8}, {id:2, name:'Batman', strength:8}, {id:3, name:'CaptainAmerica', strength:7}, {id:4, name:'SuperMan', strength:9} ] mockHeroService= jasmine.createSpyObj(['getHeroes','addHero','deleteHero']); component = new HeroesComponent(mockHeroService); }) describe('delete',()=> { it('should remove the indicated hero from the heroes list',()=>{ //Arrange mockHeroService.deleteHero.and.returnValue(of(true)); component.heroes=HEROES; //Act component.delete(HEROES[2]); //Assert expect(component.heroes.length).toBe(3); }) }) })
it('should call deleteHero in mockHeroService with the correct parameter',()=>{
//Arrange
mockHeroService.deleteHero.and.returnValue(of(true));
component.heroes=HEROES;
//Act
component.delete(HEROES[2]);
//Assert expect(mockHeroService.deleteHero).toHaveBeenCalledWith(HEROES[2]);
})
- Save the changes & we see the below output in the browser
Run below commands on CLI,
ng test
Unit Testing and Mocking Child Components
- In the HeroesComponent, notice that we have the child component, the app-hero, we don’t want to test our live hero component in this unit test.
- Well, there is another way around this, and that is to create essentially a mock child component.
- Create a component that looks just like the HeroComponent.
- We can do that by declaring the component the same way we would declare the HeroComponent.
- Paste the below code in heroes.component.spec.ts before the beforeEach initialization:
@Component({
selector: 'app-hero',
template: '<div></div>'
})
class FakeHeroComponent {
@Input() hero: Hero;
}
- Also, add the FakeHeroComponent in declarations.
- Finally, our test suite looks like this:
import { HeroesComponent } from "./heroes.component";
import { of } from "rxjs";
import { ComponentFixture, TestBed } from "@angular/core/testing";
import { Component, Input } from "@angular/core";
import { HeroService } from "../hero.service";
import { Hero } from "../hero";
import { By } from "@angular/platform-browser";
describe('Heroes Component', () => {
let fixture: ComponentFixture<heroescomponent>;
let mockHeroService;
let HEROES;
@Component({
selector: 'app-hero',
template: '<div></div>'
})
class FakeHeroComponent {
@Input() hero: Hero;
}
beforeEach(() => {
mockHeroService = jasmine.createSpyObj(['getHeroes', 'addHero', 'deleteHero']);
HEROES = [
{ id: 1, name: 'IronMan', strength: 8 },
{ id: 2, name: 'Batman', strength: 8 },
{ id: 3, name: 'CaptainAmerica', strength: 7 },
{ id: 4, name: 'SuperMan', strength: 9 }
]
TestBed.configureTestingModule({
declarations: [HeroesComponent,
FakeHeroComponent
],
providers: [
{ provide: HeroService, useValue: mockHeroService }
]
})
fixture = TestBed.createComponent(HeroesComponent);
})
it('should set heroes correctly from the service', () => {
mockHeroService.getHeroes.and.returnValue(of(HEROES));
fixture.detectChanges();
expect(fixture.componentInstance.heroes.length).toBe(4);
})
})
- Run the test using ng test command in the terminal. The test passes.
That’s All! Happy Coding !
Please bookmark this page and share it with your friends. Please Subscribe to the blog to receive notifications on freshly published(2024) best practices and guidelines for software design and development.