Mocking a JavaScript class with Jest: mocking vs. dependency injection
meticulous.ai
meticulous.ai
Post: https://tyrrrz.me/blog/fakes-over-mocks
Previous HN Discussion on the above post: https://news.ycombinator.com/item?id=24770954
This does require dependency injection (DI), and my hot take is that manual DI > framework magic. Sure it's a little extra code writing, but folks should optimize for reading code, and some of the frameworks out there have a lot of complex concepts for DI (For example dagger is a popular DI framework for JVM, and the dev-guide has a lot of stuff in it, which feels like a lot to learn for what it's providing: https://dagger.dev/dev-guide/, no knock against dagger, I've ran into this with other DI frameworks too!)
How do I replicate that with a Fake? A flag for error triggering?
I imagine there are some situations where the maintenance cost is better after the upfront investment. Seems like betting against one's self to me.
Fuzzing, e2e, property testing, & strict type system seem more worthwhile in my opinion.
They also make the tests nearly unreadable and very hard to reason about IME.
For both of those examples you can:
1. Create a temp file in a temp dir and inject the tempfile (probably the name of the tempfile) into the object under test. So if your object edits and manipulates /etc/passwd you can create a real file with some passwd contents (or a built-in fixture checked into the tests) and then point the object at that instead of /etc/passwd. Then you don't have to mock the whole POSIX API. Since you create the file you can rely on it existing.
2. Write a minimal API which runs on localhost with minimal or no state, no threading, possibly no auth (although something elsewhere in your tests needs to test auth) or anything else, which your code can setup expectations and responses on then point the object at the "server". Since you create this service in your tests you can rely on it existing and it shouldn't be brittle.
For other objects you can expand the System-Under-Test (SUT) so that you construct multiple objects and feed them into the test. This is particularly true when the object you are testing sends messages to other objects and then expects to get changed state back out of them. Often better to just construct a real object rather than a mock. In a perfect world you might refactor everything so perfect unit testing was possible, but in reality you just won't be able to do this.
And in general, unit tests are often useless because of the mocking and because if the contract changes on one side and not the other then you wind up shipping broken code. They're fast, which is great, which means you can enumerate all of the edge cases, but some kind of functional/integration testing that uses larger bits of the system together is almost always better in terms of giving confidence that you're shipping code that works. And with an infinitely fast CI system I'd argue that you should never write unit tests and you should be spinning up real APIs in your test harness and hitting them with real client workflows.
Instead I write a lot of fast unit tests, although I'm real quick to jettison the idea that a unit test only covers one single object under test, and if the result isn't "really" a unit test, I'll let the philosophers sort it out.
I might be a wee bit lazy, but I’d just rather make 1-line implementation of interface-methods which ensures the behaviour I want to mimic.
Maintaining mocks in e2e tests often becomes infeasible with a meaningful amount of e2e tests.
Unit tests are valuable if you're writing something with a very well defined interface like a base64 encoder.
But bugs usually happen in the "glue".
Static checks and e2e pound for pound catch the most bugs with the least effort.
When I write a frontend application using components (React, Preact, web components — doesn't really matter), I will often find myself in a situation when I have a component tree like this:
A --> B --> C
In which I want to test component A; but one of its grand(-grand-etc.-)children down the tree (in this case, component C) is quite complex; and setting up a test for component A in such a way as to accommodate for the dependencies of component C would be cumbersome.
Normally, I would just mock component C out entirely, using Jest's module mocking ability. However, this comes at a cost. ES6 import statements are static; and thus, when Jest finally completes its transition to ES6 modules, it won't be able to just mock plain static imports. The likely solution is that the rules for mocking will change; and if one wants to mock modules, he would have to do so via dynamic imports. Which means I'll have to go through all my tests and rewrite them :-(
I have not yet seen a good and clean story for mocking Javascript modules. A more standards-compliant project, Web Test Runner, does module mocking via import maps [0]; but this looks both clunky and not quite suited for per-test mocking.
Which leaves me confused. If mocks are evil, how do I test my components? And if they are fine, what's the future-proof standards-compliant way of module mocking in Javascript?
0 - https://modern-web.dev/docs/dev-server/plugins/import-maps/
What do you use concretely (and how) for this?
Here’s an example using dependency injection and puppeteer:
https://github.com/williamcotton/williamcotton.com/blob/mast...
I tend to find that more useful for UI testing than unit tests because it gives designers something to critique and often better fits how they want to work. (Versus trying to assert things like specific DIVs exists, which might get rearranged or eliminated in the next design change.)
It's easier to storyboard some complex component trees sometimes too.
When there's complex business logic, I'll separate that out into a separate pure function, and I write tests for that function without needing mocks.
====== COMPONENT FILE
import PeskyChild from './PeskyChild'; // THIS IS A STATIC IMPORT. STATIC IMPORTS IN ES6 ARE HOISTED
const MyComponent = () => (<div><PeskyChild /></div>);
====== TEST FILE
import { render } from '@testing-library/react';
import MyComponent from './MyComponent';
// THIS ATTEMPTS TO MOCK A STATIC IMPORT. IT WON'T WORK SOON-ISH
jest.mock('./PeskyChild', () => () => <div data-test-id="mocked-child-component" />)
describe('MyComponent', () => {
it('is awesome', () => {
render(<MyComponent />)
});
});But still, in that case I'm not sure why you'd need to mock <PeskyChild>. It's part of what the user is expecting to see, after all. (I used to see unit tests as distinct from integration tests, but really, it's pretty arbitrary where to draw the line around a "unit". If I refactor some lines out of a function/component into a separate file, should that now suddenly have its own unit tests, and be mocked out of its parent?)
Not every child is a pesky one; but suppose it makes a network request, or tries to read something from IndexedDB, or relies on window.matchMedia or IntersectionObserver. And you don't want to deal with this stuff when you are testing one of its (grand)parents.
For testing things from a functional perspective I really like Cypress component testing (actually, I really hate it, but it’s the best thing I’ve found).
I've heard that advice before; this is why I made my example a bit more complicated, by saying that I want to swap out C (or D, or E, or whatever great-grand-child might be the troublesome one). The ultimate setup, hypothetically, could look something like this:
import TroublesomeChild from './TroublesomeChild';
const ComponentA = () => (
<div>
<ComponentB />
{props.children}
</div>
);
const ComponentB = () => (
<div>
<TroublesomeChild />
{props.children}
</div>
);
One could advocate for passing `TroublesomeChild` to component B as a dedicated prop to make component B more testable; but if we are testing component A, then we would also have to pass component B in a dedicated prop. And what do we do if there are multiple troublesome children? Creating dedicated properties for them is neither idiomatic from component authoring perspective, nor very scaleable.It doesn’t really change that you need to be able to test every component in isolation. For A this means you need to be able to swap B, for B this means you need to be able to swap C, etc.
> Creating dedicated properties for them is neither idiomatic from component authoring perspective, nor very scaleable.
I’d say if you are trying to do this for more than 3 components in the same parent you are probably doing something wrong with your structure in the first place.
that way I can just instance a FakeRepository, inject it into RealService, and then inject it into RealController.
That way you can create an integration testing way more straightforward
My personal goal is to only mock the server and as low as possible or necessary.
I'm fully convinced mocking is a code smell of a bad implementation. It means the abstraction is leaky or too complex and would be better off being broken further down. I also abide by the rule of not exporting or making something public just for the sake of testing it.
Property tests are useful but can be tricky to identify, and a good type system that is well used removes the need for the majority of unit tests. No more, validate this function returns a number, or a string. The compiler will test and guarantee that for you.
Invoke the function under test and assert on its return value. This works in the majority of cases but when you do need to reach for something stronger, JS can do some fun stuff via reflection.
An example.
We want to test a function that consumes an object which is used to make an HTTP fetch call, building the `Request` object from the passed object.
// Of course we mock the fetch function
// or would have a mock server running via something like msw.
// Tests should never require an internet connection to run.
window.fetch = jest.fn().mockResolveValue(new Response());
const config = { url: 'https://myapi.com' };
// Invoke the function under test
await myFunctionThatInvokesFetch(config);
// We can access the mocked fetch function and get the argument with which it was called -
// the built up Request object from the argument passed to the function under test.
const requestObjectUsed = window.fetch.mock.calls[0][0];
const realRequestObject = new Request(requestObjectUsed);
// Validate that if no method is specified in config, we default to a GET request.
expect(realRequestObject.method).toBe('GET');
Lastly, if a function modifies one of its arguments, the function should include that modified value as part of it's return value. The same could be said for having a function invoke other functions. Rather than assert a function is called, have it return something that indicates it was called.This makes testing a breeze and is also a hint to the invoker that something was done to the thing passed to it.
Two better options:
1. If you are testing that the proper Request is built from your config, and that's hairy logic you want to test, pull that logic out as as a pure function (config => Request) and test that.
2. If you really want to test "through" the current function, explicitly make the fetch function one of its dependencies, passed in as an argument, with a default value of "window.fetch" which is used in production. Now you can keep your current approach but what you are doing is explicit, and requires no global object overrides for your tests.
To your first suggestion, that requires making a private function public which I dislike.
The 2nd suggestion of using dependency injection does make testing easier, but injecting the same dependency over and over in a codebase (IE, anywhere that makes an HTTP request) is bad ergonomics.
Mocking the global function (or standing up a test server via MSW) is the best solution to not get blocked, validate your code, and avoiding making src code changes to solely to accommodate tests.
> but injecting the same dependency over and over in a codebase (IE, anywhere that makes an HTTP request) is bad ergonomics.
The production dep is the default one, as I noted, so it gets injected automatically without the calling code needing to even know about it. This has the benefit of explicitly documenting in the signature all your functions dependencies with side effects. This alone would make it worth it, even ignoring the benefits on your test code.
> to not get blocked
"not betting blocked" isn't a justification for bad approaches. In any case, what I am suggesting will be just as fast.
> and avoiding making src code changes to solely to accommodate tests.
You won't be. The only src code change you'd be making is to the function signature to explicitly document its dependencies, which you should have done from the beginning.
I will argue that having global, side effectful dependencies scattered across your code base is one of the worst errors you can make.
Basically never mock local code that runs in-process, unless it’s to capture side-effects (eg loggers, and in that case dep inject), use a real database (in-memory or test-specific), and only mock stuff that’s a real, has-latency 3rd party service — but if possible, dependency inject it instead
In my ideal world, I can divide code into 2 different types: IO and pure logic.
In testing I will ignore IO almost completely and put all my focus on testing the logic.
What this approach fails to cover adequately (for me at least) is when you have high-level operations/behaviours you want to test which depends on conditionally nested IO and logic.
IO this, check result. If not IO that, check other result, and so on.
At that point you either don’t test, you write slow E2E tests which lacks type-safety… or you mock.
Granted in a deeply DI-based code-base that can often lead to 70% of the test-code being just preparing the mocks. And whenever your class gets a new dependency, it breaks all your tests.
It’s not good. But what other options do we have?
Mocking things at the module level seems incredibly convoluted to me. You constantly go off and look at where the imports are coming from and trying to keep all that in line.