Why Angular (8.2.x)

It is a well established framework for building single page and mobile applications with lots of users.

It provides everything in one framework to build your application.

There is no need to pick between libraries to use to build an application.

It has a built-in IoC Container to inject your dependencies.

It has a built-in testing framework (which we will replace a part of it with Jest).

There are lots of StackOverflow posts to help you through tough areas during development.

Why Jest (24.9.x)

It is an all-in-one testing framework that, “works out of the box,” and it does!

Supports snapshot testing of components.

Runs tests in parallel.

Does not require a browser (by default Jest uses jsdom ). This is great for CI environments.

). This is great for CI environments. Has built-in coverage reporting.

Why Spectator (4.0.x)

Reduces boilerplate required to set up an Angular test suite.

Built-in mocking.

Supports Jasmine or Jest out of the box.

Helps keep your tests clean and focused.

Setup

Create a new Angular project using the @angular/cli . Choose the default options when asked.

The Angular docs suggests that you install the @angular/cli globally, but you do not have to. You can simply use npx to run the cli once to create a new project which will include the cli as a devDependencies .

$ npx -p @angular/cli ng new angular-jest-spectator ? Would you like to add Angular routing? // answer: 'N'

? Which stylesheet format would you like to use? // answer: 'CSS'

Once the project is created, cd into angular-jest-spectator and run npm test -- -no-watch --browsers=ChromeHeadless to see that the stubbed tests pass.

$ cd angular-jest-spectator $ npm test -- --no-watch --browsers=ChromeHeadless > angular-jest-spectator@0.0.0 test /angular-jest-spectator

> ng test --no-watch --browsers=ChromeHeadless 30% building 16/16 modules 0 active

INFO [launcher]: Launching browsers ChromeHeadless with concurrency unlimited

INFO [launcher]: Starting browser ChromeHeadless

INFO [HeadlessChrome 76.0.3809 (Mac OS X 10.14.6)]: Connected on socket mMeLgzT2Z6Kc7RVYAAAA with id 93795886 INFO [karma-server]: Karma v4.1.0 server started at http://0.0.0.0:9876/ INFO [launcher]: Launching browsers ChromeHeadless with concurrency unlimitedINFO [launcher]: Starting browser ChromeHeadlessINFO [HeadlessChrome 76.0.3809 (Mac OS X 10.14.6)]: Connected on socket mMeLgzT2Z6Kc7RVYAAAA with id 93795886 HeadlessChrome 76.0.3809 (Mac OS X 10.14.6): Executed 3 of 3 SUCCESS (0.326 secs / 0.274 secs) TOTAL: 3 SUCCESS

Replacing Jasmine and Karma with Jest

Delete the packages that start with or contain jasmine or karma from the devDependencies section of the package.json file.

Delete the karma.config.js and the src/test.ts files.

Install the required jest packages using npm. This will also uninstall and remove the jasmine and karma packages that were deleted above:

$ npm install --save-dev jest jest-preset-angular @types/jest

Remove the test entry from the angular.json file and update the test script in the package.json to call jest .

// package.json {

...

"scripts": {

...

"test": "jest",

...

},

...

}

Update the compilerOptions in the tsconfig.spec.json with the following entries:

types — Remove the "jasmine" entry and add "jest" so you are left with: types: ["node", "jest"] .

— Remove the entry and add so you are left with: . emitDecoratorMetadata: true — Adding this will help with testing when you need to set up mocks for services that are injected into other services or components without having to mark them with @Inject .

— Adding this will help with testing when you need to set up mocks for services that are injected into other services or components without having to mark them with . esModuleInterop: true — Add this to clear the jest warning messages for jest package module imports.

— Add this to clear the warning messages for jest package module imports. files — Remove the src/test.ts entry.

// tsconfig.spec.json {

...

"compilerOptions": {

"outDir": "./out-tsc/spec",

"types": [

"jest",

"node"

],

"emitDecoratorMetadata": true,

"esModuleInterop": true

},

"files": [

"src/polyfills.ts"

]

...

}

Add a setupJest.ts file at the root of the src folder and the following implementation:

// src/setupJest.ts import 'jest-preset-angular';

Add a jest.config.js file a the root of the project to override some of the base configurations from the jest-preset-angular package.

If you tried to run npm test now, it would fail. This is because the jest-preset-angular package tries to look for the tsconfig.spec.json file in the src directory instead of at the project root where the angular/cli put it during generation.

Make the following changes to the jest.config.js file to fix the tsconfig pathing issue. The bold changes are the important ones:

// jest.config.js (located at the project root) // base config from jest-present-angular

const jestPreset = require('jest-preset-angular/jest-preset'); const { globals } = jestPreset;

const tsjest = globals['ts-jest']; // set the correct path to the spect ts-config file

// the default for the jest-preset-angular package

// points to an incorrect path:

// <rootDir/src/tsconfig.spec.js

const tsjestOverrides = {

...tsjest,

tsConfig: '<rootDir>/tsconfig.spec.json'

}; const globalOverrides = {

...globals,

'ts-jest': { ...tsjestOverrides }

}; // make sure to add in the required preset and

// and setup file entries

module.exports = {

...jestPreset,

globals: { ...globalOverrides },

preset: 'jest-preset-angular',

setupFilesAfterEnv: ['<rootDir>/src/setupJest.ts']

};

At this point, running npm test , should run using Jest and all of the tests should still pass:

$ npm test > angular-jest-spectator@0.0.0 test /angular-jest-spectator

> jest PASS src/app/app.component.spec.ts AppComponent

✓ should create the app (224ms)

✓ should have as title 'angular-jest-spectator' (135ms)

✓ should render title (151ms) Test Suites: 1 passed, 1 total

Tests: 3 passed, 3 total

Snapshots: 0 total Time: 3.796s Ran all test suites.

You can optionally update the src/app/app.component.spec.ts file’s it blocks with test blocks. They are both functionally the same in jest if you choose to keep them as it .

Add Spectator

In a medium to large project, there will be lots of tests with the same boilerplate setup required to compile and run the component, service, directive, etc. that are under test. We can save ourselves a lot of time by using Spectator to abstract out the noise from the boilerplating.

Start by installing the Spectator package:

$ npm install --save-dev @ngneat/spectator

Next, update the spec files with the Spectator setup. Make sure to import the /jest variant of the Spectator package:

// app.component.spec.ts import {

createComponentFactory,

Spectator

} from '@ngneat/spectator/jest'; // with Spectator:

describe('AppComponent', () => {

const createComponent = createComponentFactory({

component: AppComponent

}); let spectator: Spectator<AppComponent>; beforeEach(() => spectator = createComponent()); it('should create the app', () => {

const app = spectator.component;

expect(app).toBeTruthy();

}); // more 'it' blocks

}); // before (without Spectator):

// ignore the lines below, they are for comparison only

describe('AppComponent', () => {

beforeEach(async(() => {

TestBed.configureTestingModule({

declarations: [

AppComponent

]

}).compileComponents();

})); it(‘should create the app’, () => {

const fixture = TestBed.createComponent(AppComponent);

const app = fixture.debugElement.componentInstance;

expect(app).toBeTruthy();

}); // more ‘it’ blocks

});

Run npm test again to verify that tests are still passing!

$ npm test > angular-jest-spectator@0.0.0 test /angular-jest-spectator

> jest PASS src/app/app.component.spec.ts AppComponent

✓ should create the app (224ms)

✓ should have as title 'angular-jest-spectator' (143ms)

✓ should render title (159ms) Test Suites: 1 passed, 1 total

Tests: 3 passed, 3 total Snapshots: 0 total Time: 2.259s Ran all test suites.

Common Use Cases

Using the createComponentFactory(options: {}) function for creating components that are under test:

shallow: true — When you want to only render the component under test and not any of the nested components in its template. You can set this option to true so you do not have to worry about mocking the nested components in the template.

— When you want to only render the component under test and not any of the nested components in its template. You can set this option to so you do not have to worry about mocking the nested components in the template. detectChanges: false — By default this is set to true so the lifecycle hooks for a component (think ngOnInit ) will run for each test/it block. If you set this to false , you can manually trigger the change detection with spectator.detectChanges() in each test/it block that you need. This will allow you to mock/spy on services that might be called in the lifecycle hooks before they run.

During development, you can run npm run test -- --watch or add a new npm script called test:watch that will start Jest in watch mode to watch and run only tests against files that have changed.

Add the following test:watch script to your package.json file and run it in the terminal with npm run test:watch .

// package.json {

...

"scripts": {

...

"test:watch": "npm run test --watch",

...

},

...

}

Caveats