3 ms·
In all the tests I've written mocking always sucks. It's not hard, but it becomes a rabbit whole time suck. My personal goal is to only mock the server and as
by bern4444 4y ago
In all the tests I've written mocking always sucks. It's not hard, but it becomes a rabbit whole time suck.
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.
- jonahx 4y agoMocking a global function in that manner is a poor solution. 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.
- bern4444 4y agoMocking a global function in a single test is not a poor solution. 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.
- jonahx 4y agoIt is a very poor solution because you are coupling your tests to private implementation detail of the function you are testing. > 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.
- petesergeant 4y agoI’ve spent most of my career as a developer with a specialty in testing, and my gut always says mock as little as possible. 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
- josteink 4y ago> I'm fully convinced mocking is a code smell of a bad implementation. 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?
- deleted 4y ago[deleted]