CodingNic

Testing Fundamentals with Jest

Mocking Dependencies

Testing Fundamentals with Jest 20 min read

Mocking Dependencies

Objectives

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

  • Explain why a unit test shouldn’t depend on a real database
  • Create a fake dependency with jest.fn() and mockResolvedValue
  • Assert on how a mock was called, not just what it returned

💡 Why this matters: registerUser needs to check whether an email is already taken, that check normally hits a real database. A unit test for registerUser’s logic, duplicate emails rejected, new ones accepted, shouldn’t need a real database connection at all, this lesson shows how to remove that dependency for the test.

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

A Function with a Database Dependency

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

async function registerUser({ email, password }, db) {
  const existing = await db.findUserByEmail(email);
  if (existing) {
    const error = new Error('Email already registered');
    error.status = 409;
    throw error;
  }
  const passwordHash = await bcrypt.hash(password, 10);
  return db.createUser({ email, passwordHash });
}

module.exports = { registerUser };

registerUser takes db as a parameter, rather than importing a specific database module directly, this is what makes it testable, in real code, the caller passes a real database client, in a test, the caller can pass anything with matching method names.

Creating a Fake db with jest.fn()

javascript
// registerUser.test.js
const { registerUser } = require('./registerUser');

describe('registerUser', () => {
  it('creates a user when the email is not taken', async () => {
    const db = {
      findUserByEmail: jest.fn().mockResolvedValue(null),
      createUser: jest.fn().mockResolvedValue({ id: 1, email: 'erin@example.com' })
    };

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

    expect(result).toEqual({ id: 1, email: 'erin@example.com' });
    expect(db.findUserByEmail).toHaveBeenCalledWith('erin@example.com');
    expect(db.createUser).toHaveBeenCalledTimes(1);
  });
});

jest.fn() creates a mock function, a fake that records how it was called, .mockResolvedValue(value) makes it return a resolved promise with that value whenever it’s called, standing in for an async database call without touching a real database at all. toEqual checks deep equality, useful for objects, where toBe (Lesson 1) would fail even on two objects with identical contents, since it checks exact reference identity. toHaveBeenCalledWith(...) and toHaveBeenCalledTimes(...) assert on the mock’s call history, not just its return value, confirming registerUser actually called findUserByEmail with the right email, not just that it happened to return the right thing.

Testing the Duplicate-Email Case

javascript
it('throws when the email is already registered', async () => {
  const db = {
    findUserByEmail: jest.fn().mockResolvedValue({ id: 1, email: 'erin@example.com' }),
    createUser: jest.fn()
  };

  await expect(
    registerUser({ email: 'erin@example.com', password: 'sunshine123' }, db)
  ).rejects.toThrow('Email already registered');

  expect(db.createUser).not.toHaveBeenCalled();
});

This time findUserByEmail resolves to an existing user, registerUser is expected to throw before ever calling createUser, expect(db.createUser).not.toHaveBeenCalled() confirms that directly, not just that the function threw, but that it stopped at the right point and never attempted to create a duplicate.

Running Both Tests

text
PASS jesttest/registerUser.test.js
  registerUser
    ✓ creates a user when the email is not taken (117 ms)
    ✓ throws when the email is already registered (5 ms)

Neither test touched a real database, both ran in well under a second, and both confirm real, specific behavior, the success path calling both db methods correctly, and the failure path stopping before the second one.

Why This Matters Beyond Speed

A real database adds setup, teardown, and network time to every test run, and, if a test’s db module points at a shared real database, tests can interfere with each other or leave stray data behind. Mocking db removes all of that, registerUser’s own logic, its branching, its error handling, is what’s actually under test here, not whether a database happens to be reachable, module 8 comes back to genuinely testing an app together with its real routing, using a different, complementary technique.

Try It

  1. Build registerUser.js and its two tests, and confirm both pass.
  2. Change the mock in the first test so createUser returns a rejected promise (mockRejectedValue(new Error('db down'))), and observe what happens to the test, then fix the test to expect that rejection instead.
  3. Add a third test, an empty-string password, deciding for yourself whether registerUser should reject it, and if it currently doesn’t, note that as a real gap this test just found.
  4. Explain, in one or two sentences, why registerUser accepting db as a parameter, rather than importing a specific database module directly, is what makes mocking possible here.

Recap

  • jest.fn() creates a mock function, .mockResolvedValue()/.mockRejectedValue() control what it resolves or rejects to, standing in for a real dependency.
  • toHaveBeenCalledWith and toHaveBeenCalledTimes assert on how a mock was called, confirming a function’s actual behavior, not just its final output.
  • Mocking a database dependency lets a unit test check a function’s logic in isolation, fast, and without a real database.

Next module: integration testing, running real HTTP requests against a real Express app with Supertest, the technique used throughout this course to verify every endpoint so far, now formalized as an actual test suite.