RFC: Function Mocks
See original GitHub issueThis issue facilitates discussions about builtin function mocks design. First of all, we believe that using mocks is so common that making them part of the library is a sane thing to do.
Sinon’s Spy
const spy = sinon.spy(() => 5)
// and for asserts with chai-sinon:
expect(mySpy).to.have.been.calledWith("foo");
What I don’t like about this design is that it doesn’t have support for easy creation of a spy that returns different values for different calls.
Another thing is that I always felt like there is way too many sinon-spy matchers but some really useful were missing.
Jest’s fn.mock()
const mock = jest.fn(x => 42 + x)
expect(mockFunc).toHaveBeenCalledWith(arg1, arg2);
What I like about its design:
- clear design of
.mock
property exposing all the details - makes it possible to write custom expectations by hand, - Simple helpers to mock different values for different calls:
mock.mockReturnValueOnce(‘x’)
Proposed design
In my experience, I quite often want to return different values for different calls, thus something like Jest’s mock.mockReturnValueOnce
(internal queue of returned values) is a must. But this leads us to this weird state where we define returned values in one place but write expectations in a totally another. What if we combined it to something like this:
import {mockFn} from "earl"
const getSizeMock = mockFn([{inputs: [expect.a(Fs), "/path/file.txt"], out: 217}])
This mock expects to be called exactly once with a object of type Fs
and string /path/file.txt
and returns 217
.
There are at least few cool things about this:
- can throw expectations as soon as unexpected call happens,
inputs
is just an array of args so one can use all matchers that would work withbeEqual
(arrayContaining
if you don’t care about details, oranything
if you really don’t care ;d). This API design is just an example, we could utilize helpers likemockReturnValueOnce
etc but I wanted to present here that I believe that expectations on a mock can be defined as a part of a mock.
Furthermore, it should be possible to do something like expect(mock).toBeExhusted()
to verify if a mock’s internal queue of values to return is empty. So there is no need for something like expect(mock).toHaveBeenCalledTimes(5)
.
I imagine that this could be even further simplified with optional integration with test runner (this needs another proposal) which would ensure all mocks created in a testcase are exhausted after each test run.
There is only one thing that I don’t like about this idea - implementing autofix
(for inputs) will be challenging as expectations are defined first and execution happens later.
To clarify, this could be preferred way of using mocks but for sure there are cases where more traditional interface like:
const mock = jest.fn(x => 42 + x)
expect(mockFunc).toHaveBeenCalledTimes(3);
is needed and thus should be supported as well.
Thoughts?
Issue Analytics
- State:
- Created 3 years ago
- Reactions:1
- Comments:11 (7 by maintainers)
Top GitHub Comments
Yesterday I dove into various mocking frameworks outside JS to steal some good ideas. I even visited PHP land - but I couldn’t find any good patterns there 😆
Good news is that it turns out that I didn’t invent the concept of strict/loose mocks 😆
Here’s what I propose:
strictMocks (preferred) - functions with a sequence of expected calls and return values specified UPFRONT. They will throw on unexpected calls. They should be always verified by the end of the test (can be done automatically by integration with test runner TBD).
mocks (or looseMocks) - functions with dummy implementation/return value, that you can call as many times as you want. They have nice API to override return values for given args. At the end of the test can be verified by set of custom matchers.
(2) is much more similar to what
sinon.spy
/jest
provides but I feel like (1) is be more useful IRL (at least this is how I often use mocks). I also propose to implement (1) in the first place.How about a system of overrides that allows you to modify things at a later date?