<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. C#
  3. Master level
All lessons in this course

Lesson 56 of 60 18 minMaster

Unit testing with xUnit

Write code that checks your code: test projects, Arrange-Act-Assert, [Fact] and [Theory], testing exceptions and async code, fakes for dependencies, and what makes a good test.

A unit test is a small method that calls your code with known inputs and checks the result. Run hundreds of them in a few seconds after every change and you will know at once if something broke. That safety net is what lets you refactor and add features with confidence.

Interactive

Tests catch mistakes

Pick a change to the method (some are bugs), then run the tests.

BeamMath.cs
public static double MemberWeightKg(double lengthM, double kgPerM){    if (lengthM <= 0)        throw new ArgumentOutOfRangeException(nameof(lengthM));    return Math.Round(lengthM * kgPerM, 1);}
  • Weight_of_6_5m_IPE300_is_274_3
  • Weight_is_length_times_mass(1, 42.2, 42.2)
  • Weight_is_length_times_mass(10, 36.1, 361)
  • Weight_is_length_times_mass(0.5, 10, 5)
  • Throws_for_zero_length
  • Throws_for_negative_length

Creating a test project

Terminal
dotnet new classlib -n SteelToolsdotnet new xunit    -n SteelTools.Testsdotnet add SteelTools.Tests reference SteelToolsdotnet new sln && dotnet sln add SteelTools SteelTools.Testsdotnet test

The test project references the project it tests. In Visual Studio, Test Explorer shows every test with a green or red icon; dotnet test does the same from the terminal or a CI pipeline.

Arrange, Act, Assert

BeamMathTests.cs
using Xunit; public class BeamMathTests{    [Fact]    public void Weight_of_6_5m_IPE300_is_274_3()    {        // Arrange: set up inputs        double length = 6.5, kgPerM = 42.2;         // Act: call the code under test (one call)        double kg = BeamMath.MemberWeightKg(length, kgPerM);         // Assert: check the result        Assert.Equal(274.3, kg, precision: 1);    }}
  • [Fact] marks a test with no parameters.
  • Name tests as a sentence about the behaviour: Method_condition_expected or plain words.
  • One behaviour per test, so a red test tells you exactly what broke.
  • For double, compare with a precision (decimal places) or tolerance, because of floating-point rounding.

[Theory]: one test, many inputs

Theory.cs
[Theory][InlineData(1, 42.2, 42.2)][InlineData(10, 36.1, 361)][InlineData(0.5, 10, 5)]public void Weight_is_length_times_mass(double length, double kgPerM, double expected){    Assert.Equal(expected, BeamMath.MemberWeightKg(length, kgPerM), precision: 1);}

Testing exceptions and async code

Exceptions.cs
[Fact]public void Throws_for_zero_length(){    var ex = Assert.Throws<ArgumentOutOfRangeException>(() => BeamMath.MemberWeightKg(0, 42.2));    Assert.Equal("lengthM", ex.ParamName);} [Fact]public async Task Loads_three_beams_from_file(){    var source = new JsonModelSource("TestData/three-beams.json");    var beams = await source.LoadBeamsAsync();    Assert.Equal(3, beams.Count);}

Common assertions

AssertionChecks
Assert.Equal(expected, actual)values are equal (order: expected first)
Assert.True(cond) / Assert.False(cond)a condition
Assert.Null(x) / Assert.NotNull(x)null-ness
Assert.Contains(item, list) / Assert.Empty(list)collections
Assert.Throws<T>(() => …)that code throws exactly T
Assert.IsType<T>(obj)the exact run-time type

Fakes: testing code with dependencies

Thanks to dependency inversion, a class like AlertService(INotifier notifier) can be tested with a fake notifier that records calls instead of sending emails.

AlertServiceTests.cs
class FakeNotifier : INotifier{    public List<string> Sent { get; } = new();    public void Send(string subject, string body) => Sent.Add($"{subject}: {body}");} [Fact]public void Overloaded_beam_sends_one_alert(){    var fake = new FakeNotifier();    var service = new AlertService(fake);     service.Overloaded(new Beam("B12", 6.5, 42.2));     string sent = Assert.Single(fake.Sent);      // exactly one message    Assert.Equal("Overload: B12", sent);}

What makes a good unit test

QualityMeaning
Fastmilliseconds: no real database, network or disk where avoidable
Independenttests don’t share state or depend on run order
Repeatablesame result every run: inject clocks and random seeds
Cleara failure message tells you what broke without debugging
Covers edgeszero, negative, empty, null, maximum and the boundary values
Quick check

What is the role of the “Act” step?

Quick check

All tests pass, but users report that lengths over 24 m are rejected. What does that tell you?

Practice

Write xUnit tests for static string Grade(double utilisation) that returns "OK" for values up to 0.9, "WARN" up to 1.0 and "FAIL" above 1.0. Use one [Theory] that covers both sides of each boundary.

Show a hint

Boundaries are where bugs hide: test 0.9, a value just above it, 1.0 and just above that.

Show the solution
Solution.cs
public class GradeTests{    [Theory]    [InlineData(0.0, "OK")]    [InlineData(0.9, "OK")]    [InlineData(0.91, "WARN")]    [InlineData(1.0, "WARN")]    [InlineData(1.01, "FAIL")]    public void Grade_by_utilisation(double u, string expected)    {        Assert.Equal(expected, Checks.Grade(u));    }}
Output
Passed!  - Failed: 0, Passed: 5, Skipped: 0, Total: 5

Remember

  • Unit tests call your code with known inputs and assert the result: Arrange, Act, Assert.
  • [Fact] for one case, [Theory] + [InlineData] for many; Assert.Throws for errors.
  • Use fakes for dependencies, which DI and interfaces make easy.
  • Good tests are fast, independent, repeatable, clear and cover the edges.
0 of 60 lessons completed