Thanks to Logicroom for teaching me this.
With testing problems, the fix is rarely in the tests.
The fix is in the architecture.
Most tests should be unit tests.
With only some “integrated tests” (J.B. Rainsberger) at the edges.
But usually, the system is too coupled to have unit tests.
The tests make real REST API calls, and write to a real database.
You might need libraries to mock an HTTP server, and run a test database.
Now, the tests have become “integrated tests,” not unit tests.
As J.B. Rainsberger said, the fix isn’t to improve the “integrated tests.”
Like using better mocking libraries.
For example, I wrote a VS Code extension.
And its tests were slow.
They bootstrapped a real VS Code instance:
That’s because the production code imported a VS Code singleton in dozens of files:
import * as vscode from 'vscode';
…
export default async function getConfigFilePath() {
…
vscode.workspace.getWorkspaceFolder(
Code language: JavaScript (javascript)
It called the singleton without any wrapper.
So my VS Code extension was very coupled to the VS Code singleton (the outside world):
There was nothing between my VS Code extension and VS Code.
This made tests almost impossible.
- I didn’t write many tests
- If they failed, I’d only see it in CI/CD
- They were integration, not unit tests
The fix wasn’t to bootstrap VS Code better.
Or to use a better mocking library to mock the imported vscode
singleton.
The fix was to decouple the extension from VS Code.
I put EditorGateway
between my VS Code extension and the vscode
singleton:
Now, it injects EditorGateway
in the constructor
and calls EditorGateway instead of an imported vscode
singleton:
export default class ConfigFile {
constructor(
public editorGateway: EditorGateway,
) {}
…
async getPath() {
this.editorGateway.editor.workspace.getWorkspaceFolder(
Code language: TypeScript (typescript)
And because of that, you can mock the gateway in tests:
test('stored config file', async () => {
const { configFile, editorGateway } = getContainer();
editorGateway.editor.workspace.getWorkspaceFolder = jest
.fn()
.mockImplementationOnce(() => ({ name: 'baz' }));
expect(
await configFile.getPath(getMockContext('/home/example/baz'))
).toEqual('/home/example/baz');
});
});
Code language: JavaScript (javascript)
Many times, this Dependency Injection is the solution, though not with a DI library…
Just a composition root.
Here’s the repo of my VS Code extension that I fixed.
That was in TypeScript.
But in Clojure, you can also use Dependency Injection, or testing macros.
Now, tests run in 7 seconds:
But it’s not just speed.
- It’s easier to write unit tests, so I write more
- They’re complete, so I can deploy when CI/CD is green
- They’re not flaky, so I believe them when they fail
The right architecture makes tests easy.
Leave a Reply