Jean
Jean

Reputation: 5411

jest testing nodejs controller

I have the following controller

import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';
import { UserModel, isPasswordAllowed } from '../../models/User';

const saltRounds = 10;

function userController() {
  function add(req, res) {
    try {
      if (req.body.administrator) {
        res.status(400).json({
          error: {
            message: 'Bad Request',
          },
        });
        return;
      }

      if (!isPasswordAllowed(req.body.password)) {
        res.status(400).json({
          error: {
            message: 'La contraseña no cumple con los requisitos minimos',
          },
        });
        return;
      }

      bcrypt.hash(req.body.password, saltRounds, async (err, hash) => {
        if (err) {
          res.status(500).json({ error: { code: '500', message: err.errmsg } });
          return;
        }
        const user = new UserModel();
        user.email = req.body.email.toLowerCase();
        user.password = hash;

        await user
          .save()
          .then(() => {
            const token = jwt.sign(
              {
                username: user.email,
                userId: user.id,
              },
              process.env.JWT_KEY,
              {
                expiresIn: '7d',
              },
            );

            res.status(200).json({
              message: 'Usuario Creado',
              token,
              email: user.email,
            });
          })
          .catch((error) => {
            if (error.code === 11000) {
              res.status(400).json({
                error: { code: '500', message: 'El correo ya existe' },
              });
            } else {
              console.log(error);
              res.status(500).json({ error: { code: '500', message: error.message } });
            }
          });
      });
    } catch (error) {
      res.status(503).json({ error });
    }
  }
  return {
    add,
  };
}

export default userController();

As you can expect this controller works great, the user is created in the database, but I have the following test:

import UserController from './UserController';
import { connect, closeDatabase, clearDatabase } from '../../__test__/db-handler';

describe('test UserController', () => {
  const res = {};
  beforeEach(async () => {
    await connect();
    res.send = jest.fn().mockReturnValue(res);
    res.status = jest.fn().mockReturnValue(res);
    res.json = jest.fn().mockReturnValue(res);
  });

  afterEach(async () => {
    await clearDatabase();
  });

  afterAll(async () => {
    await closeDatabase();
  });

  test('should return the expect api method', () => {
    const userControllerApi = {
      add: expect.any(Function),
    };

    expect(UserController).toMatchObject(userControllerApi);
  });

  test('should return 400 error bad request is body contains administrator: true', async () => {
    const req = {
      body: {
        administrator: true,
      },
    };

    await UserController.add(req, res);
    expect(res.status).toHaveBeenCalledWith(400);
    expect(res.json).toHaveBeenCalledTimes(1);
    expect(res.json).toHaveBeenCalledWith({
      error: {
        message: 'Bad Request',
      },
    });
  });

  test('should return 400 error bad request is password is not allow', async () => {
    const req = {
      body: {
        password: '123456',
      },
    };
    await UserController.add(req, res);

    expect(res.status).toHaveBeenCalledWith(400);
    expect(res.json).toHaveBeenCalledTimes(1);
    expect(res.json).toHaveBeenCalledWith({
      error: {
        message: 'La contraseña no cumple con los requisitos minimos',
      },
    });
  });
  
  // this test is not passing
  test('should create an user and return a token', async () => {

    const req = {
      body: {
        email: '[email protected]',
        password: 'Abc123456',
      },
    };

    const expectObject = {
      message: 'Usuario Creado',
      email: '[email protected]',
    };

    await UserController.add(req, res);

    jest.useFakeTimers();

    expect(res.status).toHaveBeenCalledWith(200);
    expect(res.json).toHaveBeenCalledTimes(1);
    expect(res.json).toMatchObject(expectObject);
  });
});

but the last test 'should create an user and return a token' never pass and I get the following:

  ● test UserController › should create an user and return a token

    expect(jest.fn()).toHaveBeenCalledWith(...expected)

    Expected: 200

    Number of calls: 0

      78 |     jest.useFakeTimers();
      79 | 
    > 80 |     expect(res.status).toHaveBeenCalledWith(200);
         |                        ^
      81 |     expect(res.json).toHaveBeenCalledTimes(1);
      82 |     expect(res.json).toMatchObject(expectObject);
      83 |   });

I also debbug this code in testing mode and as you can see in the following image, the code is enter in the res.status(200).json({ .... }), so I don't understand what it is happening here.

Upvotes: 0

Views: 593

Answers (1)

eol
eol

Reputation: 24565

The problem is that you're mixing callbacks with async/await, meaning that execution of add() will be finished before the callback of bcrypt.hash has been finished. This results in res.status not to have been called yet in your test.

You can fix this by awaiting the bcrypt.hash call (it supports returning a promise by default):

// await hashing function instead of using callback
const hash = await bcrypt.hash(req.body.password, saltRounds);
const user = new UserModel();
user.email = req.body.email.toLowerCase();
user.password = hash;
// rest of the code ...

Upvotes: 1

Related Questions