Schlagwort-Archive: MSpec

Nachgefragt – Interview mit Daniel Marbach zu MSpec

In einem Online Interview habe ich Daniel Marbach zur Zukunft des Open Source BDD Frameworks MSpec befragt. Herausgekommen ist eine Aufnahme, die es jetzt auf YouTube gibt. Die Fragen wurden nicht vorab abgesprochen, um ein authentisches statt ein werbelastiges Gesprächs zu führen.

 

image

Auf das Bild klicken um zum Video zu gelangen

 

An dieser Stelle möchte ich noch die Community aufrufen sich an der Weiterentwicklung zu beteiligen. Open Source Projekte leben von freiwilligen Helfern und Daniel macht da eine sehr gute Arbeit und ein tolles Produkt!

Fragen

  • Machine.Specifications
  • Was ist eigentlich ein BDD Framework
  • Stärken
  • Schwächen
  • Roadmap
  • Wie steht es um die Zukunftssicherheit
  • Tipps und Tricks
  • Ergänzende Frameworks
  • Gute Tests

 

Show Notes

Advertisements

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.

Entity Framework Webcasts – Testing

Im siebten Teil meiner Entity Framework Webcast Serie widme ich mich dem Thema Testing. Zunächst nehmen wir uns die fertigen Queries vor, welche sich auch gut ohne Datenbank testen lassen. Schließlich will nicht jeder zum Testen einer Abfrage immer eine entsprechende Datenbank hochfahren und die Daten dafür erzeugen.

Danach stellen wir in einem Mini-Business-Pseudo-Layer ein Szenario nach, welches uns dedizierte Daten für die weitere Verarbeitung liefert, sodass das Testen der Geschäftslogik ebenfalls ohne Datenbank erfolgen kann.

Als BDD Framework für die Unit Tests verwende ich Machine.Specifications und als Mocking Framework kommt FakeItEasy zum Einsatz.

Hier noch die zwei Code Beispiele:

 

Beispiel 1: Testen der Query

   1: [Subject(typeof(GetAddressByCity))]

   2: public class When_addresses_contains_exactly_one_matching_entry

   3: {

   4:     static GetAddressByCity Sut;

   5:     static List<Address> Addresses;

   6:     static Address Actual;

   7:     static Address TestCity;

   8:  

   9:     Establish context = () =>

  10:     {

  11:         TestCity = new Address {City = "test city"};

  12:         Addresses = new List<Address>

  13:         {

  14:             new Address {City = "Karlsruhe"},

  15:             TestCity

  16:         };

  17:         Sut = new GetAddressByCity("test city");

  18:     };

  19:  

  20:     Because of = () =>

  21:     {

  22:         Actual = Sut.Execute(Addresses.AsQueryable());

  23:     };

  24:  

  25:     It should_return_exactly_this_address = () => 

  26:             Actual.ShouldEqual(TestCity);

  27: }

 

Beispiel 2: Testen von Geschäftslogik

   1: class EmployeeBusinessLogicSpecs

   2: {

   3:     [Subject(typeof(EmployeeBusinessLogic))]

   4:     class When_hire_date_is_unknown

   5:     {

   6:         static IUnitOfWork Uow;

   7:         static EmployeeBusinessLogic Sut;

   8:         static Employee DummyEmployee;

   9:  

  10:         Establish context = () =>

  11:         {

  12:             DummyEmployee = new Employee {EmployeeID = 1, 

  13:                     FirstName = "Uli", LastName = "Armbruster"};

  14:  

  15:             Uow = A.Fake<IUnitOfWork>();

  16:  

  17:             A

  18:                 .CallTo(() => Uow.ExecuteQuery(

  19:                     A<GetEmployeeById>

  20:                     .That

  21:                     .Matches(q => q.EmployeeId == DummyEmployee.EmployeeID)

  22:                                   ))

  23:                 .Returns(DummyEmployee);

  24:  

  25:             Sut = new EmployeeBusinessLogic(Uow);

  26:         };

  27:  

  28:         Because of = () => Sut.EnsureValidHireDate(DummyEmployee.EmployeeID);

  29:  

  30:         It should_update_it = () => DummyEmployee.HireDate.ShouldNotBeNull();

  31:  

  32:         It should_save_the_changed_hire_date = () => A

  33:             .CallTo(() => Uow.Commit())

  34:             .MustHaveHappened(Repeated.Exactly.Once);

  35:     }

  36: }

 

 

Weitere Quellen:

Aktuelle NuGet Packages in Build Skripten verwenden

Alex Groß von GROSSWEBER hat uns freundlicherweise eine Ruby Klasse geschrieben, welche es ermöglicht im Build Vorgang immer die aktuellste Version eines NuGet Package heranzuziehen.

Wer beispielsweise MSpec als UnitTesting Framework einsetzt und ein entsprechendes Skript in seinem Build Server verwendet, der hatte das Problem, dass das Skript angepasst werden musste, sobald man MSpec per NuGet aktualisierte. Das gilt natürlich für jegliches Package, auf welches in Skripten direkt referenziert wird.

Um die Problematik nun elegant zu lösen, sei exemplarisch unten folgendes Szenario aufgelistet:

 

Hier zeige ich den Ruby Code, welcher bei uns im Build Skript zur Ausführung der Unit Tests verwendet wird. In Zeile 3 wird die Lösung von Alex verwendet. Erster Parameter gibt den Namen des Package an, während zweiter Parameter besagt wo alle Packages im Repository liegen. Als Rückgabewert erhält man den Pfad zur aktuellsten Version. Im nächsten Schritt in Zeile 4 iteriere ich über unser Binaries Verzeichnis drüber. Dabei werden alle DLLs verarbeitet, die mit ‘comWORK’ beginnen. Als Ausnahme gebe ich alle Dateien an, die auf ‘resources’ enden. Das Ergebnis ist eine Liste von DLLs, welche MSpec dann prüft bzw. ausführt. Die Reports landen im Verzeichnis ‘Reports’ und liegen in Form von html Dateien zur Ansicht bereit.

   1: desc 'Unit Tests'

   2: task :unit do

   3:     mspec = NuGetLatest('Machine.Specifications', 'Source\_Solutions\packages')

   4:     FileList.new('Binaries/**/comWORK.*.dll').exclude('**/*.resources.dll').each do |f|

   5:         Mspec.run({

   6:             :tool => mspec + '/tools/mspec-clr4.exe',

   7:             :reportdirectory => 'Reports',

   8:             :assembly => f

   9:         })

  10:     end

  11: end

 

Hier das von Alex bereitgestellte Ruby File, welches immer das Verzeichnis der aktuellsten Package Version zurückliefert. Anpassungen sind keine nötig.

   1: class NuGetLatest

   2:   attr_reader :path

   3:  

   4:   def initialize(package_name, packages_dir = Dir.pwd)

   5:     @package_name = package_name

   6:  

   7:     raise "#{packages_dir} is not a directory" unless File.directory? packages_dir

   8:  

   9:     Dir.chdir packages_dir do

  10:       candidates = Dir["#{package_name}*"]

  11:       raise "No package '#{package_name}' was found in #{packages_dir}" unless candidates.any?

  12:  

  13:       latest = find_latest_version candidates

  14:       @path = File.join packages_dir, latest

  15:     end

  16:   end

  17:  

  18:   def to_s

  19:     @path

  20:   end

  21:  

  22:   def +(other)

  23:     to_s + other

  24:   end

  25:  

  26:   private

  27:   def find_latest_version(candidates)

  28:     return candidates.first if candidates.length == 1

  29:  

  30:     latest = candidates.map { |path|

  31:       version = path.sub /^#{@package_name}./, ''

  32:       version = parse_version version

  33:  

  34:       {

  35:         :version => version,

  36:         :path => path

  37:       }

  38:     }.max { |left, right| left[:version] <=> right[:version] }

  39:  

  40:     latest[:path]

  41:   end

  42:  

  43:   def parse_version(version_string)

  44:     major, minor, patch, revision, special = version_string.match(/^(\d+)\.(\d+)\.(\d+)\.?(\d+)?-?(.+)?$/).captures

  45:     SemVer.new(major.to_i, minor.to_i, patch.to_i, revision.to_i, special)

  46:   end

  47:  

  48:   # Borrowed from the SemVer gem and enhanced to support .NET's 4-digit versioning system.

  49:   class SemVer

  50:     attr_accessor :major, :minor, :patch, :revision, :special

  51:  

  52:     def initialize(major = 0, minor = 0, patch = 0, revision = 0, special = '')

  53:       major.kind_of? Integer or raise "invalid major: #{major}"

  54:       minor.kind_of? Integer or raise "invalid minor: #{minor}"

  55:       patch.kind_of? Integer or raise "invalid patch: #{patch}"

  56:       revision.kind_of? Integer or raise "invalid revision: #{revision}"

  57:  

  58:       unless special.nil? or special.empty?

  59:         special =~ /[A-Za-z][0-9A-Za-z-]+/ or raise "invalid special: #{special}"

  60:       end

  61:  

  62:       @major, @minor, @patch, @revision, @special = major, minor, patch, revision, special

  63:     end

  64:  

  65:     def <=>(other)

  66:       maj = @major.to_i <=> other.major.to_i

  67:       return maj unless maj == 0

  68:  

  69:       min = @minor.to_i <=> other.minor.to_i

  70:       return min unless min == 0

  71:  

  72:       pat = @patch.to_i <=> other.patch.to_i

  73:       return pat unless pat == 0

  74:  

  75:       rev = @revision.to_i <=> other.revision.to_i

  76:       return rev unless rev == 0

  77:  

  78:       spe = @special <=> other.special

  79:       return spe unless spe == 0

  80:  

  81:       0

  82:     end

  83:  

  84:     include Comparable

  85:   end

  86: end

  87:  

  88: module Conversions

  89:   def NuGetLatest(package_name, packages_dir)

  90:     NuGetLatest.new(package_name, packages_dir)

  91:   end

  92: end

  93:  

  94: include Conversions

 

Hier die MSpec Klasse zum Ausführen der Unit Tests. Vorsicht: Das Skript prüft eine Umgebungsvariable und ist dediziert auf uns zugeschnitten (Einsatz von Team City). Hier müsstet ihr also Anpassungen vornehmen.

//Update: Wie ich inzwischen erfahren habe, macht MSpec die Prüfung auf die von Team City gesetzte Umgebungsvariable inzwischen implizit!

   1: class Mspec

   2:   def self.run(attributes)

   3:     tool = attributes.fetch(:tool)

   4:     reportDirectory = attributes.fetch(:reportdirectory, '.').to_absolute

   5:     assembly = attributes.fetch(:assembly).to_absolute

   6:     

   7:     reportFile = assembly.name.ext('html').in(reportDirectory).to_absolute

   8:     FileUtils.mkdir_p reportFile.dirname

   9:     

  10:     mspec = tool.to_absolute

  11:     

  12:     Dir.chdir(assembly.dirname) do

  13:       sh "#{mspec.escape} #{'--teamcity ' if ENV['TEAMCITY_PROJECT_NAME']}--timeinfo --html #{reportFile.escape} #{assembly.escape}"

  14:     end

  15:   end

  16: end

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: }

MSpec mit ReSharper 7

Soeben habe ich testweise von ReSharper 6 auf Version 7 aktualisiert.

image

Wie auch bei den letzten Upgrades fehlen danach zunächst die Plugins:

image

Zunächst habe ich mit der aktuellen Version 0.5.7 die Installation versucht. Hier fehlt zwar noch das entsprechende Installationsskript für VS2010 + R# 7, aber das ist schnell gebastelt. Leider half dies nichts.

Um eine lange Geschichte abzukürzen: Auch Version 0.5.8 Beta 9 löst das Problem nicht. Ich habe die Info bereits an Alexander Gross von GROSSWEBER weitergeleitet.

Aktualisierung von 23.08 Uhr: AGross teilte mir soeben mit, dass er morgen die finale 0.5.8 bereitstellen will, die das Problem lösen sollte!

Aktualisierung von 23.17 Uhr: Wer nicht warten kann, installiert sich die Version 0.5.8 Beta 11. Danach läuft alles geschmeidig:

Install-Package Machine.Specifications -Version 0.5.8-beta11 -Pre

imageimage

%d Bloggern gefällt das: