I love testing

I love the idea of testing

Testing is hard

What is Jest?

Why use Jest?

Installation

npm install --save-dev jest-preset-angular

jest.config.js

          
module.exports = {
  preset: 'jest-preset-angular',
  roots: ['src'],
  setupTestFrameworkScriptFile: '<rootDir>/src/setup-jest.ts',
}

          
        

setup-jest.ts

          
import "jest-preset-angular";

Object.defineProperty(document.body.style, "transform", {
  value: () => {
    return {
      enumerable: true,
      configurable: true
    };
  }
});
          
        

Running tests

package.json

          
"scripts": {
  ...
  "test": "jest"
}
          
          
npm test
          
        
>

Test Output

          
FAIL  src/sum.spec.ts
  ● adds 1 + 2 to equal 3

    expect(received).toBe(expected) // Object.is equality

    Expected value to be:
      4
    Received:
      3

      1 | const sumFunc = require('./sum');
      2 |
    > 3 | test('adds 1 + 2 to equal 3', () => {
      4 |   expect(sumFunc(1, 2)).toBe(4);
      5 | });
      6 |

      at src/sum.spec.ts:3:27

Test Suites: 1 failed, 1 passed, 2 total
Tests:       1 failed, 3 passed, 4 total
Snapshots:   0 total
Time:        3.297s
Ran all test suites.
          
        
>

Code Coverage

package.json

                
      "scripts": {
        ...
        "test": "jest --coverage"
      }
                
              
>

Test Coverage Output

          
 PASS  src/sum.spec.ts
 PASS  src/app/app.component.spec.ts
---------------------|----------|----------|----------|----------|----------------|
File                 |  % Stmts | % Branch |  % Funcs |  % Lines |Uncovered Lines |
---------------------|----------|----------|----------|----------|----------------|
All files            |      100 |      100 |      100 |      100 |                |
 src                 |      100 |      100 |      100 |      100 |                |
  sum.ts             |      100 |      100 |      100 |      100 |                |
 src/app             |      100 |      100 |      100 |      100 |                |
  app.component.html |      100 |      100 |      100 |      100 |                |
  app.component.ts   |      100 |      100 |      100 |      100 |                |
---------------------|----------|----------|----------|----------|----------------|

Test Suites: 2 passed, 2 total
Tests:       4 passed, 4 total
Snapshots:   0 total
Time:        3.527s
          
        
>

Jest CLI Options

          
    jest sum --coverage // filters test files matching sum
    npm test -- sum // same

    jest --watch // watch mode 
    npm test -- --watch // same
          
        

Watch Mode

          
Watch Usage
 › Press a to run all tests.
 › Press f to run only failed tests.
 › Press p to filter by a filename regex pattern.
 › Press t to filter by a test name regex pattern.
 › Press q to quit watch mode.
 › Press Enter to trigger a test run.
          
        

Jest Globals

          
    afterAll(fn, timeout)
    afterEach(fn, timeout)
    beforeAll(fn, timeout)
    beforeEach(fn, timeout)
    describe(name, fn)
    describe.only(name, fn)
    describe.skip(name, fn)
    require.requireActual(moduleName)
    require.requireMock(moduleName)
    test(name, fn, timeout)
    test.only(name, fn, timeout)
    test.skip(name, fn)
          
        

A Simple Action

          
    export class Select implements Action {
      readonly type = CustomerActionTypes.Select;

      constructor(public payload: Customer) {}
    }  
          
        

Action Testing

          
    describe('Customer Select', () => {
      test('should set selected id', () => {
        const customer = <Customer>{ id: 1, name: 'Test User' },
          action = new CustomerActions.Select(customer);

        expect(action.type).toBe('[Customer] Select');
        expect(action.payload).toBe(customer);
      });
    });
          
        

Snapshot Testing

  • Compares expectation to stored snapshot
  • Stores a snapshot if it doesn't exist
  • Performs diff on snapshot
  • Easily update snapshots on refactor

Snapshot Test Example

          
    test('should set customer to payload in snapshot', () => {
      const action = new CustomerActions.Select(customer);

      expect(action.payload).toMatchSnapshot();
    });
          
        

Snapshot Stored

          
    exports[`Customer Actions Customer Select should set customer to payload in snapshot`] = `
    Object {
      "id": 1,
      "name": "Test User",
    }
    `;
          
        

Sample Reducer

          
  export function reducer(state = initialState, action: CustomerActions): State {
    switch (action.type) {
      case CustomerActionTypes.Select: {
        return {
          ...state,
          selectedCustomerId: action.payload.id
        };
      }

      default: {
        return state;
      }
    }
  }
          
        

Testing the Reducer

          
  describe('Customer Select Action', () => {
    test('should set selectedCustomerId', () => {
      const action = new Select(customer),
        result = reducer(initialState, action);

      expect(result).toMatchSnapshot();
    });
  });
          
        

Sample Effect

          
    @Effect()
    load$: Observable<Action> = this.actions$.pipe(
      ofType<Load>(CustomerActionTypes.Load),
      switchMap(query => {
        return this.customersService
          .load()
          .pipe(
            map((customers: Customer[]) => new LoadSuccess(customers)),
            catchError(err => of(new LoadFail(err)))
          );
      })
    );
          
        

RxJs Marbles

Testing the Effect

          
    describe('load$', () => {
      it('should return a new customer.LoadSuccess, with the customers, on success', () => {
        const action = new Load(),
          completion = new LoadSuccess(customers),
          response = cold('a|', { a: customers }),
          expected = cold('-b', { b: completion });

        actions$.stream = hot('-a', { a: action });

        // mock the load function to be the response
        customersService.load = jest.fn(() => response);

        expect(effects.load$).toMatchSnapshot(expected);
      });
    });
          
        

Testing the Effect Error Path

          
    it('should return a new customer.LoadError, on fail', () => {
      const action = new Load(),
        completion = new LoadFail('error'),
        response = cold('#'),
        expected = cold('-b', { b: completion });

      actions$.stream = hot('-a', { a: action });

      // mock the load function to be the response
      customersService.load = jest.fn(() => response);

      expect(effects.load$).toMatchSnapshot(expected);
    });
          
        

Components

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

  const customers = [
      { id: 1, name: 'test1' }
    ];

  test('renders customers markup to snapshot', () => {
    component.customers = customers;
    fixture.detectChanges();

    expect(fixture).toMatchSnapshot();
  });
});
          
        

BrieBug - What We do

  • Architecture Reviews
  • Customized Training
  • Angular Consulting
http://confsnap.com/#/event/ng-conf-18/123
Jesse Sanders
@JesseS_BrieBug
jesse.sanders@briebug.com

References