LyraLearn AI Learning Platform
Exams
← Module 8 Β· Testing .NET Applications
🎧 Listen

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.

🧠 Quiz yourself on this lesson →

Ask the AI Tutor

Grounded in the course lessons β€” it cites its sources and says when it doesn't know.