Thi's avatar
HomeAboutNotesBlogTopicsToolsReading
About|My sketches |Cooking |Cafe icon Support Thi
💌 [email protected]

Unit Testing with JestJS

Anh-Thi Dinh
Backend
Left aside

References

  • Official doc.
  • Using with MongoDB · Jest —
    jest-mongodb
    (more docs)

Run a single test / file

Basic

Sample

👇 https://dev.to/jackcaldwell/mocking-es6-class-methods-with-jest-bd7

Folder conventions

(SO source) The conventions for Jest, in order of best to worst in my opinion:
  1. src/file.test.js mentioned first in the Getting Started docs, and is great for keeping tests (especially unit) easy to find next to source files
  1. src/__tests__/file.js lets you have multiple __tests__ directories so tests are still near original files without cluttering the same directories
  1. __tests__/file.js more like older test frameworks that put all the tests in a separate directory; while Jest does support it, it's not as easy to keep tests organized and discoverable

Testing with Date()

Becareful that,
For example, "2011-10-10" (date-only form), "2011-10-10T14:48:00" (date-time form), or "2011-10-10T14:48:00.000+09:00" (date-time form with milliseconds and time zone) can be passed and will be parsed. When the time zone offset is absent, date-only forms are interpreted as a UTC time and date-time forms are interpreted as local time.
→ Sử dụng UTC time sẽ an toàn hơn (và ko bị ảnh hưởng bởi local time hay timezone)

Differences

beforeAll vs beforeEach

👇 This SO.
If the tests do make changes to those conditions, then you would need to use beforeEach, which will run before every test, so it can reset the conditions for the next one.

resetAllMocks, resetModules, resetModuleRegistry, restoreAllMocks

👉 Difference between resetAllMocks, resetModules, resetModuleRegistry, restoreAllMocks in Jest - Stack Overflow

jest.fn() vs jest.spyOn()

👉 Read more.
  • They are all used for mocking a method.
  • jest.fn() → return undefined if no implementation is provided. (use these methods to implement)
  • jest.spyOn() → default it calls the original implementation, it stores the original implementation in memory. ← restore by mockRestore()

Using .env variable with Jest

Put import 'dotenv/config'; at the beginning of the .spec file. It will use the current env file.
If we wanna mock some variables?

Jest CLI

  • --runInBand (or -i): source — Run all tests serially in the current process, rather than creating a worker pool of child processes that run tests.

Reset and restore

  • If using spyOn() → restoreAllMocks() in afterEach()
  • javascript - Difference between resetAllMocks, resetModules, resetModuleRegistry, restoreAllMocks in Jest - Stack Overflow

Mocking

⭐ Good to read: Mocking functions and modules with Jest | pawelgrzybek.com
👇 Source.

Basic

Mocking a named import → official jest.mock()
Mocking only the named import (and leaving other imports unmocked) → official jest.requireActual
Mocking a default import
Mocking default and named imports
Changing what the mock returns per test
→ Be careful: Calling mockReturnValue inside a test still changes the mock for all other tests after it. ← use mockReturnValueOnce instead!
Clearing mocks between tests (make sure it’ll be called once) → clearAllMocks
Mocking multiple modules with chaining,

Give mock a name

→ Use mockName() and getMockName()

Mock dependencies of a class

⚠️
This is just an example of my very specific case. It may not good (or accurate) enough!

Mock a class

If a class is not a default export from a module (official doc),

Mock get, static in a class

Read this official doc.

Mock a specific method in the class

👇 This SO answer!

Mock a module for different tests

👇 ⭐ Source (read the exmplanation there)
If we wanna import a constant?
How about a method?
(We don’t need import * from ... and also no need __esModule)

(Optional) Mock a client of @google-cloud/dialogflow

Mock got package

got has itself got and other methods get, post,…

Reject / Test Exceptions

Read this official doc.
There is also a case where in the main funtion, we throw new ClassName() and we want to catch this error in the test file!

Mock reject a function inside

Bên trong hàm cần được test có 1 hàm (updateEntityTypeSpy) và ta muốn mock hàm này throw an error để có thể test.

Testing Observable

Tips

Create a “util” function for all tests → create it in .stub.ts and then export it and import it in the .spec.ts file.

Troubleshooting

❇️  mach-o file, but is an incompatible architecture (have 'x86_64', need 'arm64'))
(On Mac M1) → make sure the terminal is open under arm architecture (run arch too see) → then reinstall everything,
❇️  Jest did not exit one second after the test run has completed.
(Not a perfect solution) ← just not to see but not solve the problem internally
✳️ Unexpected directive 'TinyBotSpinnerComponent' imported by the module 'DynamicTestModule'. Please add an @NgModule annotation.
Still not know!!!!
❇️ Jest: Cannot spy the property because it is not a function; undefined given instead getting error while executing my test cases
✳️ RequestError: getaddrinfo ENOTFOUND us-undefined
→ Forget to .mockResolvedValue()
It’s good to do something like this,
◆References◆Run a single test / file◆Basic○Sample○Folder conventions○Testing with Date()◆Differences○beforeAll vs beforeEach○resetAllMocks, resetModules, resetModuleRegistry, restoreAllMocks○jest.fn() vs jest.spyOn()◆Using .env variable with Jest◆Jest CLI◆Reset and restore◆Mocking○Basic○Give mock a name○Mock dependencies of a class○Mock a class○Mock get, static in a class○Mock a specific method in the class○Mock a module for different tests○(Optional) Mock a client of @google-cloud/dialogflow○Mock got package◆Reject / Test Exceptions○Mock reject a function inside◆Testing Observable◆Tips◆Troubleshooting
About|My sketches |Cooking |Cafe icon Support Thi
💌 [email protected]
1# From 2019
2npm test -- path/to/file.spec.js
1// We wanna mock this class
2export class ProductsClient {
3  async getById(id) {
4    const url = `http://localhost:3000/api/products/{id}`;
5    const response = await fetch(url);
6    return await response.json();
7  }
8}
9
10// We wanna test this class
11export class ProductManager {
12  async getProductToManage(id) {
13    const productsClient = new ProductsClient();
14    const productToManage = await productsClient.getById(id)
15      .catch(err => alert(err));
16    return productToManage;
17  }
18}
1// In the file of testing class ProductManager
2import { ProductsClient } from './ProductsClient';
3jest.mock('./ProductsClient');
4
5// A "mock" getById() which returns "undefined" will be created
6// But we want the mock function returns a value as we want
7
8// assign a mock function to the ProductsClient's 'getById' method
9const mockGetById = jest.fn();
10ProductsClient.prototype.getById = mockGetById;
11// We can make the mock function return what we want
12mockGetById.mockReturnValue(Promise.resolve(expectedProduct));
13
14// Restore the state of the original class
15mockFn.mockClear() // or something like that (check the doc)
1describe('process.env', () => {
2    const env = process.env
3
4    beforeEach(() => {
5        jest.resetModules() // important!!!
6        process.env = { ...env }
7    })
8
9    afterEach(() => {
10        process.env = env
11    })
12})
13
14it('should mock process.env', () => {
15    process.env.NODE_ENV = 'development'
16    console.log(process.env.NODE_ENV) // Will be "development"
17})
18
19it('should not mock process.env', () => {
20    console.log(process.env.NODE_ENV) // Will be "test"
21})
1// Use in a single suit
2describe('Enabled Zerobounce validation', () => {
3  beforeAll(() => {
4    process.env.ZEROBOUNCE_USE = 'true';
5  });
1// Usage
2import { getTime } from './time';
3
4// test.js
5jest.mock('./time', () => ({
6    getTime: () => '1:11PM',
7}));
1// Usage
2import { getTime, isMorning } from './time';
3
4// test.js
5jest.mock('./time', () => ({
6    ...jest.requireActual('./time'), 
7    getTime: () => '1:11PM',
8    // isMorning will return its true value
9}));
1// Usage
2import getDayOfWeek from './time';
3
4// test.js
5jest.mock('./time', () => () => 'Monday');
1// Usage
2import getDayOfWeek, { getTime } from './time';
3
4// test.js
5jest.mock('./time', () => ({
6    __esModule: true,
7    default: () => 'Thursday'
8    getTime: () => '1:11PM',
9}));
1import getDayOfWeek from './time';
2
3jest.mock('./time', () => jest.fn());
4
5test('App renders Monday', () => {
6    getDayOfWeek.mockReturnValue('Monday');
7    //...
8});
9
10test('App renders Tuesday', () => {
11    getDayOfWeek.mockReturnValue('Tuesday');
12    //...
13});
14
15test('App renders Monday, again', () => {
16    // Fails
17});
1beforeEach(() => {
2    jest.clearAllMocks();
3});
1jest.mock('./time', () => jest.fn())
2    .mock('./space', () => jest.fn());
1// df.service.ts
2export class DialogflowService {
3	constructor(private loggerService, botId){}
4	method() {
5		this.loggerService.log('abc');
6		// main codes
7	}
8}
1// df.service.spec.ts
2beforeAll(() => {
3	loggerService = {
4    log: jest.fn()
5  };
6	const botId = 'fake-bot-id';
7	service = new DialogflowService(loggerService, botId);
8})
1// my-class.ts
2class MyClass {
3  constructor(name) {
4    this.name = name;
5  }
6  methodOne() {
7    return 1;
8  }
9  methodTwo() {
10    return 2;
11  }
12}
13export default MyClass;
1// my-class.spec.ts
2import testSubject from './testSubject';
3jest.mock('./myClass', () => ({
4  name: 'Jody',
5  methodOne: () => 10,
6  methodTwo: () => 25,
7}));
1import {SoundPlayer} from './sound-player';
2jest.mock('./sound-player', () => {
3  // Works and lets you check for constructor calls:
4  return {
5    SoundPlayer: jest.fn().mockImplementation(() => {
6      return {playSoundFile: () => {}};
7    }),
8  };
9});
1// Original class
2export default class Person {
3    constructor(first, last) {
4        this.first = first;
5        this.last = last;
6    }
7    sayMyName() { // wanna mock
8        console.log(this.first + " " + this.last);
9    }
10    bla() { // wanna keep
11        return "bla";
12    }
13}
1// Test
2import Person from "./Person";
3
4test('Modify only instance', () => {
5    let person = new Person('Lorem', 'Ipsum');
6    let spy = jest.spyOn(person, 'sayMyName').mockImplementation(() => 'Hello');
7
8    expect(person.sayMyName()).toBe("Hello");
9    expect(person.bla()).toBe("bla");
10
11    // unnecessary in this case, putting it here just to illustrate how to "unmock" a method
12    spy.mockRestore();
13});
1// app.js
2import { CAPITALIZE } from './config';
3export const sayHello = (name) => {
4  let result = 'Hi, ';
5
6  if (CAPITALIZE) {
7    result += name[0].toUpperCase() + name.substring(1, name.length);
8  } else {
9    result += name;
10  }
11  return result;
12};
1// app.spec.ts
2import { sayHello } from './say-hello';
3import * as config from './config';
4// 👇 For typescipt: type casting
5const mockConfig = config as { CAPITALIZE: boolean };
6
7jest.mock('./config', () => ({
8  __esModule: true, // important for using import * from ...
9  CAPITALIZE: null // default: null if export default CAPITALIZE;
10}));
11
12describe('say-hello', () => {
13  test('Capitalizes name if config requires that', () => {
14    mockConfig.CAPITALIZE = true;
15
16    expect(sayHello('john')).toBe('Hi, John');
17  });
18
19  test('does not capitalize name if config does not require that', () => {
20    mockConfig.CAPITALIZE = false;
21
22    expect(sayHello('john')).toBe('Hi, john');
23  });
24});
1import { sayHello } from './say-hello';
2import { shouldCapitalize } from './config';
3
4jest.mock('./config', () => ({
5  shouldCapitalize: jest.fn()
6}));
7
8describe('say-hello', () => {
9  test('Capitalizes name if config requires that', () => {
10    shouldCapitalize.mockReturnValue(true);
11
12    expect(sayHello('john')).toBe('Hi, John');
13  });
14
15  test('does not capitalize name if config does not require that', () => {
16    shouldCapitalize.mockReturnValue(false);
17
18    expect(sayHello('john')).toBe('Hi, john');
19  });
20});
1// not working 
2expect(new TestObject()).toThrow();
3// Because new TestObject() is evaluated first
4
5// working
6expect(() => { new TestObject() }).toThrow();
1it('If there are problems with the API?', async () => {
2  // Mock
3  const serviceIdObjects = fakeServiceIdObjects().entities;
4  const lexIds = serviceIdObjects.slice(0, 2).map(obj => obj.idetaId);
5  updateEntityTypeSpy = jest
6    .spyOn(service, 'updateEntityType')
7    .mockRejectedValueOnce('Error')
8    .mockResolvedValueOnce();
9
10  // Call the method
11  await service
12    .updateLexiconsInDialogflow(...)
13    .catch(() => {
14      expect(updateEntityTypeSpy).toBeCalledTimes(1);
15    });
16
17  // Expectations
18  expect(updateIntentSpy).not.toBeCalled();
19  expect(removeLexiconServiceIdSpy).not.toBeCalled();
20  expect(removeLexiconStatusSpy).not.toBeCalled();
21});
1it('should return an object of given type', done => {
2  of(mockSnapshot()).pipe(service.methodToTest()).subscribe(res => {
3    expect(res).toStrictEqual(fakeReturnedObj);
4    done();
5  });
6});
1rm -rf node_modules && rm package-lock.json && npm i
1# Run with
2--detectOpenHandles --forceExit
1// Error
2const getAgentSpy = jest.spyOn(service, 'getAgent');
3// Without error
4const getAgentSpy = jest.spyOn(service, 'getAgent').mockResolvedValue(null);
1describe('💎 freshExport()', () => {
2  let getAgentSpy: jest.SpyInstance;
3	
4	beforeEach(() => {
5    jest.clearAllMocks();
6    getAgentSpy = jest.spyOn(service, 'getAgent');
7	});
8	
9	it('should call all the necessary methods', async () => {
10    getAgentSpy.mockResolvedValue(null);
11	});
12});