Le paysage des frameworks de tests unitaires en .NET a longtemps été dominé par des frameworks comme xUnit, NUnit ou MSTest. Avec .NET 8/9, un nouveau framework de test moderne TUnit, intégré nativement à l’écosystème .NET permet de faire des tests adaptés à du .Net moderne.
TUnit se distingue par sa syntaxe plus expressive, son intégration transparente avec le SDK .NET, sa rapidité (exécution parallèle) et permet d’avoir un async-first design.
Nous allons voir dans cet article comment utiliser TUnit et, en quoi il représente une évolution naturelle des pratiques de test dans l’univers .NET
Pour commencer avec TUnit
Pour utiliser TUnit il suffit d’installer le package TUnit (nous allons utiliser la version 0.90.6 dans cet article)
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="TUnit" Version="0.90.6" />
</ItemGroup>
</Project>
En plus d’être un framework d’exécution de tests, TUnit vient avec une librairie assez complète et fluent pour faire des assertions. Quand on utilisait XUnit ou NUnit, on ajoutait en plus un framework pour faire les assertions comme NFluent ou FluentAssertions. TUnit embarque toutes les fonctionnalités de ces framework (en mieux) et toutes les assertions sont async
Voyons dans un premier exemple simple à quoi ressemble TUnit
public class TUnitFirstExampleTest
{
[Test]
public async Task Should_Pow_Number()
{
var result = Math.Pow(5, 2);
await Assert.That(result).IsEqualTo(25);
}
[Test]
[Arguments(5, 25)]
public async Task Should_Pow_Number(int input, int expected)
{
var result = Math.Pow(input, 2);
await Assert.That(result).IsEqualTo(expected);
}
}
Quelques premières remarques par rapport à la syntaxe et une comparaison par rapport à XUnit
- Le mot clé pour méthode de test c’est
[Test] pour une méthode avec ou sans arguments (pour xunit c’est Fact et Theory/InlineData), la syntaxe est plus lisible/simple.
- Il n’est pas nécessaire de mettre un using TUnit car c’est détecté automatiquement
- Les méthodes pour faire assertions : await Assert.That() : c’est similaire à la syntaxe NFluent avec l’ajout du await pour avoir un design async-first (surtout si on fait du TDD) qui est hyper important pour les API modernes.
Utilisation de TUnit pour les collections
Supposons que nous avons une API qui permet de gérer des users avec une entité User et un service UserService
public class User
{
public int Id { get; set; }
public required string Name { get; set; }
public string? Email { get; set; }
}
public interface IUserService
{
Task<IList<User>> GetAllUsersAsync();
}
public class UserService : IUserService
{
public async Task<IList<User>> GetAllUsersAsync()
{
await Task.Delay(1000); //pour simuler l'appel à une méthode async comme l'accès à la base de données
IList<User> users =
[
new User
{
Id = 1,
Name = "Pape DIENG",
Email = "tunit@test.fr"
},
new User
{
Id = 2,
Name = "Mr Chicken",
Email = "ChickenIsLife@test.fr"
}
];
return users;
}
}
Les collections font partie des objets qui sont très utilisés pour faire des tests unitaire. Ci dessous quelques méthodes d’extensions TUnit qui permettent de faciliter les tests sur les collections
namespace TUnitDemo
{
public class UserServiceTest
{
private readonly IUserService _userService;
public UserServiceTest()
{
_userService = new UserService();
}
[Test]
public async Task Should_Check_TUnit_Collection_Items()
{
IList<User> users = await _userService.GetAllUsersAsync();
//Check sur la non nullité, la taille et exemple d'utilisation de l'opérateur And
await Assert.That(users).IsNotNull()
.And.IsNotEmpty()
.And.HasCount(2);
//Check qu'un user existe et en plus stockage du résulat dans un objet en une seule instruction
var pape = await Assert.That(users).Contains(u => u.Id == 1);
await Assert.That(pape.Name).EqualTo("Pape DIENG");
//Check sur la non existence
await Assert.That(users).DoesNotContain(u => u.Id == -1);
//Check qu'une collection contient un seul élément
await Assert.That(users.Where(u=>u.Id == 1)).HasSingleItem();
//Check si une collection est triée
await Assert.That(users).IsOrderedBy(u => u.Id);
//Check que tous les Items de la collection vérifient une condition
await Assert.That(users).All(u => u.Id > 0);
/*Pour aller plus loin on a :
* IsEquivalentTo().Using() pour comparer des collections
* IsNotEquivalentTo()
* Check sur les collections de collections
* pour plus d'exemples https://tunit.dev/docs/assertions/collections
*/
}
}
}
Comme vous pouvez le voir TUnit offre une librairie très complète et fluent pour manipuler des collections. La librairie contient aussi ce qu’il faut pour manipuler tout type d’objets en .Net :
- Les types valeurs/références
- Check sur les Enum
- Check sur le types : interface, class, types spécifiques, les types primitifs, les objets ORM,…
- une librairie riche sur les exceptions : type d’exception, message avec ou sans casse, l’inner exception, …
- Une librairie riche sur les DateTime/DateOnly
(IsUtc(), IsInFuture(), IsInPast(),IsInFutureUtc(), IsOnWeekday(), IsOnWeekend(),IsGreater/Less/ThanOrEqualTo() ...) pour plus d’infos sur la librairie spécifique aux dates => https://tunit.dev/docs/assertions/datetime
- une librairie spécifique sur les dictionnaires
- …
Si vous voulez tester/découvrir toutes les fonctionnalités que propose TUnit, vous aurez toutes les informations ici https://tunit.dev/docs/assertions/getting-started
Conclusion
Etant quelqu’un qui est passionné par les tests (unitaires, d’intégration, end to end, BDD, …) et particulièrement tout ce qui est « test driven », j’ai été séduit par le framework TUnit. Au cours des presque 10 dernières années, j’ai fait la promotion de XUnit/NFluent au sein des équipes où j’ai travaillé, et maintenant, pour les tests, je recommande fortement TUnit pour les projets modernes en .Net 8+, surtout pour les API. J’espère que ça vous motivera à le tester même si XUnit est encore plus répandu à ce jour avec une grande communauté 🙂