So imagine if we have an express route with something similar to the following:
//routes.ts this.router.get("/app-version", function (req, res) { //some code logic ... res.status(200).json({version}); }));
It becomes difficult to test the logic within.
We want to extract out the relevant function into a controller to make things simpler.
//routes.ts
this.router.get("/app-version", getAppVersion);//controller.ts
export const getAppVersion = (req: Request, res: Response) => {
try {
const version = utilityService.getAppVersion();
res.status(200).json({ version });
} catch (err) {
const msg = "Unable to retrieve version information";
logger.error(msg, err);
res.status(500).json({ msg: msg, exception: err });
}
};
After which, we will be able to test the function for getAppVersion
for the inputs, responses and all other function calls within.
Looking at the function, there are a few things we want to test.
1. The utilityService.getAppVersion()
is called
2. The call is successful and returns with 200 status as well as the version in JSON format.
3. If utilityService.getAppVersion()
fails, we want to be able to log the error
4. And return 500 status with error messages
First, we would need to define the request and response that we will be passing in. We want to mock the status
and json
functions so that we can check those when the server sends a response back to the client.
const req: Request = {};
const res: Response = {};
const mockStatus = jest.fn().mockReturnValue(res);
res.status = mockStatus;
const mockJson = jest.fn().mockReturnValue(res);
res.json = mockJson;
Next, we would want to spyOn the utilityService.getAppVersion
function to see if it was called.
const spyGetAppVersion = jest.spyOn(utilityService, "getAppVersion");
Depending on the type of testing that we are intending to implement — unit or integration tests, we can mock the result using .mockReturnValue
or .mockResolveValue
based on whether the function is synchronous or asynchronous.
We do the same spyOn for logger.error
to detect error
calls made by logger
in the catch block.
One other thing to note is that since we are testing 2 paths here — happy path (try block) and the unhappy path (catch block), we would need to reset the mocks. In this case, I am using spyGetAppVersion.mockReset()
to reset the getAppVersion
back to the original spy instance.
Here’s an example of the test file
//controller.test.ts
import { Request, Response } from "express";import { getAppVersion } from "../Util";
import { utilityService } from "../../services/Utility";
import { logger } from "../../utils/logger";describe(".getAppVersion", () => {
// @ts-ignore
const req: Request = {};
// @ts-ignore
const res: Response = {};
const mockStatus = jest.fn().mockReturnValue(res);
res.status = mockStatus;
const mockJson = jest.fn().mockReturnValue(res);
res.json = mockJson;const spyGetAppVersion = jest.spyOn(utilityService, "getAppVersion");
const spyLogger = jest.spyOn(logger, "error");describe("happy path", () => {
const mockVersion = "0.0.0";
beforeAll(() => {
spyGetAppVersion.mockReturnValue(mockVersion);
getAppVersion(req, res);
});it("calls UtilityService.getAppVersion and gets a value", () => {
expect(spyGetAppVersion).toHaveBeenCalledTimes(1);
expect(spyGetAppVersion).toHaveReturnedWith(mockVersion);
});it("returns status 200 and version", () => {
expect(res.status).toHaveBeenCalledWith(200);
expect(res.json).toHaveBeenCalledWith({ version: mockVersion });
});afterAll(() => {
spyGetAppVersion.mockReset();
});
});describe("unhappy path", () => {
const mockError = "mock error";
const expectedErrorHeader = "Unable to retrieve version information";
beforeAll(() => {
spyGetAppVersion.mockImplementation(() => {
throw new Error("mock error");
});
getAppVersion(req, res);
});it("calls UtilityService.getAppVersion and gets an error", () => {
expect(spyGetAppVersion).toHaveBeenCalledTimes(1);
expect(spyGetAppVersion).not.toHaveReturned();
});it("logs error", () => {
expect(spyLogger).toHaveBeenCalledWith(
expectedErrorHeader,
new Error(mockError)
);
});it("returns status 500 and error", () => {
expect(res.status).toHaveBeenCalledWith(500);
expect(res.json).toHaveBeenCalledWith({
msg: expectedErrorHeader,
exception: new Error(mockError)
});
});afterAll(() => {
spyGetAppVersion.mockReset();
});
});
});