Unit Test

Unit Test Tips #

Reset all global variables for each unit test case #

What environment the unit test cases are running on: Browser or Nodejs? #

  • Because Nodejs does not have browser APIs
    • Using Karma to run browser’s unit test
    • Using JS-DOM but it’s missing a lot of browser APIs

Work only when running alone #

Scenario:

A unit test case only pass when running alone but fail when running with other test cases

Check:

  • Restore all mocks after mocking things: sandbox.restore(), jest.restoreAllMocks(), vi.restoreAllMocks() and vi.unstubAllGlobals() at afterEach
  • Reset global variables inner module: create a reset function to reset all variable of module to the initial value

Example:

// calculateThings.js
import cal1Thing from "./private/cal1Thing.js";

let cached = ""; // Global variable

export default (things) => {
  if (cached) return cached;
  let result = [];
  for (let i = 0; i < things.length; i++) {
    const calculatedThing = cal1Thing(things[i]);
    result.push(calculatedThing);
  }
  cached = result;
  return cached;
};

/* start-test-code */
export const testingOnly = {
  resetCached: () => {
    cached = "";
  },
};
/* end-test-code */
// calculateThings.test.js
import calculateThings from "./calculateThings";
import cal1Thing from "./cal1Thing";
import { testingOnly } from "./calculateThings";

vi.mock("./cal1Thing");

describe("calculateThings", () => {
  const { resetCached } = testingOnly;
  afterEach(() => {
    vi.restoreAllMocks();
  });
  it("should work as expected", () => {
    cal1Thing.mockReturnValue("a string");
    const caledThings = calculateThings([1, 2, 3]);
    expect(caledThings).toEqual(["a string", "a string", "a string"]);
  });
  it("should return empty when empty cached and input is empty array", () => {
    resetCached(); // remember reset cached
    const caledThings = calculateThings([]);
    expect(caledThings).toEqual("");
  });
});

Setup code for testing only #

This setup will help you export function only when run test, not appear when build

Gulp - Rollup #

// rollup.bundle.js
import stripCode from "rollup-plugin-strip-code";
import {rollup} from rollup;

const stripcode = stripCode({
  start_comment: "start-test-code",
  end_comment: "end-test-code",
});

export default async () => {
  const bundle = await rollup({input: 'mainFilePath.js', plugins: [stripcode]});
  await bundle.write({
    file: 'dist/destinationName.js',
    format: 'iife',
    name: 'YourObjectName',
    sourcemap: false
  })
}
// gulpfile.js
import rollupBundle from "./rollup.bundle.js";

const clean = () => {
    // remove all previous build files or ST like that
  },
  lint = () => {
    // run eslint warning
  };

export default () => {
  series(clean, rollupBundle, lint);
};

Vite - Vitest #

Mock module #

When you mock a module, everything you exported in this module will be mocked and can not act like original (even if you call vi.restoreAllMocks())

Solution:

If your module exports alots, and you only want to mock one thing, you should split it into another module

Example:

// calculateThings.js
import cal1Thing from "./private/cal1Thing.js";
export default (things) => {
  let result = [];
  for (let i = 0; i < things.length; i++) {
    const calculatedThing = cal1Thing(things[i]);
    result.push(calculatedThing);
  }
  return result;
};
// calculateThings.test.js
import calculateThings from "./calculateThings";
import cal1Thing from "./cal1Thing";

vi.mock("./cal1Thing");

describe("calculateThings", () => {
  afterEach(() => {
    vi.restoreAllMocks();
  });
  it("should work as expected", () => {
    cal1Thing.mockReturnValue("a string");
    const caledThings = calculateThings([1, 2, 3]);
    expect(caledThings).toEqual(["a string", "a string", "a string"]);
  });
});

Mock library #

// authUtils.js
import jwt from "jsonwebtoken";

export const createTokenPair = (payload, privateKey) => {
	try {
		const accessToken = jwt.sign(payload, privateKey, {
			algorithm: "RS256",
			expiresIn: "2 days",
		});
		const refreshToken = jwt.sign(payload, privateKey, {
			algorithm: "RS256",
			expiresIn: "7 days",
		});

		return { accessToken, refreshToken };
	} catch (error) {
		throw new Error("createTokenPair got error");
	}
};
// authUtils.test.js
import { describe, expect, it } from "vitest";
import { createTokenPair, genKeyPairRSA } from "../../src/auth/authUtils";
import jwt from "jsonwebtoken";

describe("auth/authUtils.js", () => {
	describe("createTokenPair", () => {
		afterEach(() => {
			vi.restoreAllMocks();
			vi.unstubAllGlobals();
		});
		it("should throw error when jwt lib got error", () => {
			vi.spyOn(jwt, "sign").mockImplementation(() => {
				throw new Error("JWT error");
			});

			const payload = {
				userId: 123,
				email: "a@b.co",
			};

			const { privateKey, publicKey } = genKeyPairRSA();

			const toBeThrowError = () => {
				const { accessToken, refreshToken } = createTokenPair(
					payload,
					privateKey
				);
			};
			expect(toBeThrowError).toThrowError("createTokenPair got error");
		});
	});
});

Sinon #

Stub a function that is called by another function in the same module #

Using this.[func_name] when calling it in your module

Stub an export default function #

import * as query from "/database/query";
const makeQueryStub = sandbox.stub(query, "default").resolves([]);

Mocha - Chai - Sinon sample #

import sinon from "sinon";
import { function_name, callback_function_name } from "../module_name.js";

const sandbox = sinon.createSandbox();

describe("module_name", function () {
  afterEach(function () {
    sandbox.restore();
  });
  describe("function_name", function () {
    it("Should be a function", function () {
      expect(function_name).to.be.a("function");
    });
    it("Should return this value if window.screen is undefined", function () {
      sandbox.stub(window, "screen").value(undefined);
      expect(function_name()).equal("expected string");
    });
    it("should return expected object when running callback function", function (done) {
      callback_function_name(function (returnedData) {
        expect(returnedData).to.deep.equal({ name: "expected object" });
        done();
      });
    });
  });
});

Jest - Sinon sample #

import sinon from "sinon";
import { function_name, async_function_name } from "../module_name.js";

const sandbox = sinon.createSandbox();

describe("module_name", function () {
  afterEach(function () {
    sandbox.restore();
  });
  describe("function_name", function () {
    it("Should be a function", function () {
      expect(typeof function_name).toEqual("function");
    });
    it("Should return this value if window.screen is undefined", function () {
      sandbox.stub(window, "screen").value(undefined);
      expect(function_name()).toEqual("expected string");
    });
    it("should return expected object when handling function asynchronously", async () => {
      const returnedData = await async_function_name();
      expect(returnedData).toEqual({ name: "expected object" });
    });
  });
});