Jest is an amazing testing tool, but sometimes it throws up situations that confuse the hell out of me. Even worse is when I get confused by a problem / error message that I am 100% sure I have seen before, and solved, but it was on a different project and I can’t remember how I solved it that time.
Today’s example would be this one:
FAIL src/service/example.test.ts
● Test suite failed to run
src/service/example.test.ts:34:17 - error TS2339: Property 'mockImplementation' does not exist on type '() => { someAsyncFn: () => Promise<string>; }'.
34 dependency1.mockImplementation({
~~~~~~~~~~~~~~~~~~
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 1.09 s
Ran all test suites.
Code language: JavaScript (javascript)
I’m using TypeScript + Jest. The actual config / project setup is really not that important here, but I will include the bare bones of the package.json
setup anyway:
{
"scripts": {
"test": "jest"
},
"devDependencies": {
"@types/jest": "^29.5.5",
"@types/node": "^20.7.1",
"jest": "^29.7.0",
"prettier": "^3.0.3",
"ts-jest": "^29.1.1",
"ts-node": "^10.9.1",
"typescript": "^5.2.2"
}
}
Code language: JSON / JSON with Comments (json)
Let’s jump into an example, and then see how we work to the fix.
Example Code
The code I was working on in the actual, real project was essentially a wrapper class.
I am in the process of migrating from an old API to a new one. In theory all the data remains the same, but in reality across the 9 different endpoints, there are some variations.
My task is to migrate each endpoint from the old API to the new one.
In order to do that, I am using a very rudimentary home rolled feature flags setup. At its most basic this is simply a boolean value I can toggle in a couple of ways (env vars, and cookies) that will switch my API requests between the old and new endpoint.
The example below mimics this, but in a much more simplistic fashion. However, the errors encountered are identical. So let’s get into it.
// ./src/services/example.ts
import dependency1 from "./dependency1";
import dependency2 from "./dependency2";
import config from "../config";
export default function example() {
const dep1 = dependency1();
const dep2 = dependency2();
const getDependency = (useDependency2: boolean) =>
useDependency2 ? dep2 : dep1;
return {
doSomethingUseful() {
console.log(`doing something useful`, config);
const dependency = getDependency(config.featureFlag.useUpdatedDependency);
return dependency.doSomethingUseful();
},
};
}
Code language: JavaScript (javascript)
OK, so this is hopefully not that hard to follow.
We import
two dependency variations.
From my scenario above, this would be the old API call, and the new API call.
However, for the moment these are not async
calls. This makes things a heck of a lot easier, and as such makes for a good starting point.
There’s a simple true
/ false
checking function that determines, via the config
whether to use one or the other dependency.
Here’s the config:
// ./src/config.ts
export const config = {
featureFlag: {
useUpdatedDependency: false,
},
};
export default config;
Code language: JavaScript (javascript)
By default we use the existing dependency, which in our example code would be dependency1
.
Here are the two dependency files:
// ./src/services/dependency1.ts
export function dependency1() {
return {
doSomethingUseful: () => "dependency 1 :: actual doSomethingUseful",
};
}
export default dependency1;
Code language: JavaScript (javascript)
And looking remarkably similar:
// ./src/services/dependency2.ts
export function dependency2() {
return {
doSomethingUseful: () => "dependency 2 :: actual doSomethingUseful",
};
}
export default dependency2;
Code language: JavaScript (javascript)
That’s the gist of it.
Now, the whole point of this code is that the upstream consumer of the doSomethingUseful
function never needs to know whether they are calling the old or the new API. Everything should behave identically as far as they are concerned.
At least, that’s the theory.
In order to prove that, with some degree of confidence, we need … tests! Huzzah.
Testing The Initial Implementation
As I said above, the fact that this approach is not async
makes everything about 1000% easier.
Here’s the basic test:
// ./src/services/example.test.ts
import example from "./example";
import config from "../config";
describe("example wrapper function", () => {
it("should call dependency #1", () => {
config.featureFlag.useUpdatedDependency = false;
const exampleWrapper = example();
const result = exampleWrapper.doSomethingUseful();
expect(result).toBe("dependency 1 :: actual doSomethingUseful");
});
it("should call dependency #2", () => {
config.featureFlag.useUpdatedDependency = true;
const exampleWrapper = example();
const result = exampleWrapper.doSomethingUseful();
expect(result).toBe("dependency 2 :: actual doSomethingUseful");
});
});
Code language: JavaScript (javascript)
There’s not a whole lot to this.
We explicitly force the config
setup to be the value we want.
Then we instantiate an instance of our example function, call the doSomethingUseful
function and check that the result was exactly as expected.
The only difference is whether we see a 1
or a 2
in the output string.
This should run and pass:
console.log
doing something useful { featureFlag: { useUpdatedDependency: false } }
at Object.doSomethingUseful (src/service/example.ts:14:15)
console.log
doing something useful { featureFlag: { useUpdatedDependency: true } }
at Object.doSomethingUseful (src/service/example.ts:14:15)
PASS src/service/example.test.ts
example wrapper function
✓ should call dependency #1 (18 ms)
✓ should call dependency #2 (1 ms)
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.057 s
Ran all test suites.
Code language: JavaScript (javascript)
Of course, if life were that simple, you wouldn’t be reading this blog post.
Going Async
Our example so far was synchronous. Very straightforward and we could rely on the function’s output to response quickly and be a known quantity.
However, when calling a remote API, life is not like that.
I’m actually going to gloss over the fact that the API call may throw some kind of ‘unexpected’ error. And I put unexpected in quotes there as you should always expect the API not to behave perfectly every time.
What is important is that our real API call will be asynchronous. It will go out from our computer, across the network / internet somewhere, and then respond.
That’s not a good thing to put in our unit tests. There’s a bunch of reasons why not, but from a high level it would be down to speed and consistency of the response, potential costs in making the call (ask me for real life LOL’s on seeing 1,000’s of really financially expensive API calls made in unit tests), security, and general test reliability. I’ve probably missed a few there, too.
Anyway, enough prattle, let’s see the updated implementation using async
:
// ./src/services/example.ts
import dependency1 from "./dependency1";
import dependency2 from "./dependency2";
import config from "../config";
export default function example() {
const dep1 = dependency1();
const dep2 = dependency2();
const getDependency = (useDependency2: boolean) =>
useDependency2 ? dep2 : dep1;
return {
async doSomethingUsefulAsync() {
const dependency = getDependency(config.featureFlag.useUpdatedDependency);
return dependency.doSomethingUsefulAsync();
},
};
}
Code language: JavaScript (javascript)
Almost no change there.
We need to tell JavaScript that our doSomethingUsefulAsync
function is an async
function, and I have updated the function name to have the suffix ‘Async’ only for differentiation between the two examples.
Dependency Updates
Both of the dependencies need updating, but again the differences are not that big, nor really the important thing about this post:
// ./src/services/dependency1.ts
export function dependency1() {
return {
doSomethingUsefulAsync: async () =>
"dependency 1 :: actual doSomethingUsefulAsync",
};
}
export default dependency1;
Code language: JavaScript (javascript)
And:
// ./src/services/dependency2.ts
export function dependency2() {
return {
doSomethingUsefulAsync: async () =>
"dependency 2 :: actual doSomethingUsefulAsync",
};
}
export default dependency2;
Code language: JavaScript (javascript)
Thanks to the use of async
on our function declaration, we don’t even need to use something like Promise.resolve("text here")
, we can return a string literal and JavaScript will implicitly wrap the string for us. I do like being lazy.
With those changes made, we can get on to updating the tests.
Testing With Async – Naive Approach
Our synchronous tests can be adapted to test the asynchronous variation. This is another fairly small change.
// ./src/services/example.test.ts
import example from "./example";
import config from "../config";
describe("example wrapper function", () => {
it("should asynchronously call dependency #1", async () => {
config.featureFlag.useUpdatedDependency = false;
const exampleWrapper = example();
const result = await exampleWrapper.doSomethingUsefulAsync();
expect(result).toBe("dependency 1 :: actual doSomethingUsefulAsync");
});
it("should asynchronously call dependency #2", async () => {
config.featureFlag.useUpdatedDependency = true;
const exampleWrapper = example();
const result = await exampleWrapper.doSomethingUsefulAsync();
expect(result).toBe("dependency 2 :: actual doSomethingUsefulAsync");
});
});
Code language: JavaScript (javascript)
The changes are to make each it
test take an async function, and the await
the outcome of the function call.
Beyond that, everything is the same.
PASS src/service/example.test.ts
example wrapper function
✓ should asynchronously call dependency #1 (3 ms)
✓ should asynchronously call dependency #2
Test Suites: 1 passed, 1 total
Tests: 2 passed, 2 total
Snapshots: 0 total
Time: 1.007 s
Ran all test suites.
Code language: JavaScript (javascript)
And again, a full set of passes.
So what gives?
Simulating The Real World
Up until now, everything is too nice.
Actually the example I am using glosses over the first problem you would have already likely hit in real code. And that is that the async
calls we are making here would likely be going off to some remote API.
That API may have credentials, or be a production API that cannot be hammered in your tests, or any maybe it’s just slow as hell. Yes, let’s make our first call very slow to simulate that:
// ./src/services/dependency1.ts
export function dependency1() {
return {
doSomethingUsefulAsync: async () => {
// Use setTimeout to introduce a 5-second (5000 milliseconds) delay
await new Promise((resolve) => setTimeout(resolve, 5000));
return "dependency 1 :: actual doSomethingUsefulAsync";
},
};
}
export default dependency1;
Code language: JavaScript (javascript)
Now we re-run the tests and …
FAIL src/service/example.test.ts (5.99 s)
example wrapper function
✕ should asynchronously call dependency #1 (5005 ms)
✓ should asynchronously call dependency #2
● example wrapper function › should asynchronously call dependency #1
thrown: "Exceeded timeout of 5000 ms for a test.
Add a timeout value to this test to increase the timeout, if this is a long-running test. See https://jestjs.io/docs/api#testname-fn-timeout."
44 | // });
45 |
> 46 | it("should asynchronously call dependency #1", async () => {
| ^
47 | const exampleWrapper = example();
48 |
49 | const result = await exampleWrapper.doSomethingUsefulAsync();
at src/service/example.test.ts:46:3
at Object.<anonymous> (src/service/example.test.ts:9:1)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 passed, 2 total
Snapshots: 0 total
Time: 6.101 s
Ran all test suites.
Code language: JavaScript (javascript)
Oh…
Yeah, that looks a little more like the real world. It took over 5 seconds to return a response, so Jest timed out.
There could be many other reasons why actually hitting a remote API endpoint would lead to flaky unit tests, so let’s avoid doing that and find another way.
Introducing Mocks
Rather than hitting the real API endpoints in our unit tests, we can fake (or mock) the response.
There are pros and cons to this, but again this post isn’t going to go in to them.
If we go back to the original problem I was trying to solve: it doesn’t necessarily matter what the API responds with, I am more interested in how the endpoint is called.
In other words, I am more interested in testing that dependency1
or dependency2
is used, when I expect that it should be, than I am (at this point) about the accuracy of the response.
Perspectives.
Let’s replace that call to dependency1
with a mock.
All of this will take place in our tests:
// ./src/service/example.test.ts
import example from "./example";
import dependency1 from "./dependency1";
import config from "../config";
jest.mock("./dependency1");
describe("example wrapper function", () => {
it("should asynchronously call dependency #1", async () => {
config.featureFlag.useUpdatedDependency = false;
const exampleWrapper = example();
const result = await exampleWrapper.doSomethingUsefulAsync();
expect(result).toBe("dependency 1 :: actual doSomethingUsefulAsync");
});
});
Code language: JavaScript (javascript)
This doesn’t work, and we will get to why in a moment.
Here’s the terminal output when you run this test:
FAIL src/service/example.test.ts
example wrapper function
✕ should asynchronously call dependency #1 (2 ms)
● example wrapper function › should asynchronously call dependency #1
TypeError: Cannot read properties of undefined (reading 'doSomethingUsefulAsync')
18 | async doSomethingUsefulAsync() {
19 | const dependency = getDependency(config.featureFlag.useUpdatedDependency);
> 20 | return dependency.doSomethingUsefulAsync();
| ^
21 | },
22 | };
23 | }
at Object.<anonymous> (src/service/example.ts:20:25)
at src/service/example.ts:8:71
at Object.<anonymous>.__awaiter (src/service/example.ts:4:12)
at Object.doSomethingUsefulAsync (src/service/example.ts:29:20)
at src/service/example.test.ts:13:41
at src/service/example.test.ts:8:71
at Object.<anonymous>.__awaiter (src/service/example.test.ts:4:12)
at Object.<anonymous> (src/service/example.test.ts:8:61)
Test Suites: 1 failed, 1 total
Tests: 1 failed, 1 total
Snapshots: 0 total
Time: 1.024 s
Ran all test suites.
Code language: JavaScript (javascript)
OK, so it’s actually a little easier to see why this might not be working in the IDE:
Up on line 2 we can see that whilst we have import
‘ed dependency1
, it is not actually being used in our tests.
I find it pretty useful to do a little ‘debugging by dumping’ here.
We can add some console.log
statements in to the example.ts
file:
// ./src/service/example.ts
import dependency1 from "./dependency1";
import dependency2 from "./dependency2";
import config from "../config";
export default function example() {
const dep1 = dependency1();
const dep2 = dependency2();
const getDependency = (useDependency2: boolean) =>
useDependency2 ? dep2 : dep1;
return {
async doSomethingUsefulAsync() {
console.log("dependency1", dependency1);
const dependency = getDependency(config.featureFlag.useUpdatedDependency);
console.log(`dependency`, dependency);
return dependency.doSomethingUsefulAsync();
},
};
}
Code language: JavaScript (javascript)
We’re logging out two things here, one easier to understand than the other.
First, on line 16 we log out the imported dependency that we are expecting to be using. That’s the easier of the two.
Second, on line 18 we log out the invoked result of that function. See on line 8 how we invoke the dependency – remember, it’s a function that returns an object with (potentially) several other functions available on the object:
export function dependency1() {
return {
doSomethingUsefulAsync: async () => {
// Use setTimeout to introduce a 5-second (5000 milliseconds) delay
await new Promise((resolve) => setTimeout(resolve, 5000));
return "dependency 1 :: actual doSomethingUsefulAsync";
},
};
}
Code language: JavaScript (javascript)
When we run the test now, the two console.log
statements are printed out for us:
console.log
dependency1 [Function: dependency1] {
_isMockFunction: true,
getMockImplementation: [Function (anonymous)],
mock: [Getter/Setter],
mockClear: [Function (anonymous)],
mockReset: [Function (anonymous)],
mockRestore: [Function (anonymous)],
mockReturnValueOnce: [Function (anonymous)],
mockResolvedValueOnce: [Function (anonymous)],
mockRejectedValueOnce: [Function (anonymous)],
mockReturnValue: [Function (anonymous)],
mockResolvedValue: [Function (anonymous)],
mockRejectedValue: [Function (anonymous)],
mockImplementationOnce: [Function (anonymous)],
withImplementation: [Function: bound withImplementation],
mockImplementation: [Function (anonymous)],
mockReturnThis: [Function (anonymous)],
mockName: [Function (anonymous)],
getMockName: [Function (anonymous)]
}
at Object.<anonymous> (src/service/example.ts:14:15)
console.log
dependency undefined
at Object.<anonymous> (src/service/example.ts:16:15)
FAIL src/service/example.test.ts
example wrapper function
✕ should asynchronously call dependency #1 (19 ms)
● example wrapper function › should asynchronously call dependency #1
TypeError: Cannot read properties of undefined (reading 'doSomethingUsefulAsync')
15 | const dependency = getDependency(config.featureFlag.useUpdatedDependency);
16 | console.log(`dependency`, dependency);
> 17 | return dependency.doSomethingUsefulAsync();
| ^
18 | },
19 | };
20 | }
Code language: JavaScript (javascript)
We can therefore determine that the imported dependency is being properly mocked by Jest.
However, our setup of the mock is somehow incorrect. We need to tell Jest how that dependency should behave, when it is called as though it were a function.
One way to solve this is to provide a factory function when creating the mock:
// ./src/service/example.test.ts
import example from "./example";
import dependency1 from "./dependency1";
import config from "../config";
jest.mock("./dependency1", () => {
return () => ({
doSomethingUsefulAsync: async () => {
return "mock dependency 1 :: doSomethingUsefulAsync";
},
});
});
describe("example wrapper function", () => {
it("should asynchronously call dependency #1", async () => {
config.featureFlag.useUpdatedDependency = false;
const exampleWrapper = example();
const result = await exampleWrapper.doSomethingUsefulAsync();
expect(result).toBe("dependency 1 :: actual doSomethingUsefulAsync");
});
});
Code language: JavaScript (javascript)
Note here that this is a function that returns a function that returns an object that contains a function.
This becomes more evident when we shorten this further:
// ./src/service/example.test.ts
import example from "./example";
import dependency1 from "./dependency1";
import config from "../config";
jest.mock("./dependency1", () => () => ({
doSomethingUsefulAsync: async () => "mock dependency 1 :: doSomethingUsefulAsync",
}));
describe("example wrapper function", () => {
it("should asynchronously call dependency #1", async () => {
config.featureFlag.useUpdatedDependency = false;
const exampleWrapper = example();
const result = await exampleWrapper.doSomethingUsefulAsync();
expect(result).toBe("dependency 1 :: actual doSomethingUsefulAsync");
});
});
Code language: JavaScript (javascript)
See the function signature – () => () => ({ ...
That will confuse many people. However it is both valid, and technically correct here. I specifically made our dependency
functions return functions that return objects to create this very scenario.
Imagine that this might seem complex enough in this ‘tutorial’ example. Now what might it be like in a real world setup, with far more functions? Yeah… messy.
All that said, this test now works as expected:
It’s failing because we expect the response from our ‘live’ service, but we explicitly mocked the response. Therefore we need to update the test to expect the mocked response, not the live response:
it("should call the expected async", async () => {
config.featureFlag.useUpdatedDependency = false;
const exampleWrapper = example();
const result = await exampleWrapper.doSomethingUsefulAsync();
expect(result).toBe("mock dependency 1 :: doSomethingUsefulAsync");
});
Code language: JavaScript (javascript)
That should result in a pass.
But it’s not my preferred approach.
I find defining the mock behaviour in the factory function like this to be quite restrictive.
Refactoring The Mock Approach
My preferred approach is to define the mock as close to the test as possible. Right now we have a kind of ‘global’ setup. Great when we have one or a small handful of tests, but quickly becomes awkward on larger test suites.
If we try to refactor, we might hit the error this post was created to describe:
Again the IDE makes it easier to see.
We imported the dependency, and we mocked it.
Yet when we try to provide a mock implementation we get an error:
FAIL src/service/example.test.ts
● Test suite failed to run
src/service/example.test.ts:9:17 - error TS2339: Property 'mockImplementation' does not exist on type '() => { doSomethingUsefulAsync: () => Promise<string>; }'.
9 dependency1.mockImplementation({
~~~~~~~~~~~~~~~~~~
Test Suites: 1 failed, 1 total
Tests: 0 total
Snapshots: 0 total
Time: 1.035 s
Ran all test suites.
Code language: JavaScript (javascript)
Huh.
As best I understand it, the error here comes from TypeScript, not Jest.
There is no mockImplementation
function on the imported dependency1
.
In order to satisfy TypeScript, we must first cast dependency1
to behave as though it were a Jest Mock:
const dependency1Mock = dependency1 as jest.Mock;
Code language: JavaScript (javascript)
Now TypeScript believes that mockImplementation
is a valid property of dependency1
:
it("should asynchronously call dependency #1", async () => {
const dependency1Mock = dependency1 as jest.Mock;
dependency1Mock.mockImplementation(() => ({
doSomethingUsefulAsync: async () =>
"mock dependency 1 :: doSomethingUsefulAsync",
}));
config.featureFlag.useUpdatedDependency = false;
const exampleWrapper = example();
const result = await exampleWrapper.doSomethingUsefulAsync();
expect(result).toBe("mock dependency 1 :: doSomethingUsefulAsync");
});
Code language: JavaScript (javascript)
Again though, we could probably refactor that to remove the extra function:
const dependency1Mock = dependency1 as jest.Mock;
dependency1Mock.mockReturnValue({
doSomethingUsefulAsync: async () =>
"mock dependency 1 :: doSomethingUsefulAsync",
});
Code language: JavaScript (javascript)
The swap there is from mockImplementation
to mockReturnValue
, and then returning a shorthand object response.
We should have a pass either way:
PASS src/service/example.test.ts
example wrapper function
✓ should asynchronously call dependency #1 (3 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 1.044 s
Ran all test suites.
Code language: JavaScript (javascript)
Final Test Code
It turns out that the test code for the two is basically identical:
import example from "./example";
import dependency1 from "./dependency1";
import dependency2 from "./dependency2";
import config from "../config";
jest.mock("./dependency1");
jest.mock("./dependency2");
describe("example wrapper function", () => {
it("should asynchronously call dependency #1", async () => {
const dependency1Mock = dependency1 as jest.Mock;
dependency1Mock.mockReturnValue({
doSomethingUsefulAsync: async () =>
"mock dependency 1 :: doSomethingUsefulAsync",
});
config.featureFlag.useUpdatedDependency = false;
const exampleWrapper = example();
const result = await exampleWrapper.doSomethingUsefulAsync();
expect(result).toBe("mock dependency 1 :: doSomethingUsefulAsync");
});
it("should asynchronously call dependency #2", async () => {
const dependency2Mock = dependency2 as jest.Mock;
dependency2Mock.mockReturnValue({
doSomethingUsefulAsync: async () =>
"mock dependency 2 :: doSomethingUsefulAsync",
});
config.featureFlag.useUpdatedDependency = true;
const exampleWrapper = example();
const result = await exampleWrapper.doSomethingUsefulAsync();
expect(result).toBe("mock dependency 2 :: doSomethingUsefulAsync");
});
});
Code language: JavaScript (javascript)
That could likely be improved but the key bits are there.
Make sure to cast your imported dependency as a jest.Mock
.
Then you can set up your mock however you need.