Import Mocking with Typescript
Solving the issues with mocking dependencies inside Typescript.
The Problem: Typescript tests
We were working on a NodeJS + Typescript project. All of our files contained classes. These classes used import {} from ''; syntax to import dependencies.
The dependencies broke our tests.
There are lots of easy ways to mock out dependencies using Javascript, but we were using Typescript. There are many easy ways of mocking Typescript objects, but most require an instantiated class, and none were easy to use when replacing dependencies.
How do you mock ES6 dependencies with Typescript?
Determined to avoid using explicit dependency injection, we wanted a testing library that would:
1. Maintain the freedom of Javascript testing.
Inside Javascript, anything can be anything. This makes testing specific functionality easier, as test setup can be as minimal as is needed:
2. Natively and seamlessly handle ES6 features
We were building a brand new product using ES6+ features throughout the application. All of our files contained classes, all of our promises were handled using async/await. Yet when writing tests we were dropping into pre-ES6 code.
We needed a library that didn’t feel ‘hacky’ when mocking out classes and which worked natively with import syntax. Writing tests needed to feel like a first party experience, rather than fighting to remember how code used to be written before all the pretty new tools came along.
3. Create mocks without requiring instantiation
We needed to mock out classes before they were instantiated. Our code would fail in the constructor of the class. Changing the implementation of all of our files so that they would play nice with the testing library was not a solution we were happy with.
4. Create stub implementations
The key use-case we were looking at was making dependencies go away.
We needed a library that would create full, stub versions of mocked classes. Maintaining fake implementations of our code was untenable. We needed something that would create an object that had the same shape as the mocked class, but would simply do nothing.
5. Maintain type safety
Typescript is wonderful because it gives you static types! In Javascript! No longer do you have to build and run your code, only to find that you forgot to change the name of that variable or that you misspelled that function.
We needed a testing library that made use of these features. Too many test files were littered with as any;. We were starting to run into issues where tests were falling over because the function name had changed in the code but not in the test. This was a scenario that Typescript should have been able to detect.
The Solution: ts-mock-imports
There were many testing libraries that fit three, maybe four of the required behaviours. None fit all five.
Until this one.
ts-mock-imports creates a mock of an entire class, replacing all functions with no-op functions (functions that return undefined). This ensures none of the original code runs. It maintains type safety, ensuring that compile time errors are thrown if methods on the original class are updated without updating the tests.
It is built on top of sinon and designed to be lightweight. It handles the dependency injection part of your code, and easily plugs into a range of existing testing environments.
How to use
Install the library into your project.
npm install ts-mock-imports --saveImport the module you would like to replace. Ensure that the /path/to/file points at the same location that the file imports the dependency from (i.e. don’t point at /path/to/index in one place and then /path/to/file in another.
import * as module from './path/to/file';Mock out your class, and save the manager so you can have control over the mocked class. Add the name of the class in quotes if it’s not the default export. If it is the default, no name is necessary.
var manager = ImportMock.mockClass(module, 'Service');var manager = ImportMock.mockClass(defaultExportModule);
Use the manager to control what is returned by various functions throughout your tests.
// To configure module.foo() to return { bar: 'bar' }
manager.mock('foo', { bar: 'bar' });Be sure to restore your mocks at the end of your tests. This replaces the imports back to their original values.
manager.restore();Example:
Assuming we have a class that throws an error when instantiated while testing:
And another service that depends on the first:
We can now test SelectService in a way that is simple and clean and with good control over the mock behavior. The tests no longer throw an error on instantiation, and everything works as expected:
In Conclusion
ts-mock-imports uses the type safety of Typescript while still leveraging the fuzzy runtime types of Javascript. This ensures that your test suite is easy to set-up and maintain, without forcing complexity on your existing app.
The library can be found here: ts-mock-imports.
Full disclosure: I am the creator of ts-mock-imports and therefore I think it is pretty great. Please reach out to me if you find any issues or want to see any new features.
