Skip to content

RFC: Module mocking #1829

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
jamiebuilds opened this issue Jun 3, 2018 · 8 comments
Closed

RFC: Module mocking #1829

jamiebuilds opened this issue Jun 3, 2018 · 8 comments
Labels

Comments

@jamiebuilds
Copy link
Contributor

Talked to @sindresorhus and he said it was okay if I spammed you with ideas before I started working on them.

Module mocking can be a really powerful tool for isolating integrated parts of complex systems inside your tests and for preventing your tests from having side effects (such as with the file system).

However, I don't think I've ever seen module mocking ever done particularly well in JavaScript. There's certainly a lot of tools out there trying to do it, but they all end up pretty brittle.

For starters, I don't think that this can be done well outside of the testing framework itself. Libraries that try to just be added on top of anything have no easy way of knowing what modules have been loaded and where. But in a testing framework you can know exactly what files have already been loaded and if you try to mock over something that has been loaded you can at least send a warning.

Additionally, I think that auto generated module mocks tend to just confuse developers. They have no idea of knowing what the shape of the mock is, and they don't know what they need to modify.

I really liked the direction that Jest started heading in. But I want to improve upon it.

What if you could define a mock inside of a separate module:

// ./mocks/fs.js
const test = require('ava');
const fs = test.mock('fs');

let files = new WeakMap();

fs.readFile = (filePath) => files.get(filePath);
fs.writeFile = (filePath, fileContents) => files.set(filePath, fileContents);

test.afterEach(() => {
  files = new WeakMap();
});

module.exports = fs;

In order to use this mock, you would import it directly, and before anything else requires the mocked module (you could throw an error if the user tries to mock something that's already been loaded).

const test = require('ava');
const fs = require('./mocks/fs'); // mocks 'fs'
const lib = require('./'); // requires 'fs'

This gives us the benefit of having module mocks that are defined in a sharable place, and people will know exactly what to expect out of them.

It also opens the possibility of sharing mocks inside of packages.

If you wanted to ship a library along with a mocked version of the library, you could do so:

const fs = require('awesome-fs-lib/ava-mock');
// or a third party:
const fs = require('ava-mock-awesome-fs-lib');

I know this is kinda rough, but what do you think?

@novemberborn
Copy link
Member

I like the approach, but in line with #1825 and #1827, what specific integration with AVA would this require?

Two that I can think of:

  • the ability to exit with a nice error when defining a mock for an already loaded module
  • a guarantee that the module instances loaded by AVA can be cleared, so they can be replaced with mocks for the user code

@jamiebuilds
Copy link
Contributor Author

I think those are the primary reasons for it.

@novemberborn
Copy link
Member

I think those are the primary reasons for it.

What do you mean by "reasons"?

I think what I was trying to get at is that if we have a require('ava/runner').exit('A nice error message') method, and the guarantee that AVA does not lazy-load dependencies (so that they can be removed from the require cache and replaced by mocks), then we can leave this to a userland module.

What do you think?

@pladaria
Copy link

pladaria commented Aug 21, 2018

In my opinion, mocking should be decoupled from the test runner, you can already do the same with, for example, sinon:

import {mock} from 'sinon';
import fs from 'fs';
import test from 'ava'

test('something', t => {
  mock(fs, 'readFile').returns(...);
  
  ...

  // cleanup
  fs.readFile.restore();
});

Very clean and simple. No need to reinvent the wheel.

@jamiebuilds
Copy link
Contributor Author

That's a different type of mocking than being discussed here

@lili21
Copy link

lili21 commented Nov 2, 2018

any updates?

@jamietre
Copy link

jamietre commented Jan 24, 2020

This is honestly the killer feature of Jest that keeps me using it. It's not without it's foibles. but basically it works. Using sinon to monkey-patch isn't really practical except for trivial use cases; you can't mock default exports, and you still have to import the real version of the thing your're mocking, which can cause deep dependency hierarchies to resolve, obviously slows things down a lot, and may present large, complex setup tasks to get the code to load in a test.

Just wondering if having some mocking system is on the roadmap at all still?

@novemberborn
Copy link
Member

@jamietre, I'm reluctant to add mocking to AVA itself, but I'd be more than happy to make internal changes to improve the experience of using a third-party mocking system.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants