Expert · Testing, tooling and Unity · topic 36 of 43 · 35 min
Unit testing with xUnit
Explanation
Why automated tests?
You change the GST calculation for one product type and accidentally break it for all others. Without tests you find out from an angry customer. With tests, you run one command and see within seconds that 3 of 120 checks fail, and exactly which ones. Tests let teams change code without fear, and most .NET jobs expect you to write them.
Setting up a test project
dotnet new sln -n Billing
dotnet new classlib -o Billing.Core
dotnet new xunit -o Billing.Tests
dotnet sln add Billing.Core Billing.Tests
dotnet add Billing.Tests reference Billing.Core
dotnet testxUnit v3 is the newest major version of xUnit; check xunit.net for its current templates. [Fact], [Theory], [InlineData] and the Assert methods shown here work the same way in v2 and v3. MSTest and NUnit are other popular .NET test frameworks with very similar ideas.
The code under test
namespace Billing.Core;
public static class GstCalculator
{
public static decimal AddGst(decimal amount, decimal ratePercent)
{
if (amount < 0) throw new ArgumentOutOfRangeException(nameof(amount), "Amount cannot be negative");
return Math.Round(amount + amount * ratePercent / 100, 2, MidpointRounding.AwayFromZero);
}
}Your first tests: Arrange, Act, Assert
using Billing.Core;
namespace Billing.Tests;
public class GstCalculatorTests
{
[Fact]
public void AddGst_18Percent_AddsTax()
{
// Arrange
decimal amount = 1000m;
// Act
decimal total = GstCalculator.AddGst(amount, 18);
// Assert
Assert.Equal(1180m, total);
}
[Fact]
public void AddGst_NegativeAmount_Throws()
{
Assert.Throws<ArgumentOutOfRangeException>(() => GstCalculator.AddGst(-1m, 18));
}
}Theories: one test, many inputs
[Theory]
[InlineData(100, 5, 105)]
[InlineData(100, 12, 112)]
[InlineData(0, 18, 0)]
[InlineData(99.99, 18, 117.99)]
public void AddGst_VariousRates(double amount, double rate, double expected)
{
Assert.Equal((decimal)expected, GstCalculator.AddGst((decimal)amount, (decimal)rate));
}Useful assertions
| Assertion | Checks |
|---|---|
| Assert.Equal(expected, actual) | Values are equal |
| Assert.True(cond) / Assert.False(cond) | A condition |
| Assert.Null(x) / Assert.NotNull(x) | null or not |
| Assert.Contains(item, collection) | A collection contains an item |
| Assert.Empty(collection) | No items |
| Assert.Throws<T>(() => ...) | The code throws exactly T |
| await Assert.ThrowsAsync<T>(() => ...) | An async method throws T |
Testing async code
[Fact]
public async Task LoadAsync_ReturnsSavedItems()
{
var store = new InMemoryStore();
await store.SaveAsync("pen");
var items = await store.LoadAsync();
Assert.Contains("pen", items);
}What makes a good unit test
- Fast: milliseconds. No real network, database or sleeping.
- Independent: each test sets up its own data. xUnit creates a new instance of the test class for every test, so fields do not leak between tests.
- Repeatable: same result every run. Avoid DateTime.Now and random numbers without a fixed seed.
- One behaviour per test: when it fails, you know what broke.
- Tests behaviour, not private details: test what the public method promises.
Common mistakes and fixes
- A test with no Assert: it always passes and proves nothing.
- Testing with real time: a test that checks "today is a weekday" fails every Saturday. Inject the time (next topic).
- Big tests that check ten things: split them.
- Ignoring failing tests: a red test means something is wrong. Fix the code or the test, do not skip it silently.
Need help?
Stuck on a word or an idea? Search this course, or every course, and jump straight to the explanation or a worked question.
Try:
Watch the lesson
Unit testing with xUnit · 1/7
In this lesson you will write automated tests that check your code in seconds.
Prefer a video by a teacher? Find free videos on “xUnit unit testing C# tutorial Fact Theory InlineData dotnet test” on YouTube ↗
In real life: Guarding a loan EMI calculator
A bank's loan calculator shows customers their monthly EMI. A wrong formula could misquote thousands of customers. The team keeps a theory test with known EMI values checked against the bank's spreadsheet, and the build fails if any change breaks them.
public static class Emi
{
// EMI = P * r * (1 + r)^n / ((1 + r)^n - 1), r = monthly rate
public static decimal Monthly(decimal principal, double annualRatePercent, int months)
{
double r = annualRatePercent / 12 / 100;
double factor = Math.Pow(1 + r, months);
return Math.Round((decimal)((double)principal * r * factor / (factor - 1)), 0);
}
}
public class EmiTests
{
[Theory]
[InlineData(100000, 12, 12, 8885)]
[InlineData(500000, 9, 60, 10379)]
public void Monthly_KnownValues(int principal, double rate, int months, int expected)
=> Assert.Equal((decimal)expected, Emi.Monthly(principal, rate, months));
}Practice (0/4 done)
Which attribute marks a test that runs once with no parameters?
What is the correct argument order for Assert.Equal?
A [Theory] has three [InlineData] rows. How many test cases run?
Create a solution with a class library containing Grade.For(int marks) (90+ "A", 75+ "B", 40+ "C", else "F"; throw ArgumentOutOfRangeException outside 0 to 100) and an xUnit project. Write a [Theory] with at least 8 InlineData rows covering every boundary (0, 39, 40, 74, 75, 89, 90, 100) and two [Fact] tests for -1 and 101 throwing.
Quick quiz
Read more (free)
Related help
Before this, learn
This topic builds on these. If something here feels new, open them first.
Related topics
- Test doubles: fakes, stubs and mocksTesting, tooling and Unity · same module
- NuGet packages, solutions and project structureTesting, tooling and Unity · same module
- Game development with Unity and C#Testing, tooling and Unity · same module
- Unit testing: checking one piece at a timeBackend Development · Testing and API documentation
- Unit testsRust Programming · Testing, tooling and unsafe Rust
- Testing your codeJavaScript Programming (language in depth) · Functional patterns and testing
Questions people ask about this
Open a question to see the answer and why. The first ones are this topic's own quiz, so try the quiz above before you peek.
What does dotnet test do?
Answer: Builds the projects and runs all the tests
It builds and runs tests, reporting passes and failures.
What does Arrange-Act-Assert describe?
Answer: The structure of a test: set up, call, check
It keeps tests readable.
Why should unit tests avoid DateTime.Now?
Answer: The result changes over time, so tests become unrepeatable
Tests must give the same result every run.
How do you test that a method throws ArgumentException?
Answer: Assert.Throws<ArgumentException>(() => Method())
Assert.Throws checks the exact exception type.
What is the return type of an async xUnit test?
Answer: Task
async Task tests let xUnit await them and see failures.
In Arrange, Act, Assert, what happens in the Act step?Related
Answer: You call the code being tested
Arrange prepares, Act calls the function once, Assert checks the result.
Can unit tests call private functions of their module?Related
Answer: Yes, because the tests module is a child module
Child modules can see private items of their parent.
From Unit tests
Which test kind should you have the MOST of?Related
Answer: Unit
Unit tests are fast and pinpoint failures; the pyramid has them at the base.
From Testing your code
What does Arrange-Act-Assert mean?Related
Answer: Set up the data, call the code, check the result
It is the simple shape of almost every test.
Pass the quiz (60% or more) or mark this topic as done when you are ready.
Your progress is saved on this device only.
Next steps
- 3 more topics in “Testing, tooling and Unity” before its exam: Test doubles: fakes, stubs and mocks, NuGet packages, solutions and project structure, Game development with Unity and C#.
- Module 9 exam: Testing, tooling and Unity · 12 questions · 20 min, without help. 4 topics of this module still to finish first.
- Then the next module, Interviews, portfolio and careers. 43 topics left in the course before the final exam.