Schlagwort-Archive: UnitTest

Interview mit Daniel Marbach mitgestalten

Am 18. März wird mir Daniel Marbach ein Video Interview zu dem BDD-Framework Machine.Specifications geben. Ein kleiner Vorgeschmack gefällig?

Wodurch hebt sich MSpec von anderen BDD-Frameworks ab?

oder

Welche Features sind gerade in der Pipeline?

oder

Welche anderen Frameworks ergänzen MSpec gut?

Das Video veröffentliche ich dann auf meinem YouTube Channel. Ihr könnt die Richtung des Gesprächs mitgestalten, indem ihr mir rechtzeitig eure Fragen in die Kommentare postet.

Zusammenarbeit mit der Fachabteilung – BDD und TDD helfen

Test Driven Development (TDD) steht dafür, dass Tests vor der eigentlichen Implementierung zu schreiben sind. Es zwingt den Entwickler sich vorher genaue Gedanken über die Architektur und die Implementierung zu machen, sodass “einfaches loslaufen” und ggf. damit einhergehendes “falschlaufen” vermieden wird. Saubere Codequalität wird gefördert.

Behaviour Driven Development bringt nun einen weiteren Ansatz mit ins Spiel: Code soll ergebnisorientiert getrieben sein, d.h. die fachlichen Spezifikationen stehen im Vordergrund. Entsprechende BDD Frameworks wie Machine.Specifications (Link, gibt es auch auf NuGet) unterstützen nun dabei, dieses Ziel umzusetzen.

Damit sich auch Nicht-IT-ler und weniger Test-affine Entwickler etwas darunter vorstellen können, hier ein kleines Beispiel aus der Praxis. Wir praktizieren dies bereits seit einigen Monaten in der Form mit unseren Fachabteilungen:

Aufgabenstellung: Mehrere Produktartikel sind in einer gemeinsamen Bestellmappe zusammengefasst. Diese Mappe besitzt die Kopfdaten ‘Lieferzeit’ (LZ), ‘Bearbeitungszeit’ (BZ), ‘Bestellintervall’ (BI) und ‘nächster Mappentermin’ (MT). Der nächste Mappentermin besagt, wann die Mappe sich beim zuständigen Benutzer melden, sodass dieser eine neue Bestellung auslöst. Das Bestellintervall gibt an in welchen Abständen die Mappe neu bearbeitet werden muss, z.B. alle 3 Wochen. Die restlichen Begriffe sprechen für sich. Nun wird von der Fachabteilung gefordert, dass sogenannte ‘fiktiven Wareneingänge’ (fWE) visuell dargestellt werden (siehe Screenshot). Dabei handelt es sich um Eingänge, die rein rechnerisch in der Zukunft eingehen würden, wenn die Bestellungen so getätigt würden. Also bedarf es einer Möglichkeit, um diese Termin zu berechnen.

 

image

 

Die Tests visualisiert für die Übergabe an die Fachabteilung:

Die Tests visualisiert für die Übergabe an die Fachabteilung

Es handelt sich hier um 7 Tests. Der obere Abschnitt besagt:

Wenn von einer Bestellmappe die die Lieferzeit 5 Tage, die Bearbeitungszeit 1 Tag und das Bestellintervall 14 Tage sind und hierfür die nächsten 3 fiktiven Wareneingänge aus heutiger Sicht berechnet werden sollen:

  • Dann sollte der erste fiktive Wareneingang in 6 Tagen sein
  • Dann sollte der zweite fiktive Wareneingang in 21 Tagen sein
  • Dann sollte der dritte fiktive Wareneingang in 36 Tagen sein
  • Dann sollt es insgesamt 3 Datumsangaben errechnen

Der zweite Abschnitt ist analog zu lesen, wenn die Abkürzungen ausgeschrieben werden. Das Wesentliche daran ist, dass diese Spezifikationen von der Fachabteilungen kommen und diese so sich im Code 1 zu 1 widerspiegeln. Damit können die Entwickler die Brücke schlagen und sich mit den Anwendern in deren Domänensprache unterhalten. Die Spezifikationen garantieren, dass der Code sich wie gewünscht verhält. Die Tests lassen sich automatisiert wiederholen und werden vor jeder neuen Versionsauslieferung durchlaufen.

 

 

Zur Gänze folgen noch der Code für die Tests und die eigentliche Implementierung:

Der Code für die Tests:

   1: [Subject("Fiktive Wareneingänge")]

   2: public class Wenn_von_einer_Mappe_die_LZ_5T_die_BZ_1T_und

   3: _das_BI_14T_sind_und_3_fWE_ab_heute_berechnet_werden_sollen

   4: {

   5:     Establish context = () =>

   6:     {

   7:         Clock = new DummyClock();

   8:         Folder = new OrderProposalFolder { DeliveryPeriodInDays = 5, 

   9:                 HandlingTimeInDays = 1, OrderIntervalInDays = 14 };

  10:         Sut = new FictitiousIntakes(Clock);

  11:     };

  12:  

  13:     Because of = () =>

  14:     {

  15:         Actual = Sut.FromNow(Folder, 3);

  16:     };

  17:  

  18:     It dann_sollten_exakt_3_Datumsangaben_errechnet_werden = () => Actual.Count.ShouldEqual(3);

  19:     It dann_sollte_der_erste_fWE_in_6T_sein = () => Actual.Min().ShouldEqual(Clock.Today.AddDays(6));

  20:     It dann_sollte_der_zweite_fWE_in_21T_sein = () => Actual.ElementAt(1).ShouldEqual(Clock.Today.AddDays(21));

  21:     It dann_sollte_der_dritte_fWE_in_36T_sein = () => Actual.Max().ShouldEqual(Clock.Today.AddDays(36));

  22:  

  23:     static FictitiousIntakes Sut;

  24:     static OrderProposalFolder Folder;

  25:     static IList<DateTime> Actual;

  26:     static IClock Clock;

  27: }

  28:  

  29: [Subject("Fiktive Wareneingänge")]

  30: public class Wenn_von_einer_Mappe_die_LZ_5T_die_BZ_1T_und_das_BI_14T_sind_

  31: und_der_nächste_MT_übermorgen_ist_und_2_fWE_ab_dem_nächsten_MT_berechnet_werden_sollen

  32: {

  33:     Establish context = () =>

  34:     {

  35:         Clock = new DummyClock();

  36:         Folder = new OrderProposalFolder {NextOrderDate = Clock.Now.AddDays(2) ,DeliveryPeriodInDays = 5,

  37:                                          HandlingTimeInDays = 1, OrderIntervalInDays = 14 };

  38:         Sut = new FictitiousIntakes(Clock);

  39:     };

  40:  

  41:     Because of = () =>

  42:     {

  43:         Actual = Sut.FromNextFolderDate(Folder, 2);

  44:     };

  45:  

  46:     It dann_sollten_exakt_2_Datumsangaben_errechnet_werden = () => Actual.Count.ShouldEqual(2);

  47:     It dann_sollte_der_erste_fWE_in_8T_sein = () => Actual.Min().ShouldEqual(Clock.Today.AddDays(8));

  48:     It dann_sollte_der_dritte_fWE_in_23T_sein = () => Actual.Max().ShouldEqual(Clock.Today.AddDays(23));

  49:  

  50:     static FictitiousIntakes Sut;

  51:     static OrderProposalFolder Folder;

  52:     static IList<DateTime> Actual;

  53:     static IClock Clock;

  54: }

 

Der produktive Code, der im Programm zur Berechnung verwendet wird:

   1: public class FictitiousIntakes : ICalculateFictitiousIntakes

   2: {

   3:     readonly IClock _clock;

   4:  

   5:     public FictitiousIntakes(IClock clock)

   6:     {

   7:         _clock = clock;

   8:     }

   9:  

  10:     public IList<DateTime> FromNow(OrderProposalFolder folder, int toOrdinal)

  11:     {

  12:         return FromThis(_clock.Today, folder, toOrdinal);

  13:     }

  14:  

  15:     public IList<DateTime> FromNextFolderDate(OrderProposalFolder folder, int toOrdinal)

  16:     {

  17:         return FromThis(folder.NextOrderDate, folder, toOrdinal);

  18:  

  19:     }

  20:  

  21:     private IList<DateTime> FromThis(DateTime startDate, OrderProposalFolder folder, int toOrdinal)

  22:     {

  23:         if (toOrdinal < 1)

  24:             throw new ArgumentOutOfRangeException("toOrdinal");

  25:  

  26:         var firstIntake = startDate.AddDays(folder.DeliveryPeriodInDays + folder.HandlingTimeInDays);

  27:  

  28:         var result = new List<DateTime>();

  29:         for (int currentOrdinal = 0; currentOrdinal <= toOrdinal - 1; currentOrdinal++)

  30:             result.Add(firstIntake.AddDays(currentOrdinal * 

  31:                     (folder.OrderIntervalInDays + folder.HandlingTimeInDays)));

  32:  

  33:         return result;

  34:     }

  35: }

Clean Code Sample

In einem Gespräch mit unseren Webentwicklern habe ich den Kollegen einen Vorschlag für die Implementierung des Zugangs zu unserem Kundenportals unterbreitet: Unsere PHP Entwickler sind inzwischen ebenfalls auf den Ansatz des Testens umgestiegen, sodass sie ihre PHP und JavaScript Architektur anpassen müssen. Leider fehlt es an Frameworks, die einem ein Teil der Arbeit abnehmen könnten, wie z.B. Mocking Frameworks a la FakeItEasy. Falls jemand Frameworks kennt, dann melde er sich bitte bei mir.

Anforderungen (vereinfacht): An unserem Kundenportal ‘MyHeco’ soll man sich einloggen, ausloggen und das Password zurücksetzen können. Die Webanwendung konsumiert dabei .NET basierende Webservices!

Dies soll nun möglichst clean unter Einhaltung gängiger Prinzipien (wie z.B. dem Single Responsibility Principle) umgesetzt werden. Wie bereits erwähnt, muss die Architektur so gewählt werden, dass der Code einfach zu testen ist. Das ist v.a. auch deswegen nicht ganz trivial, weil Aufrufe nach ‘Außen’ zu Webservices gehen.

Hier also mein Vorschlag (natürlich in C# formuliert):

   1: interface IAuthenticateMyHecoUser

   2: {

   3:     MyHecoUser Login(string email, string password);

   4: }

   5:  

   6: interface ILogoutMyHecoUser

   7: {

   8:     void Logout(string session);

   9: }

  10:  

  11: interface IResetMyHecoPassword

  12: {

  13:     void ResetPassword(string email);

  14: }

Schreibe 3 Interfaces für die eigentlichen Aktionen, z.B. Login. Natürlich könnte dafür auch nur ein Interface geschrieben werden werden, allerdings empfinde ich es so als sauberer und die Interfaces lassen sich sprechend benennen (ohne Weasel Words, also Füllwörter; tollen Artikel dazu gibt es von Johannes).

Danach benötigen wir ein Interface für die Webservice-Kommunikation, welches ich an dieser Stelle leer lasse:

   1: public interface IERPCommunication

   2: {

   3:      

   4: }

Was ich ebenfalls zu den Kontrakten zähle (vergleicht dazu diesen Artikel), ist das DTO MyHecoUser, welches später die gefüllten Daten zum Austausch innerhalb von PHP enthalten soll.

   1: public class MyHecoUser

   2: {

   3:     public readonly int Kundennummer;

   4:     public readonly string Session;

   5:     //und diverse weitere

   6: }

Zu guter Letzt benötigen wir noch die Implementierung, die so aussehen könnte. Je nach Umfang und Größe des Codes, wäre es auch durchaus denkbar, die Implementierung auf verschiedene Klassen aufzusplitten. Aber in diesem Fall implementiert eine Klasse die 3 Interfaces aus dem Code Snippet ganz oben.

   1: public class MyHeco : IAuthenticateMyHecoUser, ILogoutMyHecoUser, IResetMyHecoPassword

   2: {

   3:     readonly IERPCommunication _webservice;

   4:  

   5:     public MyHeco(IERPCommunication webservice)

   6:     {

   7:         _webservice = webservice;

   8:     }

   9:  

  10:     public MyHecoUser Login(string email, string password)

  11:     {

  12:         throw new System.NotImplementedException();

  13:     }

  14:  

  15:     public void Logout(string session)

  16:     {

  17:         throw new System.NotImplementedException();

  18:     }

  19:  

  20:     public void ResetPassword(string email)

  21:     {

  22:         throw new System.NotImplementedException();

  23:     }

  24: }

 

Durch die Abstraktion der Webservice-Kommunikation hinter einem Interface und dem Injizieren im Konstruktor, können die Kollegen nun für den Test einen selbstgeschriebenen Fake hineinreichen. Ich validiere hier zugegebenermaßen den Parameter nicht, da ich in unserer Architektur auf IoC mit Castle.Windsor setze. Die Benennung spricht mich persönlich auch an, da sie sehr aussagekräftig ist:

MyHeco.Login oder Myheco.Logout sind eindeutig. Die Kohäsion scheint mir ebenfalls recht hoch, da die 3 möglichen von der Fachabteilung definierten MyHeco Aktionen in einer Klasse gekapselt sind. Trotzdem wäre eine spätere Auftrennung sehr einfach, da in der konsumierenden Klasse nur das konkrete Interface reingegeben wird. Beispiel: In einer Bestellung muss sich der Kunde erst einloggen. Also bekommt die Logik für eine Bestellung im Konstruktor ein IAuthenticateMyHecoUser reingereicht. Ist in Zukunft die Implementierung nicht mehr in der Klasse MyHeco, ändert sich die Logik der Bestellung in keiner Weise.

Castle Windsor – Handy Ruse Part III

In this blog entry I published code for an IoC Initializer. The following container Integration Test tries to create an instance of each registered service that is not of type IAmNotTestable. Definitely an Integration Tests that every application needs!

Integration Test of the container using Machine.Specifications as UnitTesting Framework:

   1: using System;

   2: using System.Diagnostics;

   3: using System.Linq;

   4: using Castle.MicroKernel;

   5: using Castle.Windsor;

   6: using comWORK.Contracts;

   7: using comWORK.Infrastructure.IoC;

   8: using Machine.Specifications;

   9: using Machine.Specifications.Utility;

  10:  

  11: namespace UAR.IntegrationTests

  12: {

  13:     public abstract class ContainerSpecs

  14:     {

  15:         protected static IWindsorContainer CreateContainer()

  16:         {

  17:             return new IoCInitializer()

  18:                     .RegisterComponents()

  19:                     .Container;

  20:         }

  21:     }

  22:  

  23:     [Subject("Container Specs")]

  24:     public class When_the_application_starts_up : ContainerSpecs

  25:     {

  26:         static IHandler[] _handlers;

  27:         static IWindsorContainer _container;

  28:  

  29:         Establish context = () => { _container = CreateContainer(); };

  30:  

  31:         Because of = () => { _handlers = _container.Kernel.GetAssignableHandlers(typeof(object)); };

  32:  

  33:         Cleanup after = () => _container.Dispose();

  34:  

  35:         It should_be_able_to_create_an_instance_of_each_registered_service =

  36:             () => _handlers

  37:                       .Where(

  38:                           handler =>

  39:                           handler.ComponentModel.Implementation.GetInterfaces().Contains(typeof(IAmNotTestable)) == false)

  40:                       .Each(handler => handler.ComponentModel.Services

  41:                                            .Each(service =>

  42:                                            {

  43:                                                Debug.WriteLine(String.Format("{0}: {1}/{2}",

  44:                                                                              handler.ComponentModel.Name,

  45:                                                                              service.Name,

  46:                                                                              handler.ComponentModel.Implementation.Name));

  47:  

  48:                                                if (service.ContainsGenericParameters)

  49:                                                {

  50:                                                    service.GetGenericArguments()

  51:                                                        .Each(argument => argument.GetGenericParameterConstraints()

  52:                                                                              .Each(

  53:                                                                                  constraint =>

  54:                                                                                  _container.Resolve(service.MakeGenericType(constraint))));

  55:                                                }

  56:                                                else

  57:                                                {

  58:                                                    _container.Resolve(service);

  59:                                                }

  60:                                            }));

  61:     }

  62: }

Implementation of IAmNotTestable

   1: public interface IAmNotTestable{}

Notice: I will publish a new version soon that addresses some open problems that i don’t want to mention here.

UnitTests & Threading – Ist grün wirklich grün?

Ich bin kürzlich auf ein Problem gestoßen: In ReSharper wurden UnitTests grün angezeigt, obwohl diese fehlschlugen. Im konkreten Fall handelt es sich um einen UnitTest im Rahmen von Logik, die parallel abläuft. Tritt in den Threads eine Exception auf, so wird dies nicht als Fehler behandelt. Der UnitTest ist weiterhin grün, wenn der Assert trotzdem korrekt ist.

Beispiel: Stellen wir uns zwei Worker Threads vor, die bei einem Controller nach neuen Druckaufträgen fragen. Der Controller ist als Singleton implementiert (oder idealerweise wird dies schon out of the box vom IoC Container gemacht). Tritt nun eine Exception in den Threads auf, der Assert ist aber trotzdem korrekt, so wird der UnitTest grün angezeigt. Hier der Code dazu:

Implementierung Controller:

   1: class PrintJobMediator : IMediatePrintjobs

   2: {

   3:     //lock object for the method

   4:     private object _lockObject = new object();

   5:  

   6:     //instance of the singleton

   7:     private static volatile PrintJobMediator instance;

   8:  

   9:     //lock object for singleton

  10:     private static object syncRoot = new Object();

  11:  

  12:     //private ctor

  13:     private PrintJobMediator() { }

  14:  

  15:     //returns the singleton instance

  16:     public static PrintJobMediator Instance

  17:     {

  18:         get

  19:         {

  20:             if (instance == null)

  21:             {

  22:                 lock (syncRoot)

  23:                 {

  24:                     if (instance == null)

  25:                         instance = new PrintJobMediator();

  26:                 }

  27:             }

  28:  

  29:             return instance;

  30:         }

  31:     }

  32:  

  33:     int _result;

  34:     public int Next()

  35:     {

  36:         lock (_lockObject)

  37:         {

  38:             System.Threading.Thread.Sleep(1000);

  39:             _result += 1;

  40:  

  41:             if (_result == 1)

  42:                 throw new ApplicationException();

  43:         }

  44:  

  45:         return _result;

  46:     }

  47: }

Unit Test:

   1: [Subject(typeof(PrintJobMediator))]

   2: public class When_2_worker_ask_for_new_jobs

   3: {

   4:     static PrintJobMediator _jobMediator;

   5:     static Thread _thread1;

   6:     static Thread _thread2;

   7:     static TimeSpan _timeTaken;

   8:  

   9:     Establish context = () =>

  10:     {

  11:         _jobMediator = PrintJobMediator.Instance;

  12:         _thread1 = new Thread(() => _jobMediator.Next());

  13:         _thread2 = new Thread(() => _jobMediator.Next());

  14:     };

  15:  

  16:     Because of = () =>

  17:     {

  18:         _timeTaken = Measure.Time(() =>

  19:         {

  20:             _thread1.Start();

  21:             _thread2.Start();

  22:             _thread1.Join();

  23:             _thread2.Join();

  24:         });

  25:     };

  26:  

  27:     It should_run_jobs_sequentially = () => _timeTaken

  28:         .ShouldBeGreaterThanOrEqualTo(TimeSpan.FromSeconds(2));

  29: }

  30:  

  31: public static class Measure

  32: {

  33:     public static TimeSpan Time(Action timedAction)

  34:     {

  35:         var watch = new System.Diagnostics.Stopwatch();

  36:         watch.Start();

  37:  

  38:         timedAction();

  39:  

  40:         watch.Stop();

  41:         return watch.Elapsed;

  42:     }

  43: }

 

In ReSharper wird der Test als erfolgreich angezeigt:

image

Wenn man sich die Details anschaut, sieht man folgendes:

image

In der Implementierung schmeiße ich im ersten Thread eine Exception (Zeile 42). Da trotzdem die Gesamtdauer der Ausführung 2 Sekunden ist, ist der Assert korrekt. Der UnitTest bricht nicht ab, weil seit .NET 2.0 eine neue Policy greift (Name fällt mir gerade nicht mehr ein), die den Abbruch des Main Thread durch Exceptions in separaten Threads verhindert.

Fake Implementation of IDbSet

   1: public class FakeDbSet<T> : IDbSet<T> where T:class 

   2:  {

   3:      readonly HashSet<T> _set;

   4:      private readonly IQueryable<T> _queryableSet; 

   5:  

   6:      public FakeDbSet()

   7:      {

   8:          _set = new HashSet<T>();

   9:          _queryableSet = _set.AsQueryable();

  10:      } 

  11:  

  12:      public IEnumerator<T> GetEnumerator()

  13:      {

  14:          return _set.GetEnumerator();

  15:      }

  16:  

  17:      IEnumerator IEnumerable.GetEnumerator()

  18:      {

  19:          return GetEnumerator();

  20:      }

  21:  

  22:      public Expression Expression

  23:      {

  24:          get { return _queryableSet.Expression; }

  25:      }

  26:  

  27:      public Type ElementType

  28:      {

  29:          get { return _queryableSet.GetType(); }

  30:      }

  31:  

  32:      public IQueryProvider Provider

  33:      {

  34:          get { return _queryableSet.Provider; }

  35:      }

  36:  

  37:      public T Find(params object[] keyValues)

  38:      {

  39:          throw new NotImplementedException();

  40:      }

  41:  

  42:      public T Add(T entity)

  43:      {

  44:          _set.Add(entity);

  45:          return entity;

  46:      }

  47:  

  48:      public T Remove(T entity)

  49:      {

  50:          _set.Remove(entity);

  51:          return entity;

  52:      }

  53:  

  54:      public T Attach(T entity)

  55:      {

  56:          _set.Add(entity);

  57:          return entity;

  58:      }

  59:  

  60:      public T Create()

  61:      {

  62:          throw new NotImplementedException();

  63:      }

  64:  

  65:      public TDerivedEntity Create<TDerivedEntity>() where TDerivedEntity : class, T

  66:      {

  67:          throw new NotImplementedException();

  68:      }

  69:  

  70:      public ObservableCollection<T> Local

  71:      {

  72:          get { throw new NotImplementedException(); }

  73:      }

  74:  }

ReSharper Settings für Unit Tests

Wer Probleme bei seinen Unit Tests hat, weil für den Unit Tests die Assemblies in ein temporäres Verzeichnis kopiert und dort ausgeführt werden, der kann dies in den ReSharper Settings über die Settings “Shadow-copy assemblies being tested” deaktivieren.

Bei mir traten Probleme in einem Unit Test für meinen IoC-Container auf. Hintergrund ist der, dass im Test alle DLLs nach IWindsorInstaller Implementierungen durchsucht und registriert werden. Da nicht alle DLLs vorhanden waren, kam es zu Fehlern.

image

%d Bloggern gefällt das: