NuGet Packages and Unit Testing
Two habits separate hobby code from professional code: reusing well-tested libraries instead of reinventing them, and writing automated tests for your own code. In .NET, libraries come as NuGet packages, and tests are ordinary C# methods run by a test framework such as xUnit.
NuGet packages
NuGet is .NET's package manager, and nuget.org hosts hundreds of thousands of free packages: JSON tools, database drivers, logging, testing, PDF generation and much more. Add one to a project with dotnet add package:
dotnet add package Humanizer
This downloads the package and records it in the project file:
<ItemGroup> <PackageReference Include="Humanizer" Version="2.14.1" /> </ItemGroup>
The next build restores (downloads) any missing packages automatically, so you commit the project file — not the packages — to version control.
Packages in single-file apps
A single-file app references packages with a #:package directive at the top, giving the package name and version:
#:package Humanizer@2.14.1 using Humanizer; Console.WriteLine("customer_order_id".Humanize()); Console.WriteLine(TimeSpan.FromMinutes(135).Humanize(2)); Console.WriteLine(3.ToWords()); Console.WriteLine("person".Pluralize()); Console.WriteLine(TimeSpan.FromDays(16).Humanize());
customer order id 2 hours, 15 minutes three people 2 weeks
Choosing packages wisely
Every package you add is code you're trusting. Before adding one, check on nuget.org that it's actively maintained, widely downloaded, from a known author, and has a licence that suits your project. Keep packages up to date — dotnet list package --outdated shows what's behind, and dotnet list package --vulnerable reports known security issues.
Why automated tests?
A unit test is a small method that calls your code with known inputs and checks the result. Run them after every change and you'll know within seconds whether you broke something. Tests also document how code is meant to be used, and they make refactoring safe.
Setting up a test project
Tests live in their own project that references the code under test. With the code in a class library called Pricing:
dotnet new classlib -o Pricing dotnet new xunit -o Pricing.Tests dotnet add Pricing.Tests reference Pricing
Here is the library code we'll test:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> </PropertyGroup> </Project>
namespace Pricing; public static class PriceCalculator { public static decimal ApplyDiscount(decimal price, int percent) { ArgumentOutOfRangeException.ThrowIfNegative(price); ArgumentOutOfRangeException.ThrowIfGreaterThan(percent, 100); return Math.Round(price * (100 - percent) / 100, 2); } public static decimal Shipping(decimal orderTotal) => orderTotal switch { >= 50m => 0m, >= 20m => 3.99m, _ => 6.99m, }; }
The test project file that dotnet new xunit creates references the test framework packages, plus our reference to the library:
<Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <TargetFramework>net10.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <IsPackable>false</IsPackable> </PropertyGroup> <ItemGroup> <PackageReference Include="coverlet.collector" Version="6.0.4" /> <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" /> <PackageReference Include="xunit" Version="2.9.3" /> <PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" /> </ItemGroup> <ItemGroup> <Using Include="Xunit" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\Pricing\Pricing.csproj" /> </ItemGroup> </Project>
Writing tests
In xUnit, a test is a public method marked [Fact]. A [Theory] runs the same test with several sets of data supplied by [InlineData]. Assert methods check the results — if one fails, the test fails:
using Pricing; namespace Pricing.Tests; public class PriceCalculatorTests { [Fact] public void ApplyDiscount_TakesPercentageOff() { decimal result = PriceCalculator.ApplyDiscount(80m, 25); Assert.Equal(60m, result); } [Fact] public void ApplyDiscount_RejectsPercentOver100() { Assert.Throws<ArgumentOutOfRangeException>(() => PriceCalculator.ApplyDiscount(10m, 150)); } [Theory] [InlineData(10, 6.99)] [InlineData(20, 3.99)] [InlineData(49.99, 3.99)] [InlineData(50, 0)] public void Shipping_DependsOnOrderTotal(decimal total, decimal expected) { Assert.Equal(expected, PriceCalculator.Shipping(total)); } }
dotnet test
Passed! - Failed: 0, Passed: 6, Skipped: 0, Total: 6
The four InlineData rows count as four separate tests, so six tests ran in total. Test names that describe the behaviour (Method_Scenario_ExpectedResult) make failures self-explanatory.
When a test fails
Suppose a change accidentally makes orders of exactly 50 pay shipping. The test run fails and reports which test, what was expected and what actually happened:
Failed Pricing.Tests.PriceCalculatorTests.Shipping_DependsOnOrderTotal(total: 50, expected: 0) [1 ms] Error Message: Assert.Equal() Failure: Values differ Expected: 0 Actual: 3.99
That's the value of tests: the bug is caught seconds after it's written, with a precise description, instead of by a customer.
The Arrange–Act–Assert pattern
Most tests follow the same three steps — set up the inputs, call the code, check the result:
[Fact] public void Withdraw_ReducesBalance() { // Arrange var account = new BankAccount("Ana"); account.Deposit(100m); // Act account.Withdraw(30m); // Assert Assert.Equal(70m, account.Balance); }
Useful assertions
| Assertion | Checks that… |
|---|---|
Assert.Equal(expected, actual) | two values are equal |
Assert.True(condition) / Assert.False | a condition holds |
Assert.Null(x) / Assert.NotNull(x) | a value is (not) null |
Assert.Contains(item, collection) | a collection contains an item |
Assert.Empty(collection) | a collection is empty |
Assert.Throws<T>(() => …) | code throws a specific exception |
await Assert.ThrowsAsync<T>(…) | async code throws |
Other test frameworks
xUnit is the most common choice for new .NET projects, but NUnit (dotnet new nunit) and Microsoft's MSTest (dotnet new mstest) are also widely used and work the same way in principle — only the attribute names differ ([Test], [TestMethod]). Editors such as VS Code with C# Dev Kit, Visual Studio and Rider show tests in a test explorer where you can run and debug them individually.
What makes a good unit test
- Fast — milliseconds, so you can run hundreds after every change.
- Independent — no test relies on another having run first.
- Repeatable — no dependence on the clock, the network or random numbers. Pass those in (for example a
TimeProvider, lesson 56) so tests can control them. - Focused — one behaviour per test, with a name that says which.
Try it yourself
Write the code under test first: a Password.IsStrong method. Then, in a test project, add a [Theory] with strong and weak examples.
abc False password123 False Tr0ub4dor&3 True CorrectHorse9 True