Chaque fois que je modifiais du code, je faisais Alt-Tab vers le terminal, flèche vers le haut, Enter. dotnet test. Attendre. Vérifier. Retour dans l’éditeur. Répéter.
C’est pas dramatique. Mais sur une journée complète, ça finit par coûter cher en concentration. Le pire, c’est que j’avais déjà dotnet watch dans mon workflow pour le hot reload d’une app web. Je ne m’étais jamais demandé si ça pouvait faire autre chose.
Spoiler : ça peut.
dotnet watch test
La commande la plus sous-estimée du CLI .NET :
dotnet watch test
C’est tout. Chaque fois qu’un fichier .cs change dans le projet, les tests se relancent automatiquement. Pas besoin d’extension VS Code, pas besoin de plugin, pas besoin de NCrunch. Juste le SDK.
Et ça supporte --filter, donc on peut cibler un sous-ensemble de tests :
dotnet watch test --filter "FullyQualifiedName~MonNamespace.MaClasse"
Ou par trait :
dotnet watch test --filter "Category=Unit"
Pendant qu’on code, seuls les tests unitaires tournent. Les tests d’intégration, on les garde pour le CI ou pour un run manuel avant le commit.
Surveiller des fichiers autres que .cs
Par défaut, dotnet watch surveille les fichiers du projet : .cs, .razor, .cshtml, etc. Mais parfois on a besoin de surveiller autre chose. Un fichier JSON de configuration, des fichiers .sql, des templates.
On peut ajouter des fichiers supplémentaires dans le .csproj :
<ItemGroup>
<Watch Include="**\*.json" Exclude="bin\**;obj\**" />
<Watch Include="TestData\**\*" />
</ItemGroup>
Maintenant, si je modifie un fichier de données de test dans TestData/, les tests se relancent. Pratique quand les tests lisent des fixtures depuis le disque.
Exemple concret
Voici un setup que j’utilise régulièrement. Un projet xUnit avec des traits pour séparer les tests unitaires des tests d’intégration.
Le trait, c’est un simple attribut :
public class CategoryAttribute : TraitAttribute
{
public CategoryAttribute(string category)
: base("Category", category) { }
}
Note :
CategoryAttributes’utilise comme[Category(...)]parce que C# permet d’omettre le suffixeAttribute. Si vous préférez rester simple, utilisez directement[Trait("Category", "Unit")]sur vos tests.
Un test unitaire :
[Fact]
[Category("Unit")]
public void ParseConfig_WithValidJson_ReturnsExpected()
{
var json = """{"retries": 3, "timeout": 30}""";
var config = ConfigParser.Parse(json);
Assert.Equal(3, config.Retries);
Assert.Equal(30, config.Timeout);
}
Un test d’intégration :
[Fact]
[Category("Integration")]
public void GetUser_FromDatabase_ReturnsValidUser()
{
// ... accès DB, plus lent
}
Et la commande qui tourne en arrière-plan pendant que je code :
dotnet watch test --project tests/MonProjet.Tests \
--filter "Category=Unit"
Je touche un fichier, les tests unitaires passent en 2 secondes. Le feedback est quasi instantané.
Bonus : terminal splitté
Ce que je fais souvent : un terminal splitté en deux.
- Gauche :
dotnet watch test --filter "Category=Unit", mes tests qui tournent en continu. - Droite :
dotnet watch run, l’app qui se recharge à chaque changement.
Dans VS Code, Ctrl+\ pour splitter le terminal. Dans Windows Terminal ou iTerm2, c’est natif aussi.
Le résultat : je sauvegarde un fichier, et en même temps mes tests passent et l’app recharge. Zéro friction.
┌──────────────────────────┬──────────────────────┐
│ $ dotnet watch test │ $ dotnet watch run │
│ --filter Category=Unit │ │
│ │ │
│ Tests passed: 14 │ Now listening on: │
│ Duration: 1.8s │ https://localhost:5001│
│ Waiting for changes… │ Waiting for changes… │
└──────────────────────────┴──────────────────────┘
Un dernier truc
Si dotnet watch détecte un changement qui ne peut pas être appliqué en hot reload (un changement de signature de méthode, par exemple), il rebuild automatiquement. Pas besoin de le relancer.
Et si vous voulez forcer un rebuild complet à chaque changement plutôt que du hot reload :
dotnet watch test --no-hot-reload
Ça m’a simplifié la vie plus que je pensais. Si vous faites encore Alt-Tab + flèche haut + Enter, essayez ça une journée. Vous allez avoir du mal à revenir en arrière.
Bon watch (et lâchez la flèche du haut).
Ce billet a été rédigé avec l’aide de l’IA.