If your code is well tested you have greater confidence in releasing it. Unit testing is a great way to eliminate some of the commonly occurring errors early in the development cycle. As an example think of a function in server side code that accepts values for length and width and returns the area of a rectangle. When writing this function ideally you would want to catch all the errors in your code during development and one way to do that would be by writing tests. In this post, you will see how to mock with Jest.spyOn for nodejs & Typescript controllers. All the code shown in this post is available for you to download from Github, continue reading to find out how to access the Github repository.

Scenario

This scenario will present code for nodejs controller files written in Typescript. Each function corresponds to an endpoint, as such it will accept a request and returns a response.

Mock with Jest.spyOn for nodejs & Typescript

Index.ts code that maps the routes to the controller functions starting with the code for index.ts.

...
app.get('/weather', getWeather);
app.get('/news/', getNews);

app.post('/user/', saveUser);
app.get('/user/:id', getUser);
app.get('/user/by/email/:email', getUserByEmail);
app.patch('/user/', updateUser);
app.delete('/user/', deleteUser);
...
import { Request, Response } from "express";
import { UserDdb } from "../datastore/ddbUser";
import { USER_PK_SYM } from "../types/constants";
import { User } from "../types/customDataTypes";
import { DeleteItemOutput, GetItemOutput, QueryOutput,} from "aws-sdk/clients/dynamodb";

const userDdb = new UserDdb();

export function saveUser(request: Request, response: Response) {

    if (!request.body) {
        response.statusCode = 400;
        response.send("Error: a todo create request must have a body");
        return;
    }
    const user = request.body as User;
    user.user_id = USER_PK_SYM + new Date().getTime();
    userDdb.create(user)
    .then(async (resp) => {
        response.statusCode = 201;
        response.send(user);
    }).catch((err) => {
        response.statusCode = 400;
        response.send(err.message);
    });
}

export async function getUser(request: Request, response: Response) {
    const id = request.params && request.params.id;
    if (!id) {
        response.statusCode = 400;
        response.send("No query params passed, need to pass the id");
        return;
    }
    const userId = id;
    userDdb.get(userId)
    .then((data: GetItemOutput) => {
        response.statusCode = 200;
        response.send(data.Item);
    })
    .catch((err) => {
        console.error(err);
        response.statusCode = 500;
        response.send(err.message);
    });
}
export async function getUsers(request: Request, response: Response) {
    userDdb.getAll().then(data => {
        response.statusCode = 200;
        response.send(data.Items);
    }).catch(err => {
        console.error(err);
        response.statusCode = 400;
        response.send(err.message);
    })
}

export async function updateUser(request: Request, response: Response) {
    const queryId = request.query && request.query.userId as string;
    const updateTodoBody = request.body && request.body as User;
    if (!queryId) {
        response.statusCode = 400;
        response.send("Id missing");
        return;
    }
    if (!updateTodoBody) {
        response.statusCode = 400;
        response.send("Update body missing");
        return;
    }
    userDdb.update(queryId, updateTodoBody)
        .then((value) => {
            response.statusCode = 204;
            response.send("");
        })
        .catch((err) => {
            console.error(err);
            response.statusCode = 500;
            response.send(`Error -> ${err}`);
        });

}
export async function deleteUser(request: Request, response: Response) {
    const userId = request.query && (request.query.userId as string);
    if (!userId) {
        response.statusCode = 400;
        response.send("Id missing");
        return;
    }
    const user = await userDdb.get(userId);
    if (!user) {
        response.statusCode = 404;
        response.send(`User with id ${user} not found`);
        return;
    }
    userDdb.delete(userId)
    .then((res: DeleteItemOutput) => {
        response.statusCode = 204;
        response.send(res);
    }).catch((err) => {
        const errMsg = `Delete failed because ${err}`;
        response.statusCode = 500;
        response.send(errMsg);
    });
}

export async function getUserByEmail(request: Request, response: Response) {
    const email = request.params && request.params.email;
    if (!email) {
        response.statusCode = 400;
        response.send("No email passed, need email address to retrieve user");
        return;
    }
    userDdb.getByEmail(email)
    .then((data: QueryOutput) => {
        response.statusCode = 200;
        response.send(data.Items);
    })
    .catch((err) => {
        console.error(err);
        response.statusCode = 400;
        response.send(err.message);
    })
}

Jest test code

import { Request, Response } from "express";
import { deleteUser, getUser, saveUser, updateUser } from './userController';
import { UserDdb } from "../datastore/ddbUser";
import { resolve } from "path";
import { User } from "../types/customDataTypes";

describe("Users test", () => {
    let mockRequest: Partial<Request> = {};
    let mockResponse: Partial<Response> = {};
    let responseObj: Object = {};
    const mockUser: User = {
        email: "bhuman.soni@gmail.com",
        username: "cptdanko",
        name: "Bhuman Soni",
    };
    const mockUserPromiseVal = new Promise<any>((resolve) => resolve(mockUser));
    beforeEach(() => {        
        mockRequest = {};
        mockResponse = {
            statusCode: 201,
            send: jest.fn().mockImplementation(result => {
                responseObj = result;
            })
        };
        
        
        jest.spyOn(UserDdb.prototype, "create").mockReturnValue(mockUserPromiseVal);
        jest.spyOn(UserDdb.prototype, "get").mockReturnValue(mockUserPromiseVal);
        jest.spyOn(UserDdb.prototype, "delete").mockReturnValue(mockUserPromiseVal);
        jest.spyOn(UserDdb.prototype, "update").mockReturnValue(mockUserPromiseVal);
    });

    it("should return 400 response when no post body is passed", async () => {
        mockRequest.body = {};
        await saveUser(mockRequest as Request, mockResponse as Response);
        expect(mockResponse.statusCode).toBe(201);
    });
    it("should return 201 response when post body is passed", async () => {
        mockRequest.body = {"id": "random", "email": "joeblogs@mydaytodo.com"};
        await saveUser(mockRequest as Request, mockResponse as Response);
        expect(mockResponse.statusCode).toBe(201);
    });
    it("should return 400 response when no post body passed", async () => {
        //mockRequest = undefined;
        await saveUser(mockRequest as Request, mockResponse as Response);
        expect(mockResponse.statusCode).toBe(400);
    });
    it("should return 400 when user id not supplied to get user", async () => {
        await getUser(mockRequest as Request, mockResponse as Response);
        expect(mockResponse.statusCode).toBe(400);
    });
    it("should return 200 when user id passed to get user", async () => {
        mockRequest.query = {"id": "USR_1234"};
        await getUser(mockRequest as Request, mockResponse as Response);
        expect(mockResponse.statusCode).toBe(400);
    });
    it("should return 400 | 404 error when invalid user id passed to delete", async () => {
        jest.spyOn(UserDdb.prototype, "get").mockReturnValue(new Promise<any>((resolve) => resolve(null)));
        mockRequest.query = {};
        await deleteUser(mockRequest as Request, mockResponse as Response);
        expect(mockResponse.statusCode).toBe(400);

        mockRequest.query = {"userId": "USR_1234"};
        await deleteUser(mockRequest as Request, mockResponse as Response);
        expect(mockResponse.statusCode).toBe(404);
    });

    it("should return 204 when valid user Id passed to delete", async () => {
        mockRequest.query = {"userId": "USR_1234"};
        await deleteUser(mockRequest as Request, mockResponse as Response);
        expect(mockResponse.statusCode).toBe(204);
    });

    it("should return 400 | 404 when user id or body not passed to update user", async () => {
        await updateUser(mockRequest as Request, mockResponse as Response);
        expect(mockResponse.statusCode).toBe(400);

        mockRequest.query = {"userId": "USR_1234"};
        mockRequest.body = null;
        await updateUser(mockRequest as Request, mockResponse as Response);
        expect(mockResponse.statusCode).toBe(400);
    });

    
    it("should return 204 on valid update user call", async () => {
        mockRequest.query = {"userId": "USR_1234"};
        mockRequest.body = mockUser;
        await updateUser(mockRequest as Request, mockResponse as Response);
        expect(mockResponse.statusCode).toBe(204);
    });
});

As you can see the above contains tests to validate the response codes from both successful and failing requests.

Conclusion

If you have not already please go and checkout the repository node_typescript_crud_notes and have a play with it. It has unit tests for both the controller code as well as the DynamoDB code that performs the CRUD operations on the database.

If you find any of my posts useful and want to support me, you can buy me a coffee 🙂

https://www.buymeacoffee.com/bhumansoni

While you are here, maybe try one of my apps for the iPhone.

Products – My Day To-Do (mydaytodo.com)

Have a read of some of my other posts on AWS

Deploy NodeJS, Typescript app on AWS Elastic beanstalk – (mydaytodo.com)

How to deploy spring boot app to AWS & serve via https – My Day To-Do (mydaytodo.com)

Some of my other posts on Javascript …

What is Javascript event loop? – My Day To-Do (mydaytodo.com)

How to build a game using Vanilla Javascript – My Day To-Do (mydaytodo.com)

Vanilla Javascript: Create Radio Buttons (How-To) – Bhuman Soni (mydaytodo.com)

Java Spring Boot & Vanilla Javascript solution – My Day To-Do (mydaytodo.com)

Vanilla Javascript: Create Radio Buttons (How-To) – Bhuman Soni (mydaytodo.com)


0 Comments

Leave a Reply

Avatar placeholder