Angular Workshop

Testing

Contents

  1. Why test
  2. Testing tools
  3. Running a first test
  4. Testing components
  5. Unit testing services

Why test?

Testing Tools

JS testing tools

  • Test frameworks - where you write your test
    • Jasmine, Mocha, Tape, Jest
  • Test environment - where your tests are executed
    • browsers - Chrome, Firefox...
    • headless browsers - JSDom, PhantomJS
  • ​​Test runners - where you run your tests
    • ​​Karma
 

Jasmine

  • A behavior-driven development framework for testing JavaScript code.
  • Create hierarchical suites of test (describe)
  • The tests are written as specifications (it)
  • Expectations and Matchers (built-in and custom)
  • Spies - a test double pattern
  • Asynchronous operations support

Jasmine

A simple jasmine test suite

describe("A resource",() => {
  const resource;

  beforeEach(() => {
    resource = new Resource();
    resource.allocateSpace();
  });

  afterEach(() => {
    resource.free();
  });

  it("should have allocated 100 units of space",() => {
    expect(resource.space).toEqual(100);
  });

  it("should be connected to at least one device",() => {
    expect(resource.isConnected).toEqual(true);
    expect(resource.connections.length).toBeGreaterThan(0);
  });
});

Jasmine

Jasmine Matchers

  • not
  • toBe
  • toEqual
  • toMatch
  • toBeDefined
  • toBeUndefined
  • toBeNull
  • toBeTruthy
  • toBeFalsy
  • toContain
  • toBeLessThan
  • toBeGreaterThan
  • toBeCloseTo
  • toThrow

Jasmine

A simple jasmine spy

describe('A Person', () => {
    let fakePerson;
    beforeEach( () => {fakePerson = new Person();})

    it('should call the sayHello() function', () => {
        spyOn(fakePerson, 'sayHello');
        fakePerson.helloSomeone('world');
        expect(fakePerson.sayHello).toHaveBeenCalled();
    });

    it('should greet the world', () => {
        spyOn(fakePerson, 'helloSomeone');
        fakePerson.helloSomeone('world');
        expect(fakePerson.helloSomeone).toHaveBeenCalledWith('world')
    });
});
class Person {
  helloSomeone(toGreet) {
    return `${this.sayHello()} ${toGreet}`;
  };
  sayHello() {
    return 'Hello';
  };
}

Jasmine

Advanced spy behaviour

// Actually calling the method
spyOn(fakePerson, 'sayHello').and.callThrough();

// Set a return value for a call
spyOn(fakePerson, 'sayHello').and.returnValue('Hello World');

// Set a return value for a call
spyOn(fakePerson, 'sayHello').and.returnValue('Hello World');

// Call a different function
spyOn(fakePerson, 'sayHello').and.callFake((arguments, can, be, received) => ...);

// Get number of calls
spyOn(fakePerson, 'sayHello')
expect(fakePerson.sayHello.calls.count()).toBe(3)
fakePerson.sayHello.calls.reset() // reset the counts

// Create a "bare" spy
spy = jasmine.createSpy('whatAmI');
expect(spy).toHaveBeenCalled();

// Create a spy object
tape = jasmine.createSpyObj('tape', ['play', 'pause', 'stop', 'rewind']);

Jasmine

Async support

describe("asynchronous specs",() => {
  it("should take a long time",(done) => {
    setTimeout(function() {
        done();
      }, 9000);
    });
})

Karma

  • JavaScript test runner that integrates with a browser environment
  • Created by the AngularJS team
  • Configuration file
    • browser launchers
    • test framework
    • reporters
    • preprocessors

Assignment

Setting up Karma with Jasmine and TypeScript

Testing Angular Components

Testing Components

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

@Component({
  selector: 'greet-message',
  template: '<h1>{{message}}</h1>'
})

export class GreetComponent {
  public message: string = '';

  constructor() {}

  setMessage(newMessage: string) {
      this.message = newMessage;
  }

  clearMessage() {
    this.message = '';
  }
}

Verifying Methods and Properties - isolated test

Testing Components

import {GreetComponent} from './greet.component';

describe('Testing message state in greet.component', () => {
  let greetComponent: GreetComponent;

  beforeEach(() => {
    greetComponent = new GreetComponent();
  });

  it('should set new message', () => {
    greetComponent.setMessage('Testing');
    expect(greetComponent.message).toBe('Testing');
  });

  it('should clear message', () => {
    greetComponent.clearMessage();
    expect(greetComponent.message).toBe('');
  });
});

Verifying Methods and Properties - isolated test

Testing Components

describe('Testing GreetComponent', () => {

  let component: GreetComponent;
  let fixture: ComponentFixture<GreetComponent>;
  let debugElement: DebugElement;
  let element: HTMLElement;

  beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [ GreetComponent ],
      imports: [ /* HttpModule, etc. */ ],
      providers: [ /* { provide: ServiceA, useClass: TestServiceA } */ ]
    });

    fixture = TestBed.createComponent(GreetComponent);

    component = fixture.componentInstance; 
    debugElement = fixture.debugElement.query(By.css('h1'));
    element = debugElement.nativeElement;
  });
});

Testing with Angular testing utilities - setup

Testing Components

import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By }              from '@angular/platform-browser';
import { DebugElement }    from '@angular/core';

import { GreetComponent } from './greet.component';

Testing with Angular testing utilities - setup

Testing Components

TestBed.configureTestingModule({
  declarations: [ GreetComponent ],
});

fixture = TestBed.createComponent(GreetComponent);

Testing with Angular testing utilities - setup

TestBed

  • Created in beforeEach() - called before every test
  • An Angular testing module—an @NgModule class
  • call configureTestingModule with a metadata object

Testing Components

TestBed.configureTestingModule({
  declarations: [ GreetComponent ],
});

fixture = TestBed.createComponent(GreetComponent);

Testing with Angular testing utilities - setup

TestBed

  • Supports optional override methods
TestBed.overrideComponent(SomeComponent, {
  set: {
    template: '<div>Overridden template here</div>'
    // ...
  }
});

Testing Components

TestBed.configureTestingModule({
  declarations: [ GreetComponent ],
});

fixture = TestBed.createComponent(GreetComponent);

component = fixture.componentInstance; 
debugElement = fixture.debugElement.query(By.css('h1'));
element = debugElement.nativeElement;

Testing with Angular testing utilities - setup

createComponent()

  • creates an instance of the component to test
  • returns a component fixture
    • access to the component instance
    • access to the debug element
  • closes current TestBed configurations

Testing Components

Testing with Angular testing utilities - the tests

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

@Component({
  selector: 'greet-message',
  template: '<h1>{{message}}</h1>'
})

export class GreetComponent {
  public message: string = 'Hello';

  constructor() {}

  setMessage(newMessage: string) {
      this.message = newMessage;
  }

  clearMessage() {
    this.message = '';
  }
}

Testing Components

it('should display original greet', () => {
  fixture.detectChanges();
  expect(element.textContent).toContain(component.message);
});

it('should display a different test greet', () => {
  component.setMessage('Test Greet');
  fixture.detectChanges();
  expect(element.textContent).toContain('Test Greet');
});

it('should clear the message', () => {
  fixture.detectChanges();
  component.clearMessage();
  fixture.detectChanges();
  expect(element.textContent).toBe('');
});

Testing with Angular testing utilities - the tests

Testing Components

import { ComponentFixtureAutoDetect } from '@angular/core/testing';

TestBed.configureTestingModule({
  declarations: [ GreetComponent ],
  providers: [
    { provide: ComponentFixtureAutoDetect, useValue: true }
  ]
})

Testing with Angular testing utilities - the tests

detectChanges()

  • tells Angular when to perform change detection
  • TestBed.createComponent() does not trigger change detection
  • It is possible to set up automatic change detection

Testing Components

import { async } from '@angular/core/testing';

// async beforeEach
beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [ BannerComponent ], // declare the test component
  })
  .compileComponents();  // compile template and css
}));

Testing with Angular testing utilities - external templates

  • compileComponents()​ - asynchronously compiles all the components configured in the testing module
  • async() - arranges for the body of the beforeEach to run in a special async test zone that hides the mechanics of asynchronous execution

Testing Components

import { async } from '@angular/core/testing';

// async beforeEach
beforeEach(async(() => {
  TestBed.configureTestingModule({
    declarations: [ GreetComponent ],
  })
  .compileComponents();
}));

// synchronous beforeEach
beforeEach(() => {
  fixture = TestBed.createComponent(GreetComponent);

  component = fixture.componentInstance;
  debugElement = fixture.debugElement.query(By.css('h1'));
  element = debugElement.nativeElement;
});

Testing with Angular testing utilities - external templates

Testing Components


import {Component} from '@angular/core';
import {UserService} from './user.service';

@Component({
  selector: 'greet-message',
  template: '<h1>{{message}}</h1>'
})

export class GreetComponent {
  public message: string = 'Hello';

  constructor(private userService: UserService) { }

  ngOnInit(): void {
    this.message = this.userService.isLoggedIn ?
      'Welcome, ' + this.userService.user.name :
      'Please log in.';
  }
}

Testing Components with a dependency

Testing Components

userServiceStub = {
  isLoggedIn: true,
  user: { name: 'Test User'}
};

TestBed.configureTestingModule({
   declarations: [ WelcomeComponent ],
   // providers:    [ UserService ]  // Don't provide the real service!
   providers:    [ {provide: UserService, useValue: userServiceStub } ]
});

userService = TestBed.get(UserService);

Testing Components with a dependency - stubbing the service

  • always access service from injector, never directly - the injected service is a clone

Testing Components

// UserService actually injected into the component
userService = fixture.debugElement.injector.get(UserService);

Testing Components with a dependency - accessing the service

// UserService from the root injector
userService = TestBed.get(UserService);
  • The component injector
  • The TestBed injector
it('stub object and injected UserService should not be the same', () => {
  expect(userServiceStub === userService).toBe(false);

  userServiceStub.isLoggedIn = false;
  expect(userService.isLoggedIn).toBe(true);
});

Testing Components

it('should welcome the user', () => {
  fixture.detectChanges();
  const content = element.textContent;
  expect(content).toContain('Welcome', '"Welcome ..."');
  expect(content).toContain('Test User', 'expected name');
});

it('should welcome "Bubba"', () => {
  userService.user.name = 'Bubba';
  fixture.detectChanges();
  expect(element.textContent).toContain('Bubba');
});

it('should request login if not logged in', () => {
  userService.isLoggedIn = false;
  fixture.detectChanges();
  const content = element.textContent;
  expect(content).not.toContain('Welcome', 'not welcomed');
  expect(content).toMatch(/log in/i, '"log in"');
});

Testing Components with a dependency -testing the service

  • The component injector

Testing Components


import {Component} from '@angular/core';
import {GreetingsService} from './geetings.service';

@Component({
  selector: 'greet-message',
  template: '<h1>{{message}}</h1>'
})

export class GreetComponent {
  public message: string = 'Hello';

  constructor(private greetingsService: GreetingsService) { }

  ngOnInit(): void {
      this.greetingsService.getGreets()
        .then(greets => this.message = greets[0]);
  }
}

Testing Components with an async dependency

Testing Components

beforeEach(() => {
  TestBed.configureTestingModule({
     declarations: [ GreetComponent ],
     providers:    [ GreetingsService ],
  });

  fixture = TestBed.createComponent(GreetComponent);
  component = fixture.componentInstance;

  greetingsService = fixture.debugElement.injector.get(GreetingsService);

  spy = spyOn(greetingsService, 'getGreetings')
        .and.returnValue(Promise.resolve(testGreetings));

  debugElement = fixture.debugElement.query(By.css('h1'));
  element = debugElement.nativeElement;
});

Testing Components with an async dependency

  • Inject real service (not a stub) and use a Spy for the called method - bypasses the actual method implementation

Testing Components

it('should not show greeting before OnInit', () => {
  expect(element.textContent).toBe('', 'nothing displayed');
  expect(spy.calls.any()).toBe(false, 'getGreetings not yet called');
});
 
it('should still not show greeting after component initialized', () => {
  fixture.detectChanges();

  expect(element.textContent).toBe('Hello', 'no greeting yet');
  expect(spy.calls.any()).toBe(true, 'getGreetings called');
});
 

Testing Components with an async dependency

Sync test

  • The spy allows us to test that the method has been called
  • Neither test can access the value in the returned (and resolved) promise (from the spy)

Testing Components

it('should show message after getGreetings promise (async)', async(() => {
  fixture.detectChanges();
 
  fixture.whenStable().then(() => { 
    fixture.detectChanges();        
    expect(element.textContent).toBe(testGreeting);
  });
}));
 
it('should show message after getGreetings promise (fakeAsync)', fakeAsync(() => {
  fixture.detectChanges();
  tick();                  
  fixture.detectChanges();
  expect(element.textContent).toBe(testGreeting);
}));

Testing Components with an async dependency

Async test

Testing Components

Testing Components with an async dependency

  • the async() function - running the test in a special async test zone
    • no access to promises called in component
    • whenStable() - called when all async operations in the test complete (still need to detect changes)
  • fakeAsync() - allows for a more linear coding style, no need for then()-s
  • tick() - can only be called inside a fakeAsync, “waits” for all async operations to finish
  • async and fake async - just sugar over jasmine.done()

Testing Components

Testing Components with inputs and outputs

<greet-message 
    [greeting]=greet
    (onLiked)="incrementLikes($event)" 
>
</greet-message>
@Component({
  selector: 'greet-message',
  template: `<h1>
    {{message}} <button (click)=handleClick>LIKE</button>
  </h1>`
})
export class GreetComponent {
  @Input() message: string;
  @Output() onLiked = new EventEmitter<string>();
  
  handleClick() { 
    this.selected.emit(this.message); 
  }
}

Testing Components

Testing Components with inputs and outputs

Goal

  • Test if inputs and outputs work correctly

Approaches

  • test as standalone component
  • test inside a container component
beforeEach(() => {
  fixture = TestBed.createComponent(GreetingsComponent);
  component = fixture.componentInstance;
  element = fixture.debugElement.query(By.css('h1'));

  expectedMessage = 'Wassuuup?!?';
  component.message = expectedMessage;
  fixture.detectChanges();
});

Testing Components

Testing Components with inputs and outputs - standalone

Test standalone

  • Outputs - subscribe to EventEmitter, trigger click event
it('should raise selected event when clicked', () => {
  let likedMessage: string;
  comp.selected.subscribe((message: string) => likedMessage = message);

  buttonElement.triggerEventHandler('click', null);
  expect(likedMessage).toBe(expectedMessage);
});
  • Inputs - set a value to the input property on the component object
it('should display greeting', () => {
  expect(element.nativeElement.textContent).toContain(expectedMessage);
});

Testing Components

Testing Components with inputs and outputs - host component

Test in host component

  • create an on the fly component to test the target component
@Component({
  template: `
    <greet-message 
      [greeting]=greet
      (onLiked)="handleLike($event)" 
    >
    </greet-message>
`
})
class TestHostComponent {
  greet = 'Wassuuuup?!?';
  handleLike(string: message) { /* do something with with the like */ }
}

Testing Components

Testing Components with inputs and outputs - host component

Test in host component

  • Declare both components in TestBed
  • Create only the test host component
beforeEach( async(() => {
  TestBed.configureTestingModule({
    declarations: [ GreetComponent, TestHostComponent ],
  }).compileComponents();
}));

beforeEach(() => {
  fixture  = TestBed.createComponent(TestHostComponent);
  testHost = fixture.componentInstance;
  greetElement   = fixture.debugElement.query(By.css('h1')); 
  fixture.detectChanges();
});

Testing Components

Testing Components that uses the router

@Component({
  selector: 'greet-message',
  template: `<h1>
    {{message}}
    <br />
    <button (click)=handleClick>go to your page</button>
  </h1>`
})
export class GreetComponent {
  @Input() message: string;
  @Output() onLiked = new EventEmitter<string>();
  
  constructor(
    private router: Router,
    private userService: UserService) {
  }

  handleClick() { 
    let url = `/users/${this.userService.id}`;
    this.router.navigateByUrl(url);
  }
}

Testing Components

Testing Components that uses the router

class RouterStub {
  navigateByUrl(url: string) { return url; }
}
beforeEach( async(() => {
  TestBed.configureTestingModule({
    declarations: [GreetingComponent],
    providers: [
      { provide: UserService, useClass: UserServiceStub },
      { provide: Router, useClass: RouterStub }
    ]
  })
  .compileComponents().then(() => {
    fixture = TestBed.createComponent(GreetingComponent);
    comp = fixture.componentInstance;
  });
  • Just stub the used methods for the Router

Testing Components

Testing Components that uses the router

it('should tell ROUTER to navigate when greet button is clicked',
  inject([Router], (router: Router) => {

  const spy = spyOn(router, 'navigateByUrl');

  greetClick(component); // should trigger the click on the greeting button

  const navArgs = spy.calls.first().args[0]; // arguments for call
  
  // get actual service injected in component
  userService = fixture.debugElement.injector.get(UserService);
  const id = userService.getUser().id;

  expect(navArgs).toBe('/users/' + id, 'should nav to User details ');
}));
  • Router is injected using TestBed injector
  • The UserService is actual service injected by the component

Testing Components

Other advanced testing techniques

  • Test doubles for observables
  • Override component providers
  • Testing router outlet components
  • Shallow render
  • Testing directives

Testing Angular Services

Testing Services

Isolated unit test advantages:

  • Import from the Angular test libraries.
  • Configure a module.
  • Prepare dependency injection providers.
  • Call inject or async or fakeAsync.

Testing Services

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

@Injectable()
export class Engine {
  getHorsepower() {
    return 150;
  }

  getName() {
    return 'Basic engine';
  }
}

Verifying Methods and Properties - isolated test

Testing Services

describe('Engine', () => {
  let subject: Engine;

  beforeEach(() => {
    subject = new Engine();
  });

  it('should return it\'s horsepower', () => {
    expect(subject.getHorsepower()).toEqual(150);
  });

  it('should return it\'s horsepower', () => {
    expect(subject.getName()).toEqual('Basic engine');
  });
});

Verifying Methods and Properties - isolated test

Testing Services

import { Injectable } from '@angular/core';
import { Engine } from './engine.service';

@Injectable()
export class Car {
  constructor(private engine: Engine) {}

  getName() {
    return `Car with ${this.engine.getName()}(${this.engine.getHorsepower()} HP)`;
  }
}

Using Dependency Injection

Testing Services

import { TestBed, inject } from '@angular/core/testing';
import { Engine } from './engine.service';
import { Car } from './car.service';

describe('Car', () => {

  beforeEach(() => {
    TestBed.configureTestingModule({
      providers: [Engine, Car]
    });
  });

  it('should display name with engine', inject([Car], (car: Car) => {
    expect(car.getName()).toEqual('Car with Basic engine(150 HP)');
  }));
});

Using Dependency Injection

Testing Services

...
beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [Engine, Car]
  });

  spyOn(Engine.prototype, 'getHorsepower').and.returnValue(400);
  spyOn(Engine.prototype, 'getName').and.returnValue('V8 engine');
});

...

it('should display name with engine', () => {
  expect(subject.getName()).toEqual('Car with V8 engine(400 HP)');
});

Using Dependency Injection - mocking

Testing Services

@Injectable()
class V8Engine {
  getHorsepower() {
    return 400;
  }

  getName() {
    return 'V8 engine';
  }
}



beforeEach(() => {
  TestBed.configureTestingModule({
    providers: [
      { provide: Engine, useClass: V8Engine },
      Car
    ]
  });
});

Using Dependency Injection - stubbing

Assignment

Angular Workshop - testing

By Andrei Antal

Angular Workshop - testing

  • 1,395