Unit tests verify a single function or module in complete isolation, no Express server, no real database. This lesson uses Jest to test a service layer function, mocking its dependencies.
Testing a Service Function in Isolation
Because services (from the Services lesson) don't touch req/res, they're the easiest and most valuable place to write unit tests, you can call the function directly and assert on its return value or thrown error.
For functions that depend on a database or external API, mock those dependencies so the test only verifies the function's own logic, not the behavior of everything it calls.
jest.mock() replaces a module with an automatically generated mock version.
.mockResolvedValue()/.mockRejectedValue() control what a mocked async function returns.
Mocking database calls keeps unit tests fast and independent of any real database state.
Reset mocks between tests (jest.clearAllMocks() in beforeEach) to avoid state leaking across tests.
Jest Unit Testing Cheatsheet
Common Jest assertions and mocking utilities.
Task
Code
Basic assertion
expect(value).toBe(expected)
Deep equality
expect(obj).toEqual({...})
Async rejection
await expect(fn()).rejects.toThrow('message')
Mock a module
jest.mock('../repo')
Mock return value
fn.mockResolvedValue(value)
Spy on a function
jest.spyOn(obj, 'method')
Testing a Service With a Mocked Repository
When a service calls a data access layer (a repository or model), mock that dependency so the test focuses purely on the service's own business rules.
jest.mock('../repositories/user.repository');
const userRepository = require('../repositories/user.repository');
const userService = require('./user.service');
test('throws when email already exists', async () => {
userRepository.findByEmail.mockResolvedValue({ id: 1 });
await expect(userService.createUser({ email: 'ada@example.com' }))
.rejects.toThrow('Email already registered');
});
test('creates a user when email is available', async () => {
userRepository.findByEmail.mockResolvedValue(null);
userRepository.create.mockResolvedValue({ id: 2, email: 'new@example.com' });
const user = await userService.createUser({ email: 'new@example.com' });
expect(user.id).toBe(2);
});
Testing Error Cases, Not Just the Happy Path
A common gap in test suites is only testing successful scenarios. Deliberately test invalid input, missing data, and thrown errors, these paths are exactly where bugs tend to hide.
Common Mistakes
Testing only the happy path and skipping validation/error scenarios entirely.
Letting unit tests hit a real database or external API, making them slow and flaky.
Forgetting to reset mocks between tests, letting one test's setup leak into another.
Writing unit tests for trivial one-line functions while ignoring complex business logic.
Key Takeaways
Unit tests exercise a single function in isolation, with no Express server or real database.
Service functions are the easiest and most valuable target for unit tests.
Mock external dependencies (repositories, APIs) so tests focus purely on the function's own logic.
Test error cases and edge cases deliberately, not just the successful path.
Pro Tip
Write the failing test for a bug before fixing it, watching it fail confirms the test actually catches the problem, and watching it pass afterward confirms your fix actually worked.
You now know how to unit test services in isolation. Next, learn how to test full routes in the Integration Testing lesson.