diff --git a/docs/.vitepress/sidebar.mjs b/docs/.vitepress/sidebar.mjs index da8a5cd81..a85e5b377 100644 --- a/docs/.vitepress/sidebar.mjs +++ b/docs/.vitepress/sidebar.mjs @@ -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" diff --git a/docs/guides/how-to/dependency-injection.md b/docs/guides/how-to/dependency-injection.md new file mode 100644 index 000000000..463d540fb --- /dev/null +++ b/docs/guides/how-to/dependency-injection.md @@ -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 "); +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. diff --git a/docs/guides/how-to/index.md b/docs/guides/how-to/index.md index 3c32aa89c..108f670c8 100644 --- a/docs/guides/how-to/index.md +++ b/docs/guides/how-to/index.md @@ -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 diff --git a/docs/scripts/generate-sidebar.mjs b/docs/scripts/generate-sidebar.mjs index fa665a0a5..5af615d62 100644 --- a/docs/scripts/generate-sidebar.mjs +++ b/docs/scripts/generate-sidebar.mjs @@ -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", diff --git a/docs/tests/docs/guides/how-to/dependency-injection.test.js b/docs/tests/docs/guides/how-to/dependency-injection.test.js new file mode 100644 index 000000000..c22288bcf --- /dev/null +++ b/docs/tests/docs/guides/how-to/dependency-injection.test.js @@ -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 "); + sinon.assert.calledOnceWithExactly(fetchUser, 42); +});