Angular

Testing Recipes

  slides.com/gerardsans | @gerardsans

Google Developer Expert

Master of Ceremonies

Blogger

International Speaker

Angular Trainer (v4+)

Community Leader

850

1K

Angular In Flip Flops

Introduction

Unit Tests

Assertion Libraries

Spies, Stubs

Test

Automation

Browsers

Coverage Reports

e2e Tests

Test

Runner

Testing Architecture

WebDriverJS

Selenium

Protractor

Overview

  • Does this method work?

 

  • Does this feature work?

 

  • Does this product work?

Unit tests

e2e Tests

Acceptance Tests

Angular 2 Rapid Development

  • app.component.ts
  • app.component.spec.ts
  • app.e2e.ts

Filename conventions

$ npm run tests 
$ npm run e2e
        

Tools Online

Mocks vs Stubs

Mocks

  • Used to replace Complex Objects/APIs
  • Examples:
    • MockBackend
    • MockEventEmitter
    • MockLocationStrategy

Stubs

  • Used to cherry pick calls and change their behaviour for a single test
  • When to use:
    • control behaviour to favour/avoid certain path

Jasmine

Main Concepts

  • Suites  ​ describe('', function)
  • Specs  it('', function)
  • Expectations and Matchers
    • expect(x).toBe(expected)
    • expect(x).toEqual(expected)

Basic Test

let calculator = { 
  add: (a, b) => a + b 
};

describe('Calculator', () => {  
  it('should add two numbers', () => {
    expect(calculator.add(1,1)).toBe(2);
  })  
})

Setup and teardown

  • beforeAll (once)
    • beforeEach (many)
    • afterEach (many)
  • afterAll (once)

Useful techniques

  • Nesting suites and using scopes
  • Utility APIs
    • fail(msg), pending(msg)
  • Disable
    • xdescribe, xit
  • Focused
    • fdescribe, fit

Jasmine Spies

Test double functions that record calls, arguments and return values

Tracking Calls

describe('Spies', () => {
  let calculator = { add: (a,b) => a+b };
  
  it('should track calls but NOT call through', () => {
    spyOn(calculator, 'add'); 
    let result = calculator.add(1,1);
    expect(calculator.add).toHaveBeenCalled();
    expect(calculator.add).toHaveBeenCalledTimes(1);
    expect(calculator.add).toHaveBeenCalledWith(1,1);
    expect(result).not.toEqual(2);
  })  
})

Calling Through

describe('Spies', () => {
  it('should call through', () => {
    spyOn(calculator, 'add').and.callThrough(); 
    let result = calculator.add(1,1);
    expect(result).toEqual(2);

    //restore stub behaviour
    calculator.add.and.stub();
    expect(calculator.add(1,1)).not.toEqual(2);
  })  
})

Set return values

describe('Spies', () => {
  it('should return value with 42', () => {
    spyOn(calculator, 'add').and.returnValue(42);
    let result = calculator.add(1,1);
    expect(result).toEqual(42);
  }) 
  
  it('should return values 1, 2, 3', () => {
    spyOn(calculator, 'add').and.returnValues(1, 2, 3);
    expect(calculator.add(1,1)).toEqual(1);
    expect(calculator.add(1,1)).toEqual(2);
    expect(calculator.add(1,1)).toEqual(3);
  })   
})

Error handling

describe('Spies', () => {
  it('should throw with error', () => {
    spyOn(calculator, 'add').and.throwError("Ups");
    expect(() => calculator.add(1,1)).toThrowError("Ups");
  }) 
})

Creating Spies

describe('Spies', () => {
  it('should be able to create a spy manually', () => {
    let add = jasmine.createSpy('add');
    add();
    expect(add).toHaveBeenCalled();
  }) 
})

// usage: create spy to use as a callback
//  setTimeout(add, 100);

Creating Spies

describe('Spies', () => {
  it('should be able to create multiple spies manually', () => {
    let calculator = jasmine.createSpyObj('calculator', ['add']);
    calculator.add.and.returnValue(42);

    let result = calculator.add(1,1);
    expect(calculator.add).toHaveBeenCalled();
    expect(result).toEqual(42);
  }) 
})

Angular Testing

Testing APIs

  • inject,TestBed
  • async
  • fakeAsync/tick

Setup

import { TestBed } from '@angular/core/testing';
import {
  BrowserDynamicTestingModule, 
  platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';

TestBed.initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting()
);

Testing a Service

import {Injectable} from '@angular/core';

@Injectable()
export class LanguagesService {
  get() {
    return ['en', 'es', 'fr'];
  }
}

Testing a Service

describe('Service: LanguagesService', () => {
  //setup
  beforeEach(() => TestBed.configureTestingModule({
    providers: [ LanguagesService ]
  }));

  //specs
  it('should return available languages', inject([LanguagesService], service => {
    let languages = service.get();
    expect(languages).toContain('en');
    expect(languages).toContain('es');
    expect(languages).toContain('fr');
    expect(languages.length).toEqual(3);
  });
});

refactoring inject

describe('Service: LanguagesService', () => {
  let service;

  beforeEach(() => TestBed.configureTestingModule({
    providers: [ LanguagesService ]
  }));
  beforeEach(inject([LanguagesService], s => {
    service = s;
  }));

  it('should return available languages', () => {
    let languages = service.get();
    expect(languages).toContain('en');
    expect(languages).toContain('es');
    expect(languages).toContain('fr');
    expect(languages.length).toEqual(3);
  });
});

Asynchronous Testing

Asynchronous APIs

  • Jasmine.done
  • async
  • fakeAsync/tick

Http Service

import { Injectable } from '@angular/core';
import { Http } from '@angular/http';
import 'rxjs/add/operator/map';

@Injectable()
export class LanguagesServiceHttp {
  constructor(private http:Http) { }
  
  get(){
    return this.http.get('api/languages.json')
      .map(response => response.json());
  }
}

Testing Http Service (mock)

describe('MockBackend: LanguagesServiceHttp', () => {
  let mockbackend, service;

  beforeEach(() => TestBed.configureTestingModule({
    providers: [
      BaseRequestOptions,
      MockBackend,
      { provide: Http,
        useFactory: (backend, options) => new Http(backend, options),
        deps: [MockBackend, BaseRequestOptions] },
      LanguagesServiceHttp
    ]
  }));
  
  beforeEach(inject([MockBackend, LanguagesServiceHttp], (m, s) => {
    mockbackend = m;
    service = s;
  }))

Testing MockBackend

  it('should return mocked response (sync)', () => {

    //setup mock response
    let response = ["ru", "es"];
    mockbackend.connections.subscribe(connection => {
      connection.mockRespond(new Response({body: JSON.stringify(response)}));
    });

    //check expectations
    service.get().subscribe(languages => {
      expect(languages).toContain('ru');
      expect(languages).toContain('es');
      expect(languages.length).toBe(2);
    });
  });  

Components Testing

Greeter Component

import {Component, Input} from '@angular/core';

@Component({
  selector: 'greeter',   // <greeter name="Igor"></greeter>
  template: `<h1>Hello {{name}}!</h1>`
})
export class Greeter { 
  @Input() name;
}

Testing Fixtures (sync)

describe('Component: Greeter', () => {
  let fixture, greeter, element, de;
  
  //setup
  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ Greeter ]
    });

    fixture = TestBed.createComponent(Greeter);
    greeter = fixture.componentInstance;
    element = fixture.nativeElement;
    de = fixture.debugElement;
  });
}

Testing Fixtures (async)

describe('Component: Greeter', () => {
  let fixture, greeter, element, de;
  
  //setup
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ Greeter ],
    })
    .compileComponents() // compile external templates and css
    .then(() => {
      fixture = TestBed.createComponent(Greeter);
      greeter = fixture.componentInstance;
      element = fixture.nativeElement;
      de = fixture.debugElement;
    });
 ))
});

Using Change Detection

describe('Component: Greeter', () => {
  it('should render `Hello World!`', async(() => {
    greeter.name = 'World';
    //trigger change detection
    fixture.detectChanges();
    fixture.whenStable().then(() => { 
      expect(element.querySelector('h1').innerText).toBe('Hello World!');
      expect(de.query(By.css('h1')).nativeElement.innerText).toBe('Hello World!');
    });
  }));
}

Using fakeAsync

describe('Component: Greeter', () => {
  it('should render `Hello World!`', fakeAsync(() => {
    greeter.name = 'World';
    //trigger change detection
    fixture.detectChanges();
    //execute all pending asynchronous calls
    tick();
    expect(element.querySelector('h1').innerText).toBe('Hello World!');
    expect(de.query(By.css('h1')).nativeElement.innerText).toBe('Hello World!');
  }));
}

Shallow Testing

NO_ERRORS_SCHEMA

beforeEach(() => {
  TestBed.configureTestingModule({
    declarations: [ MyComponent ],
    schemas: [ NO_ERRORS_SCHEMA ]
  })
});

e2e Testing

Main concepts

  • Test features instead of methods
  • Test as final user no Mocking
  • Run on multiple browsers
  • Complex to create/debug
  • Resource intensive (slow)

Protractor

  • Automate browser testing
  • WebDriverJS Wrapper
  • ControlFlow
    • Deals with async code (zones)

Protractor

  • Browser
    • browser.driver
    • browser.get(url)
  • DOM
    • by.id('user')
    • element(selector).getText()

More?

Examples covering

  • Components, Directives, Pipes

  • Services, Http, MockBackend

  • Router, Observables

  • Spies

Blog Post

Thanks!

Дякую

Angular Testing Recipes (v4+)

By Gerard Sans

Angular Testing Recipes (v4+)

In this talk, we will cover the most common testing scenarios to use while developing rock solid Angular Applications, like: Components, Services, Http and Pipes; but also some less covered areas like: Directives, the Router and Observables. We will provide examples for using TestBed, fixtures, async and fakeAsync/tick while recommending best practices.

  • 1,344
Loading comments...

More from Gerard Sans