We use cookies to enhance your experience on the site
CodeWorlds

Mocking - simulating battle conditions

You want to test a service that pays out wages. The trouble is it reaches into a database and into the Empire's external treasury. The test would therefore need a running database, a network and somebody else's server - it would be slow, and it would fail on the first broken connection even though your code was faultless.

The legion trains differently. On the drill ground you set up dummies instead of real opponents. A dummy strikes no blows back, but it lets you check whether the legionary holds formation. In testing we call such dummies test doubles.

Four kinds of dummy

Test doubles differ in how much they can do. The whole quartet is worth knowing, because the names recur in every piece of documentation - here from the simplest to the most elaborate:

  1. Dummy - a straw-stuffed figure. It fills a parameter slot the method will not use anyway. It does nothing.
  2. Stub - a dummy with a ready answer. Asked anything, it always says the same, with no trace of logic. "Legionary with id 1? Here is Marcus."
  3. Spy - a dummy that remembers the blows. It answers like a stub, but additionally records how many times it was called and with what.
  4. Mock - a dummy with expectations. It not only records calls but serves to verify interactions: did the service really call the repository, and exactly once?

In practice Jest blurs the line between the last three - the same function often acts as both stub and spy. But the distinction of roles remains: a stub supplies data, a spy observes, a mock verifies.

jest.fn() - a dummy built from scratch

The basic tool creates an empty stand-in function:

1const findOne = jest.fn();

This function does nothing and returns

undefined
, but Jest tracks every call to it - remembering how many times it fired and with which arguments. So
jest.fn()
alone is already a spy; all it lacks is an answer.

We add the answer with one of four methods, which differ in what they return:

1const repo = {
2  findOne: jest.fn().mockResolvedValue({ id: 1, name: 'Marcus' }),
3  count: jest.fn().mockReturnValue(42),
4  save: jest.fn().mockRejectedValue(new Error('Treasury unavailable')),
5};

mockReturnValue
returns a value immediately, synchronously - fit for methods that are not asynchronous.
mockResolvedValue
returns a resolved promise, so it suits anywhere the code does an
await
.
mockRejectedValue
returns a rejected promise - this is how you check that your service handles a failure properly.

There is also a variant with the

Once
suffix:
mockReturnValueOnce
answers that way only the first time and then falls back to the default behaviour. It comes in handy when you want the first call to fail and the retry to succeed.

We slot the prepared dummy in place of the real dependency:

1const module = await Test.createTestingModule({
2  providers: [
3    PayService,
4    { provide: getRepositoryToken(Legionary), useValue: repo },
5  ],
6}).compile();

useValue
tells NestJS: when somebody asks for the legionary repository, hand them this object instead of the real one. The service notices nothing - it receives something with the same methods.

jest.spyOn() - a spy on an existing method

Sometimes you do not want to build a dummy from scratch but to watch a real object. That is what the second method is for:

1const spy = jest.spyOn(legionService, 'findAll');

The difference between the two is fundamental, and it is what gets asked most often.

jest.fn()
creates a new function that did not exist anywhere before.
jest.spyOn()
takes an existing method of an existing object
and wraps it in observation.

The crucial part is what

spyOn
by default does not do: it does not replace the behaviour. The real method still runs, and you merely see that it was called. When you also want to substitute an answer, you add it exactly as before:

1jest.spyOn(legionService, 'findAll').mockResolvedValue([]);

Now the real method no longer runs. That pair - observe, or observe and replace - is all of

spyOn
.

Verification - was the dummy called

Since doubles record calls, we can ask about them in assertions:

1expect(spy).toHaveBeenCalledTimes(1);
2expect(repo.findOne).toHaveBeenCalledWith({ where: { id: 1 } });

toHaveBeenCalledTimes
checks the number of calls,
toHaveBeenCalledWith
the arguments. This is the moment the double acts as a mock: we no longer care what it returned, only whether the conversation happened at all and in what form.

Be careful not to overdo it, though. Testing every call ties the test to the service's internal structure - a small refactor will then break the tests even though the code's behaviour has not changed. Check interactions where the interaction itself is the point: that an email was sent, that a write to the database occurred. For ordinary reads, checking the result is enough.

Cleaning up after drill

Doubles remember calls - including those from the previous test. Without clearing,

toHaveBeenCalledTimes(1)
will start seeing two calls in the second test:

1afterEach(() => {
2  jest.clearAllMocks();
3});

clearAllMocks
resets the counters and recorded arguments of every stand-in. It is one of those lines whose absence shows up only when tests start passing or failing depending on the order they run in - the hardest kind of fault to track down. Put it in straight away, @name.

Summary

The drill ground is ready and the dummies are in place:

  • test doubles replace real dependencies, so a test needs neither a database nor a network,
  • the quartet from simplest up: Dummy (filler), Stub (ready answer), Spy (records calls), Mock (verifies interactions),
  • jest.fn()
    creates a new stand-in function and tracks its calls from the outset,
  • answers:
    mockReturnValue
    synchronously,
    mockResolvedValue
    a resolved promise,
    mockRejectedValue
    a rejected one, the
    ...Once
    variants apply a single time,
  • useValue
    in
    createTestingModule
    substitutes the double for the real dependency,
  • jest.spyOn()
    observes an existing method and by default does not change its behaviour - only
    mockResolvedValue
    replaces it,
  • toHaveBeenCalledTimes
    checks the number of calls,
    toHaveBeenCalledWith
    the arguments,
  • do not verify every call: a test bound to internal structure breaks on refactoring,
  • jest.clearAllMocks()
    in
    afterEach
    resets the counters - without it, tests start depending on their order.

In the next lesson we will check how much of the code your tests really reached - you will meet test coverage. For now remember: a double is a dummy on the drill ground;

jest.fn()
builds one,
jest.spyOn()
dresses a real object as one, and the assertions ask whether the legionary struck at all.

Go to CodeWorlds