Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/.vitepress/sidebar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -1014,6 +1014,10 @@ export default {
"text": "Overview",
"link": "/guides/how-to/"
},
{
"text": "Dependency injection with Sinon",
"link": "/guides/how-to/dependency-injection"
},
{
"text": "Async functions with fake timers",
"link": "/guides/how-to/fake-timers-async"
Expand Down
70 changes: 70 additions & 0 deletions docs/guides/how-to/dependency-injection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
---
title: How to use dependency injection with Sinon
description: Inject a Sinon test double directly instead of replacing an imported module.
---

# How to use dependency injection with Sinon

Dependency injection means that a module receives its collaborators from the
outside instead of importing or creating them itself. Tests can then pass a
Sinon test double directly, without a module loader hook or a dependency
injection framework.

## Accept the dependency

This factory accepts a function that retrieves a user. The returned service
only depends on that function's behavior, not on where it came from:

```js
// profile-service.js
export function createProfileService({ fetchUser }) {
return {
async getProfile(userId) {
const user = await fetchUser(userId);
return `${user.name} <${user.email}>`;
}
};
}
```

The application wires the real dependency at its composition root:

```js
// app.js
import { fetchUser } from "./user-api.js";
import { createProfileService } from "./profile-service.js";

export const profileService = createProfileService({ fetchUser });
```

## Inject a Sinon test double

The test creates a stub with the behavior it needs and injects it through the
same factory argument:

```js
import assert from "node:assert/strict";
import sinon from "sinon";
import { createProfileService } from "./profile-service.js";

const fetchUser = sinon.stub().resolves({
name: "Ada",
email: "ada@example.test"
});
const profileService = createProfileService({ fetchUser });

const profile = await profileService.getProfile(42);

assert.equal(profile, "Ada <ada@example.test>");
sinon.assert.calledOnceWithExactly(fetchUser, 42);
```

The production module stays independent of Sinon. Sinon is used only in the
test to create and inspect the injected collaborator.

## When to use this pattern

Prefer explicit dependency injection when you control the module under test.
It makes dependencies visible and works with both CommonJS and ES modules. If
you cannot change legacy CommonJS code that imports its dependency directly,
see [Link seams (CommonJS)](./link-seams-commonjs) instead.
1 change: 1 addition & 0 deletions docs/guides/how-to/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ description: Practical how-to guides for common Sinon.JS scenarios — stubbing
Practical guides for common testing scenarios.

- [Async functions with fake timers](./fake-timers-async) — Speed up tests that depend on timers
- [Dependency injection with Sinon](./dependency-injection) — Inject a Sinon test double without intercepting imports
- [Link seams (CommonJS)](./link-seams-commonjs) — Isolate your system under test with proxyquire
- [Stub a dependency](./stub-dependency) — Stub a dependency of a CommonJS module
- [Stub ES module imports](./stub-esm) — Make ES module namespaces mutable for stubbing
Expand Down
1 change: 1 addition & 0 deletions docs/scripts/generate-sidebar.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,7 @@ function generateSidebar() {
.sort();

const howToDisplayNames = {
"dependency-injection": "Dependency injection with Sinon",
"stub-dependency": "Stub a dependency",
"link-seams-commonjs": "Link seams (CommonJS)",
"stub-esm": "Stub ES module imports",
Expand Down
25 changes: 25 additions & 0 deletions docs/tests/docs/guides/how-to/dependency-injection.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// Source: docs/guides/how-to/dependency-injection.md
import tap from "tap";
import sinon from "sinon";

function createProfileService({ fetchUser }) {
return {
async getProfile(userId) {
const user = await fetchUser(userId);
return `${user.name} <${user.email}>`;
}
};
}

tap.test("injects a Sinon stub into the service", async (t) => {
const fetchUser = sinon.stub().resolves({
name: "Ada",
email: "ada@example.test"
});
const profileService = createProfileService({ fetchUser });

const profile = await profileService.getProfile(42);

t.equal(profile, "Ada <ada@example.test>");
sinon.assert.calledOnceWithExactly(fetchUser, 42);
});