Unit Testing Recipes for Angular 2

By: Victor Mejia

Does this describe your current front-end dev workflow?

build your app

refresh page

check console in dev tools

console.log()?

rely on QA?

 

we can do better

Some Convincing...

  • Unit tests guard against breaking existing code (“regressions”) when we make changes.
  • clarify what the code does (use as documentation)
  • They reveal mistakes in design and implementation. Tests force us to look at our code from many angles and also make your code more modular

https://angular.io/docs/ts/latest/guide/testing.html

Agenda

  • ng-cli overview and testing setup
  • Testing Components
  • Testing Http
  • Testing Components with async actions
  • Testing pipes

Jasmine

  • A BDD framework for JS code
  • standalone, no DOM required
  • Clean syntax: describe, it, expect
  • Others: Mocha, QUnit, Jest (Facebook)
  • Often used with a mocking library like Sinon

Suites

  • test suite begins with "describe"
  • takes a string (spec suite title) and a function (block of code being tested)
  • suites can be nested
describe('SuperAwesomeModule', function() {

    describe('featureA', function() {

    });

    describe('featureB', function() {
    
    });

});

Specs

  • call global Jasmine function:
    • it(<string>, <fn>)
  • a spec contains one or more expectations
  • expectation: an assertion that is either true or false.
  • spec with all true expectations: pass
  • spec with one or more false expectations: fail
describe('SuperAwesomeModule', function() {

    describe('featureA', function() {

        it('should calculate some super awesome calculation', function() {
            ...
        });

        it('should also do this correctly', function() {
            ...
        });

    });
});

Expectations and Matchers

  • call global Jasmine function:
  • expect(<actual>).<matcher(expectedValue)>
  • a matcher implements boolean comparison between the actual value and the expected value
describe('SuperAwesomeModule', function() {

    describe('featureA', function() {

        it('should calculate some super awesome calculation', function() {
            expect(SuperAwesomeModule.featureA([1, 2, 4]).toEqual(7);
        });

        it('should also do this correctly', function() {
            expect(SuperAwesomeModule.featureB('...').toBe(true);
        });

    });
});

Included Matchers

expect(foo).toBe(true); // uses JS strict equality

expect(foo).not.toBe(true);

expect(foo).toEqual(482); // uses deep equality, recursive search through objects

expect(foo).toBeDefined();

expect(foo).not.toBeDefined();

expect(foo).toBeUndefined();

expect(foo).toBeTruthy(); // boolean cast testing

expect(foo).toBeFalsy();

expect(foo).toContain('student'); // find item in array

expect(e).toBeLessThan(pi);

expect(pi).toBeGreaterThan(e);

expect(a).toBeCloseTo(b, 2); // a to be close to b by 2 decimal points

Included Matchers: Exceptions

expect(function() {
    foo(1, '2')
}).toThrowError();

expect(function() {

    foo(1, '2')
}).toThrow(new Error('Invalid parameter type.')

Setup and Teardown

describe("A spec using beforeEach and afterEach", function() {
  var foo = 0;

  beforeEach(function() {
    foo += 1;
  });

  afterEach(function() {
    foo = 0;
  });

  it("is just a function, so it can contain any code", function() {
    expect(foo).toEqual(1);
  });

  it("can have more than one expectation", function() {
    expect(foo).toEqual(1);
    expect(true).toEqual(true);
  });
});

Setup and Teardown

describe("A spec using beforeAll and afterAll", function() {
  var foo;

  beforeAll(function() {
    foo = 1;
  });

  afterAll(function() {
    foo = 0;
  });

  it("sets the initial value of foo before specs run", function() {
    expect(foo).toEqual(1);
    foo += 1;
  });

  it("does not reset foo between specs", function() {
    expect(foo).toEqual(2);
  });
});

Disabling suites/specs

describe('SuperAwesomeModule', function() {

    xdescribe('featureA', function() {
        it('should ...', function() {

        });

        it('should ...', function() {

        });
    });

    describe('featureB', function() {
        xit('should ...', function() {

        });

        it('should ...', function() {

        });
    });

});

Spies

  • test double functions called spies.
  • can stub any function and tracks calls to it and all arguments.
  • A spy only exists in the describe or it block in which it is defined, and will be removed after each spec.
describe('SuperAwesomeModule', function() {
    beforeEach(function() {
        // track all calls to SuperAwesomeModule.coolHelperFunction() 
        // and also delegate to the actual implementation
        spyOn(SuperAwesomeModule, 'coolHelperFunction').and.callThrough();
    });

    describe('featureA', function() {
        it('should ...', function() {
            expect(SuperAwesomeModule.featureA(2)).toBe(5);
            
            // matchers for spies
            expect(SuperAwesomeModule.coolHelperFunction).toHaveBeenCalled();
            expect(SuperAwesomeModule.coolHelperFunction).toHaveBeenCalledTimes(1);
        });
    });
});

Spies: and.returnValue

  • Useful when you want to stub out return values
describe('SuperAwesomeModule', function() {
    beforeEach(function() {
        spyOn(SuperAwesomeModule, 'coolHelperFunction').and.returnValue('myValue');
    });
});

Karma.conf.js configuration

// list of files / patterns to load in the browser
files: [
  'src/*.js',
  'spec/*.js'
],


browsers: ['PhantomJS'], // run your tests in a headless browser!

Make terminal reporting pretty

Make terminal reporting pretty

update karma.conf.js:

 

plugins: [
  require("karma-jasmine"),
  require("karma-phantomjs-launcher"),
  require("karma-spec-reporter")
],

...

reporters: ['spec'],

Build Integration

npm install husky --save-dev
// package.json
{
  "scripts": {
    "precommit": "npm test",
    "prepush": "npm test",
    "...": "..."
  }
}

On npm install, that will install git commit hooks for you, and enable them by adding npm scripts

Testing Angular 2

Just kidding :)

Sample App

Testing Components

Testing Components

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

import { MyComponent } from './header.component';


describe('Component: MyComponent', () => {

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

});

Text

Set up each spec and configure the TestBed

Testing Components (fixtures)

let fixture = TestBed.createComponent(MyComponent);

https://angular.io/docs/ts/latest/api/core/testing/ComponentFixture-class.html

 

Returns a fixture for debugging and testing a component.

Testing Components (element)

let elem = fixture.debugElement.nativeElement;

https://angular.io/docs/ts/latest/api/core/testing/ComponentFixture-class.html

 

Returns the debug element associated with this component

Testing Components (component)

let component: HeaderComponent = fixture.debugElement.componentInstance;

https://angular.io/docs/ts/latest/api/core/testing/ComponentFixture-class.html

 

Returns the component instance.

Testing Components

  it('should render a title', () => {
    let fixture = TestBed.createComponent(HeaderComponent);
    let elem = fixture.debugElement.nativeElement;
    let component: HeaderComponent = fixture.debugElement.componentInstance;

    expect(elem.querySelector('h1.header').innerHTML).toBe('ng2 storefront');
  });

Testing Services

Testing Services: Http

  • When testing services that make HTTP calls, we don't want to hit the server with real requests
  • Time constraints, isolate from outside points of failure
  • mock server requests

Testing Services: Http

Services often require dependencies that Angular injects through the constructor of the service's class

@Injectable()
export class StorefrontService {

  constructor(public http: Http) { }

  getProducts(): Observable<Response> {
    return this.http.get('/mock/products.json')
      .map(res => res.json());
  }

}

Testing Services: Http

Setup: configure TestBed with providers

  beforeEach(() => {

    TestBed.configureTestingModule({
     providers: [
       MockBackend,
       BaseRequestOptions,
       {
         provide: Http,
         useFactory: (mockbackend: ConnectionBackend, defaultOptions: BaseRequestOptions) => {
          return new Http(mockbackend, defaultOptions);
        },
        deps: [MockBackend, BaseRequestOptions]
       },
       StorefrontService,
     ]
    });

  });

Testing Services: Http

For each test, use the "async" and "inject" functions in @angular/core/testing

 

use the mock connection to test requests

it('should call api with correct url',
  async(inject(
         [ApiService, MockBackend], 
         (apiService: ApiService, mockBackend: MockBackend) => {
           
           mockBackend.connections.subscribe( (connection: MockConnection) => {
             expect(connection.request.method).toBe(RequestMethod.Get);
             expect(connection.request.url).toBe('/mock/products.json');
           });
            
           apiService.getProducts();
          }
       )
  )
);

Testing Compents with Dependencies

use the "providers" property when configuring the test bed

beforeEach(() => {
    TestBed.configureTestingModule({
      declarations: [AppComponent],
      providers: [
        {
          provide: ApiService,
          useClass: MockApiService
        }
      ]
    });
    
    TestBed.compileComponents();
});
class MockApiService {
  getProducts() {
    return Promise.resolve(mockProductList);
  }
}

Testing Pipes

Format Price

  • There is a "CurrencyPipe", but doesn't work in all browsers
  • Let's create our own
@Pipe({
  name: 'formatPrice'
})
export class FormatPricePipe implements PipeTransform {

  transform(value: number): string {
    return '$' + ( (value / 100).toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ",") );
  }

}

Testing Pipes

Nothing special, it's just a class!

describe('Pipe: FormatPrice', () => {

  let pipe: FormatPricePipe;

  beforeEach(() => {
    pipe = new FormatPricePipe();
  });

  it('should format price correclty', () => {
    expect(pipe.transform(2499)).toBe('$24.99');
  });

});

Links

https://github.com/victormejia/ng2-storefront

http://slides.com/victormejia/unit-testing-ng2

Thanks!

Unit Testing Recipes for Angular 2

By Victor Mejia

Unit Testing Recipes for Angular 2

  • 3,254