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:
// 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:
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:
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.