Search code examples
typescriptexpressjestjstypeormts-jest

How to make a correct unit test for this controller with Jest


Well, I'm trying to understand how I could unit test my Controller on my express app, but I haven't found out how to do so... These are my code files for now:

// CreateUserController.ts
import { Request, Response } from "express";
import { CreateUserService } from "../services/CreateUserService";

class CreateUserController {
    async handle(request: Request, response: Response) {
        try {
            const { name, email, admin, password } = request.body;

            const createUserService = new CreateUserService();

            const user = await createUserService.execute({ name, email, admin, password });
            
            // if(user instanceof Error) {
            //     return response.send({ error: user.message });
            // }

            return response.send(user);
        } catch (error) {
            return response.send({ error });
        }
    }
}

export { CreateUserController };
// CreateUserService.ts
import { getCustomRepository } from "typeorm";
import { UsersRepositories } from "../repositories/UsersRepositories";
import { hash } from "bcryptjs";

interface IUserRequest {
    name: string;
    email: string;
    admin?: boolean;
    password: string;
}

class CreateUserService {
    async execute({ name, email, admin = false, password }: IUserRequest) {
            const usersRepository = getCustomRepository(UsersRepositories);

            if(!email) {
                throw new Error('Incorrect e-mail.');
            }

            const userAlreadyExists = await usersRepository.findOne({ email });

            if(userAlreadyExists) {
                throw new Error('User already exists.');
            }

            const passwordHash = await hash(password, 8);

            const user = usersRepository.create({ name, email, admin, password: passwordHash });

            const savedUser = await usersRepository.save(user);

            return savedUser;
    }
}

export { CreateUserService };
// Users.ts
import { Entity, PrimaryColumn, Column, CreateDateColumn, UpdateDateColumn } from "typeorm";
import { Exclude } from "class-transformer";
import { v4 as uuid } from "uuid";

@Entity("users")
class User {

    @PrimaryColumn()
    readonly id: string;

    @Column()
    name: string;

    @Column()
    email: string;

    @Column()
    admin: boolean;

    @Exclude()
    @Column()
    password: string;

    @CreateDateColumn()
    created_at: Date;

    @UpdateDateColumn()
    updated_at: Date;

    constructor() {
        if (!this.id) {
            this.id = uuid();
        }
    }
}

export default User;
// UsersRepositories.ts
import { EntityRepository, Repository } from "typeorm";
import User from "../entities/User";

@EntityRepository(User)
class UsersRepositories extends Repository<User> {}

export { UsersRepositories };

and what I want to test is the CreateUserController, but I think it is written in a way I can't test, or can I? For now, I have something like this:

// CreateUserController.unit.test.ts
import { CreateUserController } from "./CreateUserController";
import { getCustomRepository } from "typeorm";
import { UsersRepositories } from "../repositories/UsersRepositories";
import { hash } from "bcryptjs";
import { v4 as uuid } from "uuid";


describe('testando', () => {
    it('primeiro it', async () => {
        const userId = uuid();
        const userData = {
            name: 'Fulano',
            email: '[email protected]',
            password: '123456',
            admin: true
        };
        const returnUserData = {
            id: userId,
            name: 'Fulano',
            email: '[email protected]',
            password: await hash('123456', 8),
            admin: true,
            created_at: new Date(),
            updated_at: new Date()
        };

        jest.spyOn(getCustomRepository(UsersRepositories), 'save').mockImplementation(async () => returnUserData);

        const user = await CreateUserController.handle(userData);
    });
});

Yeah, I know there are errors and it won't run, but that's all I got so far


Solution

  • Unit test for the controller means another controller's dependencies work well. In your case, the controller only has one dependence - CreateUserService.

    To test the controller, you can imagine that the service returns a user or throw an unexpected exception. And you expect that .send function will be called with the correct parameter.

    There are too many things that need to explain(I think so), you can try to investigate by yourself.

    // CreateUserController.test.ts
    import { mocked } from "ts-jest/utils";
    import { Request, Response } from "express";
    import { CreateUserController } from "./CreateUserController"
    import { CreateUserService } from "./CreateUserService";
    
    jest.mock('./CreateUserService'); // mock CreateUserService constructor
    
    describe('CreateUserController', () => {
      const name = 'name';
      const email = 'email';
      const admin = true;
      const password = 'password';
    
      let controller: CreateUserController;
      let service: jest.Mocked<CreateUserService>;
    
      let request: Request;
      let response: Response;
    
      beforeEach(() => {
        request = {
          body: { name, email, admin, password },
        } as Request;
        response = {
          send: jest.fn().mockReturnThis(),
          status: jest.fn().mockReturnThis(),
        } as any;
    
        service = {
          execute: jest.fn(),
        }; // an instance of the service
        mocked(CreateUserService).mockImplementation(() => {
          return service;
        }); // mock the service constructor to return our mocked instance
    
        controller = new CreateUserController();
      });
    
      it('should response with user object when service returns the user', async () => {
        const user: any = 'user mocked';
        service.execute.mockResolvedValue(user);
    
        await controller.handle(request, response);
    
        expect(response.send).toHaveBeenCalledWith(user);
        expect(service.execute).toHaveBeenLastCalledWith({ name, email, admin, password });
      });
    
      it('should response with status 500 and error object when service throws a error', async () => {
        const error = new Error('Timed out!');
        service.execute.mockRejectedValue(error);
    
        await controller.handle(request, response);
    
        expect(response.status).toHaveBeenCalledWith(500);
        expect(response.send).toHaveBeenCalledWith({ error });
        expect(service.execute).toHaveBeenLastCalledWith({ name, email, admin, password });
      });
    });
    

    It will fail in the second case because I expect that the controller has to respond with status 500, to make it pass, let update your controller:

    ...
        } catch (error) {
          return response.status(500).send({ error });
        }
    ...