Bonnes pratiques MSTest (MSTest 3.x/4.x)
Votre objectif est de vous aider à écrire des tests unitaires efficaces avec MSTest moderne, en utilisant les APIs actuelles et les bonnes pratiques.
Configuration du projet
- Utilisez un projet de test séparé avec la convention de nommage
[NomDuProjet].Tests - Référencez les packages NuGet MSTest 3.x+ (inclut les analyseurs)
- Envisagez d'utiliser MSTest.Sdk pour simplifier la configuration du projet
- Exécutez les tests avec
dotnet test
Structure des classes de test
- Utilisez l'attribut
[TestClass]pour les classes de test - Scellez les classes de test par défaut pour les performances et la clarté de la conception
- Utilisez
[TestMethod]pour les méthodes de test (préférez à[DataTestMethod]) - Suivez le modèle Arrange-Act-Assert (AAA)
- Nommez les tests selon le modèle
NomDeLaMéthode_Scénario_ComportementAttendu
[TestClass]
public sealed class CalculatorTests
{
[TestMethod]
public void Add_TwoPositiveNumbers_ReturnsSum()
{
// Arrange
var calculator = new Calculator();
// Act
var result = calculator.Add(2, 3);
// Assert
Assert.AreEqual(5, result);
}
}
Cycle de vie des tests
- Préférez les constructeurs à
[TestInitialize]- permet les champsreadonlyet suit les modèles C# standard - Utilisez
[TestCleanup]pour le nettoyage qui doit s'exécuter même si le test échoue - Combinez le constructeur avec
[TestInitialize]async quand une initialisation asynchrone est nécessaire
[TestClass]
public sealed class ServiceTests
{
private readonly MyService _service; // readonly activé par le constructeur
public ServiceTests()
{
_service = new MyService();
}
[TestInitialize]
public async Task InitAsync()
{
// Utilisez pour l'initialisation asynchrone uniquement
await _service.WarmupAsync();
}
[TestCleanup]
public void Cleanup() => _service.Reset();
}
Ordre d'exécution
- Initialisation d'assembly -
[AssemblyInitialize](une fois par assembly de test) - Initialisation de classe -
[ClassInitialize](une fois par classe de test) - Initialisation de test (pour chaque méthode de test) :
- Constructeur
- Définir la propriété
TestContext [TestInitialize]
- Exécution du test - la méthode de test s'exécute
- Nettoyage du test (pour chaque méthode de test) :
[TestCleanup]DisposeAsync(si implémenté)Dispose(si implémenté)
- Nettoyage de classe -
[ClassCleanup](une fois par classe de test) - Nettoyage d'assembly -
[AssemblyCleanup](une fois par assembly de test)
APIs d'assertion modernes
MSTest fournit trois classes d'assertion : Assert, StringAssert et CollectionAssert.
Classe Assert - Assertions principales
// Égalité
Assert.AreEqual(expected, actual);
Assert.AreNotEqual(notExpected, actual);
Assert.AreSame(expectedObject, actualObject); // Égalité de référence
Assert.AreNotSame(notExpectedObject, actualObject);
// Vérifications null
Assert.IsNull(value);
Assert.IsNotNull(value);
// Booléen
Assert.IsTrue(condition);
Assert.IsFalse(condition);
// Échouer/Inconclussif
Assert.Fail("Test failed due to...");
Assert.Inconclusive("Test cannot be completed because...");
Test d'exceptions (Préférez à [ExpectedException])
// Assert.Throws - correspond à TException ou les types dérivés
var ex = Assert.Throws<ArgumentException>(() => Method(null));
Assert.AreEqual("Value cannot be null.", ex.Message);
// Assert.ThrowsExactly - correspond uniquement au type exact
var ex = Assert.ThrowsExactly<InvalidOperationException>(() => Method());
// Versions asynchrones
var ex = await Assert.ThrowsAsync<HttpRequestException>(async () => await client.GetAsync(url));
var ex = await Assert.ThrowsExactlyAsync<InvalidOperationException>(async () => await Method());
Assertions de collection (classe Assert)
Assert.Contains(expectedItem, collection);
Assert.DoesNotContain(unexpectedItem, collection);
Assert.ContainsSingle(collection); // exactement un élément
Assert.HasCount(5, collection);
Assert.IsEmpty(collection);
Assert.IsNotEmpty(collection);
Assertions de chaîne (classe Assert)
Assert.Contains("expected", actualString);
Assert.StartsWith("prefix", actualString);
Assert.EndsWith("suffix", actualString);
Assert.DoesNotStartWith("prefix", actualString);
Assert.DoesNotEndWith("suffix", actualString);
Assert.MatchesRegex(@"\d{3}-\d{4}", phoneNumber);
Assert.DoesNotMatchRegex(@"\d+", textOnly);
Assertions de comparaison
Assert.IsGreaterThan(lowerBound, actual);
Assert.IsGreaterThanOrEqualTo(lowerBound, actual);
Assert.IsLessThan(upperBound, actual);
Assert.IsLessThanOrEqualTo(upperBound, actual);
Assert.IsInRange(actual, low, high);
Assert.IsPositive(number);
Assert.IsNegative(number);
Assertions de type
// MSTest 3.x - utilise le paramètre out
Assert.IsInstanceOfType<MyClass>(obj, out var typed);
typed.DoSomething();
// MSTest 4.x - retourne le résultat typé directement
var typed = Assert.IsInstanceOfType<MyClass>(obj);
typed.DoSomething();
Assert.IsNotInstanceOfType<WrongType>(obj);
Assert.That (MSTest 4.0+)
Assert.That(result.Count > 0); // Capture automatiquement l'expression dans le message d'échec
Classe StringAssert
Remarque : Préférez les équivalents de la classe
Assertquand disponibles (par exemple,Assert.Contains("expected", actual)àStringAssert.Contains(actual, "expected")).
StringAssert.Contains(actualString, "expected");
StringAssert.StartsWith(actualString, "prefix");
StringAssert.EndsWith(actualString, "suffix");
StringAssert.Matches(actualString, new Regex(@"\d{3}-\d{4}"));
StringAssert.DoesNotMatch(actualString, new Regex(@"\d+"));
Classe CollectionAssert
Remarque : Préférez les équivalents de la classe
Assertquand disponibles (par exemple,Assert.Contains).
// Contenance
CollectionAssert.Contains(collection, expectedItem);
CollectionAssert.DoesNotContain(collection, unexpectedItem);
// Égalité (mêmes éléments, même ordre)
CollectionAssert.AreEqual(expectedCollection, actualCollection);
CollectionAssert.AreNotEqual(unexpectedCollection, actualCollection);
// Équivalence (mêmes éléments, n'importe quel ordre)
CollectionAssert.AreEquivalent(expectedCollection, actualCollection);
CollectionAssert.AreNotEquivalent(unexpectedCollection, actualCollection);
// Vérifications de sous-ensemble
CollectionAssert.IsSubsetOf(subset, superset);
CollectionAssert.IsNotSubsetOf(notSubset, collection);
// Validation d'éléments
CollectionAssert.AllItemsAreInstancesOfType(collection, typeof(MyClass));
CollectionAssert.AllItemsAreNotNull(collection);
CollectionAssert.AllItemsAreUnique(collection);
Tests pilotés par les données
DataRow
[TestMethod]
[DataRow(1, 2, 3)]
[DataRow(0, 0, 0, DisplayName = "Zeros")]
[DataRow(-1, 1, 0, IgnoreMessage = "Known issue #123")] // MSTest 3.8+
public void Add_ReturnsSum(int a, int b, int expected)
{
Assert.AreEqual(expected, Calculator.Add(a, b));
}
DynamicData
La source de données peut retourner l'un des types suivants :
IEnumerable<(T1, T2, ...)>(ValueTuple) - préféré, fournit la sécurité des types (MSTest 3.7+)IEnumerable<Tuple<T1, T2, ...>>- fournit la sécurité des typesIEnumerable<TestDataRow>- fournit la sécurité des types plus le contrôle sur les métadonnées de test (nom d'affichage, catégories)IEnumerable<object[]>- moins préféré, pas de sécurité des types
Remarque : Lors de la création de nouvelles méthodes de données de test, préférez
ValueTupleouTestDataRowàIEnumerable<object[]>. L'approcheobject[]ne fournit aucune vérification de type au moment de la compilation et peut entraîner des erreurs d'exécution dues aux incompatibilités de type.
[TestMethod]
[DynamicData(nameof(TestData))]
public void DynamicTest(int a, int b, int expected)
{
Assert.AreEqual(expected, Calculator.Add(a, b));
}
// ValueTuple - préféré (MSTest 3.7+)
public static IEnumerable<(int a, int b, int expected)> TestData =>
[
(1, 2, 3),
(0, 0, 0),
];
// TestDataRow - quand vous avez besoin de noms d'affichage ou métadonnées personnalisés
public static IEnumerable<TestDataRow<(int a, int b, int expected)>> TestDataWithMetadata =>
[
new((1, 2, 3)) { DisplayName = "Positive numbers" },
new((0, 0, 0)) { DisplayName = "Zeros" },
new((-1, 1, 0)) { DisplayName = "Mixed signs", IgnoreMessage = "Known issue #123" },
];
// IEnumerable<object[]> - évitez pour le nouveau code (pas de sécurité des types)
public static IEnumerable<object[]> LegacyTestData =>
[
[1, 2, 3],
[0, 0, 0],
];
TestContext
La classe TestContext fournit des informations sur l'exécution du test, le support d'annulation et les méthodes de sortie.
Consultez la documentation TestContext pour la référence complète.
Accès à TestContext
// Propriété (MSTest supprime CS8618 - n'utilisez pas nullable ou = null!)
public TestContext TestContext { get; set; }
// Injection via constructeur (MSTest 3.6+) - préféré pour l'immuabilité
[TestClass]
public sealed class MyTests
{
private readonly TestContext _testContext;
public MyTests(TestContext testContext)
{
_testContext = testContext;
}
}
// Les méthodes statiques la reçoivent en paramètre
[ClassInitialize]
public static void ClassInit(TestContext context) { }
// Optionnel pour les méthodes de nettoyage (MSTest 3.6+)
[ClassCleanup]
public static void ClassCleanup(TestContext context) { }
[AssemblyCleanup]
public static void AssemblyCleanup(TestContext context) { }
Jeton d'annulation
Utilisez toujours TestContext.CancellationToken pour l'annulation coopérative avec [Timeout] :
[TestMethod]
[Timeout(5000)]
public async Task LongRunningTest()
{
await _httpClient.GetAsync(url, TestContext.CancellationToken);
}
Propriétés d'exécution du test
TestContext.TestName // Nom de la méthode de test actuelle
TestContext.TestDisplayName // Nom d'affichage (3.7+)
TestContext.CurrentTestOutcome // Pass/Fail/InProgress
TestContext.TestData // Données de test paramétrées (3.7+, dans TestInitialize/Cleanup)
TestContext.TestException // Exception si le test a échoué (3.7+, dans TestCleanup)
TestContext.DeploymentDirectory // Répertoire avec les éléments déployés
Sortie et fichiers de résultats
// Écrire dans la sortie du test (utile pour le débogage)
TestContext.WriteLine("Processing item {0}", itemId);
// Joindre des fichiers aux résultats du test (journaux, captures d'écran)
TestContext.AddResultFile(screenshotPath);
// Stocker/récupérer des données entre les méthodes de test
TestContext.Properties["SharedKey"] = computedValue;
Fonctionnalités avancées
Nouvelle tentative pour les tests instables (MSTest 3.9+)
[TestMethod]
[Retry(3)]
public void FlakyTest() { }
Exécution conditionnelle (MSTest 3.10+)
Ignorez ou exécutez les tests en fonction du système d'exploitation ou de l'environnement CI :
// Tests spécifiques au système d'exploitation
[TestMethod]
[OSCondition(OperatingSystems.Windows)]
public void WindowsOnlyTest() { }
[TestMethod]
[OSCondition(OperatingSystems.Linux | OperatingSystems.MacOS)]
public void UnixOnlyTest() { }
[TestMethod]
[OSCondition(ConditionMode.Exclude, OperatingSystems.Windows)]
public void SkipOnWindowsTest() { }
// Tests d'environnement CI
[TestMethod]
[CICondition] // S'exécute uniquement en CI (par défaut : ConditionMode.Include)
public void CIOnlyTest() { }
[TestMethod]
[CICondition(ConditionMode.Exclude)] // Ignore en CI, s'exécute localement
public void LocalOnlyTest() { }
Parallélisation
// Niveau d'assembly
[assembly: Parallelize(Workers = 4, Scope = ExecutionScope.MethodLevel)]
// Désactiver pour une classe spécifique
[TestClass]
[DoNotParallelize]
public sealed class SequentialTests { }
Traçabilité des éléments de travail (MSTest 3.8+)
Liez les tests aux éléments de travail pour la traçabilité dans les rapports de test :
// Éléments de travail Azure DevOps
[TestMethod]
[WorkItem(12345)] // Lie à l'élément de travail #12345
public void Feature_Scenario_ExpectedBehavior() { }
// Éléments de travail multiples
[TestMethod]
[WorkItem(12345)]
[WorkItem(67890)]
public void Feature_CoversMultipleRequirements() { }
// Problèmes GitHub (MSTest 3.8+)
[TestMethod]
[GitHubWorkItem("https://github.com/owner/repo/issues/42")]
public void BugFix_Issue42_IsResolved() { }
Les associations d'éléments de travail apparaissent dans les résultats de test et peuvent être utilisées pour :
- Tracer la couverture de test aux exigences
- Lier les corrections de bugs aux tests de régression
- Générer des rapports de traçabilité dans les pipelines CI/CD
Erreurs courantes à éviter
// ❌ Mauvais ordre d'arguments
Assert.AreEqual(actual, expected);
// ✅ Correct
Assert.AreEqual(expected, actual);
// ❌ Utiliser ExpectedException (obsolète)
[ExpectedException(typeof(ArgumentException))]
// ✅ Utiliser Assert.Throws
Assert.Throws<ArgumentException>(() => Method());
// ❌ Utiliser LINQ Single() - exception peu claire
var item = items.Single();
// ✅ Utiliser ContainsSingle - meilleur message d'échec
var item = Assert.ContainsSingle(items);
// ❌ Cast difficile - exception peu claire
var handler = (MyHandler)result;
// ✅ Assertion de type - affiche le type réel en cas d'échec
var handler = Assert.IsInstanceOfType<MyHandler>(result);
// ❌ Ignorer le jeton d'annulation
await client.GetAsync(url, CancellationToken.None);
// ✅ Transférer l'annulation du test
await client.GetAsync(url, TestContext.CancellationToken);
// ❌ Rendre TestContext nullable - conduit à des vérifications null inutiles
public TestContext? TestContext { get; set; }
// ❌ Utiliser null! - MSTest supprime déjà CS8618 pour cette propriété
public TestContext TestContext { get; set; } = null!;
// ✅ Déclarer sans nullable ou initialiseur - MSTest gère l'avertissement
public TestContext TestContext { get; set; }
Organisation des tests
- Groupez les tests par fonctionnalité ou composant
- Utilisez
[TestCategory("Category")]pour le filtrage - Utilisez
[TestProperty("Name", "Value")]pour les métadonnées personnalisées (par exemple,[TestProperty("Bug", "12345")]) - Utilisez
[Priority(1)]pour les tests critiques - Activez les analyseurs MSTest pertinents (MSTEST0020 pour la préférence du constructeur)
Mocking et isolation
- Utilisez Moq ou NSubstitute pour simuler les dépendances
- Utilisez les interfaces pour faciliter la simulation
- Simulez les dépendances pour isoler les unités testées