Skip to content

Mocker une fonction ou une dépendance dans un test snippet

Remplacer une vraie dépendance par un faux pendant un test — l'appel réseau, l'horloge, la source aléatoire.

Remplacer une vraie dépendance par un faux pendant un test — l'appel réseau, l'horloge, la source aléatoire. Le piège, c'est de se battre contre le système de modules du langage : JavaScript ne peut réassigner les exports qu'en passant par jest/vitest qui mocke le registre de modules, Python patche avec monkeypatch ou unittest.mock (et oublie d'arrêter — pollution de tests), Go génère des mocks d'interface parce que le compilateur lie concrètement, et Rust n'a aucun mocking à l'exécution — les traits plus les génériques sont la réponse de conception. Ce que tu mockes dit à quoi ton code devrait ressembler : dépends d'interfaces, injecte-les, et le mock devient trivial.

Recette exécutable · 12 langagesOpen the mock-llm-responder tool →
Testing & QAmockingtest-doublesunit-testingdependency-injectioninterfacesstubs

Every language

12 langages, copy-ready. One at a time with syntax highlighting, or all inline.

JSJavaScript
import { describe, expect, it, vi } from 'vitest';
import { headline } from './news.js';
import { api } from './api.js';

// hoisted ABOVE the imports — the factory form is required for ESM,
// and nothing declared in this file exists yet when the factory runs:
vi.mock('./api.js', () => ({
  api: { fetch: vi.fn().mockResolvedValue({ title: 'Zig 1.0 ships' }) },
}));

describe('headline', () => {
  it('uses the API title', async () => {
    await expect(headline()).resolves.toBe('Zig 1.0 ships');
  });

  it('falls back when the call fails', async () => {
    api.fetch.mockRejectedValueOnce(new Error('offline'));
    await expect(headline()).resolves.toBe('(no headline)');
  });
});

vi.mock is hoisted above the imports — the framework rewrites the module registry before the file body runs, so the factory cannot close over anything you declare later. For an already-owned object, vi.spyOn(object, 'method').mockReturnValue(...) patches in place instead; ESM namespace imports are read-only, which is why whole-module replacement needs vi.mock at all.

TSTypeScript
import { describe, expect, it, vi } from 'vitest';

interface ApiClient {
  fetch(url: string): Promise<{ title: string }>;
}

// the seam is the parameter — no module registry involved:
async function headline(client: ApiClient): Promise<string> {
  try {
    const res = await client.fetch('/news');
    return res.title || '(no headline)';
  } catch {
    return '(no headline)';
  }
}

describe('headline', () => {
  it('uses the API title', async () => {
    const client: ApiClient = {
      fetch: vi.fn().mockResolvedValue({ title: 'Zig 1.0 ships' }),
    };
    await expect(headline(client)).resolves.toBe('Zig 1.0 ships');
  });

  it('keeps the original type signature', async () => {
    const real: ApiClient = { fetch: (url) => Promise.resolve({ title: url }) };
    const spy = vi.spyOn(real, 'fetch').mockResolvedValue({ title: 'spied' });
    await expect(headline(real)).resolves.toBe('spied');
    expect(spy).toHaveBeenCalledWith('/news');
  });
});

vi.spyOn(client, 'fetch').mockResolvedValue(fake) keeps the original type signature — the fake must still satisfy Promise<{ title: string }> or the file stops compiling, so the stub cannot drift from the real client. Handing the client in as a parameter is the typed seam: the test builds a plain object literal and never touches the module registry.

GoGo
type Fetcher interface {
	Fetch(url string) (string, error)
}

func Headline(f Fetcher) string {
	title, err := f.Fetch("/news")
	if err != nil || title == "" {
		return "(no headline)"
	}
	return title
}

// hand-rolled fake: a struct whose func field IS the canned answer
type fakeFetcher struct {
	fn    func(url string) (string, error)
	calls []string
}

func (f *fakeFetcher) Fetch(url string) (string, error) {
	f.calls = append(f.calls, url)
	return f.fn(url)
}

func TestHeadline(t *testing.T) {
	fake := &fakeFetcher{fn: func(string) (string, error) { return "Zig 1.0 ships", nil }}
	if got := Headline(fake); got != "Zig 1.0 ships" {
		t.Fatalf("Headline() = %q", got)
	}
	if len(fake.calls) != 1 || fake.calls[0] != "/news" {
		t.Fatalf("calls = %v, want [/news]", fake.calls)
	}
}

Go binds calls at compile time — only an interface can be swapped, so mockability is a design decision made in production code: Headline takes a Fetcher, main wires the real HTTP fetcher, the test wires fakeFetcher{fn: ...}. The calls slice is the hand-rolled verify; testify/mock or mockery generates the boilerplate once you have many methods, but a one-field struct wins for small seams.

RsRust
trait NewsSource {
    fn fetch(&self, url: &str) -> Result<String, String>;
}

// generic over the source — the design IS the mockability:
fn headline<S: NewsSource>(src: &S) -> String {
    match src.fetch("/news") {
        Ok(title) if !title.is_empty() => title,
        _ => "(no headline)".to_string(),
    }
}

struct FakeSource {
    answer: Result<String, String>,
}

impl NewsSource for FakeSource {
    fn fetch(&self, _url: &str) -> Result<String, String> {
        self.answer.clone()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn uses_the_source_title() {
        let src = FakeSource { answer: Ok("Zig 1.0 ships".into()) };
        assert_eq!(headline(&src), "Zig 1.0 ships");
    }
}

No runtime patching exists — nothing can rewire another crate's function after compilation — so the trait parameter came first and the test simply passes its own impl. The mockall crate automates exactly this boilerplate (#[automock] gives src.expect_fetch().returning(...)), but it can only mock what is already a trait; without the generic there is no seam for any crate to help with.

PHPPHP
use PHPUnit\Framework\TestCase;

interface HttpClient
{
    public function get(string $url): string;
}

function headline(HttpClient $client): string
{
    return $client->get('/news') ?: '(no headline)';
}

final class HeadlineTest extends TestCase
{
    public function testUsesTheApiTitle(): void
    {
        $client = $this->createMock(HttpClient::class);
        $client->method('get')
               ->willReturn('Zig 1.0 ships');

        $this->assertSame('Zig 1.0 ships', headline($client));
        $client->expects($this->once())
               ->method('get')
               ->with('/news');
    }
}

Interfaces are the seam — createMock builds a runtime subclass, so final classes and final methods cannot be mocked (PHPUnit refuses rather than silently stubbing nothing). ->method('get')->willReturn(...) is the canned answer, and expects($this->once())->with('/news') turns the mock into the assertion that the call actually happened.

PyPython
from unittest.mock import patch

# news.py does:  from api import Client   → headline() calls Client().fetch()
from news import headline


def test_uses_the_api_title():
    # patch where the name is USED ('news.Client'), not where it lives ('api.Client'):
    with patch('news.Client.fetch', return_value='Zig 1.0 ships') as fake:
        assert headline() == 'Zig 1.0 ships'
    assert fake.call_count == 1  # the spy rides along with the patch


@patch('news.Client.fetch', return_value='Zig 1.0 ships')
def test_decorator_spelling(fake_fetch):
    assert headline() == 'Zig 1.0 ships'  # auto-stopped when the test exits


def test_pytest_monkeypatch(monkeypatch):
    monkeypatch.setattr('news.Client.fetch', lambda self: 'Zig 1.0 ships')
    assert headline() == 'Zig 1.0 ships'  # undone after the test — no pollution

The #1 mock failure: patch where the name is USED, not where it is DEFINED — news.py's `from api import Client` binds its own reference, so patch('api.Client.fetch') changes nothing headline can see. Every spelling here stops itself (with-block, decorator, pytest's monkeypatch fixture undoes all its edits per test); a manual patch.object(...) + start() without stop() leaks the fake into every later test in the run.

C#C#
using Moq;
using Xunit;

public interface IApiClient
{
    string Fetch(string url);
}

public class HeadlineTests
{
    // the interface is the seam — injected, not newed up inside:
    static string Headline(IApiClient client) =>
        client.Fetch("/news") is { Length: > 0 } title ? title : "(no headline)";

    [Fact]
    public void UsesTheApiTitle()
    {
        var client = new Mock<IApiClient>();
        client.Setup(x => x.Fetch(It.IsAny<string>()))
              .Returns("Zig 1.0 ships");

        Assert.Equal("Zig 1.0 ships", Headline(client.Object));
        client.Verify(x => x.Fetch("/news"), Times.Once);
    }
}

Moq intercepts through the interface — it cannot mock non-virtual methods, so a Setup on a plain concrete method silently never fires and the test sees the real call (or a null). Setup(...).Returns(...) stubs, Verify(..., Times.Once) asserts, and x => x.Fetch(...) is an expression tree: a renamed method is a compile error, not a string typo discovered at runtime.

JvJava
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

interface ApiClient {
    String fetch(String url);
}

class HeadlineTest {
    // constructor-injected dependency — the seam Mockito plugs into:
    static String headline(ApiClient client) {
        String title = client.fetch("/news");
        return title == null || title.isEmpty() ? "(no headline)" : title;
    }

    @Test
    void usesTheApiTitle() {
        ApiClient client = mock(ApiClient.class);
        when(client.fetch("/news")).thenReturn("Zig 1.0 ships");

        assertEquals("Zig 1.0 ships", headline(client));
        verify(client).fetch("/news");
    }
}

mock(ApiClient.class) builds a runtime subclass, so the interface is the seam and constructor injection is how production code exposes it — final classes and static methods need mockito-inline instead of core Mockito. when(...).thenReturn(...) stubs, verify(...) asserts the call; both are typed, so renaming fetch is a compile error here, not a silent null later.

SwSwift
import XCTest

protocol ApiClient {
    func fetch(_ url: String) -> String
}

func headline(client: ApiClient) -> String {
    let title = client.fetch("/news")
    return title.isEmpty ? "(no headline)" : title
}

// a test conformer — same answer as Rust: design for mockability:
private final class FakeClient: ApiClient {
    var answers: [String] = []
    private(set) var calls: [String] = []

    func fetch(_ url: String) -> String {
        calls.append(url)
        return answers.isEmpty ? "" : answers.removeFirst()
    }
}

final class HeadlineTests: XCTestCase {
    func testUsesTheApiTitle() {
        let fake = FakeClient()
        fake.answers = ["Zig 1.0 ships"]

        XCTAssertEqual(headline(client: fake), "Zig 1.0 ships")
        XCTAssertEqual(fake.calls, ["/news"])
    }
}

There is no runtime method-swizzling path (none at all on Linux), so mockability is a protocol plus a test conformer: a tiny final class whose stored answers are the canned output and whose recorded calls array is the assertion surface. The conformance is checked at compile time, so when ApiClient changes shape the fake fails to build instead of lying in tests.

KtKotlin
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import kotlin.test.assertEquals

interface ApiClient {
    suspend fun fetch(url: String): String
}

class HeadlineTest {
    private suspend fun headline(client: ApiClient): String =
        client.fetch("/news").ifEmpty { "(no headline)" }

    @Test
    fun usesTheApiTitle() = runTest {
        val client = mockk<ApiClient>()
        every { client.fetch("/news") } returns "Zig 1.0 ships"

        assertEquals("Zig 1.0 ships", headline(client))
        verify { client.fetch("/news") }
    }

    @Test
    fun stubsTheSuspendCall() = runTest {
        val client = mockk<ApiClient>()
        coEvery { client.fetch(any()) } returns "Zig 1.0 ships"
        assertEquals("Zig 1.0 ships", headline(client))
    }
}

Suspend functions need coEvery/coVerify, not every/verify — the answer arrives inside a coroutine, so MockK gives the stub a second spelling for it. mockk(relaxed = true) fills unstubbed calls with defaults, which rescues deep builder chains but hides a missing stub; every { client.fetch(any()) } returns ... is the explicit form that fails loudly instead.

RbRuby
# the seam: the client arrives as a parameter
def headline(client)
  client.fetch('/news').to_s.empty? ? '(no headline)' : client.fetch('/news')
end

RSpec.describe 'headline' do
  it 'uses the API title' do
    client = instance_double('ApiClient', fetch: 'Zig 1.0 ships')
    expect(headline(client)).to eq('Zig 1.0 ships')
  end

  it 'records the call' do
    client = instance_double('ApiClient')
    allow(client).to receive(:fetch).and_return('Zig 1.0 ships')

    expect(headline(client)).to eq('Zig 1.0 ships')
    expect(client).to have_received(:fetch).with('/news')
  end
end

instance_double is a verifying double — it loads ApiClient and fails the example if fetch does not exist (or its arity changed), so renames surface in specs instead of production. allow(client).to receive(:fetch).and_return(fake) is the stub; have_received asserts the call after the fact. Doubles reset automatically between examples — there is no stop() to forget.

ZigZig
const std = @import("std");

const Fetcher = struct {
    // a function pointer field = the seam; no runtime patching exists
    fetch: *const fn (url: []const u8) []const u8,
};

fn headline(f: Fetcher) []const u8 {
    const title = f.fetch("/news");
    return if (title.len == 0) "(no headline)" else title;
}

fn realFetch(_: []const u8) []const u8 {
    return "the real HTTP answer"; // whatever the network said
}

test "headline uses the fake fetch" {
    const fake = struct {
        fn fetch(_: []const u8) []const u8 {
            return "Zig 1.0 ships";
        }
    }.fetch;

    // the test passes the fake exactly how production passes the real one:
    try std.testing.expectEqualStrings(
        "Zig 1.0 ships",
        headline(.{ .fetch = fake }),
    );
}

No runtime patching — the dependency is a function pointer field on a context struct (a comptime-known vtable when you need several methods), and the test passes its fake the same way production code passes realFetch. The cost is explicitness: everything that transitively fetches must thread the Fetcher through, which is the lesson — if wiring the fake is painful, the production design already was.

Keep going

Try the interactive mock-llm-responder tool →