This is a work in progress, so for now it's more of a concept than a library.
TestFlex is a dependency inversion layer for your unit test code. Write your code with TestFlex, then use any test runner you want.
Running Jest but want to try Bun? Maybe the Node.js test runner? What happens when Jest falls out of favour and we need to move away from it? jQuery was the best way to build websites, then Angular, then React, then Vue, then Solid, then Vite, then whatever else. You get the idea - things change, so tying ourselves to a single implementation or tool is perhaps not the best idea.
I appreciate the irony in me recommending tying yourselves to TestFlex! But it's less about the tool and perhaps more about the design pattern. If we depend on an abstraction layer rather than a concrete implementation, we can keep ourselves more flexible to changes in the future.
| Test runner | Weekly downloads | it/test | describe | global expect (not via it) |
mocking (e.g. spies/function mocks) | ESM module mocking |
|---|---|---|---|---|---|---|
| Jest | 22,060,565 | ✅ | ✅ | ✅ | ✅ | ✅ |
| Bun | ??? | ✅ | ✅ | ✅ | ✅ | ✅ |
| Vitest | 3,457,771 | ✅ | ✅ | ✅ | ✅ | ✅ |
| Jasmine | 1,434,086 | ✅ | ✅ | ✅ | ✅ | |
| Node test runner | I'm sure a lot | ✅ | ✅ | ✅ | ✅ | ❌ |
| Mocha | 7,300,267 | ✅ | ✅ | ❌ | ❌ | ❌ |
| Modern Web Test Runner | 48,087 | ✅ | ✅ | ❌ | ❌ | ❌ Not supported |
| uvu | 2,853,593 | ✅ | ✅ | ❌ | ❌ | |
| AVA | 267,968 | ✅ | ❌ | ❌ | ✅ | ❌ |
| Supertape | 417 | ✅ | ❌ | ❌ | ❌ | |
| Tap | 168,881 | ✅ | ❌ | ❌ | ❌ |
Karma is deprecated, so not being considered.
When I say module mocking, I'm referring to something like this (contrived) example using Jest;
// Mock the whole file, not just the function
import math from './math';
jest.mock('./math', () => ({
add: jest.fn().mockImplementation((a, b) => {
return a + b;
})
})
it('should add', () => {
expect(math.add).toHaveBeenCalled();
expect(math.add(1,2)).toEqual(3);
});
// Rather than mocking/spying on the individual function
import math from './math';
spyOn(math, 'add', (a, b) => {
return a + b;
});
it('should add', () => {
expect(math.add).toHaveBeenCalled();
expect(math.add(1,2)).toEqual(3);
});This mocking of the whole module means that other systems that import the same module also share the same mock.
It looks like you can mock require statements fairly easily with tools like cjs-mock or proxyquire, but import statements are harder. I think there is something in the ES specification that states that imports should be immutable, which makes mocking out the whole module "difficult".
testdouble seems to think it's possible, but it requires compiling down the CJS first.
I wonder if this is what Jest is doing under the hood before every test is run 🤔 (and why it's so slow comparatively)
So it seems that mocking out a whole file is handy, but not often the way to do things. Is Dependency Injection (DI) the answer? I spent about 30 minutes talking to ChatGPT on the topic, and from what I can tell DI can help get around this issue. It does make code more testable. But the problem is that it comes with a lot of overheads. You have to establish and maintain "DI Containers" - but where are the boundaries to these containers? As a React developer, I know the pain of prop drilling. Do I now need to instantiate this container and "prop drill" it down from some top-level area to the place I need it to be consumed?
I'm torn on DI. It seems like it would make things easier, but it also feels very much a relic of Object Orientated languages that had no concept of functional programming, or how JS works. DI was a great way to overcome these limitations, but does that apply in a JS world?
I'm going to go away and ponder that fact. I might try and see how it looks/works on my jobs' codebase. No better acid test than seeing how it works in the "real world".