Angular Testing Tips: Ng-Mocks

Bobby Galli
ITNEXT
Published in
11 min readMar 19, 2023

--

Write Better Tests Faster With Ng-Mocks and Jasmine-Auto-Spies

Angular Testing Tips (📷 bobbyg603)

Clean Your Room 🧹

Writing unit tests is a lot like cleaning your room; it can be tedious and time-consuming, but ultimately leads to a cleaner and more organized space. Similarly, unit testing can be challenging and require a significant investment of time and effort, but it ultimately leads to more reliable and better-organized code that meets the needs of users and stakeholders. Cleaning your room and writing tests both seem overwhelming at first, but breaking each task down into smaller, more manageable tasks can make it easier to tackle. Just as a clean room can help you feel more productive and focused, reliable and well-tested code can help you work more efficiently and with greater confidence.

A reference project that demonstrates some simple ng-mocks examples is available here:

Mocks 🦆

According to the CircleCI blog, “mocking means creating a fake version of an external or internal service that can stand in for the real one, helping your tests run more quickly and more reliably. When your implementation interacts with an object’s properties, rather than its function or behavior, a mock can be used”.

To write tests that are fast and reliable, it’s important to narrow the scope of the system under test. The best way to narrow the scope of the component being tested is to mock all of the component’s dependencies. Replacing child components with empty fakes prevents us from testing beyond the boundaries of our parent component. By mocking component dependencies, we can replace child components with empty templates that render faster, allowing us to iterate quickly and shorten the testing feedback loop.

In both component and service tests, mocking allows us to manufacture different scenarios and test code that would be difficult to test under normal circumstances. For instance, testing error handling code requires generating an error response from an external server. By replacing an external server with a mock implementation we can easily generate an error response and test the corresponding error-handling logic.

Getting Started 🐯

The Angular documentation provides a bit of guidance on how to create mocks in unit tests. According to the Angular docs, you can provide a mock service implementation by following one of their examples:

class MockUserService {
isLoggedIn = true;
user = { name: 'Test User'};
}

beforeEach(() => {
TestBed.configureTestingModule({
// provide the component-under-test and dependent service
providers: [
WelcomeComponent,
{ provide: UserService, useClass: MockUserService }
]
});
// inject both the component and the dependent service.
comp = TestBed.inject(WelcomeComponent);
userService = TestBed.inject(UserService);
});

Similarly, the Angular docs demonstrate how to declare implementations for mocked components:

@Component({selector: 'app-banner', template: ''})
class BannerStubComponent { }

@Component({selector: 'router-outlet', template: ''})
class RouterOutletStubComponent { }

@Component({selector: 'app-welcome', template: ''})
class WelcomeStubComponent { }

TestBed
.configureTestingModule({
imports: [RouterLink],
providers: [provideRouter([])],
declarations:
[AppComponent, BannerStubComponent, RouterOutletStubComponent, WelcomeStubComponent]
})

The mock implementations from the Angular docs work, but require a lot of boilerplate code each time you define a new mock. Fortunately, ng-mocks provides several tools that make it easier to create fake implementations.

Ng-Mocks 🐅

According to the docs, “ng-mocks is a testing library that helps with mocking services, components, directives, pipes, and modules in tests for Angular applications. When we have a noisy child component, or any other annoying dependency, ng-mocks has tools to turn these declarations into their mocks, keeping interfaces as they are, but suppressing their implementation.”

MockComponent

The easiest way to start with ng-mocks is to replace child components with their MockComponent equivalents. Let’s have a look at a snippet from an example Angular component.

<div class="main-content">
<div class="form">
<div class="breeds">
<app-form [breeds]="(breeds$ | async)!" [breed]="breed" [count]="count"
(formChange)="onFormChange($event)"></app-form>
</div>
</div>
<div class="cards">
<mat-spinner *ngIf="loading$ | async"></mat-spinner>
<app-card *ngFor="let dog of dogs$ | async" [imgSrc]="dog"></app-card>
</div>
</div>

Notice we have 3 child component dependencies app-form, mat-spinner, and app-card. If we were to define mocks manually, they would look like this.

@Component({selector: 'app-form', template: ''})
class MockAppFormComponent {
@Input breeds: Array<any>;
@Input breed: any;
@Input count: any;
@Output formChange = new EventEmitter<any>();
}

@Component({selector: 'mat-spinner', template: ''})
class MockMatProgressSpinnerComponent { }

@Component({selector: 'app-card', template: ''})
class MockAppCardComponent {
@Input imgSrc: any;
}

describe('AppComponent', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
AppComponent,
MockAppFormComponent,
MockMatProgressSpinnerComponent,
MockAppCardComponent
]
}).compileComponents();

fixture = TestBed.createComponent(AppComponent);
app = fixture.componentInstance;
});
});

We have to define all of the same inputs as the real component otherwise we’ll see several errors like Can't bind to 'count' since it isn't a known property of 'app-form'. Each time we add a new input to our real component, we also have to remember to add an input to our mock. Manually syncing inputs between our real and mocked components is a small pain that causes big productivity losses over time — yuck!

With ng-mocks, we can simplify the snippet above, reducing code, and automatically keeping our mocks in sync with our real components.

describe('AppComponent', () => { 
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [
AppComponent,
MockComponent(MatProgressSpinnerComponent),
MockComponent(FormComponent),
MockComponent(CardComponent),
]
}).compileComponents();

fixture = TestBed.createComponent(AppComponent);
app = fixture.componentInstance;
});
});

Using MockComponent we’ve completely removed the need to manage component mock inputs!

MockProvider

Mocking services allows developers to short-circuit deep, complex dependency trees and write simpler tests. When a component depends on a service that depends on Angular’s HttpClient, you can mock the service and forgo the import of HttpClientTestingModule.

Typically when we create mock services it will look something like this.

let dogService: jasmine.SpyObj<DogService>;

describe('AppComponent', () => {
const breeds = ['affenpinscher', 'african', 'airedale'];
const dogs = ['https://images.dog.ceo/breeds/affenpinscher/n02110627_10047.jpg'];
dogService = jasmine.createSpyObj('DogService', ['getBreeds', 'getDogs'];
dogService.getBreeds.and.returnValue(of(breeds));
dogService.getDogs.and.returnValue(of(dogs));

beforeEach(async () => {
await TestBed.configureTestingModule({
providers: [
{
provide: DogService,
useValue: dogService
}
]
}).compileComponents();

fixture = TestBed.createComponent(AppComponent);
app = fixture.componentInstance;
});
});

We can simplify the TestBed configuration in the example above using MockProvider.

await TestBed.configureTestingModule({
providers: [
MockProvider(DogService, dogService)
]
}).compileComponents();

There’s another trick we can employ — we can use jasmine-auto-spies to save a few keystrokes. Here’s a version of DogService created by jasmine-auto-spies.

import { createSpyFromClass, Spy } from 'jasmine-auto-spies';

let dogService: Spy<DogService>;

dogService = createSpyFromClass(DogService);
dogService.getBreeds.and.returnValue(of(breeds));
dogService.getDogs.and.returnValue(of(dogs));

It ain’t much, but it’s honest work.

MockProvider is cleaner and shorter than it’s provide/useValue counterpart, and createSpyFromClass allows us to create spied objects without needing to type out the names of each method.

MockBuilder

We can continue to improve our code cleanliness with MockBuilder. MockBuilder leverages the builder pattern and fluent syntax to chain function calls and succinctly construct an ng-mocks equivalent to TestBed. Take a look at the following example that sets up AppComponent in our companion repo via configureTestingModule.

describe('AppComponent', () => {
breeds = ['affenpinscher', 'african', 'airedale'];
dogs = ['https://images.dog.ceo/breeds/affenpinscher/n02110627_10047.jpg'];
dogService = createSpyFromClass(DogService);
dogService.getBreeds.and.returnValue(of(breeds));
dogService.getDogs.and.returnValue(of(dogs));
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
MockModule(FontAwesomeModule),
MockModule(MatProgressSpinnerModule),
MockModule(MatToolbarModule)
],
declarations: [
AppComponent,
MockComponent(FormComponent),
MockComponent(CardComponent)
],
providers: [
MockProvider(DogService, dogService)
]
}).compileComponents();
});
});

Using MockBuilder we can reduce our setup by several lines of code.

describe('AppComponent', () => {
beforeEach(async () => {
breeds = ['affenpinscher', 'african', 'airedale'];
dogs = ['https://images.dog.ceo/breeds/affenpinscher/n02110627_10047.jpg'];
dogService = createSpyFromClass(DogService);
dogService.getBreeds.and.returnValue(of(breeds));
dogService.getDogs.and.returnValue(of(dogs));
await MockBuilder(AppComponent, AppModule)
.mock(FontAwesomeModule)
.mock(MatProgressSpinnerModule)
.mock(MatToolbarModule)
.mock(CardComponent)
.mock(DialogComponent)
.mock(FormComponent)
.provide({ provide: DogService, useValue: dogService });
});
});

We chain calls to mock to replace our real modules and components with mocked equivalents. The provide function is also chain-able and lets us inject our mocked version of DogService.

MockRender

Ng-mocks provides MockRender to help test Inputs, Outputs, ChildContent, and render custom templates. Additionally, MockRender provides the find and findAll convenience methods that return appropriately-typed references to child components.

The MockComponent and MockProvider functions are used in tandem with Angular’s TestBed. MockRender is typically used in lieu of TestBed and is most useful for testing directives. Similarly to component mocks/stubs, the Angular docs suggest creating a fake component for directive testing.

@Component({
template: `
<h2 highlight="yellow">Something Yellow</h2>
<h2 highlight>The Default (Gray)</h2>
<h2>No Highlight</h2>
<input #box [highlight]="box.value" value="cyan"/>`
})
class TestComponent { }

The tests proposed by the Angular docs are quite messy.

beforeEach(() => {
fixture = TestBed.configureTestingModule({
declarations: [ HighlightDirective, TestComponent ]
})
.createComponent(TestComponent);

fixture.detectChanges(); // initial binding

// all elements with an attached HighlightDirective
des = fixture.debugElement.queryAll(By.directive(HighlightDirective));

// the h2 without the HighlightDirective
bareH2 = fixture.debugElement.query(By.css('h2:not([highlight])'));
});

// color tests
it('should have three highlighted elements', () => {
expect(des.length).toBe(3);
});

it('should color 1st <h2> background "yellow"', () => {
const bgColor = des[0].nativeElement.style.backgroundColor;
expect(bgColor).toBe('yellow');
});

it('should color 2nd <h2> background w/ default color', () => {
const dir = des[1].injector.get(HighlightDirective) as HighlightDirective;
const bgColor = des[1].nativeElement.style.backgroundColor;
expect(bgColor).toBe(dir.defaultColor);
});

it('should bind <input> background to value color', () => {
// easier to work with nativeElement
const input = des[2].nativeElement as HTMLInputElement;
expect(input.style.backgroundColor)
.withContext('initial backgroundColor')
.toBe('cyan');

input.value = 'green';

// Dispatch a DOM event so that Angular responds to the input value change.
input.dispatchEvent(new Event('input'));
fixture.detectChanges();

expect(input.style.backgroundColor)
.withContext('changed backgroundColor')
.toBe('green');
});

it('bare <h2> should not have a customProperty', () => {
expect(bareH2.properties['customProperty']).toBeUndefined();
});

The tests above are hard-coded with the indexes of different scenarios in the test component. Creating a large test component with several cases is a brittle solution because the component being tested is defined outside of the test with no type safety to protect the next developer from footguns.

A better solution for testing various directive cases is to define a fake component per test. However, the Angular component decorator is verbose, and thus a “one component, one test case” approach would require a lot of extra code.

Fortunately, ng-mocks allows developers to use MockRender to render small, one-off templates in their tests. Here’s a snippet from the ng-mocks docs that demonstrates test cases for a highlight directive.

it('uses default background color', () => {
const fixture = MockRender('<div target></div>');

expect(fixture.nativeElement.innerHTML).not.toContain(
'style="background-color: yellow;"',
);
});

it('sets provided background color', () => {
const fixture = MockRender('<div [color]="color" target></div>', {
color: 'red',
});

fixture.point.triggerEventHandler('mouseenter', null);
expect(fixture.nativeElement.innerHTML).toContain(
'style="background-color: red;"',
);
});

The implementations of the highlight directive in the Angular docs and the ng-mocks docs are slightly different. Regardless, using MockRender allows developers to build small, isolated sandboxes that are incredibly useful for testing directives.

Putting It All Together 🧩

Here’s a look at the AppComponent template from the companion repo.

<mat-toolbar color="primary">
<span class="title">
<fa-icon [icon]="faAngular" size="xl"></fa-icon>
{{ title }}
</span>
<span class="spacer"></span>
<a mat-icon-button href="https://github.com/bobbyg603/ng-testing-tips-ng-mocks" target="_blank" rel="noopener noreferrer"
aria-label="ngx-testing-tips on GitHub">
<fa-icon [icon]="faGithub"></fa-icon>
</a>
<a mat-icon-button href="https://bobbyg603.medium.com" target="_blank" rel="noopener noreferrer"
aria-label="@bobbyg603 on Medium">
<fa-icon [icon]="faMedium"></fa-icon>
</a>
<a mat-icon-button href="https://twitter.com/bobbyg603" target="_blank" rel="noopener noreferrer"
aria-label="@bobbyg603 on Twitter">
<fa-icon [icon]="faTwitter"></fa-icon>
</a>
</mat-toolbar>
<div class="main-content">
<div class="form">
<div class="breeds">
<app-form [breeds]="(breeds$ | async)!" [breed]="breed" [count]="count"
(formChange)="onFormChange($event)"></app-form>
</div>
</div>
<div class="cards">
<mat-spinner *ngIf="loading$ | async"></mat-spinner>
<app-card *ngFor="let dog of dogs$ | async" [imgSrc]="dog"></app-card>
</div>
</div>

Here’s the logic that backs the component template.

import { Component } from '@angular/core';
import { faAngular, faGithub, faMedium, faTwitter } from '@fortawesome/free-brands-svg-icons';
import { BehaviorSubject, Observable, Subject, tap } from 'rxjs';
import { DogService } from './dog/dog.service';
import { DogsForm } from './form/form.component';
import { nextTurn } from './utils/next-turn';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss']
})
export class AppComponent {
readonly title = 'ng-testing-tips';

readonly faAngular = faAngular;
readonly faGithub = faGithub;
readonly faMedium = faMedium;
readonly faTwitter = faTwitter;

breeds$: Observable<string[]>;
dogs$: Observable<string[]>;
loading$: Observable<boolean>;

breed: string;
count: number;

private loadingSubject: Subject<boolean>;

constructor(private dogService: DogService) {
this.breed = 'husky';
this.count = 3;
this.loadingSubject = new BehaviorSubject(true);
this.breeds$ = this.getBreeds();
this.dogs$ = this.getDogs();
this.loading$ = this.loadingSubject.asObservable().pipe(nextTurn());
}

onFormChange(form: DogsForm) {
const { breed, count } = form;
this.breed = breed;
this.count = count;
this.dogs$ = this.getDogs();
}

private getBreeds() {
return this.dogService.getBreeds();
}

private getDogs() {
this.loadingSubject.next(true);

return this.dogService.getDogs(this.breed, this.count)
.pipe(
tap(() => this.loadingSubject.next(false))
);
}
}

What’s important to test in the component above? A good way to answer the question of what to test is “what does the component need to do in order to function properly”. Another good question to ask is “how can we document what this component does so it can be understood by the next person who works on it”.

It seems like a good idea to ensure that the title shows up. We also have an input form that needs to be initialized with the correct defaults for breeds, breed, and count. When the input form changes, we should handle the change event and update our count. If the form changes, we need to ensure that we call our dogsService with updated breed and count values. Before calling the getDogs function we should emit values for loading$, show the loading indicator, and hide the loading indicator when the network call returns. The result of the call to getDogs should be emitted via our dogs$ observable.

By breaking our task down into smaller pieces, and leveraging ng-mocks, writing our tests is easier than it looks.

import { MatProgressSpinner, MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatToolbarModule } from '@angular/material/toolbar';
import { FontAwesomeModule } from '@fortawesome/angular-fontawesome';
import { createSpyFromClass, Spy } from 'jasmine-auto-spies';
import { MockBuilder, MockedComponentFixture, MockRender, ngMocks } from 'ng-mocks';
import { firstValueFrom, of, skip } from 'rxjs';
import { AppComponent } from './app.component';
import { AppModule } from './app.module';
import { CardComponent } from './card/card.component';
import { DialogComponent } from './dialog/dialog.component';
import { DogService } from './dog/dog.service';
import { FormComponent } from './form/form.component';

let dogService: Spy<DogService>;
let app: AppComponent;
let breeds: string[];
let dogs: string[];

let rendered: MockedComponentFixture<AppComponent>;
let cardComponents: CardComponent[];
let formComponent: FormComponent;

describe('AppComponent', () => {
beforeEach(async () => {
breeds = ['affenpinscher', 'african', 'airedale'];
dogs = ['https://images.dog.ceo/breeds/affenpinscher/n02110627_10047.jpg'];
dogService = createSpyFromClass(DogService);
dogService.getBreeds.and.returnValue(of(breeds));
dogService.getDogs.and.returnValue(of(dogs));
await MockBuilder(AppComponent, AppModule)
.mock(FontAwesomeModule)
.mock(MatProgressSpinnerModule)
.mock(MatToolbarModule)
.mock(CardComponent)
.mock(DialogComponent)
.mock(FormComponent)
.provide({ provide: DogService, useValue: dogService });
rendered = MockRender(AppComponent, null, { detectChanges: false });
app = rendered.point.componentInstance;
});

it('should create the app', () => {
expect(app).toBeTruthy();
});

it(`should have as title 'ng-testing-tips'`, () => {
expect(app.title).toEqual('ng-testing-tips');
});

describe('breeds$', () => {
it('should emit result of getBreeds from DogService', () => {
return expectAsync(firstValueFrom(app.breeds$)).toBeResolvedTo(breeds);
});
});

describe('dogs$', () => {
it('should emit result of getDogs from DogService', () => {
return expectAsync(firstValueFrom(app.dogs$)).toBeResolvedTo(dogs);
});
});

describe('loading$', () => {
it('should start with true', () => {
return expectAsync(firstValueFrom(app.loading$)).toBeResolvedTo(true);
});

it('should emit false after call to getDogs', async () => {
const resultPromise = firstValueFrom(app.loading$.pipe(skip(1)));

rendered.detectChanges();
const result = await resultPromise;

expect(result).toBe(false);
});
});

describe('onFormChange', () => {
it('should call getDogs with breed and count', () => {
const breed = 'affenpinscher';
const count = 3;

app.onFormChange({ breed, count });

expect(dogService.getDogs).toHaveBeenCalledWith(breed, count);
});
});

describe('template', () => {
beforeEach(() => {
rendered.detectChanges();
cardComponents = ngMocks.findAll(CardComponent).map(c => c.componentInstance);
formComponent = ngMocks.find<FormComponent>('app-form').componentInstance;
});

it('should render title', () => {
expect(rendered.nativeElement.querySelector('span.title')?.textContent).toMatch(app.title);
});

it('should pass breed to form', () => {
expect(formComponent.breed).toBe(app.breed);
});

it('should pass breeds to form', () => {
expect(formComponent.breeds).toBe(breeds);
});

it('should pass count to form', () => {
expect(formComponent.count).toBe(app.count);
});

it('should create card for each dog', () => {
dogs.forEach(dog => {
expect(cardComponents.find(c => c.imgSrc === dog)).toBeTruthy();
});
});

it('should show spinner when loading', () => {
rendered.componentInstance.loading$ = of(true);
rendered.detectChanges();
expect(ngMocks.find(MatProgressSpinner)).toBeTruthy();
});

it('should not show spinner when not loading', () => {
rendered.componentInstance.loading$ = of(false);
rendered.detectChanges();
expect(ngMocks.findAll(MatProgressSpinner).length).toBe(0);
});

it('should call onFormChange when form component raises formChange event', () => {
const event = { breed: 'affenpinscher', count: 3 };
const spy = spyOn(rendered.componentInstance, 'onFormChange');

formComponent.formChange.emit(event);

expect(spy).toHaveBeenCalledWith(event);
});
});
});

Take a moment to read over the test suite, you’ll notice most of the tests are straightforward and easy to write (especially with CoPilot). In beforeEach, we passed { detectChanges: false } to MockRender so that more closely mimics the way TestBed works and allows us to perform assertions against the component's class without any change detection or rendering.

The RxJS firstValueFrom function helps us to convert observables to promises, and expectAsync allows us to create tests with a single expression. We can also use firstValueFrom with skip to test subsequent emissions in loading$ for instance.

Our template tests use ngMocks.detectChanges to render our component under test. We use find and findAll to get references to various component DebugElements. By using map we can convert collections of DebugElements into collections of NativeElements, query the DOM with querySelector, and perform various expectations. We can do simple assertions against child component inputs and MockRender automatically creates EventEmitters for component outputs so we are able to test the binding between the child component's formChange event and the parent’s onFormChange handler.

Thanks for reading!

Want to Connect?

If you found the information in this tutorial useful please subscribe on Medium, follow me on Twitter, and/or subscribe to my YouTube channel.

--

--

Software developer at BugSplat. Big friendly giant, golf ball whacker guy, goofball, lover of tacos.