Skip to content
End To End Tester

Mocking Frameworks

Moq, NSubstitute, Mockito, unittest.mock, Sinon and the rest — what each ecosystem's mocking library does well, and the failure modes they share.

2 min read · updated 19 September 2026

Every ecosystem has one, they all do roughly the same thing, and the differences that matter are in verification style and in how gracefully they fail.

#.NET

Moq — the most used, lambda-based setup, and the most complete verification API.

csharp
var gateway = new Mock<IPaymentGateway>();
gateway.Setup(g => g.ChargeAsync(It.Is<ChargeRequest>(r => r.Cents == 4_000)))
       .ReturnsAsync(ChargeResult.Succeeded("pi_1"));

// ...

gateway.Verify(g => g.ChargeAsync(It.IsAny<ChargeRequest>()), Times.Once);

NSubstitute — the same capability with a much lighter syntax. Many teams prefer it purely for readability.

csharp
var gateway = Substitute.For<IPaymentGateway>();
gateway.ChargeAsync(Arg.Is<ChargeRequest>(r => r.Cents == 4_000))
       .Returns(ChargeResult.Succeeded("pi_1"));

// ...

await gateway.Received(1).ChargeAsync(Arg.Any<ChargeRequest>());

FakeItEasy — a third option, similar in spirit to NSubstitute.

All three can only substitute what is virtual or interface-based. That constraint is a feature: it forces dependency injection rather than letting you paper over a hard-coded dependency.

#Java

Mockito is effectively the standard.

java
@ExtendWith(MockitoExtension.class)
class CheckoutTest {

    @Mock PaymentGateway gateway;
    @Captor ArgumentCaptor<ChargeRequest> request;

    @Test
    void chargesTheOrderTotal() {
        when(gateway.charge(any())).thenReturn(ChargeResult.succeeded("pi_1"));

        new Checkout(gateway).pay(order);

        verify(gateway).charge(request.capture());
        assertThat(request.getValue().cents()).isEqualTo(4_000);
    }
}

ArgumentCaptor is the nicest thing in Mockito: rather than expressing the expectation as a matcher, capture the argument and assert on it with your normal assertion library. The failure message is then about the value, not about "wanted but not invoked".

mockStatic exists for legacy code. Treat reaching for it as a finding: something untestable is being worked around rather than fixed. See writing testable code.

#Python

unittest.mock is in the standard library and does everything.

python
from unittest.mock import Mock, patch, ANY

def test_charges_the_order_total():
    gateway = Mock()
    gateway.charge.return_value = ChargeResult(ok=True, id="pi_1")

    Checkout(gateway).pay(order)

    gateway.charge.assert_called_once_with(cents=4_000, token=ANY)


# Patch where the name is USED
@patch("checkout.service.fetch_rates", side_effect=TimeoutError)
def test_falls_back_when_rates_time_out(fetch_rates):
    assert quote(order).cents == FALLBACK_CENTS

Two Python-specific hazards:

Patch location. patch("module.where.it.is.used"), not where it is defined. This catches everyone at least once.

Mock() accepts anything. gateway.chrage(...) returns a new Mock and the test passes. Use autospec=True or create_autospec so the double has the real signature:

python
gateway = create_autospec(PaymentGateway, instance=True)
gateway.chrage(...)     # AttributeError, as it should be

#JavaScript and TypeScript

Jest's built-in mocking covers most needs. Sinon is the framework-agnostic alternative, used with Mocha and in browser contexts.

javascript
const sandbox = sinon.createSandbox();
afterEach(() => sandbox.restore());

const charge = sandbox.stub(gateway, 'charge').resolves({ id: 'pi_1' });

await new Checkout(gateway).pay(order);

sinon.assert.calledWithMatch(charge, { cents: 4_000 });

TypeScript adds a specific pleasure and a specific pain: jest.Mocked<T> and ts-mockito give typed doubles, and mocking a module with a large surface means satisfying its whole type. That friction is a useful signal — see the boundary rule below.

#Ruby

RSpec's built-ins, with one strong recommendation: use instance_double rather than double, so the framework fails when you stub a method that does not exist. See RSpec.

#The failure modes they all share

Over-specification. Verifying call counts, argument order and exact values couples the test to the implementation. Ask: if the code were refactored without changing behaviour, would this test still pass?

Doubles that lie. A stub returns what you told it to. The real dependency may return something else, throw, or be slow. Back mocked units with real integration tests or contract tests.

Mocking your own code. A test that mocks four modules you wrote is testing a wiring diagram. See unit testing without mocks.

Unverified doubles. Any framework that lets you stub a method that does not exist will eventually let you ship a test that proves nothing. Use the verifying variant everywhere it exists: autospec in Python, instance_double in Ruby, generic type parameters in .NET and Java.

#The boundary rule

The one rule that resolves most of this:

Mock at the process boundary. Use real objects inside it.

The database, the HTTP client, the broker, the clock, the filesystem, the third-party SDK — double them. Your own value objects, domain entities and pure functions — construct them. Where the tooling makes that awkward, the awkwardness is usually pointing at the design.

Common questions

Which mocking framework should I use?
Whichever is standard in your ecosystem — Moq or NSubstitute in .NET, Mockito in Java, unittest.mock in Python, Jest or Sinon in JavaScript, RSpec's built-in doubles in Ruby. The differences between them are small next to the difference between using them well and badly.
Can I mock static methods?
In most languages, only with tooling that rewrites bytecode or intercepts at the runtime level — Mockito's mockStatic, Moq's limitations here, typemock-style products. It is almost always better to treat the need as a design signal and wrap the static call in something injectable.
Why do people warn against mocking?
Because a mock encodes an assumption about a collaborator, and a suite full of mocks is a suite full of assumptions that are never checked against reality. Used at the process boundary they are indispensable; used between your own classes they produce tests that break on every refactor.

Runnable samples for this page

last test results ↗

Working tests, not fragments — they run in CI on every push to 8exgh/endtoendtester-samples.

Was this page useful?