We do not inject data. We inject behavior and resources (functions and streams).
✅ Inject what does the work ❌ Do NOT inject the result of the work
const approximatelyBad = (x, value) => {
return x + value - 0.5;
};This does not improve testability. You still depend on how that value was produced.
const approximately = (x, randomFn) => {
return x + randomFn() - 0.5;
};Now the function does not care where randomness comes from.
approximately(10, Math.random); // real randomness
approximately(10, () => 0.5); // fake randomness for tests👉 DI = inject the function, not the number.
If your function reads input, the input should be injected, not created inside.
export async function takeFromStdin() {
const input = Deno.stdin.readable;
// impossible to test without real stdin
}This forces tests to use real files or real stdin.
export async function takeFromStream(inputStream) {
const reader = inputStream.getReader();
const chunks = [];
while (true) {
const { value, done } = await reader.read();
if (done) break;
chunks.push(value);
}
let size = 0;
for (const c of chunks) size += c.length;
const merged = new Uint8Array(size);
let offset = 0;
for (const c of chunks) {
merged.set(c, offset);
offset += c.length;
}
return new TextDecoder().decode(merged);
}const text = await takeFromStream(Deno.stdin.readable);No files. No stdin. Just memory.
function stringToStream(text) {
const encoder = new TextEncoder();
return new ReadableStream({
start(controller) {
controller.enqueue(encoder.encode(text));
controller.close();
},
});
}Deno.test('reads injected stream', async () => {
const fakeInput = stringToStream('In a sunny');
const result = await takeFromStream(fakeInput);
if (result !== 'In a sunny') {
throw new Error('Test failed');
}
});Same rule for writing.
await Deno.stdout.write(data);Hard to test.
export async function writeToStream(outputStream, text) {
const writer = outputStream.getWriter();
await writer.write(new TextEncoder().encode(text));
await writer.close();
}await writeToStream(Deno.stdout.writable, 'hello');function captureWritable() {
const chunks = [];
const stream = new WritableStream({
write(chunk) {
chunks.push(chunk);
},
});
return {
stream,
getText() {
let size = 0;
for (const c of chunks) size += c.length;
const merged = new Uint8Array(size);
let offset = 0;
for (const c of chunks) {
merged.set(c, offset);
offset += c.length;
}
return new TextDecoder().decode(merged);
},
};
}Deno.test('writes to injected output', async () => {
const output = captureWritable();
await writeToStream(output.stream, 'hello');
if (output.getText() !== 'hello') {
throw new Error('Test failed');
}
});Bad test pattern:
const input = await Deno.open('./testInput.txt', { read: true });
const output = await Deno.open('./testOutput.txt', { write: true });Problems:
- Uses disk (slow)
- Needs cleanup
- Breaks on paths/permissions
- Tests file system, not your logic
If your function accepts streams, use fake streams, not files.
If your function depends on:
| Dependency | Inject This |
|---|---|
| Random | function |
| Time | function |
| Input | ReadableStream |
| Output | WritableStream |
| Network | fetch function |
👉 Inject how things happen, not what already happened.
Dependency Injection means your function receives the things that perform work, instead of creating or hard-coding them.