C# unit testing: first, decide what working means
Your first xUnit test doesn't prove the code works, it forces you to say what working is. Real dotnet test output, green and red, and how to read both.
C# unit testing comes down to one command, dotnet test, and one decision
about what working means. By the end you’ll have a project with two xUnit tests
and you’ll know what to do when the result is red. The first test fails before
it passes, and that’s what this post is about.
Step 0: the template is already green
Before you write anything, create a solution with a library and a test project:
dotnet new sln -n Oceny
dotnet new classlib -n Grades
dotnet new xunit -n Tests
dotnet sln add Grades Tests
dotnet add Tests reference Grades
dotnet test Tests
The xUnit template ships with one test called Test1, and its body is empty.
It still reports success:
Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1, Duration: 23 ms - Tests.dll (net10.0)
Green only means nothing threw an exception. A method with no assertion can’t fail, so it says nothing about your code. On my machine that first run, including the build of both projects, took 10.8 seconds.
The first test: say what working means
The library has one method, which averages a list of grades:
namespace Grades;
public static class Average
{
public static double Of(int[] grades)
{
return grades.Sum() / grades.Length;
}
}
To test it, you first have to answer a question the code never asked: what should the average of 3 and 4 be? Three and a half, three, or four after rounding? The test writes the answer down. It doesn’t check whether the code already knew it:
using Grades;
namespace Tests;
public class AverageTests
{
[Fact]
public void Of_ThreeAndFour_ReturnsThreePointFive()
{
var result = Average.Of(new[] { 3, 4 });
Assert.Equal(3.5, result);
}
}
The method name states the input and the expectation, and the body has three parts: set up the data, make the call, assert. One assertion per test isn’t a rule from on high, just a convenience. When it fails, you know exactly which promise the code broke.
How to read a red result
It fails. The method divides two integers, so the result gets truncated
to an integer before it’s widened to double. Here’s what dotnet test
prints:
[xUnit.net 00:00:00.42] Tests.AverageTests.Of_ThreeAndFour_ReturnsThreePointFive [FAIL]
Failed Tests.AverageTests.Of_ThreeAndFour_ReturnsThreePointFive [206 ms]
Error Message:
Assert.Equal() Failure: Values differ
Expected: 3.5
Actual: 3
I cut the stack trace. Below its first line, the one pointing into
UnitTest1.cs, there’s nothing but reflection frames from the test runner.
Expected is the value you wrote in the test and Actual is what the code
returned. So the code returns 3 where you wanted 3.5, and the failing test’s
name tells you which method to open.
Swapping the arguments is easy, though. The first parameter of Assert.Equal
is the expected value and the second is the result. Write
Assert.Equal(result, 3.5) and the same bug reads like this:
Expected: 3
Actual: 3.5
The labels are reversed, so the message claims the code was supposed to return
3 and returned 3.5, which is exactly backwards. Trust that output and you’ll
fix the code in the wrong direction. When a test goes red, check that the test
itself is written correctly before touching anything else.
The fix to the code is a cast before the division:
return (double)grades.Sum() / grades.Length;
After it, dotnet test ends with Passed! and one test. The test now means
something. You’ve seen it fail. A test you’ve never watched go red may
have been green from the start for the wrong reason.
The second test: an empty list
The first test raised a rounding question. The next one is just as obvious: what’s the average of zero grades? Start with the simplest answer, zero:
[Fact]
public void Of_NoGrades_ReturnsZero()
{
var result = Average.Of(Array.Empty<int>());
Assert.Equal(0, result);
}
The result:
Failed Tests.AverageTests.Of_NoGrades_ReturnsZero [2 ms]
Error Message:
Assert.Equal() Failure: Values differ
Expected: 0
Actual: NaN
Failed! - Failed: 1, Passed: 1, Skipped: 0, Total: 2, Duration: 28 ms - Tests.dll (net10.0)
The first test still passes and the second doesn’t. The summary counts both, so one red result doesn’t void the rest.
With the cast in place, zero divided by zero gives NaN, a value that isn’t a
number, and the method hands it back without a warning. Further along in a
program that NaN flows through every calculation and ruins each number it
touches. This time the answer was wrong, not the code. An average of nothing
doesn’t exist, so throwing beats returning anything:
if (grades.Length == 0)
{
throw new ArgumentException(
"The average of an empty list doesn't exist.", nameof(grades));
}
The test changes along with the decision:
[Fact]
public void Of_NoGrades_Throws()
{
Assert.Throws<ArgumentException>(() => Average.Of(Array.Empty<int>()));
}
Now dotnet test shows two green tests. Both record decisions that used to
exist only in your head.
Two mistakes from the first project
The first is the project reference. Skip dotnet add Tests reference Grades
and the test project can’t see the Average class, so the build stops before
anything runs:
UnitTest1.cs(1,7): error CS0246: The type or namespace name 'Grades' could not be found (are you missing a using directive or an assembly reference?)
I trimmed the path in front of the file name. This isn’t a red test. It’s a compile error about the project structure, not your code.
The second is nastier, because the result stays green. Delete the [Fact]
attribute above the first test method and dotnet test doesn’t report a
failure. It prints a compiler warning and a summary with fewer tests:
UnitTest1.cs(7,17): warning xUnit1013: Public method 'Of_ThreeAndFour_ReturnsThreePointFive' on test class 'AverageTests' should be marked as a Fact. ...
Passed! - Failed: 0, Passed: 1, Skipped: 0, Total: 1, Duration: 15 ms - Tests.dll (net10.0)
A method without the attribute is an ordinary method, and the runner skips it.
With two tests written, Total: 1 is the only trace. Read the count, not just
the color.
To see which tests the runner finds at all, run
dotnet test Tests --list-tests:
The following Tests are available:
Tests.AverageTests.Of_ThreeAndFour_ReturnsThreePointFive
Tests.AverageTests.Of_NoGrades_Throws
A test missing from that list didn’t run, whatever its code looks like. Once
the project grows, you can run a single test with a filter, dotnet test Tests --filter "FullyQualifiedName~NoGrades". With
--logger "console;verbosity=normal", dotnet test prints one Passed line
with the test’s name instead of the whole suite.
What to test first, and what to skip
In C# unit testing, start with a case where you know the answer without calculating, then move to the edges: an empty list, a single element, zero. Bugs gather at the edges more often than in the middle of the range. Two or three such tests per method are plenty for the first day.
Three things aren’t worth testing at the start. The first is standard library
code. A test that checks Sum() adds numbers says nothing about your program.
The second is an expected value computed with the same formula as the code. If
the test divided two integers exactly as the method does, the green result would
confirm the bug instead of catching it. Type the expected value by hand, from a
piece of paper. The third is trivial properties with no logic, which have no way
to break.
The best moment for the next test comes when you find a bug in working code. Before you fix it, write a test that reproduces it and watch it go red. The fix turns it green, and the suite starts to work like a record of what the program once got wrong. It stays in the project and keeps the same bug from coming back.
When a red test says the code returns something other than what you want and you can’t see why, a debugger beats another round of printing. And if the failure is an exception rather than a mismatch, a NullReferenceException points at a different line than you’d guess.
The course curriculum has a chapter called “Unit Testing with
xUnit”, and the built-in environment runs dotnet test without leaving
the course.