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.
Creating a test project
dotnet new classlib -n SteelToolsdotnet new xunit -n SteelTools.Testsdotnet add SteelTools.Tests reference SteelToolsdotnet new sln && dotnet sln add SteelTools SteelTools.Testsdotnet testdotnet test does the same from the terminal or a CI pipeline.
Arrange, Act, Assert
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_expectedor plain words.One behaviour per test, so a red test tells you exactly what broke. For double, compare with aprecision(decimal places) or tolerance, because of floating-point rounding.
[Theory]: one test, many inputs
[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
[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
| Assertion | Checks |
|---|---|
Assert.Equal(expected, actual) | |
Assert.True(cond) / Assert.False(cond) | |
Assert.Null(x) / Assert.NotNull(x) | |
Assert.Contains(item, list) / Assert.Empty(list) | |
Assert.Throws<T>(() => …) | T |
Assert.IsType<T>(obj) |
Fakes: testing code with dependencies
AlertService(INotifier notifier) can be tested with a fake notifier that records calls instead of sending emails.
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
| Quality | Meaning |
|---|---|
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
Show the solution
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)); }}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.Throwsfor errors.Use fakes for dependencies, which DI and interfaces make easy. Good tests are fast, independent, repeatable, clear and cover the edges.