středa 15. června 2016

Unit testy bez mockování


Při testování lze občas narazit na situace, kdy je napsání unit testů běžně zažitým způsobem nemožné a nejde použít ani mockování. Například proto, že v kódu je použita uzavřená (sealed) třída, která ani neimplementuje žádné rozhraní. To je například případ některých tříd pro přístup k datům na Azure, ale i mnoho dalších. I přesto se dá ale unit test napsat.

Pro ukázku jsem si zvolil velmi jednoduchou třídu s jedinou metodou, která umí vrátit jako text obsah souboru.  Pokud dojde k IO výjimce, pokusí se metoda o čtení ještě x-krát a poté vrátí prázdný řetězec. Kód tedy vypadá nějak takto:

public class FileReader
{
   public string ReadContent(string fileName, int maxAttempts=1)
   {
      var attempts = 0;
      do
      {
         try
         {
 
            return File.ReadAllText(fileName);
         }
         catch (IOException) when (attempts < maxAttempts)
         {
            attempts++;
         }
         catch(IOException)
         {
            return string.Empty;
         }
      } while (true);
   }
}


A jak k tomu napsat unit test? Těžko asi budeme nějak simulovat chybu na reálném souboru, stejně tak není ani řešením si třeba nadefinovat nějaké rozhraní s metodami dostupnými pro File a poté File obalit třídou implementující toto rozhraní.  Respektive, bylo by to řešeni, ale třeba jsme líní a nechce se nám do toho, nebo je daná třída tak rozsáhlá, že by to vedlo k velkému zdržení.

I tak lze ale unit test napsat a to díky MS Fakes. Stačí jenom na příslušnou knihovnu, tedy tu co obsahuje nemokovatelnou třídu, kliknout v testovacím projektu pravým tlačítkem a vybrat Add Fakes Assembly.

A vygenerují se nám kopie, které můžeme nastavit jak potřebujeme a použít místo původních - a přidají se do seznamu  References našeho testovacího projektu. Jejich použití je pak velmi snadné:

[TestMethod()]
public void ReadXTimesForIOExceptionAndReturnEmptyString()
{
 
   var reader = new FileReader();
   var maxAttempts = 2;
 
   using (ShimsContext.Create())
   {
      var attempts = 0;
      ShimFile.ReadAllTextString = (fileName) =>
      {
         attempts++;
         throw new IOException();
      };
 
      var result = reader.ReadContent("myfile", maxAttempts);
      attempts--;
 
      Assert.AreEqual(maxAttempts, attempts);
      Assert.AreEqual(result, string.Empty);
   }
}

A je to - MS vygeneroval Shim třídy a vložil je do stejných namespaců, ty ovšem mají příponu .Fakes, tedy místo System.IO používáme namespace System.IO.Fakes a místo třídy File pak ShimFile. To, že se mají použít tyto fake třídy, deklarujeme pomocí using(ShimContext.Create()) a poté jen upravím, co se má při volání té či oné metody vlastně stát.
Ostatně lze si takto napsat více testů:

[TestMethod()]
[ExpectedException(typeof(SecurityException))]
public void AnyExceptionThrowsException()
{
   var reader = new FileReader();
 
   using (ShimsContext.Create())
   {
      var attempts = 0;
 
      ShimFile.ReadAllTextString = (fileName) =>
      {
         attempts++;
         throw new SecurityException();
      };
 
      var result = reader.ReadContent("myfile", 20);
   }
}

A ještě jeden tip - výše uvedeným postupem se vytvoří kopie všeho, což je často zbytečné - v projektu se po kliknutí po kliknuti Add Fake Assembly vytvořila složka Fakes a v ní jsou konfigurační soubory, ty si lze zeditovat a říci, co se má udělat: 

<Fakes xmlns="http://schemas.microsoft.com/fakes/2011/">
  <Assembly Name="mscorlib" Version="4.0.0.0"/>
   <StubGeneration>
      <Clear/>
   </StubGeneration>
   <ShimGeneration>
      <Clear/>
      <Add FullName="System.IO.File!"/>
   </ShimGeneration>
</Fakes>

Blíže je vše popsáno třeba tady:
http://stackoverflow.com/questions/24733827/shims-warning-messages 
a nebo tady
 https://msdn.microsoft.com/en-us/library/hh549175(v=vs.140).aspx


1 komentář: