Unit Testing: Testing Pieces of Code in Isolation with xUnit and Moq
We cover unit testing: what testing a unit in isolation means, why we write tests, the test structure with xUnit, faking dependencies with Moq, and the basic principles of writing good tests.
Your API is now secure and well-documented. But one question still hangs in the air: how will you be sure it actually works correctly? Trying all the endpoints one by one by hand after every change is both tiring and unreliable. This is exactly the gap testing fills. Today we take the first step into the world of testing: unit testing. We talk about how to verify the smallest pieces of your code, on their own, automatically.
Why Do We Write Tests?
Maybe you're thinking "my code already works, why should I also write a test?" A fair question. But think about this: the code you wrote today may be working, but three months later, when you change another place, will you be able to be sure that this code still works? Without writing tests, you have to check the whole application by hand with every change. When you write tests, you run hundreds of checks in seconds with a single command.
Think of a test like a safety net. It gives an acrobat walking on a rope the confidence that even if they fall, the net below will keep them from harm. A test gives you this too: you make changes to the code boldly, because when you break something, the tests report it instantly. In untested code, every change is a gamble; while fixing one place, you can never fully know whether you broke another.
What Is a Unit Test?
A unit test is testing the smallest meaningful piece of your code, usually a single method, in isolation. The key word here is "isolation." The method you're testing should work independently of the outside world (the database, the network, other services) so that the test measures only the logic of that method, not the malfunction of something else.
Think of a car being tested in the factory. When testing the brake system, they don't put the whole car on the road; they try the brake mechanism in a separate setup, under controlled conditions. This way they get a clear answer to the question "does the brake work?"; the engine or the tires don't come into play and muddy the result. A unit test is like this too: it isolates one piece from the others and tests only it.
Setting Up the Test Project
Tests live not inside the main project but in a separate test project. In .NET you create this from the command line. In this series we'll use xUnit; one of the most common test frameworks in the .NET world:
dotnet new xunit -n KullaniciApi.Tests
Then you add a reference between them so that the test project can see the main project it'll test:
dotnet add KullaniciApi.Tests reference KullaniciApi
Now your test project is ready and can access the classes in the main project. We'll also add the Moq package to fake dependencies, but we'll come to that shortly.
Writing the First Test: The AAA Pattern
A good test usually consists of three stages, and this structure is called the AAA pattern: Arrange, Act, Assert. First you set up the environment needed for the test, then you run the method you're testing, and finally you check whether the result is as you expected. Let's see with a simple example; let's test a method that adds two numbers:
public class CalculatorTests
{
[Fact]
public void Add_TwoNumbers_ReturnsSum()
{
// Arrange
var calculator = new Calculator();
// Act
var result = calculator.Add(2, 3);
// Assert
Assert.Equal(5, result);
}
}
Notice a few things. The [Fact] at the top of the method tells xUnit "this is a test, run it." The test method's name is also meaningful: it describes what's being tested, under which condition, and what's expected. Assert.Equal(5, result) is the heart of the matter: we expect the result to be 5; if so, the test passes, if not, it fails and xUnit tells you exactly what didn't match. To run the test, the dotnet test command is enough.
The Problem: What If the Method Needs a Dependency?
Testing the addition method was easy because it needed nothing. But our real services aren't like that. Recall the UserService we wrote in the tenth article: it had a DbContext inside, that is, it depended on the database. When we want to test this service, we hit a deadlock: we don't want to connect to the real database during the test. This is slow, fragile, and not isolated.
So how will we test the service without a real database? This is where mocking comes in. The idea is: we use not the real dependency but a fake version of it that behaves like it. Our service thinks it's talking to a database, but actually it's talking to a fake object we control and told in advance what to return.
Faking Dependencies with Moq
Moq is a library that lets us easily produce exactly these fake objects. You add it to the test project:
dotnet add KullaniciApi.Tests package Moq
Let's say our service depends on an IUserRepository interface (in the tenth article we mentioned that interfaces provide testing ease; this is exactly why). With Moq, we produce a fake version of this interface and tell it what to return:
[Fact]
public async Task GetById_ExistingUser_ReturnsUser()
{
// Arrange
var fakeRepo = new Mock();
fakeRepo
.Setup(r => r.GetByIdAsync(1))
.ReturnsAsync(new User { Id = 1, Name = "Anna" });
var service = new UserService(fakeRepo.Object);
// Act
var result = await service.GetByIdAsync(1);
// Assert
Assert.NotNull(result);
Assert.Equal("Anna", result.Name);
}
Let's unpack the magic here. First we produced a fake repository with Mock<IUserRepository>. Then we gave it an instruction with Setup: "If a GetByIdAsync(1) call comes to you, return this user." That is, we determined the repository's behavior. Next we gave this fake object (fakeRepo.Object) to the service. The service now works with our controlled fake object instead of the real database. The rest of the test measures whether the service behaves correctly with this data. We never touched the real database; the test is fast, isolated, and reliable.
The Principles of Writing Good Tests
Writing tests is easy, but writing good tests takes some care. A few basic principles: Each test should check a single thing; if you check ten different things in one test, when it fails it becomes hard to understand which one broke. Tests should be independent of each other; one working shouldn't depend on another's result. Test names should be descriptive, so that when a test fails, you can look at its name and understand what broke. And tests should be fast; slow tests stop being run over time, and a test that isn't run is worthless.
A Small Experiment
First set up a simple test project and write a test following the AAA pattern for a small method with no dependencies (for example, a function that reverses a string). Run it with dotnet test and see the green result. Then go one step further: take one of your services, fake its dependency with Moq, and test the service's logic in isolation. Also deliberately write a failing test (for example, expect a wrong value) and observe how xUnit reports the error. This experiment turns testing from an abstract concept into a concrete assurance in your hands.
In the next article we'll move on to integration testing. Unit tests were checking pieces one by one; integration tests check whether the pieces work correctly when they come together, while a real request flows end to end. We'll talk about how to test your API as a whole with WebApplicationFactory. The isolation logic you learned today will be completed there with the opposite perspective.