Skip to content

Testing

Alex Chugaev edited this page Nov 14, 2018 · 3 revisions

General information

Test runner is Jest.

Testing is a part of build process. In case at least one test fail, whole Travis build will be marked as failed.

Test coverage is important metric. Coverage threshold is 90%.

To run tests locally, run:

npm test

File structure and naming conventions

Tests located in test directory of each package. Test file name must end up with .test.ts. Specification file name must end up with .spec.ts. Test files will be automatically detected and executed by Jest (see configuration in main package.json)

For each class or interface in src directory there should be test or specification in test directory with the same path.

packages/
  package/
    src/
      Lifecycle.ts
      ChildProcess.ts
    test/
      Lifecycle.spec.ts
      ChildProcess.test.ts
    package.json
    ...

Test and specification files

Core difference between them: test files describes test scenarios for classes, specification files describes test scenarios for interfaces. Specifications can be re-used. That's why specifications exported as function.

Example of specification testing

Interface

// src/Lifecycle.ts

export interface Lifecycle {
  readonly isRunning: boolean;
  public start(): boolean;
  public stop(): boolean;
}

Specification test suite

// test/Lifecycle.spec.ts

export function testLifecycle(create: () => Lifecycle) {
  describe('Lifecycle', () => {
    describe('start()', () => {
      it('should start lifecycle', () => {
        const component: Lifecycle = create();

        expect(component.isRunning).toBe(false);
        expect(component.start()).toBe(true);
        expect(component.isRunning).toBe(true);
      }

      it('should not take effect when being called more then once in row', () => {
        const component: Lifecycle = create();

        expect(component.isRunning).toBe(false);
        expect(component.start()).toBe(true);
        expect(component.isRunning).toBe(true);
        expect(component.start()).toBe(false);
        expect(component.isRunning).toBe(true);
      }
    }
  }
}

Interface implementation

// src/ChildProcess.ts

export class ChildProcess implements Lifecycle {
  // implementation omitted
}

Implementation test

// test/ChildProcess.test.ts
import {testLifecycle} from './Lifecycle.spec';

function create(): ChildProcess {
  // return new instance of child process
}

describe('ChildProcess', () => {
  testLifecycle(create);

  // additional tests of child process
});

Test template

describe('ClassOrInterfaceName', () => {
  // beforeEach() and other hooks at the beginning of 'describe'

  // add parentheses to show that it's a test of method
  describe('method()', () => {
    // start test case with "should", example: "it should start lifecycle"
    it('should <description of behavior>', () => {
      // PREPARE
      // Create or obtain instance
      // Check pre-conditions in needed

      // ACT
      // Do something with instance

      // CHECK
      // expect(...).toBe(...)
    });
  });

  // describe signature of method, it's especially useful when testing overloads of method
  describe('method(string, number)', () => {
    it('should <description of behavior>', () => {
      // PREPARE
      // Create or obtain instance
      // Check pre-conditions in needed

      // ACT
      // Do something with instance

      // CHECK
      // expect(...).toBe(...)
    });
  });

  describe('propertyName/accessorName', () => {
    it('should <description of behavior>', () => {
      // PREPARE
      // Create or obtain instance
      // Check pre-conditions in needed

      // ACT
      // Do something with instance

      // CHECK
      // expect(...).toBe(...)
    });
  });
});