Opening three questions:

Why unit tests?

What are the benefits of unit testing?

How do I write Angular unit tests?

What if there were no unit tests?

Or in other words, why develop and write unit tests?

In a tight business development situation, it’s tempting to skip unit testing, go straight to it, and unlock the unforgettable memories below

What are the benefits of unit testing?

When we add unit tests after development, the underlined part will probably disappear

How do Angular unit tests work?

Angular prerequisite background

  • To build the Angular framework, the Angular-CLI command creates the test case **.spec.ts file while creating the Service, pipe, and Component
  • Running unit tests
Ng test --no-watch --code-coverage // The coverage directory is generated under the root directory, where index. HTML records component coverageCopy the code
  • To view

Write angular8 unit tests

Test service- no dependencies

Framework New instance test

The code is as follows:

@Injectable() // Inject dependency export Class ValueService {value:string; constructor() { } getValue() { return this.value} }Copy the code

For example:

Let service: ValueService; beforeEach(() => { service = new ValueService(); }); it('#getValue should return real value', () => { expect(service.getValue()).toBe('real value'); }); Let service: ValueService; let service: ValueService; beforeEach(() => { TestBed.configureTestingModule({ providers: [ValueService] }); UseClass}); it('should use ValueService', () => { service = TestBed.get(ValueService); expect(service.getValue()).toBe('real value'); });Copy the code

Test service – has dependencies

Using spyOn mock

The code is as follows:

@Injectable() export class MasterService { constructor(private valueService: ValueService) { } getValue() { return this.valueService.getValue(); }}Copy the code

Obtaining a real dependent service is often difficult to create because of dependencies in the service. At this point, spy, which skips the real service business logic and tests it separately, is the easiest way. ** does not skip dependencies, which is integration testing. **

The tests are as follows:

let masterService: MasterService; let valueServiceSpy: jasmine.SpyObj<ValueService>; beforeEach(() => { const spy = jasmine.createSpyObj('ValueService', ['getValue']); // Pay attention to the location, In beforeEach TestBed. ConfigureTestingModule ({/ / dojo.provide to both the service - to - test and its (spy) dependency will: {provide: ValueService, useValue: spy}]}); // Inject both the service-to-test and its (spy) dependency masterService = TestBed.get(MasterService); valueServiceSpy = TestBed.get(ValueService); });Copy the code
it('#getValue should return stubbed value from a spy', () => { const stubValue = 'stub value'; # mock return value valueServiceSpy. GetValue. And. ReturnValue (stubValue); expect(masterService.getValue()) .toBe(stubValue, 'service returned stub value'); // Use mock dependencies to return values for expected business logic});Copy the code

Test component – No dependencies

The code is as follows:

@Component({ selector: 'lightswitch-comp', template: ` <button (click)="clicked()">Click me! </button> <span>{{message}}</span>` }) export class LightswitchComponent { isOn = false; clicked() { this.isOn = ! this.isOn; } get message() { return `The light is ${this.isOn ? 'On' : 'Off'}`; }}Copy the code

The test code is as follows:

// New it('#clicked() should set #message to "is on"', () => {const comp = new LightswitchComponent(); expect(comp.message).toMatch(/is off/i, 'off at first'); comp.clicked(); expect(comp.message).toMatch(/is on/i, 'on after clicked'); }); / / or obtaining component instance, to the framework to create new let comp: LightswitchComponent; beforeEach(() => { TestBed.configureTestingModule({ // provide the component-under-test and dependent service providers:  [ LightswitchComponent, ] }); // inject both the component and the dependent service. comp = TestBed.get(LightswitchComponent); }); it('#clicked() should set #message to "is on"', () => { expect(comp.message).toMatch(/is off/i, 'off at first'); comp.clicked(); expect(comp.message).toMatch(/is on/i, 'on after clicked'); });Copy the code

Test components – input, output

export class DashboardHeroComponent { @Input() hero: Hero; @Output() selected = new EventEmitter<Hero>(); click() { this.selected.emit(this.hero); }}Copy the code

The test code is as follows:

let comp:DashboardHeroComponent; beforeEach(() => { TestBed.configureTestingModule({ // provide the component-under-test and dependent service providers:  [ DashboardHeroComponent, ] }); // inject both the component and the dependent service. comp = TestBed.get(DashboardHeroComponent); }); it('raises the selected event when clicked', () => { const hero: Hero = { id: 42, name: 'Test' }; comp.hero = hero; comp.selected.subscribe((selectedHero: Hero) => expect(selectedHero).toBe(hero)); comp.click(); });Copy the code

Test component – has dependencies

WelcomeComponent relies on UserService

export class WelcomeComponent implements OnInit { welcome: string; constructor(private userService: UserService) { } ngOnInit(): void { this.welcome = this.userService.isLoggedIn ? 'Welcome, ' + this.userService.user.name : 'Please log in.'; }}Copy the code

The test code

# spect.ts 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 } // {provide: UserService, useVale: UserServiceSpy} # both can be used, but in different ways]}); // inject both the component and the dependent service. comp = TestBed.get(WelcomeComponent); // Easy to remember and not too verbose. However, it only works if Angular injects a component with a service instance into the component in the root injector of the test. userService = TestBed.get(UserService); //userService = fixture.debugElement.injector.get(UserService); });Copy the code
it('should not have welcome message after construction', () => {
	expect(comp.welcome).toBeUndefined();
});
it('should welcome logged in user after Angular calls ngOnInit', () => {
	comp.ngOnInit();
	expect(comp.welcome).toContain(userService.user.name);
});
it('should ask user to log in if not logged in after ngOnInit', () => {
	userService.isLoggedIn = false;
	comp.ngOnInit();
	expect(comp.welcome).not.toContain(userService.user.name);
	expect(comp.welcome).toContain('log in');
});
Copy the code

Test the DOM element in the component

Component creation tests

import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { BannerComponent } from './banner.component'; describe('BannerComponent', () => { let component: BannerComponent; let fixture: ComponentFixture<BannerComponent>; beforeEach(async(() => { TestBed.configureTestingModule({ declarations: [ BannerComponent ] }) .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(BannerComponent); component = fixture.componentInstance; fixture.detectChanges(); }); It ('should create', () => {expect(component).tobedefined (); }); });Copy the code

Page element fixation

it('should contain "banner works!" ', () => { const bannerElement: HTMLElement = fixture.nativeElement; expect(bannerElement.textContent).toContain('banner works! '); }); it('should have <p> with "banner works!" ', () => { const bannerElement: HTMLElement = fixture.nativeElement; const p = bannerElement.querySelector('p'); expect(p.textContent).toEqual('banner works! '); }); it('should find the <p> with fixture.debugElement.nativeElement)', () => { const bannerDe: DebugElement = fixture.debugElement; const bannerEl: HTMLElement = bannerDe.nativeElement; const p = bannerEl.querySelector('p'); expect(p.textContent).toEqual('banner works! '); });Copy the code

If querySelector isn’t available,

import { By } from '@angular/platform-browser'; it('should find the <p> with fixture.debugElement.query(By.css)', () => { const bannerDe: DebugElement = fixture.debugElement; const paragraphDe = bannerDe.query(By.css('p')); const p: HTMLElement = paragraphDe.nativeElement; expect(p.textContent).toEqual('banner works! '); });Copy the code

Page elements are dynamically modified

Page element dynamic modification, test

it('should display a different test title', () => { component.title = 'Test Title'; fixture.detectChanges(); // Expect (h1.textContent).tocontain ('Test Title'); });Copy the code

In addition to the above display declaration, detectChanges, you can also do this using automatic detection

import { ComponentFixtureAutoDetect } from '@angular/core/testing';
TestBed.configureTestingModule({
  declarations: [ BannerComponent ],
  providers: [
    { provide: ComponentFixtureAutoDetect, useValue: true }
  ]
});
Copy the code

Render2 style test

import {Type ,Render2 } from 'angular/core'; let renderer2: Renderer2; . beforeEach(async( () => { TestBed.configureTestingModule({ ... providers: [Renderer2] }).compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(BannerComponent); renderer2 = fixture.componentRef.injector.get<Renderer2>(Renderer2 as Type<Renderer2>); // and spy on it spyOn(renderer2, 'addClass').and.callThrough(); // or replace // spyOn(renderer2, 'addClass').and.callFake(..) ; // etc }); it('should call renderer', () => { expect(renderer2.addClass).toHaveBeenCalledWith(jasmine.any(Object), 'css-class'); });Copy the code

Observables test

The following code

getQuote() { this.errorMessage = ''; this.quote = this.twainService.getQuote().pipe( startWith('... '), catchError( (err: any) => { // Wait a turn because errorMessage already set once this turn () => this.errorMessage = err.message || err.toString() return of('... '); // reset message to placeholder }) );Copy the code

Return to normal

beforeEach(() => { testQuote = 'Test Quote'; const twainServiceSpy = jasmine.createSpyObj('TwainService', ['getQuote']); getQuoteSpy = twainServiceSpy.getQuote.and.returnValue( of(testQuote) ); / / the key in the TestBed. ConfigureTestingModule ({declarations: [TwainComponent], will: [{dojo.provide: TwainService, useValue: twainServiceSpy } ] }); fixture = TestBed.createComponent(TwainComponent); component = fixture.componentInstance; quoteEl = fixture.nativeElement.querySelector('.twain'); }); it('should show quote after component initialized', () => { fixture.detectChanges(); // onInit() expect(quoteEl.textContent).toBe(testQuote); expect(getQuoteSpy.calls.any()).toBe(true, 'getQuote called'); });Copy the code

Abnormal returns

beforeEach(() => { const twainService = jasmine.createSpyObj('TwainService', ['getQuote']); getQuoteSpy = twainService.getQuote.and.returnValue( throwError('ops') ); / / the key in the TestBed. ConfigureTestingModule ({declarations: [TwainComponent], will: [{dojo.provide: TwainService, useValue: twainService } ] }); fixture = TestBed.createComponent(TwainComponent); component = fixture.componentInstance; quoteEl = fixture.nativeElement.querySelector('.twain'); }); it('should show quote after component initialized', () => { fixture.detectChanges(); // onInit() expect(errorMessage()).toMatch(/test failure/, 'should display error'); expect(quoteEl.textContent).toBe('... ', 'should show placeholder'); });Copy the code

An exception is returned, but handled asynchronously

getQuote() { this.errorMessage = ''; this.quote = this.twainService.getQuote().pipe( startWith('... '), catchError( (err: any) => { setTimeout(() => this.errorMessage = err.message || err.toString()); return of('... '); })); beforeEach(() => { const twainService = jasmine.createSpyObj('TwainService', ['getQuote']); getQuoteSpy = twainService.getQuote.and.returnValue( throwError('ops') ); / / the key in the TestBed. ConfigureTestingModule ({declarations: [TwainComponent], will: [{dojo.provide: TwainService, useValue: twainService } ] }); fixture = TestBed.createComponent(TwainComponent); component = fixture.componentInstance; quoteEl = fixture.nativeElement.querySelector('.twain'); }); it('should display error when TwainService fails', FakeAsync (() = > {/ / fakeAsync shall not apply with ajax getQuoteSpy. And the returnValue (throwError (' TwainService test failure ')); fixture.detectChanges(); // onInit() tick(); // flush the component's setTimeout() fixture.detectChanges(); // update errorMessage within setTimeout() expect(errorMessage()).toMatch(/test failure/, 'should display error'); expect(quoteEl.textContent).toBe('... ', 'should show placeholder'); }));Copy the code

Asynchronous code testing

Using fakeAsync

it('should get Date diff correctly in fakeAsync', fakeAsync(() => {
    const start = Date.now();
    tick(100);
    const end = Date.now();
    expect(end - start).toBe(100);
 }));
 
Copy the code

FakeAsync supports the following asynchronous tasks:

  • setTimeout
  • setInterval
  • requestAnimationFrame
  • webkitRequestAnimationFrame
  • mozRequestAnimationFrame
  • Rxjs-delay and interval

Ajax request testing

it('should show quote after getQuote (async)', async(() => { fixture.detectChanges(); // ngOnInit() expect(quoteEl.textContent).toBe('... ', 'should show placeholder'); fixture.whenStable().then(() => { // wait for async getQuote fixture.detectChanges(); // update view with quote expect(quoteEl.textContent).toBe(testQuote); expect(errorMessage()).toBeNull('should not show error'); }); }));Copy the code

jasmine done

it('should show quote after getQuote (spy done)', (done: DoneFn) => {
  fixture.detectChanges();

  // the spy's most recent call returns the observable with the test quote
  getQuoteSpy.calls.mostRecent().returnValue.subscribe(() => {
    fixture.detectChanges(); // update view with quote
    expect(quoteEl.textContent).toBe(testQuote);
    expect(errorMessage()).toBeNull('should not show error');
    done();
  });
});
Copy the code

Component nested tests

Service dependency error

TypeError: ctor is not a constructor
Copy the code

Cause: Incorrect configuration in provide

Providers: [{provide: OrderService, useClass: new OrderServiceMock()}]Copy the code
// Providers: [{provide: OrderService, useValue: new OrderServiceMock()}]Copy the code

The HTTP service test

Similar to service tests, use Spy

Using HttpTestingController

configuration

  let service: BlogPostsService;
  let backend: HttpTestingController;
  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [BlogPostsService],
      imports: [
        HttpClientTestingModule
      ]
    });
  });
  beforeEach(() => {
    service = TestBed.get(BlogPostsService);
    backend = TestBed.get(HttpTestingController);
  });
Copy the code

ExpectOne determine the url

it('should expectOne url', () => {
  service.getAll().subscribe();
  backend.expectOne(`https://rails-rest.herokuapp.com/posts`);
  backend.verify();
});
Copy the code

Method to determine

it('should expectOne url and method', () => {
  service.getAll().subscribe();
  backend.expectOne({url: `https://rails-rest.herokuapp.com/posts`});
  service.getAll().subscribe();
  backend.expectOne({url: `https://rails-rest.herokuapp.com/posts`, method: 'GET'});
  backend.verify();
});
Copy the code

None to determine

 it('should not expect one when not subscribed', () => {
    service.getAll()// .subscribe();
    backend.expectNone(`https://rails-rest.herokuapp.com/posts`);
    backend.verify();
  });
Copy the code

Match regular decision

it('should match two requests', () => { service.getAll().subscribe(); service.get(1).subscribe(); Const calls = backend.match((request) => {return request.url.match(/posts/) && # url matches request.method === 'GET'; }); expect(calls.length).toEqual(2); expect(calls[0].request.url).toEqual(`https://rails-rest.herokuapp.com/posts`); expect(calls[1].request.url).toEqual(`https://rails-rest.herokuapp.com/posts/1.json`); backend.verify(); });Copy the code

Match different urls

it('should match different requests', () => { service.getAll().subscribe(); service.get(1).subscribe(); const otherCalls = backend.match((request) => { return request.url == `https://rails-rest.herokuapp.com/posts/1.json` &&  request.method === 'GET'; }); const calls = backend.match((request) => { return request.url == `https://rails-rest.herokuapp.com/posts` && request.method === 'GET'; }); expect(calls.length).toEqual(1); expect(otherCalls.length).toEqual(1); expect(calls[0].request.url).toEqual(`https://rails-rest.herokuapp.com/posts`); expect(otherCalls[0].request.url).toEqual(`https://rails-rest.herokuapp.com/posts/1.json`); backend.verify(); });Copy the code

The match determined urlWithParams

it('should have url and urlWithParams', () => {
      service.getAll({page: 1}).subscribe();
      const calls = backend.match((request) => {
        return request.url == `https://rails-rest.herokuapp.com/posts` &&
               request.urlWithParams == `https://rails-rest.herokuapp.com/posts?page=1` &&
               request.method === 'GET';
      });
      backend.expectNone(`https://rails-rest.herokuapp.com/posts`); // If url with params, use `.match`
      backend.verify();
    });
Copy the code

Match Other request parameters

it('should have a few more attributes on request that are useful', () => {
    service.getAll({page: 1}).subscribe();
    const calls = backend.match((request: HttpRequest<any>) => {
      return request.url == `https://rails-rest.herokuapp.com/posts` &&
             request.urlWithParams == `https://rails-rest.herokuapp.com/posts?page=1` &&
             request.method === 'GET' &&
             request.params.get('page') == '1' &&
             request.body == null &&
             request.headers instanceof HttpHeaders &&
             request.responseType == 'json' &&
             request.withCredentials == false;
    });
    backend.expectNone(`https://rails-rest.herokuapp.com/posts`); // If url with params, use `.match`
    backend.verify();
  });
Copy the code

Subscribe result validation

it('should create post', () => { service.save({ title: 'Creating a post', content: 'Another long description... ' }).subscribe((response) => { expect(response).toEqual(jasmine.objectContaining({ id: 2, title: 'Creating a POST ', Content: jasmine. Any (String), creATED_at: new Date(' 2017-12-07T04:39:49.447z '), updated_at: jasmine.any(Date) })); }); const response = { 'id': 2, 'title': 'Creating a post', 'content': 'Another long description... ', 'created_at' : 'the 2017-12-07 T04:39:49. 447 z', 'updated_at' : 'the 2017-12-07 T04:39:49. 447 z'}; const call = backend.expectOne(`https://rails-rest.herokuapp.com/posts`); expect(call.request.method).toEqual('POST'); call.flush(response); Backend.verify (); });Copy the code

Personal insight

  • Test cases should be written to simplify the test object logic as much as possible, divided and measured,
  • Avoid one call and nail down all the tests, which falls under the category of integration testing
  • When writing code, you need to consciously split the code to facilitate unit testing, not a method of a large screen can not see low

More recommended

Angular develops an improved vscode plug-in

RXJS operator practices angular development needs to know

Angular8 Daily Development Guide to Avoiding pitfalls

reference

The window variable

Angular

D3 test

HttpTestingContrller

Author: Front-end ramble

Contact email: [email protected]

Copyright Notice: This article is licensed under a CC BY-NC-SA 4.0 license unless otherwise stated. Reprint please indicate the source!