Skip to content

Instantly share code, notes, and snippets.

@ivanbtrujillo
Last active October 20, 2019 15:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ivanbtrujillo/b8dd09e37f67400786964b348d9bf2c2 to your computer and use it in GitHub Desktop.
Save ivanbtrujillo/b8dd09e37f67400786964b348d9bf2c2 to your computer and use it in GitHub Desktop.
Angular2 Testing using AngularCli, Jasmine, Karma, Chai and PhantomJS

Angular2 Testing using AngularCli, Jasmine, Karma, Chai and PhantomJS

Tools

For unit testing in Angular2 we can configure a good environment using:

  • Jasmine: a framework to write test
  • karma: a test runner
  • chai: a bdd assertions library
  • Protractor: used to write end-to-end test. It explore the app as users experience it simulating user behaviour.
  • PhantomJS: a script browser to run the angular code and check if it works properly.

How to add chai to test our component and our template

Chai allow us to make assertion using a friendly syntax (this is BDD) similar to english sentences. Read the Chai API Docs to learn which verb or syntax you need to use in certain cases: http://chaijs.com/api/

First, install chai as dev dependency:

npm install --save-dev chai

In our spec.ts files, we need to require (not to import) the chai library:

var chai = require('chai');

Inside our test we can make assertions using chai instead the default assertions library. An example:

// -- about.component.ts -- //

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

@Component({
  selector: 'app-about',
  templateUrl: './about.component.html',
  styleUrls: ['./about.component.css']
})
export class AboutComponent implements OnInit {
  title: string = 'about works';
  constructor() { }

  ngOnInit() {
  }

  setTitle(title:string){
    this.title = title;
  }

  clearTitle(){
    this.title = '';
  }

}
// -- about.component.spec.ts -- //

/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

var chai = require('chai');
import { AboutComponent } from './about.component';

describe('AboutComponent', () => {
  let component: AboutComponent;
  let fixture: ComponentFixture<AboutComponent>;

  // Before each test, configure the component (async)
  beforeEach(async(() => {
    TestBed.configureTestingModule({
      declarations: [ AboutComponent ]
    })
    .compileComponents();
  }));

  /* Before each text, creates the fixture and a component instance.
     Wait for changes
  */
  beforeEach(() => {
    fixture = TestBed.createComponent(AboutComponent);
    // This component is an instance of our AboutComponent class in TS.
    component = fixture.debugElement.componentInstance;
    fixture.detectChanges();
  });

  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should have expected text', () => {
    chai.expect(component.title).to.equal('about works');
  });

  it('should change the title', () => {
    component.setTitle('About page');
    chai.expect(component.title).to.equal('About page');
  });

  it('should clear the title', () => {
    component.clearTitle();
    chai.expect(component.title).to.equal('');
  });
});

To test the HTML view with chai, we need to install chai-dom and tell to chai to use it:

npm install -save--dev chai-dom

Example:

var chai = require('chai');
chai.use(require('chai-dom'));

it('Should render the title', () => {
  chai.expect(template.querySelector('p')).to.contain.text('about works');
});

Isolated unit test vs Angular testing utilities

We should write isolated unit test to check the behaviour of a class, pipes and services. We should write test using the angular testing utilities to check the behaviour of a component and the entire app on itself.

Where we should place our test files

Karma automatically detects all the *.spec.ts files inside app folder. The test files allways must have the extension .spec.ts which is used by Jasmine, and should be placed inside the same folder than our module as a good practice.

Test fundamentals

To test modules in Angular2, we need to import the following modules:

import { ComponentFixture, TestBed } from '@angular/core/testing';

// ComponentFixture allow us to access to an component instance:
let fixture: ComponentFixture<MyComponent>;
fixture = TestBed.createComponent(SearchComponent);
// TestBed creates a test NgModule where we declare the module that we want to test and there we can indicate its imports, providers, etc (as a normal module).
import { DebugElement } from '@angular/core';
// DebugElement is a handle on the component's element. Using it, we can access to the HTML template and to the component.

// The component:
component = fixture.debugElement.componentInstance

// Using the component we can call its methods and access to it's properties
component.searchResults;
component.getAll();

// The view:
template = fixture.debugElement.nativeElement

// Using the templae we can query our dom
template.querySelector('p'))
import { By } from '@angular/platform-browser';

/*
  The By module allow us to make queries to the debugElement (which is a handle on the component's DOM element) by:
  * All: debugElement.query(By.all());
  * CSS selector: debugElement.query(By.css('[attribute']));
  * Directive: debugElement.query(By.directive(MyDirective));
*/

Test change detection

Each test tell Angular when to perform a change detection by calling fixture.detectChanges().

The first thing the test does when it detects a change is immediately trigger data binding and propagation of the property value to the DOM Element.

The sencond step is to change the component's element property and then to call fixture.detectChanges().

Test public values and methods, non private.

In order to test our components efficiently, we shouldn't test private methods / variables. We just have to test the public component API as most of the developers don't recommend testing private function. So:

// DONT TEST
private title: string = title;

private modifyTitle(){
   ...
}   

// TEST 
title: string = title;

modifyTitle(){
   ...
}

But if you want to test the private methods, you can create an instance of it's class inside your test file and then, check it:

// Variable with type any
let fooBar;

// Class instance
fooBar = new FooBar();

//Example method 1
//Now this will be visible and we can test it
fooBar.initFooBar();

Testing view and model separately

For legibility purposes, we should split our component test into two separated spec.ts files: one for the model and one for the view.

When we creates a component instance, we can access to the component itself and to the view:

fixture = TestBed.createComponent(AboutComponent);

// the component:
component = fixture.debugElement.componentInstance

// the view:
template = fixture.debugElement.nativeElement

As we said, in one file we want to test just the component: about.component.spec.ts

And in the other file, we will test the view: about.view.spec.ts

The difference between both is when we test the component, we are checking the TS class (their methods and their properties):

// Testing the component
chai.expect(component.title).to.equal('about works');

When we test the view, we use the angular testing tools to check the DOM and test if an especific element has the values as expected, accessing by a CSS selector or a directive:

// Testing the view
chai.expect(template.querySelector('p')).to.containt.text('about works');

How to test components that depends of a service with observables

When we write unit test for our A2 components, sometimes we have Observables in some of our methods that depends on a service. Our problem occurs when we try to test if the component has the data after, for example, that the user click a button that fires the method which calls to the service and returns the observable of our data.

To solve this problem, we will create a mockData and a mockService that our component will use in order to we can test it.

Here is the original service:

import { Injectable } from '@angular/core';
import { Http, Response } from '@angular/http';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';
import { Person } from '../person';

@Injectable()
export class SearchService {

  constructor(private http: Http) { }

  getAll(): Observable<Person[]>{
    return this.http.get('app/shared/search/data/people.json')
      .map((res: Response) =>
        res.json()
      )
  }

}

And here is the original component which uses the previous service:

search.component.ts

// This is the component that we want to test
import { Component } from '@angular/core';
import { Person, SearchService } from '../shared/index';

@Component({
  selector: 'app-search',
  templateUrl: './search.component.html',
  styleUrls: ['./search.component.css'],
  providers: [SearchService]
})
export class SearchComponent implements OnInit {

  searchResults: Array<Person>;

  constructor(private searchService: SearchService) {
  }
  
  getAll(){
    this.searchService.getAll().subscribe(
      data => { this.searchResults = data},
      error => console.log(error)
    );
  }
}

As you see the getAll method returns an Observable which is an array of Person. By the other hand, our component suscribes to this observable. It means whenever the testData changes, our component will be informed and will update whatever we want.

Now we need to test this component, but according to the unit testing principle, we can't use the original service and we can't make http request. Our component should be tested as isolated component and for that, we have to create a fake data and a fake service.

First, we create a mockdata which normally is a small copy of the real data that we spected in our component:

//mockData.ts
export const mockData = [
  {
    "id": 1,
    "name": "Peyton Manning",
    "phone": "(303) 567-8910",
    "address": {
      "street": "1234 Main Street",
      "city": "Greenwood Village",
      "state": "CO",
      "zip": "80111"
    }
  },
  {
    "id": 2,
    "name": "Demaryius Thomas",
    "phone": "(720) 213-9876",
    "address": {
      "street": "5555 Marion Street",
      "city": "Denver",
      "state": "CO",
      "zip": "80202"
    }
  }
];

Then, we have to create a MockSearchService class which will use our mockData, instead of our real data (people.json):

// mocksearch.service.ts
import { Observable } from 'rxjs/Observable';
import { Person } from '../app/shared/person';
import { mockData } from '../mock/mockdata';

export class MockSearchService {
  public getAll(): Observable<Person[]> {
    return Observable.of(mockData);
  }
}

So here is our test file, you can see the explanation below:

/* tslint:disable:no-unused-variable */
import { async, ComponentFixture, TestBed, fakeAsync, tick} from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';

var chai = require('chai');

import { AppModule } from '../app.module';
import {APP_BASE_HREF} from '@angular/common';

import { SearchComponent } from './search.component';

import { SearchService } from '../shared/search/search.service';
import { mockData } from '../../mock/mockdata';
import { MockSearchService } from '../../mock/mocksearch.service';
import { Observable } from 'rxjs/Rx';

describe('SearchComponent', () => {
  let component: SearchComponent;
  let fixture: ComponentFixture<SearchComponent>;
  let mockSearchService: MockSearchService;


  beforeEach(async(() => {
    TestBed.configureTestingModule({
      imports: [AppModule],
      providers: [
        { provide: APP_BASE_HREF, useValue: '/' }
      ]
    }).overrideComponent(SearchComponent, {
      set: {
        providers: [
          { provide: SearchService, useClass: MockSearchService },
        ]
      }
    })
    .compileComponents();
  }));

  beforeEach(() => {
    fixture = TestBed.createComponent(SearchComponent);
    component = fixture.debugElement.componentInstance;

    mockSearchService = fixture.debugElement.injector.get(SearchService);

    fixture.detectChanges();
  });


  it('should create', () => {
    expect(component).toBeTruthy();
  });

  it('should get all the persons', fakeAsync(() => {
    const spy = spyOn(mockSearchService, 'getAll').and.returnValue(
      Observable.of(mockData)
    );
    component.getAll();
    fixture.detectChanges();
    chai.expect(component.searchResults).to.be.equal(mockData);
    expect(spy.calls.any()).toEqual(true);

  }));
});

Explanation:

First, we tell to our testBed that our SearchComponent will be use the SearchService as provider, but it's class will be MockSearchService (instead of the original SearchService). It means that now, when our component calls getAll() method, it will get the information from the MockedService instead of the original service.

.overrideComponent(SearchComponent, {
  set: {
    providers: [
      { provide: SearchService, useClass: MockSearchService },
    ]
  }
})

Then, before each test, we will inject the mockedService into our component:

mockSearchService = fixture.debugElement.injector.get(SearchService);

Secondly in our test case we will use fakeAsync to create a async zone where our component will expect till the request is completed:

  it('should get all the persons', fakeAsync(() => {
    //.......//
  }));
  

Thirdly we creates a spy where we indicate to our mockSearchService.getAll() method that it must return an Observable of our mockData.

  const spy = spyOn(mockSearchService, 'getAll').and.returnValue(
    Observable.of(mockData)
  );
  

Finally, we call the getAll method of our component, detect the changes and check if now our searchResult variables is equal to the mockedData returned by our mockedService:

  component.getAll();
  fixture.detectChanges();
  chai.expect(component.searchResults).to.be.equal(mockData);
  
@pallamollasai
Copy link

Thnx a lot. It worked for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment