Gerard Sans
@gerardsans
Gerard Sans
@gerardsans
Â
SANS
GERARD
Spoken at 110 events in 27 countries
900
1.6K
Unit Tests
Assertion Libraries
Spies, Stubs
Test
Automation
Browsers
Coverage Reports
e2e Tests
Test
Runner
WebDriverJS
Selenium
Protractor
Â
Â
Unit tests
e2e Tests
Acceptance Tests
$ npm run tests $ npm run e2e
let calculator = { 
  add: (a, b) => a + b 
};
describe('Calculator', () => {  
  it('should add two numbers', () => {
    expect(calculator.add(1,1)).toBe(2);
  })  
})fail(msg), pending(msg)
		xdescribe, xit
		fdescribe, fit
		Test double functions that record calls, arguments and return values
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);
  })  
})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);
  })  
})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);
  })   
})describe('Spies', () => {
  it('should call fake function returning 42', () => {
    spyOn(calculator, 'add').and.callFake((a,b) => 42);
    expect(calculator.add(1,1)).toEqual(42);
  }) 
})describe('Spies', () => {
  it('should throw with error', () => {
    spyOn(calculator, 'add').and.throwError("Ups");
    expect(() => calculator.add(1,1)).toThrowError("Ups");
  }) 
})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);
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);
  }) 
})import { TestBed } from '@angular/core/testing';
import {
  BrowserDynamicTestingModule, 
  platformBrowserDynamicTesting
} from '@angular/platform-browser-dynamic/testing';
TestBed.initTestEnvironment(
  BrowserDynamicTestingModule,
  platformBrowserDynamicTesting()
);import {Injectable} from '@angular/core';
@Injectable()
export class LanguagesService {
  get() {
    return ['en', 'es', 'fr'];
  }
}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);
  });
});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);
  });
});import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import 'rxjs/add/operator/map';
@Injectable()
export class UsersService {
  constructor(private http: HttpClient) { }
  public get() { 
    return this.http.get('./src/assets/users.json')
      .map(response => response.users);
  }
}describe('Service: UsersService', () => {
  let service, http;
  beforeEach(() => TestBed.configureTestingModule({
    imports: [ HttpClientModule ],
    providers: [ UsersService ]
  }));
  beforeEach(inject([UsersService, HttpClient], (s, h) => {
    service = s;
    http = h;
  }));
  [...]
describe('Service: UsersService', () => {
  [...]
  it('should return available users (LIVE)', done => {
    service.get()
      .subscribe({
        next: res => {
          expect(res.users).toBe(USERS);
          expect(res.users.length).toEqual(2);
          done();
        }
      });
  });
});
describe('Service: UsersService', () => {
  let service, httpMock;
  beforeEach(() => TestBed.configureTestingModule({
    imports: [ HttpClientTestingModule ],
    providers: [ UsersService ]
  }));
  beforeEach(inject([UsersService, HttpTestingController], (s, h) => {
    service = s;
    httpMock = h;
  }));
  afterEach(httpMock.verify);
  [...]
describe('Service: UsersService', () => {
  [...]
  it('should return available users', done => {
    service.get()
      .subscribe({
        next: res => {
          expect(res.users).toBe(USERS);
          expect(res.users.length).toEqual(2);
          done();
        }
      });
    httpMock.expectOne('./src/assets/users.json')
      .flush(USERS);
  });
});
import {Component, Input} from '@angular/core';
@Component({
  selector: 'greeter',   // <greeter name="Igor"></greeter>
  template: `<h1>Hello {{name}}!</h1>`
})
export class Greeter { 
  @Input() name;
}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;
  });
}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;
    });
 ))
});
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!');
    });
  }));
}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!');
  }));
}describe('Component: Greeter', () => {
  let fixture, greeter, element, de;
  
  //setup
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ Greeter ],
    })
    .compileComponents() // compile external templates and css
    .then(() => {
      TestBed.overrideTemplate(Greeter, '<h1>Hi</h1>');
      fixture = TestBed.createComponent(Greeter);
      greeter = fixture.componentInstance;
      element = fixture.nativeElement;
      de = fixture.debugElement;
    });
 ))
});
beforeEach(() => {
  TestBed.configureTestingModule({
    declarations: [ MyComponent ],
    schemas: [ NO_ERRORS_SCHEMA ]
  })
});import {marbles} from "rxjs-marbles";
describe("Cold Observables", () => {
  describe("basic marbles", () => {
    it("should support simple values as strings", marbles(m => {
      const input    = m.cold("--1--1|");
      const expected = m.cold("--1--2|");
      
      const output = input.pipe(map(x => x));
      m.expect(output).toBeObservable(expected);
    }));
  });
}); const input    = m.cold("--1--1|");
 const expected = m.cold("--1--2|");
...
Cold Observables basic marbles should support simple values as strings
Error: 
Expected 
	{"frame":20,"notification":{"kind":"N","value":"1","hasValue":true}}
	{"frame":50,"notification":{"kind":"N","value":"1","hasValue":true}}
	{"frame":60,"notification":{"kind":"C","hasValue":false}}
	
to deep equal 
	{"frame":20,"notification":{"kind":"N","value":"1","hasValue":true}}
	{"frame":50,"notification":{"kind":"N","value":"2","hasValue":true}}
	{"frame":60,"notification":{"kind":"C","hasValue":false}}import {marbles} from "rxjs-marbles";
describe("Cold Observables", () => {
  describe("basic marbles", () => {
    it("should support simple values as strings", marbles(m => {
      const values   = { a: 1, b: 2 };
      const input    = m.cold("--a--a|", values);
      const expected = m.cold("--b--b|", values);
      
      const output = input.pipe(map(x => x+1));
      m.expect(output).toBeObservable(expected);
    }));
  });
});import {marbles} from "rxjs-marbles";
describe("Cold Observables", () => {
  describe("basic marbles", () => {
    it("should support custom errors (symbols)", marbles(m => {
      const values   = { a: 1 };
      const input    = m.cold("--a#", values, new Error('Ups'));
      const expected = m.cold("--a#", values, new Error('Ups'));
      
      const output = input.pipe(map(x => x));
      m.expect(output).toBeObservable(expected);
    }));
  });
});import {marbles} from "rxjs-marbles";
describe("Cold Observables", () => {
  describe("basic marbles", () => {
    it("should support custom Observables", marbles(m => {
      const input    = throwError(new Error('Ups')));
      const expected = m.cold("#", undefined, new Error('Ups'));
      
      const output = input.pipe(map(x => x));
      m.expect(output).toBeObservable(expected);
    }));
  });
});import {marbles} from "rxjs-marbles";
describe("Hot Observables", () => {
  describe("Subscriptions", () => {
    it("should support basic subscriptions", marbles(m => {
      const values   = { a: 1, b: 2 };
      const input    = m.hot( "--a^-a|", values);
      const expected = m.cold(   "--b|", values);
      
      const output = input.pipe(map(x => x+1));
      m.expect(output).toBeObservable(expected);
    }));
  });
});import {marbles} from "rxjs-marbles";
describe("Hot Observables", () => {
  describe("Subscriptions", () => {
    it("should support testing subscriptions", marbles(m => {
      const values   = { a: 1, b: 2 };
      const input    = m.hot( "--a^-a|", values);
      const subs     =           "^-!";
      const expected = m.cold(   "--b|", values);
      
      const output = input.pipe(map(x => x+1));
      m.expect(output).toBeObservable(expected);
      m.expect(input).toHaveSubscriptions(subs);
    }));
  });
});  const values   = { a: 1, b: 2 };
  const input    = m.hot( "--a^-a|", values);
  const subs     =           "^-!";
  const expected = m.cold(   "--b|", values);
...
Hot Observables Subscriptions should support complex subscriptions
Error: 
Expected 
	{"subscribedFrame":0,"unsubscribedFrame":30}
	
to deep equal 
	{"subscribedFrame":0,"unsubscribedFrame":20}// actions/spinner.actions.spec.ts
import { SpinnerShow, SpinnerHide, SpinnerActionTypes } from './spinner.actions';
describe('SpinnerShow', () => {
  it('should create an instance', () => {
    const action = new SpinnerShow();
    expect(action).toBeTruthy();
    expect(action.type).toBe(SpinnerActionTypes.Show);
  });
});
describe('SpinnerHide', () => {
  it('should create an instance', () => {
    const action = new SpinnerHide();
    expect(action).toBeTruthy();
    expect(action.type).toBe(SpinnerActionTypes.Hide);
  });
});// reducers/spinner.reducer.spec.ts
describe('Spinner Reducer', () => {
  it('should return the initial state', () => {
    const action = { type: '🚀' } as any;
    const result = reducer(initialState, action);
    expect(result).toBe(initialState);
    expect(initialState).toBe(false);
  });
});
// reducers/spinner.reducer.spec.ts
describe('Spinner Reducer', () => {
    it('should handle SpinnerShow Action', () => {
      const action = new SpinnerShow();
      const result = reducer(initialState, action);
      expect(result).toBe(true);
    });
    it('should handle SpinnerHide Action', () => {
      const action = new SpinnerHide();
      const result = reducer(initialState, action);
      expect(result).toBe(false);
    });
});
// reducers/selectors.spec.ts
describe('Selectors', () => {
  let adapter : EntityAdapter<Todo>;
  let initialState : any;
  let t: Array<Todo> = [
    { id: 1, text: 'Learn French', completed: false },
    { id: 2, text: 'Try Poutine', completed: true }
  ];
  beforeAll(() => {
    adapter = createEntityAdapter<Todo>();
    initialState = adapter.getInitialState();
  })
})// reducers/selectors.spec.ts
describe('Selectors', () => {
  describe('getFilteredTodos', () => {
    it('should return only active todos', () => {
      const todos = adapter.addMany(t, initialState);
      const state: TodosState = {
        todos,
        currentFilter: "SHOW_ACTIVE"
      }
      expect(getFilteredTodos(state)).toEqual([t[0]]);
    })
  })
})// loading/loading.component.spec.ts
describe('LoadingComponent', () => {
  let fixture, loading, element, de;
  // setup
  beforeEach(() => {                        
    TestBed.configureTestingModule({
      declarations: [ LoadingComponent ]
    });
    fixture = TestBed.createComponent(LoadingComponent);
    loading = fixture.componentInstance;
    element = fixture.nativeElement;
    de = fixture.debugElement;
  });
}) 
// loading/loading.component.spec.ts
describe('LoadingComponent', () => {
  let fixture, loading, element, de;
  // specs
  it('should render Spinner', () => {        
    loading.loading = true;
    fixture.detectChanges(); //trigger change detection
    fixture.whenStable().then(() => { 
      expect(element.querySelector('svg')).toBeTruthy();
    });
  });
}) 
Components, Directives, Pipes
Services, Http, MockBackend
Router, Observables, Spies
Animations
Marble Testing