Unit Testing
Check one behaviour fast, with no database, so a failure points at one place
Guess first
Pick an answer. You will see if you were right at the end of the lesson.
What makes a test a unit test rather than an integration test?
Real-world analogy
What is it?
A unit test runs a small piece of your code in isolation and checks one result. In .NET you usually write them with xUnit: a method marked [Fact] or [Theory] that arranges inputs, acts, and asserts. Dependencies such as a repository are replaced with fakes or mocks. That works because the class receives them through its constructor. Unit tests are fast and cheap, so you can have hundreds. They cannot prove that the real database or HTTP pipeline works. That is what integration tests are for.
Real-world relevance
A mid-level developer changes the discount rule in OrderService and runs 300 unit tests in four seconds before pushing. One fails with a clear name, DiscountIsNotApplied_WhenCouponExpired, and the fix takes a minute. If those checks needed a database, the suite would take minutes, and people would stop running it. The usual trap goes the other way: tests that mock every line, and pass while the real code is broken.
Key points
- One behaviour, no outside world — A unit test checks one behaviour of one piece of code. It runs in milliseconds with no database, file or network, so a failure points at one place.
- Arrange, Act, Assert — Set up the inputs, call the code once, then check the result. One Act per test keeps the failure message clear.
- Fact and Theory — [Fact] is one test with fixed data. [Theory] with [InlineData] runs the same test for several inputs, such as 0 and -3.
- Fakes and mocks — A fake is a simple working stand-in, like an in-memory list. A mock, from Moq or NSubstitute, also records calls so you can verify them. Prefer checking results over checking calls when you can.
- Mock your own boundary — Mock an interface you own, such as IOrderRepository, and not a library type like DbContext. A mock of a library type can drift away from how the real one behaves.
- Names state behaviour — PlaceOrder_Fails_WhenQuantityIsNotPositive tells you what broke without opening the file. A name like Test1 tells you nothing.
- DI makes this possible — The service takes IOrderRepository in its constructor (lesson 9). The test passes a fake, so no database is needed.
- What not to unit test — Skip framework code and trivial getters, since they hold no logic of yours. Spend the effort on rules, branches and edge cases.
- React: test what users see — React Testing Library finds elements by role and text, like getByRole with a button name. It does not read component state, so a refactor does not break the test.
Quick check
Answer from memory — scrolling up first makes it stick less.
When is [Theory] better than [Fact]?
You want to test OrderService without a database. What lets you do that?
Code example
public class OrderServiceTests
{
private readonly Mock<IOrderRepository> repo = new();
private readonly OrderService service;
public OrderServiceTests() => service = new OrderService(repo.Object);
[Fact]
public async Task PlaceOrder_SavesOrder_WhenLinesAreValid()
{
var request = new PlaceOrderRequest("c-1", new List<OrderLine> { new("sku-1", 2) });
var result = await service.PlaceAsync(request);
Assert.True(result.Succeeded);
repo.Verify(r => r.AddAsync(It.Is<Order>(o => o.CustomerId == "c-1"),
It.IsAny<CancellationToken>()), Times.Once);
}
[Theory]
[InlineData(0)]
[InlineData(-3)]
public async Task PlaceOrder_Fails_WhenQuantityIsNotPositive(int quantity)
{
var request = new PlaceOrderRequest("c-1", new List<OrderLine> { new("sku-1", quantity) });
var result = await service.PlaceAsync(request);
Assert.False(result.Succeeded);
repo.Verify(r => r.AddAsync(It.IsAny<Order>(), It.IsAny<CancellationToken>()), Times.Never);
}
}Line-by-line walkthrough
- 1. This is a fragment: OrderService, IOrderRepository and the request types are assumed to exist. The test class creates a Moq mock of the repository.
- 2. The constructor builds OrderService with the mock object, so no database is involved.
- 3. The first test is a [Fact]. It arranges a request with one valid line, calls PlaceAsync once, and asserts that the result succeeded.
- 4. Verify then checks that the service asked the repository to add an order for customer c-1, exactly once. This is the side effect we care about.
- 5. The second test is a [Theory] with two [InlineData] rows. The same test runs once with 0 and once with -3.
- 6. It asserts the result failed, and that AddAsync was never called, so a bad order is not saved.
Spot the bug
[Fact]
public async Task GetOrder_ReturnsOrder()
{
var order = new Order { Id = 1 };
repo.Setup(r => r.FindAsync(1)).ReturnsAsync(order);
var result = await repo.Object.FindAsync(1);
Assert.Equal(order, result);
}Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Unit testing best practices for .NET (Microsoft Learn)
- xUnit.net documentation (xUnit.net)
- React Testing Library introduction (Testing Library)
Lock it in