How to mock dependencies in .NET unit tests

Introduction
A unit test is supposed to test one unit - one class, one method - not the database it talks to, the email server it calls, or the token generator it depends on. Mocking is how you get there: you hand the class under test fake versions of its dependencies that you control completely, so the only real code running is the code you are testing.
This video tests an AccountService.LoginAsync method on .NET 7 with xUnit and Moq. The service depends on a password hasher, a token generator, an email service, and two repositories. None of those run for real in the test - each is a Moq mock, configured per test to return exactly what that scenario needs.
🎬 Watch the full video here:
Why the dependencies are interfaces
Every collaborator AccountService uses is injected as an interface (IStringHasher, ITokenGenerator, IAccountRepository, ...). That is what makes mocking possible - Moq generates a stand-in implementation of an interface at runtime. If the service new-ed up a concrete SqlAccountRepository internally, there would be no seam to replace it. Constructor injection plus interfaces is the design that makes code testable.
The test class fixture
The constructor sets up the shared arrangement once - the mocks, some canned data, and the system under test built from mock.Object:
public AccountServiceTest()
{
_stringHasherMock = new Mock<IStringHasher>();
_tokenGeneratorMock = new Mock<ITokenGenerator>();
_accountRepositoryMock = new Mock<IAccountRepository>();
// ...
_accountRepositoryMock
.Setup(x => x.GetByEmailAsync(_loginRequest.Email))
.ReturnsAsync(_user);
_accountService = new AccountService(
_stringHasherMock.Object,
_tokenGeneratorMock.Object,
/* ... */);
}
xUnit creates a fresh instance of the test class for every [Fact], so each test starts from this clean baseline with no leakage between tests.
Arrange, Act, Assert
Each test follows the same three-part shape.
Arrange - configure the mocks for this scenario. For the "wrong password" test:
_stringHasherMock
.Setup(x => x.HashesMatch(_user.HashedPassword, _loginRequest.Password))
.Returns(false);
Act - call the method:
var act = () => _accountService.LoginAsync(_loginRequest);
Assert - check the outcome. Here, that it throws:
await Assert.ThrowsAsync<LoginFailedException>(act);
Setup: controlling what a dependency returns
Setup(...).Returns(value) for synchronous methods, Setup(...).ReturnsAsync(value) for Task<T>. This is how you drive the code down a specific path - a mock that returns false from HashesMatch forces the failure branch; one that returns true lets execution continue to token generation. You are not testing the hasher; you are testing what AccountService does with each answer.
Anything you do not set up returns the type's default (null, 0, false), which is often fine and sometimes the source of a confusing NullReferenceException in the test.
Verify: asserting on interactions
Return values are not the only thing worth checking. Verify asserts that a dependency was called - or not called - a specific number of times:
_accountRepositoryMock.Verify(x => x.GetByEmailAsync(_loginRequest.Email), Times.Once);
_tokenGeneratorMock.Verify(x => x.GenerateRefreshToken(), Times.Never);
In the wrong-password test, Times.Never on GenerateRefreshToken proves the method bailed out before doing any token work. In the happy-path test, Times.Once on UpdateAsync proves the new refresh token was persisted. This is behavior verification, and it is what catches "the method returned the right thing but also did something it should not have".
State-based vs interaction-based testing
The two tests show both styles:
- State-based: call the method, assert on the returned
UserDto. - Interaction-based: assert on which mocks were called and how often.
Lean on state-based assertions where you can - they are less brittle. Reach for Verify when the behavior you care about is an interaction (an email was sent, a record was saved, a cache was invalidated) and there is no return value that proves it.
Common pitfalls
- Over-verifying. Verifying every single call couples the test to the implementation, so any refactor breaks it. Verify the interactions that matter to the behavior.
- Mocking what you do not own. Prefer mocking your own interfaces. Mocking a third-party type you do not control leads to tests that assert your assumptions about that library, not reality.
- Mocking a class instead of an interface. Moq can only mock non-sealed classes with virtual members; interfaces are the clean case.
- Shared mutable state between tests. Rely on xUnit's per-test instance; do not use
staticfields for mocks. - Testing the mock. If a test only exercises
SetupthenVerifyof the same call, it is testing Moq, not your code. - Integration concerns in a unit test. If you need a real database to have confidence, that is an integration test - use one, do not fake the database into meaninglessness.
Key Takeaways
- Mocking replaces a class's real dependencies with controllable fakes so only the code under test actually runs.
- It works because dependencies are injected as interfaces - the design enables the testing.
Setup(...).Returns / .ReturnsAsyncdrives the class down a chosen code path by controlling what a dependency returns.Verify(..., Times.Once / Times.Never)asserts that the right calls happened the right number of times.- Prefer state-based assertions on return values; use interaction verification when the behavior is the interaction.
- Do not over-verify, and do not mock types you do not own.
Get the Full Source Code
The complete runnable solution - the AccountService, its interfaces, and the full xUnit + Moq test class with both the failure and happy-path tests - is available to Patreon supporters. If you want to run the tests and experiment with the setups instead of rebuilding it from the walkthrough above, you can find it on Patreon.