Een MS Unit DeploymentItem is soms een beetje schizofreen

De in Visual Studio standaard meegeleverde Unittest library heeft een
mindere naam op het gebied van unittesten. Gebruikers van andere unittest-tools zoals Nunit geven graag af op MSUnit maar ik vind dit niet terecht. Zelf ben ik na veelvuldig gebruik nog niet tegen specifieke beperkingen in de mogelijkheden aangelopen. Het feit dat het volledig in Visual Studio geïntegreerd is (..) en de standaard keuze binnen Microsoft is (..) maakt heel veel goed op de (verborgen) nadelen.

Het zal wel koudwatervrees zijn omdat het allemaal net iets anders
http://blogs.msdn.com/nnaderi/archive/2007/02/01/mstest-vs-nunit-frameworks.aspx  opgelost is. Maar soms zijn er zaken die je gewoon even moet weten voordat je het succesvol kunt toepassen.

Schizofreen

Zo wilden we voor ons project, waarbij heel veel met Word documenten
geschoven wordt, juist unittesten met specifieke testdocumenten toepassen. Dit
is namelijk mogelijk via het DeploymentItem attribuut. Maar voordat je dit goed
kunt toepassen, moet je wel op de hoogte zijn van enkele scherpe randjes.

Je begint dus met het opnemen van een document in je project.

Doc document aan project toegevoegd

Deze wil je refereren in de code met een relatief pad. En deze wil je ook
met het relatieve pad weer uitlezen.

[TestMethod][DeploymentItem("\\Submap1\\Test1.doc")]
public void TestMethod1()
{
  FileStream fileStream = new FileStream("\\Submap1\\Test1.doc",
    FileMode.Open);
  BinaryReader binaryReader = new BinaryReader(fileStream);
  long length = new FileInfo("\\Submap1\\Test1.doc").Length;
  byte[] byteArray = binaryReader.ReadBytes((int) length);
  Assert.IsNotNull(byteArray);
  Assert.AreNotEqual(0, length);
}

Dit ziet er goed uit. En het compileert. Maar bij het draaien van de
unittest komt een rood bolletje tevoorschijn.

Foutmelding, bestand niet aangetroffen

De melding liegt er niet om:

Test method TestProject1.UnitTest1.TestMethod1 threw exception:System.IO.DirectoryNotFoundException: Could not find a part of the path ‘c:\Submap1\Test1.doc’.

Het bestand ontbreekt? Maar het is toch netjes gekoppeld? Nou, nee. Om
dit op te lossen moeten er drie problemen gefixed worden:

  • 1. Van het te deployen document moet de property “Copy to Output Directory” ongelijk “Do not copy” zijn. (Dit heeft overigens niets met de “Build Action” te maken)

Property, copy to output directory: Copy Always

  • 2. Bij het DeploymentItem attribuut moet een submap opgegeven worden. Alle deployment item worden namelijk standaard slechts in de root van de output test map geplaatst. Een opgegeven submap met (hier eenzelfde naam) wordt door de unittest zeer gewaardeerd.
[TestMethod][DeploymentItem("Submap1\\Test1.doc", "Submap1")]
public void TestMethod1()
{
  FileStream fileStream = new FileStream("Submap1\\Test1.doc",
    FileMode.Open);
  BinaryReader binaryReader = new BinaryReader(fileStream);
  long length = new FileInfo("Submap1\\Test1.doc").Length;
  byte[] byteArray = binaryReader.ReadBytes((int) length);
  Assert.IsNotNull(byteArray);
  Assert.AreNotEqual(0, length);
}
  • 3. In Visual Studio 2005 zouden bovenstaande stappen al voldoende geweest zijn. Maar in Visual Studio 2010 faalt de unittest nog steeds na bovenstaande uitbreidingen. Nader onderzoek wijst de dader aan: de “Deployment” is nog niet actief.

Foutmelding, deployment is nog niet actief

Open hiervoor de Local.testsettings binnen de solution Items (via een dubbelclick). Daar moest namelijk nog even het vinkje bij Deployment aangezet worden.

Deployment aangevinkt

En als de unittest nu nog eens uitgevoerd wordt, verschijnt er een fraai groen bolletje snot.

Groen bolletje

Ok, dit was even wat uitzoekwerk. En hiermee kunnen nu hele fraaie reeksen van unittesten met bestanden uitgevoerd worden. Maar ik wil jullie ook even op een ander gedrag wijzen. Kijk eens naar de volgende situatie. Hier zijn binnen het testproject meerdere uit te rollen bestanden met eenzelfde naam aanwezig.

Een tweede document met dezelfde naam

Er zijn vervolgens twee bijna gelijke testen met ieder een verwijzing naar een ander bestand maar met dezelfde bestandsnaam.

[TestMethod][DeploymentItem("Submap1\\Test1.doc", "Submap")]
public void TestMethod1()
{
  FileStream fileStream = new FileStream("Submap\\Test1.doc",
    FileMode.Open);
  BinaryReader binaryReader = new BinaryReader(fileStream);
  long length = new FileInfo("Submap\\Test1.doc").Length;
  byte[] byteArray = binaryReader.ReadBytes((int) length);
  Assert.IsNotNull(byteArray);
  Assert.AreNotEqual(0, length);
}
[TestMethod]
[DeploymentItem("Submap2\\Test1.doc", "Submap")]
public void TestMethod2()
{
  FileStream fileStream = new FileStream("Submap\\Test1.doc",
    FileMode.Open);
  BinaryReader binaryReader = new BinaryReader(fileStream);
  long length = new FileInfo("Submap\\Test1.doc").Length;
  byte[] byteArray = binaryReader.ReadBytes((int)length);
  Assert.IsNotNull(byteArray);
  Assert.AreNotEqual(0, length);
}

Beide unittesten kunnen in afzonderlijke testruns prima werken. Maar
helaas kunnen ze niet direct achter elkaar in eenzelfde testtun uitgevoerd
worden.

Tweede test failt

De melding hierachter is de volgende:

Test method TestProject1.UnitTest1.TestMethod2 threw exception:
System.IO.IOException: The process cannot access the file ‘c:\documents and
settings\nl26742\my documents\visual studio
2010\Projects\TestProject1\TestResults\NL26742_NL462P13J 2010-04-15
21_42_58\Out\Submap1\Test1.doc’ because it is being used by another process.

Tijdens het uitvoeren van de tweede test is het in te lezen bestand dus al in gebruik? Laten we eens op de lokatie gaan kijken. Ik zie daar maar één bestand. Hé, beide unittesten gebruiken bestanden met dezelfde benaming. Het blijkt dat zij beiden moeten strijden voor hetzelfde plaatsje in de map. Helaas verliest altijd de laatste.

Bestanden met dezelfde naam verdringen elkaar

En dus blijkt de reden van het falen van de tweede unittest, de eerste unittest te zijn. In beide testen is de Close() methode van de FileStream vergeten. Deze eerste unittest houdt de lock vast op het eerste deployment bestand, zelfs nadat deze test succesvol is uitgevoerd.

Dus is een oplossing simpel:

[TestMethod][DeploymentItem("Submap1\\Test1.doc", "TestMethod1")]
public void TestMethod1()
{
  FileStream fileStream = new FileStream("TestMethod1\\Test1.doc",
    FileMode.Open);
  try
  {
    BinaryReader binaryReader = new BinaryReader(fileStream);
    long length = new FileInfo("TestMethod1\\Test1.doc").Length;
    byte[] byteArray = binaryReader.ReadBytes((int)length);
    Assert.IsNotNull(byteArray);
    Assert.AreNotEqual(0, length);
  }
  finally
  {
    fileStream.Close();
  }
}

De close is toegevoegd. Dit is inderdaad fraaie code en een hele verbetering.
Toch geef ik de voorkeur aan nog een andere verbetering.

Om te forceren dat deployment items van unittest met eenzelfde naam
elkaar nooit in de weg kunnen zitten, stel ik voor om voortaan altijd bij het
DeploymentItem attribuut de submap in te vullen op een specifieke manier:

[TestMethod][DeploymentItem("Submap1\\Test1.doc", "UnitTest1\\TestMethod1")]
public void TestMethod1()
{
  FileStream fileStream = new FileStream("UnitTest1\\TestMethod1\\Test1.doc",
    FileMode.Open);
  BinaryReader binaryReader = new BinaryReader(fileStream);
  long length = new FileInfo("UnitTest1\\TestMethod1\\Test1.doc").Length;
  byte[] byteArray = binaryReader.ReadBytes((int) length);
  Assert.IsNotNull(byteArray);
  Assert.AreNotEqual(0, length);
}
[TestMethod]
[DeploymentItem("Submap2\\Test1.doc", "UnitTest1\\TestMethod2")]
public void TestMethod2()
{
  FileStream fileStream = new FileStream("UnitTest1\\TestMethod2\\Test1.doc",
    FileMode.Open);
  BinaryReader binaryReader = new BinaryReader(fileStream);
  long length = new FileInfo("UnitTest1\\TestMethod2\\Test1.doc").Length;
  byte[] byteArray = binaryReader.ReadBytes((int)length);
  Assert.IsNotNull(byteArray);
  Assert.AreNotEqual(0, length);
}

Binnen een unittest project kunnen namelijk meerdere unittest methodes in
verschillende unittest classes voorkomen met eenzelfde method naam. Met
bovenstaande dubbele submap-verwijzing, een combinatie van unittest class naam en method naam, wordt het dubbel uitlezen voorkommen.

Twee groene bolletjes

En zo zullen de unittesten voortaan weer netjes groen kleuren.