web-dev-qa-db-fra.com

Test unitaire des fichiers I/O

En parcourant les threads liés aux tests unitaires existants ici sur Stack Overflow, je n’en ai trouvé aucun avec une réponse claire sur la façon de tester en bloc les opérations d’E/S des fichiers. Je n’ai que récemment commencé à étudier les tests unitaires, étant au courant des avantages, mais ayant du mal à s’habituer à la rédaction de tests au préalable. J'ai configuré mon projet pour utiliser NUnit et Rhino Mocks et bien que je comprenne le concept qui les sous-tend, j'ai un peu de difficulté à comprendre comment utiliser Mock Objects.

Plus précisément, j'ai deux questions auxquelles j'aimerais avoir une réponse. Tout d’abord, quelle est la bonne façon de tester les opérations d’E/S sur les fichiers? Deuxièmement, dans mes tentatives pour en apprendre davantage sur les tests unitaires, je suis tombé sur une injection de dépendance. Après avoir configuré et utilisé Ninject, je me demandais si je devais utiliser DI dans mes tests unitaires, ou simplement instancier directement des objets.

56
Shaun Hamman

Consultez Tutoriel pour TDD utilisez Rhino Mocks et SystemWrapper .

SystemWrapper enveloppe de nombreuses classes System.IO, notamment File, FileInfo, Directory, DirectoryInfo, .... Vous pouvez voir la liste complète .

Dans ce tutoriel, je montre comment faire des tests avec MbUnit, mais c'est exactement la même chose pour NUnit.

Votre test va ressembler à ceci:

[Test]
public void When_try_to_create_directory_that_already_exists_return_false()
{
    var directoryInfoStub = MockRepository.GenerateStub<IDirectoryInfoWrap>();
    directoryInfoStub.Stub(x => x.Exists).Return(true);
    Assert.AreEqual(false, new DirectoryInfoSample().TryToCreateDirectory(directoryInfoStub));

    directoryInfoStub.AssertWasNotCalled(x => x.Create());
}
31
Vadim

Il n'y a pas nécessairement une chose à faire lors du test du système de fichiers. En vérité, vous pouvez faire plusieurs choses, selon les circonstances.

La question que vous devez poser est la suivante: Qu'est-ce que je teste?

  • Que le système de fichiers fonctionne? Vous n'avez probablement pas besoin de tester que à moins que vous n'utilisiez un système d'exploitation avec lequel vous êtes extrêmement peu familier. Ainsi, si vous donnez simplement une commande pour enregistrer des fichiers, par exemple, vous perdrez du temps à écrire un test pour vous assurer qu'ils sont réellement sauvegardés.

  • Que les fichiers sont enregistrés au bon endroit? Bien, comment savez-vous quel est le bon endroit? Vous avez probablement un code qui combine un chemin d'accès avec un nom de fichier. Vous pouvez facilement tester ce code: votre entrée est composée de deux chaînes et votre sortie doit être une chaîne représentant un emplacement de fichier valide construit à l'aide de ces deux chaînes.

  • Que vous obtenez le bon ensemble de fichiers d'un répertoire? Vous devrez probablement écrire un test pour votre classe file-getter qui teste réellement le système de fichiers. Mais vous devriez utiliser un répertoire de test contenant des fichiers qui ne changeront pas. Vous devez également placer ce test dans un projet de test d'intégration, car il ne s'agit pas d'un véritable test unitaire, car il dépend du système de fichiers.

  • Mais, je dois faire quelque chose avec les fichiers que je reçois. Pour that test, vous devez utiliser un fake pour votre classe file-getter. Votre faux doit renvoyer une liste de fichiers codée en dur. Si vous utilisez un récupérateur de fichiers real et un processeur de fichiers real, vous ne saurez pas lequel provoque l'échec du test. Ainsi, lors de vos tests, votre classe de processeur de fichiers devrait utiliser une fausse classe de lecture de fichiers. Votre classe de traitement de fichiers doit prendre le fichier getter interface. Dans le code réel, vous passerez dans le véritable fichier-getter. Dans le code de test, vous passerez un faux fichier-getter qui renvoie une liste statique connue.

Les principes fondamentaux sont:

  • Utilisez un faux système de fichiers, caché derrière une interface, lorsque vous ne testez pas le système de fichiers lui-même.
  • Si vous avez besoin de tester des opérations de fichiers réels, alors
    • marquer le test comme un test d'intégration, pas un test unitaire.
    • avoir un répertoire de test désigné, un ensemble de fichiers, etc. qui resteront toujours dans un état inchangé, afin que vos tests d'intégration orientés fichier puissent réussir de manière cohérente.
40
Ryan Lundy

Q1: 

Vous avez trois options ici. 

Option 1: vivre avec.  

(pas d'exemple: P)

Option 2: Créez une légère abstraction si nécessaire.  

Au lieu de faire le fichier I/O (File.ReadAllBytes ou autre) dans la méthode testée, vous pouvez le changer pour que le IO soit fait à l'extérieur et qu'un flux soit passé.

public class MyClassThatOpensFiles
{
    public bool IsDataValid(string filename)
    {
        var filebytes = File.ReadAllBytes(filename);
        DoSomethingWithFile(fileBytes);
    }
}

deviendrait

// File IO is done outside prior to this call, so in the level 
// above the caller would open a file and pass in the stream
public class MyClassThatNoLongerOpensFiles
{
    public bool IsDataValid(Stream stream) // or byte[]
    {
        DoSomethingWithStreamInstead(stream); // can be a memorystream in tests
    }
}

Cette approche est un compromis. Tout d’abord, oui, c’est plus testable. Cependant, la testabilité est traitée pour une légère augmentation de la complexité. Cela peut affecter la maintenabilité et la quantité de code que vous devez écrire. De plus, vous pouvez simplement déplacer votre problème de test d'un niveau supérieur. 

Cependant, selon mon expérience, il s'agit d'une approche sympa et équilibrée, car vous pouvez généraliser et rendre testable la logique importante sans vous engager dans un système de fichiers entièrement encapsulé. C'est à dire. vous pouvez généraliser les éléments qui vous intéressent le plus, tout en laissant le reste tel quel.

Option 3: Envelopper le système de fichiers entier

Pour aller plus loin, se moquer du système de fichiers peut être une approche valable; cela dépend de combien de ballot vous êtes prêt à vivre avec. 

J'ai déjà emprunté cette voie; J'avais implémenté un système de fichiers encapsulé, mais je l'ai simplement supprimé. Il y avait des différences subtiles dans l'API, je devais l'injecter partout et au final, c'était une douleur supplémentaire pour un gain minime, car beaucoup de classes l'utilisant n'étaient pas extrêmement importantes pour moi. Si j'avais utilisé un conteneur IoC ou écrit quelque chose de critique et que les tests devaient être rapides, je l'aurais peut-être bloqué. Comme pour toutes ces options, votre kilométrage peut varier.

En ce qui concerne votre question sur les conteneurs IoC:

Injectez vos doubles de test manuellement. Si vous devez effectuer beaucoup de travail répétitif, utilisez simplement les méthodes de configuration/usine dans vos tests. Utiliser un conteneur IoC pour les tests serait excessif à l'extrême! Peut-être que je ne comprends pas votre deuxième question, cependant.

10
Mark Simpson

Actuellement, je consomme un objet IFileSystem via une injection de dépendance. Pour le code de production, une classe wrapper implémente l'interface en encapsulant les fonctions IO spécifiques dont j'ai besoin. Lors du test, je peux créer une implémentation null ou stub et la fournir à la classe sous test. La classe testée n’est pas la plus sage.

1
Grant Palin

Depuis 2012, vous pouvez le faire en utilisant Microsoft Fakes sans qu'il soit nécessaire de modifier votre base de code par exemple car il était déjà gelé.

D'abord générez un faux Assembly for System.dll - ou tout autre paquet, puis modifiez les retours attendus comme suit:

using Microsoft.QualityTools.Testing.Fakes;
...
using (ShimsContext.Create())
{
     System.IO.Fakes.ShimFile.ExistsString = (p) => true;
     System.IO.Fakes.ShimFile.ReadAllTextString = (p) => "your file content";

      //Your methods to test
}
1