Unit Testing with Mocks
A unit test isolates one class and proves one behavior. The isolation part is where mocks
come in: your EvaluationService depends on a repository and an email sender, and the test
needs to control both without touching a database or an SMTP server.
xUnit, NUnit, and Arrange-Act-Assert
xUnit and NUnit are the two mainstream frameworks; both are fine, and you'll use
whichever the codebase already has. xUnit marks tests with [Fact] (and [Theory] +
[InlineData] for parameterized cases); NUnit uses [Test]/[TestCase]. The structural
convention that matters more than the framework is Arrange-Act-Assert: set up the world,
do the one thing, check the outcome. Keep the three phases visually distinct, and give tests
names that read as specifications β
Evaluate_WhenExamScoreExpired_DoesNotSatisfyDomain tells the next developer the rule without
opening the production code. One behavior per test: three focused tests beat one test with
twelve asserts, because the failure message tells you what broke.
Mocking dependencies with Moq
This only works if dependencies arrive as interfaces via constructor injection β the design payoff of the layered architecture from earlier modules. Moq builds fake implementations inline:
var repo = new Mock<ICandidateRepository>();
repo.Setup(r => r.GetById(42)).Returns(candidate);
var service = new EvaluationService(repo.Object, Mock.Of<IEmailSender>());
var result = service.Evaluate(42);
Setup scripts what the mock returns (a stub role); Verify asserts the interaction
happened (a mock role):
email.Verify(e => e.Send(It.Is<string>(s => s.Contains("determination"))), Times.Once).
Use Verify sparingly β assert outcomes (return values, state) where you can, interactions
only when the interaction is the outcome (an email sent, an audit row written). Tests that
verify every call become mirrors of the implementation and shatter on harmless refactors.
Testing async code
Modern service methods are async Task<T>, and test frameworks support them natively β make
the test itself async Task (never async void, which the runner can't await, and never
.Result, which blocks the thread and hides the real stack trace):
[Fact]
public async Task Evaluate_ReturnsGaps_WhenDomainsUnmet()
{
repo.Setup(r => r.GetByIdAsync(42)).ReturnsAsync(candidate);
var result = await service.EvaluateAsync(42);
Assert.NotEmpty(result.Gaps);
}
ReturnsAsync is Moq's helper for Task<T>-returning members; ThrowsAsync covers the
failure paths. Exception assertions use await Assert.ThrowsAsync<NotFoundException>(() => service.EvaluateAsync(999)).
When mocking hurts
If a test needs eight mocks, the class under test has eight responsibilities β fix the class,
not the test. And don't mock what you don't own (EF's DbContext, HttpClient) when a better
seam exists; the next lesson covers the honest ways to test data access.