csharp-mstest

Par github · awesome-copilot

Obtenez les bonnes pratiques pour les tests unitaires MSTest 3.x/4.x, notamment les API d'assertion modernes et les tests pilotés par les données

npx skills add https://github.com/github/awesome-copilot --skill csharp-mstest

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 champs readonly et 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

  1. Initialisation d'assembly - [AssemblyInitialize] (une fois par assembly de test)
  2. Initialisation de classe - [ClassInitialize] (une fois par classe de test)
  3. Initialisation de test (pour chaque méthode de test) :
    1. Constructeur
    2. Définir la propriété TestContext
    3. [TestInitialize]
  4. Exécution du test - la méthode de test s'exécute
  5. Nettoyage du test (pour chaque méthode de test) :
    1. [TestCleanup]
    2. DisposeAsync (si implémenté)
    3. Dispose (si implémenté)
  6. Nettoyage de classe - [ClassCleanup] (une fois par classe de test)
  7. 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 Assert quand 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 Assert quand 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 types
  • IEnumerable<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 ValueTuple ou TestDataRow à IEnumerable<object[]>. L'approche object[] 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

Skills similaires