Mocking third party classes in TypeScript
Tags: typescript, testing, mocking
Published: 2020-10-19 06:54:11

[TypeScript](https://www.typescriptlang.org) is becoming increasingly popular as a replacement for plain JavaScript; the strict type checking should result in more resilient code. Initiatives like [Definitely Typed](https://github.com/DefinitelyTyped/DefinitelyTyped) make that the majority of often used [NPM packages](https://www.typescriptlang.org) have type definitions. This is great when writing code, but can become a nuisance when writing tests. Especially when [mocking](https://en.wikipedia.org/wiki/Mock_object) 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](https://www.npmjs.com/package/aws-sdk):

```typescript
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](https://jestjs.io/) is my go-to tool to write tests and that means `jest.fn()`: 

```typescript
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<any, any>; }' is not assignable to parameter of type 'S3'.
    Type '{ getObject: Mock<any, any>; putObject: Mock<any, any>; }' 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__`](https://medium.com/@davguij/mocking-typescript-classes-with-jest-8ef992170d1d) functionality, [introducing interfaces](https://www.jonathancreamer.com/testing-typescript-classes-with-jest-and-jest-mocks/) and using [`jest.mock()`](https://dev.to/codedivoire/how-to-mock-an-imported-typescript-class-with-jest-2g7j).

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`](https://github.com/florinn/typemoq), [`ts-mockito`](https://github.com/NagRock/ts-mockito) or [`substitute`](https://www.npmjs.com/package/@fluffy-spoon/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](https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys)! 

> Constructs a type by picking the set of properties `Keys` from `Type`.

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:

```typescript
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.