Why and What to Test

A test is a program that fails when your code is wrong. Create a test project, run dotnet test, and read a failure.

Syntaxdotnet new xunit -o MyApp.Tests then dotnet test

A test is a small program that runs your code and reports a failure if the answer is wrong. That is the whole idea. You write the expected result down once, and from then on a machine checks it for you every time the code changes.

The alternative is checking by hand: run the app, click through it, read the screen. That works while the code is small. It stops working when a change in one file can break something three files away, because you cannot re-click everything.

How a test project works

Tests live in their own project next to the code they test. dotnet new xunit -o Calc.Tests creates one, already referencing a test framework and a test runner. The test project cannot see your code until you add a reference to it with dotnet add. Then dotnet test builds both projects, finds every method marked as a test, runs each one and prints a summary line with the pass and fail counts.

A test fails when an assertion fails. An assertion is a statement of what should be true, written as a call such as Assert.Equal(5, result). If the two values differ, the framework throws, and the runner prints the test name, the expected value and the actual value.

What trips people up

A test that passes whatever the code does is worse than no test, because it buys false confidence. Check a new test by breaking the code on purpose and watching it go red. If it stays green, the test is not testing what you think.

Example

Example · csharp
// A test is just code that runs other code and reports a failure.
// This is what a test framework does for you, written out by hand.

int Add(int a, int b) => a + b;

int failures = 0;

void Check(string name, int expected, int actual)
{
    if (expected == actual)
    {
        Console.WriteLine($"PASS {name}");
    }
    else
    {
        Console.WriteLine($"FAIL {name}: expected {expected}, got {actual}");
        failures++;
    }
}

Check("adds two positives", 5, Add(2, 3));
Check("adds a negative", 1, Add(-2, 3));
Check("adds zero", 7, Add(7, 0));

Console.WriteLine($"failures: {failures}");

// Output:
// PASS adds two positives
// PASS adds a negative
// PASS adds zero
// failures: 0

When to use it

  • Pin down the behaviour of a method you are about to rewrite, so you notice if the rewrite changes it.
  • Reproduce a reported bug as a failing test, fix the code, and keep the test as proof it stays fixed.
  • Run the whole suite from one command before you commit, instead of clicking through the app.

More examples

Creating the library and its test project

Three commands and one reference is the entire setup. The reference is the step people forget: a brand new test project compiles fine on its own, so the mistake only shows up as "the type or namespace name 'Calc' could not be found" when you write your first test. Reach for this whenever you start testing an existing project.

Example · bash
# the code being tested
dotnet new classlib -o Calc

# the test project
dotnet new xunit -o Calc.Tests

# without this reference the tests cannot see the Calc types
dotnet add Calc.Tests/Calc.Tests.csproj reference Calc/Calc.csproj

# builds both projects, then runs every test it finds
dotnet test

The hand-written check as a real xUnit test

The same check as the primary example, with the framework doing the comparing and the reporting. [Fact] marks a method as one test case; Assert.Equal takes the expected value first and the actual value second. Note there is no Main method anywhere: the test runner is the program, and your test methods are what it calls.

Example · csharp
// ---- Calc/Adder.cs ----
namespace Calc;

public static class Adder
{
    public static int Add(int a, int b) => a + b;
}

// ---- Calc.Tests/AdderTests.cs ----
using Calc;
using Xunit;

namespace Calc.Tests;

public class AdderTests
{
    [Fact]
    public void Add_TwoPositives_ReturnsSum()
    {
        int result = Adder.Add(2, 3);

        Assert.Equal(5, result);
    }
}

// dotnet test reports: Failed: 0, Passed: 1, Skipped: 0, Total: 1

Reading the output of a failure

Four things to find in any failure: the full test name, the assertion that failed, the expected and actual values, and the file and line number. The argument order of Assert.Equal is why the labels are the right way round, so passing them backwards produces a correct-looking but misleading report. Break a test deliberately once, so this output is familiar before you meet it under pressure.

Example · text
# Adder.Add was changed to "a + b + 1" on purpose, then:
#   dotnet test

  Failed Calc.Tests.AdderTests.Add_TwoPositives_ReturnsSum [< 1 ms]
  Error Message:
   Assert.Equal() Failure: Values differ
   Expected: 5
   Actual:   6
  Stack Trace:
     at Calc.Tests.AdderTests.Add_TwoPositives_ReturnsSum()
       in /src/Calc.Tests/AdderTests.cs:line 12

Failed!  - Failed: 1, Passed: 0, Skipped: 0, Total: 1

Discussion

  • Be the first to comment on this lesson.