First, it is good to keep in mind, that every component, module, service or directive, that is dedicated to the component which is under test, has to be imported. That means, that if e.g. the view should be tested and Angualr Material components are included, they have to be imported to the test suite, like the component under test itself.
import { async, fakeAsync, tick, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
import { FormsModule } from '@angular/forms';
import { ComputerComponent } from '../computer/computer.component';
import { GameComponent } from './game.component';
import { HighscoresService } from '../highscores.service';
import { MdCard } from '@angular/material';
describe('Rock, Paper, Stone Game - GameComponent (container component)', () => {
beforeEach( () => { // Setups for all suites and specs
TestBed.configureTestingModule({ // Configuartion of testbed...
declarations: [ // ... with all declarations...
ComputerComponent,
GameComponent,
MdCard
],
imports: [ // ... related modules...
FormsModule,
BrowserAnimationsModule,
],
providers: [
HighscoresService // ... and services
]
})
.compileComponents();
});
describe('/ Description of a test suite', () => { // The first test suite
let component: GameComponent;
let fixture: ComponentFixture<GameComponent>;
beforeEach(() => { // Setups for each spec in this
fixture = TestBed.createComponent(GameComponent); // specific test suite
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => { // The initial test spec
expect(component).toBeTruthy();
});
describe('/ Gamelogic timing tests - properties', () => { // A further nested test suite...
let app;
beforeEach( () => { // ...and setups only for this suite
app = fixture.debugElement.componentInstance;
jasmine.clock().install(); // Install clock instead of timeout
});
afterEach( () => { // Reset to basics
jasmine.clock().uninstall(); // Uninstall clock to clear the thimer
});
it('Before 0.8s the property "computerText" should be empty', () => {
app.countdown();
jasmine.clock().tick(795);
expect(app.computerText).toEqual('' || 'Computer is waiting...');
});
});
});
});
a) Test a specific tag
an its textContent
b) Test if a specific tag
is present
c) Test if a button
queried by tag
and id
is labeled with 'Reset'
d) Test an attribute
of a tag
in this case: the name attribute like here: <input name="name">
e) + f) When there are more than one button
in this component, it is easier to test if they have id's for querying them.
describe('/ The ListComponent view', () => {
// a)
it('should render title in a h1 tag', () => {
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('This is the hadline');
});
// b)
it('should be able to render PlayerComponent tag (<app-player>)', () => {
const compiled = fixture.debugElement.query(By.css('app-player'));
expect(compiled).not.toBe(null);
});
// c)
it('should render button "Neustart"', () => {
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('button#new-game').textContent).toContain('Reset');
});
// d)
it('should render a <input> tag with attribute name="name"', () => {
const compiled = fixture.debugElement.query(By.css('input'));
expect(compiled).not.toBe(null);
expect(compiled.attributes['name']).toBe('name');
});
// e)
it('should render a <button> tag with id="save"', () => {
const compiled = fixture.debugElement.query(By.css('#save'));
expect(compiled).not.toBe(null);
});
// f)
it('should render a <button> tag with id="abort"', () => {
const compiled = fixture.debugElement.query(By.css('#abort'));
fixture.detectChanges();
expect(compiled).not.toBe(null);
});
});
a) Test for property
and its assigned value
b) Doubled test for a property
- one for truthiness is present (I don't think this test is perfect) and one for not to be undefined
(has more value for the result)
describe('/ ListComponent - properties', () => {
// a)
it(`should have a property "title": 'The list of games'`, () => {
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('The list of games');
});
// b)
it('should have a property "playersScore"', () => {
const app = fixture.debugElement.componentInstance;
expect(app.playersScore).toBeTruthy;
expect(app.playersScore).not.toBe(undefined);
});
});
a) The basic test for a method is like testing a property's presence
b) As well a basic test but with manual starting the Change Detection of Angular
c) Testing a method with several linked properties. In this case the method sets these properties to an initial value. To test it, these properties are declared as local variables of the specific test spec. Then the method 'newGame()' is called and the spec compares the values.
d) This spec calls the method with an argument
and tests if the property
, which should be assigned with a specific value
returned by this method, is equal.
e) This spec calls the method with several different arguments and tests if the expected value will be returned.
f) Testing a method which returns a random value is nearly useless but one can test if the minimum and maximum will not be exceeded.
g) Test if a function/method has been called. In this case a spy will be set on a component and the method to be tested (h spyOn(component, 'calculateWinner');
the parameters are: component, nameOfTheMethod. The component has to be assigned upfront e.g. let component: GameComponent;
h) Another test with expectation to a set property after a method has been called.
i) Test whether a property
is a function or not
describe('/ 1. Game - methods general', () => {
// a)
it('should have a method "newGame()"', () => {
const app = fixture.debugElement.componentInstance;
expect(app.newGame).not.toBe(undefined);
});
// b)
it('should have a method "countdown()"', () => {
fixture.detectChanges();
const app = fixture.debugElement.componentInstance;
expect(app.countdown).not.toBe(undefined);
});
// c)
it('"newGame()" should reset all properties', () => {
const playersScore = 0;
const computersScore = 0;
const winnerDisplayText = 'Neues Spiel, neues Glück!';
const playersChoice = null;
const computersChoice = null;
const playerText = 'Spiel deine Hand!';
const computerText = 'Computer wartet auf dich...';
this.restartIsActive = false;
this.buttonsDisabled = false;
fixture.detectChanges();
const app = fixture.debugElement.componentInstance;
app.newGame();
console.log('app.computerText: ', app.computerText);
expect(app.playersScore === playersScore).toBe(true);
expect(app.computersScore === computersScore).toBe(true);
expect(app.winnerDisplayText === winnerDisplayText).toBe(true);
expect(app.playersChoice).toBe(null);
expect(app.computersChoice).toBe(null);
expect(app.playerText === playerText).toBe(true);
expect(app.computerText === computerText).toBe(true);
});
// d)
it('"setPlayersChoice(scissors)" should set property "restartIsActive" to true', () => {
const app = fixture.debugElement.componentInstance;
app.setPlayersChoice('scissors');
expect(app.restartIsActive).toBe(true);
});
// e)
it('"lookup()" should return 5 for 4 or 9 for 5', () => {
fixture.detectChanges();
const app = fixture.debugElement.componentInstance;
expect(app.lookup(1)).toBe(1);
expect(app.lookup(2)).toBe(2);
expect(app.lookup(3)).toBe(3);
expect(app.lookup(4)).toBe(5);
expect(app.lookup(5)).toBe(9);
expect(app.lookup(42)).toBe(undefined);
});
// f)
it('"setComputersChoice()" it\'s variable should get a random number', () => {
const app = fixture.debugElement.componentInstance;
app.setComputersChoice();
fixture.detectChanges();
expect(app.computersChoice).toBeGreaterThan(0);
expect(app.computersChoice).toBeLessThan(10);
});
// g)
it('"setComputersChoice()" should have called setComputersChoiceText()', () => {
const app = fixture.debugElement.componentInstance;
const spy = spyOn(component, 'setComputersChoiceText');
app.setComputersChoice();
fixture.detectChanges();
expect(spy).toHaveBeenCalled();
});
// h)
it('"setComputersChoice()" should have set property buttonsDisabled to false', () => {
const app = fixture.debugElement.componentInstance;
const buttonsDisabled = app.buttonsDisabled;
app.setComputersChoice();
fixture.detectChanges();
expect(buttonsDisabled).toBe(false);
});
// i )
it('should have a method emitPlayerData()', () => {
fixture.detectChanges();
const app = fixture.debugElement.componentInstance;
expect(typeof component.emitPlayerData === 'function').toBe(true);
});
});
a) Getting the compilation of the native elements (the tags of the view):
const compiled = fixture.debugElement.nativeElement;
... and puts them directly into the compiled variable - completing the line above like this:
const compiled = fixture.debugElement.nativeElement.querySelector('#buttons');
... or then extends the code with on extra line to query a tag:
buttonsDiv = compiled.querySelector('#buttons');
b) Getting an element by css selector - tag:
const compiled = fixture.debugElement.query(By.css('input'));
c) Getting an element by css selector - id:
const compiled = fixture.debugElement.query(By.css('#abort'));
d) Getting an instance of the app:
const app = fixture.debugElement.componentInstance;
e) Getting the component:
let component: GameComponent;
f) Getting the fixture:
let fixture: ComponentFixture<GameComponent>;
g) triggering the change detection of Angular:
fixture.detectChanges();
f) Installing jasmine clock (e.g. in beforeEach
method):
jasmine.clock().install();
h) Uninstalling the jasmine clock (e.g. in afterEach
method)
jasmine.clock().uninstall();
i) Setting the jasmine clock (e.g. in the specific spec before a function call)
jasmine.clock().tick(795);
j) getting a specific attribute of a tested dom element (e.g. the name attribute <input name="name">
)
expect(compiled.attributes['name']).toBe('name');
The standard call for the test is ng test
but this leads to a slow pace of the test cycles. turning off the source maps makes it fast as a flash For turning off the source maps call ng test --sourcemaps=false
Another way to speed-up the testing is to set a focus to only one spec or suite with using fdescribe()
or fit()
. Or with turning off several suites with xdescribe().
The speed issue is under observation and there are already some fixes. Maybe a new version of the cli will get big improvements. For following the issue ticket on github see "Test development cycle is slow unless sourcemaps are turned off#5423".
It's always good practice to have an eye on the unit test code coverage. A good cover is nearly or exactly 100 percent.
To find out what coverage the current project actually has just type either
ng test --code-coverage=true
ng test -cc=true