CodingNic

Testing Fundamentals with Jest

Exercises

Testing Fundamentals with Jest 25 min read

Exercises

Objectives

By the end of this lesson, you should be able to:

  • Write a mocked unit test suite for a function with both a success and a failure path
  • Confirm a security property, identical error messages for two different failure reasons, with a test, not just by eye

⚠️ A note on verification: every command and every output in this lesson was actually run.

Exercise: Testing loginUser

a) Write the function, taking db as a parameter, exactly like registerUser in the last lesson:

javascript
// loginUser.js
const bcrypt = require('bcryptjs');

async function loginUser({ email, password }, db) {
  const user = await db.findUserByEmail(email);
  if (!user || !(await bcrypt.compare(password, user.passwordHash))) {
    const error = new Error('Incorrect email or password');
    error.status = 401;
    throw error;
  }
  return { id: user.id, email: user.email };
}

module.exports = { loginUser };

b) Write three tests, a real hash from bcrypt.hash, mocked only at the db layer:

javascript
const bcrypt = require('bcryptjs');
const { loginUser } = require('./loginUser');

describe('loginUser', () => {
  it('returns the user for correct credentials', async () => {
    const passwordHash = await bcrypt.hash('sunshine123', 10);
    const db = { findUserByEmail: jest.fn().mockResolvedValue({ id: 1, email: 'erin@example.com', passwordHash }) };

    const result = await loginUser({ email: 'erin@example.com', password: 'sunshine123' }, db);

    expect(result).toEqual({ id: 1, email: 'erin@example.com' });
  });

  it('throws for a wrong password', async () => {
    const passwordHash = await bcrypt.hash('sunshine123', 10);
    const db = { findUserByEmail: jest.fn().mockResolvedValue({ id: 1, email: 'erin@example.com', passwordHash }) };

    await expect(loginUser({ email: 'erin@example.com', password: 'wrong' }, db)).rejects.toThrow('Incorrect email or password');
  });

  it('throws for a nonexistent user, with the same message as a wrong password', async () => {
    const db = { findUserByEmail: jest.fn().mockResolvedValue(null) };

    await expect(loginUser({ email: 'nobody@example.com', password: 'sunshine123' }, db)).rejects.toThrow('Incorrect email or password');
  });
});

Only db.findUserByEmail is mocked, bcrypt.hash and bcrypt.compare run for real, this test suite genuinely exercises the real hashing and comparison logic, while avoiding a real database.

c) Run it:

text
PASS jesttest/loginUser.test.js
  loginUser
    ✓ returns the user for correct credentials (196 ms)
    ✓ throws for a wrong password (145 ms)
    ✓ throws for a nonexistent user, with the same message as a wrong password (1 ms)

The third test is worth pausing on, it’s a test for a security property from Module 1: a wrong password and a nonexistent user produce the exact same error message, .toThrow('Incorrect email or password') on both, this test would fail immediately if that message ever diverged between the two cases, catching a user-enumeration regression automatically, not just by someone happening to notice during manual review.

d) Extend it. Add a fourth test confirming db.findUserByEmail is called with the lowercased version of whatever email was passed in (a reasonable real-world behavior to add, case-insensitive email lookup), write the test first, watch it fail, then update loginUser to make it pass.

e) Reflect. Look back at Module 1 Lesson 4’s manually-run credential-check script. Rewrite its core assertion, identical responses for “wrong password” and “no such user”, as a Jest test, and explain, in one or two sentences, what’s gained by having it as an automated test instead of a script someone has to remember to rerun.

Recap

This module replaced manually running scripts and reading output by eye with an automated Jest suite, describe/it/expect for structure and assertions, toThrow/rejects for error cases, and jest.fn() with mockResolvedValue for removing a real dependency from a unit test, all of it reusable on every function this course has built so far.

Next module: integration testing, running real HTTP requests against a real Express app with Supertest and Jest together, testing routes and middleware as a whole, not just individual functions in isolation.