TypeScript is becoming increasingly popular as a replacement for plain JavaScript; the strict type checking should result in more resilient code. Initiatives like Definitely Typed make that the majority of often used NPM packages have type definitions. This is great when writing code, but can become a nuisance when writing tests. Especially when mocking these sometimes large third party classes.
Mocking with type definitions
Let's consider the following code that takes a payload and stores it on an AWS S3 bucket using the AWS SDK:
import { S3 } from 'aws-sdk';
export class MyS3Repository implements MyRepositoryInterface {
private readonly s3: S3;
private readonly bucketName: string;
public constructor(s3: S3, bucketName: string) {
this.s3 = s3;
this.bucketName = bucketName;
}
async store(entity: MyEntity): Promise<void> {
return this.s3.putObject({
Body: JSON.stringify(entity),
Bucket: this.bucketName,
Key: entity.getIdentifier()
}).promise()
.then((response: S3.PutObjectOutput) => undefined);
}
}
NB This is a simplified example, but it will work as an example.
After creating an implementation the next logical step is to create a unit test for this class. We don't want to actually make a connection with an S3 bucket every time we run our test suite. One way to prevent this is to mock the S3 object that is injected into the repository. Coming from JavaScript Jest is my go-to tool to write tests and that means jest.fn()
:
describe('MyS3Repository', () => {
describe('store()', () => {
it('should store on S3', async () => {
const s3 = {
putObject: jest.fn().mockReturnValue({
promise: jest.fn().mockResolvedValue({})
})
}
const repository = new MyS3Repository(s3);
const entity = new MyEntity(/* ... */);
expect.assertions(1);
await expect(repository.store()).resolves.toBe(undefined);
})
});
});
When working with "plain" JavaScript this would work as expected, and the unit test would pass. But when using TypeScript you will encounter an error:
Argument of type '{ putObject: jest.Mock; }' is not assignable to parameter of type 'S3'.
Type '{ getObject: Mock; putObject: Mock; }' is missing the following properties from type 'S3': config, abortMultipartUpload, completeMultipartUpload, copyObject, and 98 more.ts(2345)
What happens here is that TypeScript is detecting that the object that should impersonate the S3
object is missing 98 methods. A trade off needs to be made here; do I use an existing package and try to work with it or do I write the necessary code myself. When not working on personal projects the scale typically tilts towards the first. So let's see if we can make this work.
Pick me!
A number of solutions for this problem are described like using the __mocks__
functionality, introducing interfaces and using jest.mock()
.
For multiple reasons none of these really worked well for me. As I don't have control over the code introducing an interface is not possible. jest.mock()
seems to only work on modules that have a default export and __mocks__
still requires mocking the entire signature of a class.
Another option is to use additional tooling like typemoq
, ts-mockito
or substitute
. But I am a bit reluctant to add a full-fledged mocking library if it is not completely necessary.
Here I find myself with a very useful object with 99 methods and I only use one. Enter the Pick<Type, Keys>
utility!
Constructs a type by picking the set of properties
Keys
fromType
.
It allows us to tell TypeScript that even if we have an object with 99 methods we only really care about a small subset of those methods:
import { S3 } from 'aws-sdk';
export class MyS3Repository implements MyRepositoryInterface {
private readonly s3: Pick<S3, 'putObject'>;
private readonly bucketName: string;
public constructor(s3: Pick<S3, 'putObject'>, bucketName: string) {
this.s3 = s3;
this.bucketName = bucketName;
}
}
Result of this is that in our unit test we only have to mock putObject()
without TypeScript complaining about missing types.
Conclusion
If you find yourself with a large third party class that you want to mock you can resort to tooling like typemoq
, ts-mockito
or substitute
. Or you can leverage features offered by TypeScript. By using Pick
you are able to specify exactly what properties you need in your code. Doing that will make your mocking work a lot simpler.
Top comments (2)
Interesting feature for sure but like this you are changing your implementation for you tests. Sometimes necessary but not optimal.
Sadly I cannot verify it right now but I think you may be able to fix your cited error by casting your mock object:
Thank you for your feedback. Your suggestion will result in the following error:
But this can be remedied by casting to
unknown
first:I opted for the
Pick
solution as it actually changes the interface of the object to be more concise and more explicit about what part of the object is actually required.I think this mostly will come down to preference.