Visual Studio 2015–Diagnostic Tools

Posted January 24th, 2015 by Piotr Zieliński
Categories: Visual Studio

Nie dawno pojawił się VS 2015 CTP5 -  zachęcam do ściągnięcia.

VS 2015 posiada wiele nowych narzędzi pozwalających na diagnostykę i profilowanie aplikacji. Chcę poświęcić na to kilka wpisów, zaczynając dzisiaj od Diagnostic Tools.

Oczywiście wszystkie te udogodnienia były wcześniej dostępne w formie osobnych narzędzi typu Memory czy CPU profiler. Trend jednak jest taki, że Visual Studio z każdym wydaniem zawiera więcej narzędzi wbudowanych, które kiedyś były osiągalne wyłącznie różne profilery czy Resharper.  Wciąż oczywiście jest długa droga przed VS (zwłaszcza w przypadku Resharper), ale pomysł wbudowanych profilerów bardzo podoba mi się.

Okno diagnostic tools otwiera się automatycznie po rozpoczęciu debuggowania. Jest zatem uzupełnieniem klasycznych narzędzi diagnostycznych takich jak Output czy lista ze zmiennymi (Watch, Locals itp.).  Diagnostic Tools składa się z 3 elementów: Debugger Events, zużycie pamięci oraz zużycie procesora (CPU). Po uruchomieniu, okno prezentuje się następująco:

image

Pierwsza sekcja, Debugger events pokazuje zdarzenia takie jak wywołanie Debugger.Break czy odpalanie danego breakpoint’a. Innymi słowy, mamy tam informacje, co spowodowało zatrzymanie pracy i uruchomienie trybu debuggowania. Przyjrzyjmy się oknu Debugger w momencie, gdy zostanie wyrzucony wyjątek:

image

Całą historie zdarzeń mamy na wykresie, a mianowicie:

image

Screen został zrobiony na podstawie następującego kodu:

image

 

Czerwony romb oznacza wyjątek. Fioletowy pasek reprezentuje przerwanie wykonania aplikacji z powodu wyjątku, czerwony pasek oznacza odpalenie breakpoint’u, z kolei niebieski wywołanie Debugger.Break().

Jak widzimy wielkość pasku zależy od czasu. Pierwszy breakpoint został wykonany prawie natychmiast od czasu uruchomienia aplikacji, dlatego jest bardzo mały. Następnie są uruchamiane co 2 sekundy stąd są nieco większe. Najeżdżając kursorem na pasek, dostaniemy szczegóły:

image

Wykres jest interaktywny. Klikając na zdarzeniu w oknie debugger dostaniemy informacje o nim. Na przykład, klikając na niebieskim pasku, zobaczymy w oknie debugger następujące informacje:

image

Można również zaznaczyć kilka zdarzeń jednocześnie, zaznaczając kursorem przedział czasu, który nas interesuje:

image

image

Kolejne okno pozwoli nam monitorować całkowite zużycie pamięci w aplikacji. Bardzo wygodne, bo debuggując na bieżąco widzimy, czy coś złego nie dzieje się w aplikacji. Załóżmy, że mamy następujący kod:

int a = 0; Thread.Sleep(2000); int[] data = new int[100000000]; Thread.Sleep(2000); data = null; GC.Collect(); GC.WaitForFullGCComplete(); Thread.Sleep(2000); data = new int[10000000]; Thread.Sleep(2000); Debugger.Break();

Na początku deklarujemy jedną zmienną, potem dużą tablice, następnie ją zwalniamy i alokujemy nieco mniejszą. Na wykresie zobaczymy zatem następujące dane:

image

 

Pierwsze 2 sekundy to deklaracja tylko jednej zmiennej, następnie tworzymy dużą tablicę zatem zużycie pamięci rośnie. Kolejna linia kodu usuwa tablicę z pamięci, stąd następne dwie sekundy to znów niskie zużycie pamięci. Ostatni fragment to alokacja małej tablicy i widzimy wyraźnie, że zużycie trochę wzrosło. Oczywiście jest to całkowite zużycie pamięci, a nie tylko zmiennych, które sami tworzymy.

Ostatnie okno, które chciałem pokazać to po prostu zużycie CPU:

image

Wykres, jak każdy poprzedni jest interaktywny i najeżdżając kursorem dostaniemy dodatkowe informacje.

Jak wspomniałem, nie są to żadne nowości, ale ze względu na to, że są zintegrowane z VS i tak łatwo dostępne, debugging aplikacji pozwala na bieżąco monitorować, co dzieje się z kodem.

Antywzorce w pisaniu testów

Posted January 21st, 2015 by Piotr Zieliński
Categories: Patterns & Practices, Testy

W kolejnych kilku wpisach przedstawię parę luźnych porad odnośnie pisania testów. Nie będzie to żaden przewodnik, ale moje własne spostrzeżenia nabyte podczas przeglądania kodu różnych projektów.

Zacznę od pewnego anty-wzorca, znalezionego na blogu http://watirmelon.com/:

s

Na rysunku widać skrajnie złą sytuację w systemie – liczbę testów w zależności od kategorii. Powinno być dokładnie odwrotnie (na górze testy jednostkowe, potem integracyjne, UI i ręczne).

Jeśli większość systemu jest przetestowana przez manualne testy to znaczy, że koszt produkcji jest bardzo wysoki, release’y długie,a błędy regresyjne stają się normą. W dobrze zaprojektowanej aplikacji, najwięcej powinno być testów jednostkowych, ponieważ są one najszybsze w uruchomieniu. Nie potrzebują żadnej zewnętrznej konfiguracji i każdy programista może je odpalić w lokalnym środowisku. Włączając do tego TDD\BDD są bardzo potężnym narzędziem i wyłapanie błędu na wczesnym etapie jest najłatwiejsze.

Oczywiście są miejsca w systemie, które nie mogą zostać pokryte testami jednostkowymi i tu w grę wchodzą testy integracyjne. Niestety bardzo często widzę błędne testy integracyjne. Powszechna zła praktyka to duplikowanie asercji logiki biznesowej w testach integracyjnych. Logikę biznesową powinniśmy przetestować w unit testach. W testach integracyjnych, jak sama nazwa wskazuję, zajmujemy się punktami integracji (np. dwóch serwisów ze sobą). Musimy się zatem upewnić, że odpowiednie interfejsy zostały wstrzyknięte i wywołane. Nie przejmujemy się z kolei, czy dany algorytm zwrócił wartość 10 czy 5. Dokładna wartość została już pokryta w unit testach.

Testy integracyjne wymagają czasami pewnej konfiguracji wstępnej, dlatego są mniej wygodne niż testy jednostkowe – stąd nie powinny stanowić trzonu naszego  QA.

Kolejna grupa to testy UI. Bardzo często  są nadużywane i używanie wyłącznie dlatego, że można je zaprezentować łatwo na sprint demo itp.  Musimy pamiętać, że wymagają one pełnego środowiska i uruchomienie ich jest najwolniejsze. W przypadku unit test, możemy skorzystać z pewnych narzędzi, które wykonują je za nas w tle – dając nam bardzo szybki feedback.  Naturalnie UI tests są automatyczne, dlatego dużo bardziej preferowane są niż testy ręczne. Nie powinniśmy jednak skupiać się na logice biznesowej, a wyłącznie weryfikować kluczowe punkty sytemu lub UI. Stanowią one pełne testy integracyjne i to jest ich ogromna zaleta.

Dodam jeszcze, że w wielu typach aplikacji, testy manualne praktycznie są nie potrzebne. Jeśli zespół potrzebuje kilka osób regularnie wykonujących ręczne testy, to zdecydowanie przedstawiony anty-wzorzec ma tam miejsce. Oczywiście są pewne specyficzne aplikacje (szczególne gry, wizualizacje itp), które wymagają stosunkowo dużej liczby testów manualnych. Natomiast w przypadku oprogramowania biznesowego (web, services) itp. nie powinno być już żadnych wymówek.

Z powyższych rozważań wynika, że testy jednostkowe powinny być najbardziej preferowane.  Ktoś może powiedzieć, że jeśli klasa A wywołuje klasę B, to bez mock’ow taki test stanowi tak naprawdę test integracyjny. O tym temacie napiszę za kilka wpisów, ale przedstawiając powyższy anty-wzorzec, mam na myśli, że testy jednostkowe to wszystkie te, które mogą być wykonane w izolacji (nie wymagają dostępu do bazy danych, web service itp.).

Warto zwrócić uwagę, że programowanie defensywne jest również bardzo skuteczne i potrafi wykryć błędy przed testami jednostkowymi. W środowisku .NET mam na myśli głównie Code Contracts. Dla chętnych polecam przejrzenie blogu ponieważ kilka razy o nich już pisałem.

nServiceBus–Gateways, część VII

Posted January 17th, 2015 by Piotr Zieliński
Categories: Patterns & Practices, Programowanie rozproszone

Dzisiaj o implementacji wzorca gateway w NSB. Wzorzec bramki jest bardzo znany, ale w programowaniu rozproszonym ma swoją konkretną implementację. Bardzo często jest nadużywany i dlatego kluczowe jest jak działa jego implementacja w NSB oraz kiedy należy z niej korzystać.

Brama w NSB to punkt dostępowy do systemu, który jest wykorzystywany, gdy dwa węzły znajdują się poza LAN\VPN.  Innymi słowy, jeśli dwa serwisy są publiczne (połączone Internetem) wtedy nie możemy skorzystać ze standardowego mechanizmu kolejek Windows i musimy zastosować np. protokół HTTP lub HTTPS. Bramki nie mają sensu w lokalnych sieciach, gdzie najlepszą technologią są czyste kolejki – zapewniają wydajność i gwarancje dostarczenia wiadomości, nawet jak sieć będzie tymczasowo niedostępna.

Załóżmy, że mamy dwa serwisy NodeA oraz NodeB, które umieszczone są w kompletnie dwóch różnych lokalizacjach. Bramki będą stanowić zwykłe handlery i mogą wyglądać następująco:

public class SendEmailHandler : IHandleMessages<Email> { public IBus Bus { get; set; } public void Handle(Email email) { // jakis kod Bus.Reply<EmailSent>(m=> { m.Id = email.Id; }); }

W tym momencie kod jeszcze niczym nie różni się od klasycznego handler’a. To, że handler będzie wyeksponowany jako bramka, zależy wyłącznie od konfiguracji:

public class EndpointConfig : IConfigureThisEndpoint, AsA_Server { public void Customize(BusConfiguration configuration) { configuration.UsePersistence<RavenDb>(); configuration.EnableFeature<Gateway>(); } }

EnableFeature<Gateway> to pierwsza zmiana, jaką musimy dokonać. Kolejna konfiguracja to plik .config, gdzie musimy zawrzeć następującą deklarację:

<GatewayConfig TransactionTimeout="00:10:00"> <Channels> <Channel Address="http://localhost:8081/NodeA/" ChannelType="Http" Default="true" /> </Channels> </GatewayConfig>

Za pomocą tego adresu, będziemy mogli wysyłać wiadomości do bramki NodeA. Analogicznie sytuacja wygląda oczywiście z NodeB.

Następnie z dowolnego miejsca możemy wysyłać wiadomości do bramek za pomocą SendToSites:

Bus.SendToSites(new[] { "NodeA", "NodeB" }, new Email { ID=1, Content="test" });

Niezbędna jest również konfiguracja danego węzła, który będzie komunikował się z innymi bramkami:

<GatewayConfig> <Sites> <Site Key="NodeA" Address="http://localhost:8081/NodeA/" ChannelType="Http" /> <Site Key="NodeB" Address="http://localhost:8081/NodeB/" ChannelType="Http" LegacyMode="false" /> </Sites> <Channels> <Channel Address="http://localhost:8081/Master/" ChannelType="Http" /> </Channels> </GatewayConfig>

Powyższa sekcja z pliku konfiguracyjnego, definiuje adres danej bramki (Channel) jak i innych bramek za pomocą sekcji <Sites>. Innymi słowy, będziemy mieli w systemie węzły Master, NodeA, NodeB:

image

Oczywiście również należy pamiętać o EnableFeature<Gateway> dla węzła master.

Wiadomości są zatem wysyłane za pomocą HTTP\HTTPS. Załóżmy, że master chce wysłać komendę do NodeA. Na początku zostanie ona umieszczona w kolejce jak to w klasycznych systemach kolejkowych. Następna bramka zdejmie wiadomość z kolejki i wyśle ją przez HTTP\HTTPS do bramki NodeA. Bramka NodeA przyjmie połącznie HTTP\HTTP, odtworzy wiadomość i policzy hash danej wiadomości.  W kolejnym etpaie hash zostanie odesłany do master i jeśli zgadzają się, tzn. jeśli hash wiadomości wysłanej i odebranej są takie same, wtedy NodeA umieści wiadomość w kolejce wiadomości przychodzących. Dalsze etapy są już standardowe czyli w momencie, gdy tylko będzie to możliwe, wiadomość zostanie przetworzona przez handler. W przypadku, gdy hash’e nie będą zgadzały się, proces będzie rozpoczęty od nowa.

Podsumowując jeszcze raz, jeśli mamy VPN\LAN to możemy zapomnieć o bramkach i korzystać z czystych kolejek. NSB dostarcza jednak bardzo łatwo konfigurację i przełączenie się między zwykłym handlerem, a bramką jest bardzo szybkie. Implementacja gwarantuje, że wszelkie duplikaty wiadomości zostaną rozpoznane i usunięte. W przypadku błędów w przetwarzaniu wiadomości również zostanie zastosowany mechanizm automatycznego powtarzania opisany w poprzednich już postach.

Quartz.NET–planowanie zadań

Posted January 14th, 2015 by Piotr Zieliński
Categories: Ogólne

Czasami potrzeba nam prostego narzędzia, które będzie wykonywało jakieś zadania w określonych ramach czasowych. Można użyć prostego Timera z .NET Framework, ale ma on dość ograniczone możliwości. Na przykład, stan zadań nie może być zapisany w bazie danych. Dla bardzo zaawansowanych rozwiązań, zwykle mamy inną architekturę, na przykład opartą na kolejkach. W takich sytuacjach, zwykle poszczególne technologie posiadają swoje mechanizmy, tak jak nServiceBus o który już wiele razy pisałem.

Dzisiaj jednak chciałbym przedstawić Quartz.NET – lekka biblioteka, która nada się do do prostych przypadków, dla których jednak czysty, standardowy timer ma zbyt małe możliwości.

Standardowo instalujemy pakiet z NuGet:

image

API jest bardzo proste. Standardowy szablon  wygląda następująco:

try { IScheduler scheduler = StdSchedulerFactory.GetDefaultScheduler(); scheduler.Start(); Thread.Sleep(1000000); scheduler.Shutdown(); } catch (SchedulerException ex) { Console.WriteLine(ex); }

Koniecznie należy wywołać Shutdown na koniec.  W przeciwnym wypadku, wciąż w aplikacji istniałyby wątki przeznaczone do wykonywania zadań.  Same zadanie definiuje się poprzez implementację interfejsu IJob:

class PrintTextJob : IJob { public void Execute(IJobExecutionContext context) { Console.WriteLine("Test: {0}",DateTime.Now); } }

Następnie należy ją dodać do scheduler’a:

IJobDetail job = JobBuilder.Create<PrintTextJob>().Build(); ITrigger trigger = TriggerBuilder.Create(). StartNow(). WithSimpleSchedule(x => x.WithIntervalInSeconds(10).RepeatForever()).Build(); scheduler.ScheduleJob(job, trigger);

Definicja Triggera jest tutaj chyba najważniejsza. Szczegóły znajdują się oczywiście w dokumentacji, ale istnieje wiele sposób zdefiniowania kiedy zadanie ma wykonać się. Do dyspozycji mamy nawet cron expression:

TriggerBuilder.Create().WithCronSchedule("0 42 10 * * ?")

Można również zdefiniować zachowanie scheduler’a za pomocą pliku konfiguracyjnego:

quartz.scheduler.instanceName = MyScheduler quartz.threadPool.threadCount = 3 quartz.jobStore.type = Quartz.Simpl.RAMJobStore, Quartz

Jak widać, można określić liczbę wątków oraz typ bazy. Domyślnie jest to in-memory, ale nic nie stoi na przeszkodzie, aby zapisać wyzwalacze czy zadania w innym typie bazy.

Quartz wspiera wykonywanie logów i możemy je zdefiniować za pomocą:

Common.Logging.LogManager.Adapter = new Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter {Level = Common.Logging.LogLevel.Info};

Możemy również śledzić wszelkie wykonywane operacje np. poprzez implementacje interfejsu ISchedulerListener:

public interface ISchedulerListener { void JobScheduled(Trigger trigger); void JobUnscheduled(string triggerName, string triggerGroup); void TriggerFinalized(Trigger trigger); void TriggersPaused(string triggerName, string triggerGroup); void TriggersResumed(string triggerName, string triggerGroup); void JobsPaused(string jobName, string jobGroup); void JobsResumed(string jobName, string jobGroup); void SchedulerError(string msg, SchedulerException cause); void SchedulerShutdown(); }

Następnie zaimplementowany listener należy dodać, a po wszystkim oczywiście usunąć:

scheduler.ListenerManager.AddSchedulerListener(mySchedListener); scheduler.ListenerManager.RemoveSchedulerListener(mySchedListener);

Analogicznie sprawa wygląda z ITriggerListener oraz IJobListener:

public interface ITriggerListener { string Name { get; } void TriggerFired(ITrigger trigger, IJobExecutionContext context); bool VetoJobExecution(ITrigger trigger, IJobExecutionContext context); void TriggerMisfired(ITrigger trigger); void TriggerComplete(ITrigger trigger, IJobExecutionContext context, int triggerInstructionCode); } public interface IJobListener { string Name { get; } void JobToBeExecuted(IJobExecutionContext context); void JobExecutionVetoed(IJobExecutionContext context); void JobWasExecuted(IJobExecutionContext context, JobExecutionException jobException); }

Następnie wystarczy wywołać scheduler.ListenerManager.AddJobListener.

Programowanie aspektowe

Posted January 11th, 2015 by Piotr Zieliński
Categories: Patterns & Practices

Właśnie został opublikowany mój artykuł o programowaniu aspektowym (od podstaw). Zapraszam do lektury:

http://msdn.microsoft.com/pl-pl/dn890694

CodeReview: Struktura readonly a wydajność

Posted January 8th, 2015 by Piotr Zieliński
Categories: C#, Patterns & Practices, Uncategorized

Kiedyś na blogu wyjaśniłem jak działa readonly, gdy jest wywoływany po za konstruktorem. Posłużyłem się następującym przykładem:

internal class Program { public static readonly RiskInfo RiskInfo = new RiskInfo(); private static void Main(string[] args) { Console.WriteLine(RiskInfo.CalculateScore()); Console.WriteLine(RiskInfo.CalculateScore()); Console.WriteLine(RiskInfo.CalculateScore()); } }

Gdzie RiskInfo to:

struct RiskInfo { public int Condition { get; set; } public double Score { get; set; } public double CalculateScore() { Score = Score + 1; return Score; } }

Okazuje się, że wynik na ekranie za każdym razem wynosi 1:

image

Gdybyśmy usunęli modyfikator readonly, albo przesunęli logię do konstruktora, wtedy zobaczylibyśmy 1,2,3.

Oczywiście, każda struktura powinna być immutable, zatem w dobrze zaprojektowanym kodzie, nie musimy martwić o efekty uboczne wynikające z wewnętrznej implementacji języka. 

Dzisiaj chciałbym jednak przestrzec przed pułapką, która wynika z powyższych uwag. Za każdym razem, gdy odwołujemy się do takiego pola, kopiujemy wartość. Wyobraźmy sobie, że mamy następującą strukturę:

struct RiskInfo { private double _factor1; private double _factor2; private double _factor3; private double _factor4; private double _factor5; private double _factor6; private double _factor7; private double _factor8; public double Factor1 { get { return _factor1; } } }

Nie ważne co dokładnie w niej mamy. Chodzi mi o to,  że struktura może zawierać wiele pól, a zatem za każdym razem będą one kopiowane.

Napiszmy prosty benchmark:

internal class Program { private static readonly RiskInfo _riskInfo = new RiskInfo(); private static void Main(string[] args) { const int n = 100000000; Stopwatch stopwatch = Stopwatch.StartNew(); for(int i=0;i<n;i++) { double factor = _riskInfo.Factor1; } Console.WriteLine(stopwatch.ElapsedMilliseconds); } }

Wynik:

image

Następnie analogiczna konstrukcja, ale bez modyfikatora readonly:

internal class Program { private static RiskInfo _riskInfo = new RiskInfo(); private static void Main(string[] args) { const int n = 100000000; Stopwatch stopwatch = Stopwatch.StartNew(); for(int i=0;i<n;i++) { double factor = _riskInfo.Factor1; } Console.WriteLine(stopwatch.ElapsedMilliseconds); } }

Wynik:

image

Widzimy, że różnica jest ogromna i nie nazwałbym tego mikro-optymalizacją. Nic dziwnego, wywołanie metody (właściwości) powoduje przekopiowanie całego obiektu. W konstruktorze jak wspomniałem wcześniej, nie doszłoby do tego problemu. Wywoływanie  metod lub właściwości na typach prostych, tylko do odczytu, po za konstruktorem jest równoznaczne z wywołaniem właściwości zwracającej typ prosty (czyli dochodzi do kopiowania).

Przyjrzyjmy się również IL, aby zrozumieć jak to działa. Tak naprawdę tworzona jest tymczasowa zmienna i w c# mogłoby to wyglądać następująco:

RiskInfo temp = _riskInfo; double factor1 = temp.Factor1;

W przypadku, gdy pole nie jest readonly, wywołanie wygląda po prostu:

double factor1 = _riskInfo.Factor1;

IL dla readonly:

.method private hidebysig static void Main ( string[] args ) cil managed { // Method begins at RVA 0x2058 // Code size 48 (0x30) .maxstack 2 .entrypoint .locals init ( [0] class [System]System.Diagnostics.Stopwatch stopwatch, [1] int32 i, [2] valuetype RiskInfo CS$0$0000 ) IL_0000: call class [System]System.Diagnostics.Stopwatch [System]System.Diagnostics.Stopwatch::StartNew() IL_0005: stloc.0 IL_0006: ldc.i4.0 IL_0007: stloc.1 IL_0008: br.s IL_001c // loop start (head: IL_001c) IL_000a: ldsfld valuetype RiskInfo Program::_riskInfo IL_000f: stloc.2 IL_0010: ldloca.s CS$0$0000 IL_0012: call instance float64 RiskInfo::get_Factor1() IL_0017: pop IL_0018: ldloc.1 IL_0019: ldc.i4.1 IL_001a: add IL_001b: stloc.1 IL_001c: ldloc.1 IL_001d: ldc.i4 100000000 IL_0022: blt.s IL_000a // end loop IL_0024: ldloc.0 IL_0025: callvirt instance int64 [System]System.Diagnostics.Stopwatch::get_ElapsedMilliseconds() IL_002a: call void [mscorlib]System.Console::WriteLine(int64) IL_002f: ret } // end of method Program::Main

IL bez readonly:

.method private hidebysig static void Main ( string[] args ) cil managed { // Method begins at RVA 0x2058 // Code size 45 (0x2d) .maxstack 2 .entrypoint .locals init ( [0] class [System]System.Diagnostics.Stopwatch stopwatch, [1] int32 i ) IL_0000: call class [System]System.Diagnostics.Stopwatch [System]System.Diagnostics.Stopwatch::StartNew() IL_0005: stloc.0 IL_0006: ldc.i4.0 IL_0007: stloc.1 IL_0008: br.s IL_0019 // loop start (head: IL_0019) IL_000a: ldsflda valuetype RiskInfo Program::_riskInfo IL_000f: call instance float64 RiskInfo::get_Factor1() IL_0014: pop IL_0015: ldloc.1 IL_0016: ldc.i4.1 IL_0017: add IL_0018: stloc.1 IL_0019: ldloc.1 IL_001a: ldc.i4 100000000 IL_001f: blt.s IL_000a // end loop IL_0021: ldloc.0 IL_0022: callvirt instance int64 [System]System.Diagnostics.Stopwatch::get_ElapsedMilliseconds() IL_0027: call void [mscorlib]System.Console::WriteLine(int64) IL_002c: ret } // end of method Program::Main

Widzimy, że w przypadku readonly jest tworzona tymczasowa zmienna CS$0$0000, do której kopiujemy wartość w każdej iteracji. Jeszcze raz podkreślam, że problem występuje wyłącznie przy wywoływaniu metod, co niestety ma miejsce w przypadku typów immutable. Nigdy nie deklarujemy publicznych pól (aby uniemożliwić ich modyfikację), więc jedynym sposobem jest wywołanie właściwości, która jest metodą.

Kowariancja tablic a wydajność

Posted January 5th, 2015 by Piotr Zieliński
Categories: C#, Patterns & Practices

Dziś kolejna ciekawostka z C#. Wiemy, że tablice w c# wspierają kowariancje. Oznacza to, że możemy:

string[]names=new string[5]; object[] objectNames = names; objectNames[0]="hello";

Niestety ma to pewną pułapkę. Co stanie się, gdy będziemy chcieli przypisać niepoprawny typ?

string[] names = new string[5]; object[] objectNames = names; objectNames[0] = 5;

Sytuacja zostanie wykryta przez CLR i wyrzucony zostanie wyjątek ArrayTypeMismatchException. Gdyby nie ten wyjątek, doszłoby do poważnego zamieszania w pamięci, więc jest to absolutnie potrzebne. Co to jednak oznacza? Za każdym razem, gdy zapisujemy dane w tablicy, musi istnieć warunek sprawdzający, czy nie chcemy zapisać niepoprawny typ. Nie ma to znaczenia dla pojedynczych wywołań, ale w pętli (np. while dla wzorca producent\konsument) może to spowodować zauważalne problemy.

Jakie jest rozwiązanie? Najprościej używać value type, które oczywiście nigdy nie spowodują podobnych problemów, stąd CLR nie musi tego za każdym razem sprawdzać. Każdy typ referencyjny może w końcu zostać opakowany w typ prosty. Przykład prostego wrappera:

public struct ReferenceValueWrapper<T> where T:class { public T Value; }

Porównajmy więc wydajność następujących przykładów:

private static void Test1(int arraySize) { Base[] data = new Base[arraySize]; Child child=new Child(); Stopwatch stopwatch = Stopwatch.StartNew(); for (int i = 0; i < arraySize; i++) { data[i] = child; } Console.WriteLine(stopwatch.ElapsedMilliseconds); } private static void Test2(int arraySize) { ReferenceValueWrapper<Base>[] data = new ReferenceValueWrapper<Base>[arraySize]; Child child = new Child(); Stopwatch stopwatch = Stopwatch.StartNew(); for (int i = 0; i < arraySize; i++) { data[i] = new ReferenceValueWrapper<Base>() {Value = child}; } Console.WriteLine(stopwatch.ElapsedMilliseconds); }

Tablica zawiera typ bazowy, a przypisujemy poszczególnym elementom obiekt potomny. Wynik:

image

Różnica czterokrotna… Nie zawsze jednak wydajność jest dużo gorsza. Modyfikując trochę powyższy przykład dostaniemy:

private static void Test1(int arraySize) { Base[] data = new Child[arraySize]; Child child=new Child(); Stopwatch stopwatch = Stopwatch.StartNew(); for (int i = 0; i < arraySize; i++) { data[i] = child; } Console.WriteLine(stopwatch.ElapsedMilliseconds); }

Wynik:

image

Wniosek z tego taki, że w powyższym przykładzie nie został wygenerowany warunek sprawdzający i wyrzucający wyjątek. Mając tablicę z Child, naturalne jest, że nie dojdzie do konfliktu typów.

SpecFlow: Uruchamianie testów za pomocą SpecRun

Posted January 2nd, 2015 by Piotr Zieliński
Categories: Testy

W poprzednich postach, używałem domyślnego runner’a dla danego framework’u.  Standardowo SpecFlow może być zintegrowany np.  z nUnit lub Mbunit. Jeśli tylko to możliwe, polecam zainstalowanie SpecRun, bo jak zaraz pokażę, jest dużo wygodniejszy w przypadku BDD.

Na początku sprawdźmy, co jest wygenerowane dla poniższego testu:

Feature: NewPost Jakis opis tutaj... Scenario: Create a new post Given I have logged into CMS When I press the create a post button Then article should be created.

Code-behind:

// ------------------------------------------------------------------------------ // <auto-generated> // This code was generated by SpecFlow (http://www.specflow.org/). // SpecFlow Version:1.9.0.77 // SpecFlow Generator Version:1.9.0.0 // Runtime Version:4.0.30319.0 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> // ------------------------------------------------------------------------------ #region Designer generated code #pragma warning disable namespace ConsoleApplication4 { using TechTalk.SpecFlow; [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "1.9.0.77")] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [NUnit.Framework.TestFixtureAttribute()] [NUnit.Framework.DescriptionAttribute("NewPost")] public partial class NewPostFeature { private static TechTalk.SpecFlow.ITestRunner testRunner; #line 1 "NewPost.feature" #line hidden [NUnit.Framework.TestFixtureSetUpAttribute()] public virtual void FeatureSetup() { testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "NewPost", " Jakis opis tutaj...", ProgrammingLanguage.CSharp, ((string[])(null))); testRunner.OnFeatureStart(featureInfo); } [NUnit.Framework.TestFixtureTearDownAttribute()] public virtual void FeatureTearDown() { testRunner.OnFeatureEnd(); testRunner = null; } [NUnit.Framework.SetUpAttribute()] public virtual void TestInitialize() { } [NUnit.Framework.TearDownAttribute()] public virtual void ScenarioTearDown() { testRunner.OnScenarioEnd(); } public virtual void ScenarioSetup(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) { testRunner.OnScenarioStart(scenarioInfo); } public virtual void ScenarioCleanup() { testRunner.CollectScenarioErrors(); } [NUnit.Framework.TestAttribute()] [NUnit.Framework.DescriptionAttribute("Create a new post")] public virtual void CreateANewPost() { TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Create a new post", ((string[])(null))); #line 3 this.ScenarioSetup(scenarioInfo); #line 4 testRunner.Given("I have logged into CMS", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line 5 testRunner.When("I press the create a post button", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line 6 testRunner.Then("article should be created.", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden this.ScenarioCleanup(); } } } #pragma warning restore #endregion

Z ciekawszych rzeczy, widzimy wyraźnie jak odpalany jest test:

[NUnit.Framework.TestAttribute()] [NUnit.Framework.DescriptionAttribute("Create a new post")] public virtual void CreateANewPost() { TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Create a new post", ((string[])(null))); #line 3 this.ScenarioSetup(scenarioInfo); #line 4 testRunner.Given("I have logged into CMS", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line 5 testRunner.When("I press the create a post button", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line 6 testRunner.Then("article should be created.", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden this.ScenarioCleanup(); } }

CreateANewPost to zwykły test jednostkowy. Zaletą jest, że nie musimy nic zmieniać na serwerach CI. We wszelkich raportach jednak, będziemy mieli nazwę CreateANewPost, co nie jest zbyt eleganckie. Na przykład, w Test Explorer mamy:

image

Instalacja SpecRun odbywa się za pomocą NuGet:

image

 

W App.Config zobaczymy, że nUnit został zastąpiony SpecRun:

<specFlow> <!-- For additional details on SpecFlow configuration options see http://go.specflow.org/doc-config --> <!-- For additional details on SpecFlow configuration options see http://go.specflow.org/doc-config --><!-- use unit test provider SpecRun+NUnit or SpecRun+MsTest for being able to execute the tests with SpecRun and another provider --><unitTestProvider name="SpecRun" /><plugins> <add name="SpecRun" /> </plugins> </specFlow>

CodeBehind również zostanie ponownie wygenerowany:

// ------------------------------------------------------------------------------ // <auto-generated> // This code was generated by SpecFlow (http://www.specflow.org/). // SpecFlow Version:1.9.0.77 // SpecFlow Generator Version:1.9.0.0 // Runtime Version:4.0.30319.0 // // Changes to this file may cause incorrect behavior and will be lost if // the code is regenerated. // </auto-generated> // ------------------------------------------------------------------------------ #region Designer generated code #pragma warning disable namespace ConsoleApplication4 { using TechTalk.SpecFlow; [System.CodeDom.Compiler.GeneratedCodeAttribute("TechTalk.SpecFlow", "1.9.0.77")] [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] [TechTalk.SpecRun.FeatureAttribute("NewPost", Description=" Jakis opis tutaj...", SourceFile="NewPost.feature", SourceLine=0)] public partial class NewPostFeature { private static TechTalk.SpecFlow.ITestRunner testRunner; #line 1 "NewPost.feature" #line hidden [TechTalk.SpecRun.FeatureInitialize()] public virtual void FeatureSetup() { testRunner = TechTalk.SpecFlow.TestRunnerManager.GetTestRunner(); TechTalk.SpecFlow.FeatureInfo featureInfo = new TechTalk.SpecFlow.FeatureInfo(new System.Globalization.CultureInfo("en-US"), "NewPost", " Jakis opis tutaj...", ProgrammingLanguage.CSharp, ((string[])(null))); testRunner.OnFeatureStart(featureInfo); } [TechTalk.SpecRun.FeatureCleanup()] public virtual void FeatureTearDown() { testRunner.OnFeatureEnd(); testRunner = null; } public virtual void TestInitialize() { } [TechTalk.SpecRun.ScenarioCleanup()] public virtual void ScenarioTearDown() { testRunner.OnScenarioEnd(); } public virtual void ScenarioSetup(TechTalk.SpecFlow.ScenarioInfo scenarioInfo) { testRunner.OnScenarioStart(scenarioInfo); } public virtual void ScenarioCleanup() { testRunner.CollectScenarioErrors(); } [TechTalk.SpecRun.ScenarioAttribute("Create a new post", SourceLine=2)] public virtual void CreateANewPost() { TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Create a new post", ((string[])(null))); #line 3 this.ScenarioSetup(scenarioInfo); #line 4 testRunner.Given("I have logged into CMS", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line 5 testRunner.When("I press the create a post button", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line 6 testRunner.Then("article should be created.", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden this.ScenarioCleanup(); } [TechTalk.SpecRun.TestRunCleanup()] public virtual void TestRunCleanup() { TechTalk.SpecFlow.TestRunnerManager.GetTestRunner().OnTestRunEnd(); } } } #pragma warning restore #endregion

Nadal będziemy mieli metodę o nazwie CreateANewPost, ale atrybuty nUnit zostały zastąpione SpecRun:

[TechTalk.SpecRun.ScenarioAttribute("Create a new post", SourceLine=2)] public virtual void CreateANewPost() { TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Create a new post", ((string[])(null))); #line 3 this.ScenarioSetup(scenarioInfo); #line 4 testRunner.Given("I have logged into CMS", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given "); #line 5 testRunner.When("I press the create a post button", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When "); #line 6 testRunner.Then("article should be created.", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then "); #line hidden this.ScenarioCleanup(); } [TechTalk.SpecRun.TestRunCleanup()] public virtual void TestRunCleanup() { TechTalk.SpecFlow.TestRunnerManager.GetTestRunner().OnTestRunEnd(); } }

Warto upewnić się, że w opcjach (Tools->Options), został faktycznie ustawiony SpecRun:

image

W Test Explorer mamy teraz dużo czytelniejszą nazwę testu:

image

Co więcej, po odpaleniu testów, zostanie również wygenerowany raport:

image

Klikając na nazwę testu, dostaniemy ładne podsumowanie w formacie GWT+kod:

image

Przeglądając wygenerowane pliki w Solution Explorer, zobaczymy również runtests.cmd. Oczywiście to skrypt odpalający testy:

image

Jeśli ktoś posiada kilkadziesiąt testów, napisanych w SpecFlow, warto pomyśleć o SpecRun. Oprócz raportu i przejrzystości, zyskamy również możliwość równoległego wykonywania testów, co w przypadku testów systemowych, UI może mieć znaczenie. Niestety za darmo mamy do dyspozycji wyłącznie evaluation mode. Nie jest on limitowany czasowo, ale testy są specjalnie opóźniane co można zaobserwować w Test Explorer.,

Modyfikator sealed dla klas–wydajność

Posted December 30th, 2014 by Piotr Zieliński
Categories: C#, Patterns & Practices

O korzyściach z modyfikatora sealed, od strony projektowej pisałem już tutaj.  Dzisiaj postanowiłem jednak napisać prosty program, który pokaże nam czy faktycznie są jakieś różnice wydajnościowe. Oczywiście jest to raczej ciekawostka dla ludzi zajmujących się c# internals. Jeśli zależy nam na optymalizacji, zawsze zaczynajmy od ulepszenia samego algorytmu (zmniejszenia jego złożoności), a w ostateczności sięgajmy po mikro-optymalizacje. Warto stosować wspominane wskazówki, ale ze względu na dobre praktyki, a nie realny czas, który algorytm zyska dzięki nim.

Zacznijmy od sprawdzenia, czy słowo sealed jest rozpoznawane tylko na poziomie kompilatora, czy również IL:

class BaseClass { } sealed class ChildClass : BaseClass { }

IL:

.class private auto ansi sealed beforefieldinit ConsoleApplication3.ChildClass extends ConsoleApplication3.BaseClass { } // end of class ConsoleApplication3.ChildClass

Widzimy, że faktycznie sealed występuje w IL, więc jest szansa, że w jakiś sposób możemy zyskać na wydajności.

W swoim benchmark’u zadeklarowałem następujące metody:

class BaseClass { public virtual void VirtualMethod() { } public void BaseNonVirtualMethod() { } } sealed class ChildClass : BaseClass { public override void VirtualMethod() { base.VirtualMethod(); } public void ChildNotVirtualMethod() { } }

Innymi słowy, mamy wirtualną metodę, która potem jest rozszerzona w klasie potomnej, niewirtualną bazową metodę oraz niewirtualną metodę zadeklarowaną w klasie potomnej. Wiemy (ze wspomnianego wpisu), że jedynie kiedy możemy zyskać wydajność jest to wywołanie wirtualnej metody.  Wywołując VirtualMethod na klasie “zapieczętowanej”, kompilator mógłby pominąć sprawdzanie wirtualnej tabeli. Szkielet testu wygląda następująco:

const int n = 1000000; TestBaseNonVirtualMethod(n); TestChildNotVirtualMethod(n); TestVirtualMethod(n);

Następnie stworzyłem metodę, która zwróci średni czas wykonania danej funkcji:

private static double GetAverageTime(Action action,int n) { const int samplingCount = 100; double sum = 0; for (int i = 0; i < samplingCount; i++) { Stopwatch stopwatch = Stopwatch.StartNew(); for (int j = 0; j < n; j++) { action(); } sum += stopwatch.ElapsedTicks; } return sum / samplingCount; }

Na końcu metody, które wywołują zaimplementowane funkcje:

private static void TestVirtualMethod(int n) { ChildClass childClass = new ChildClass(); Console.WriteLine("TestVirtualMethod: {0}", GetAverageTime(() => childClass.VirtualMethod(), n)); } static void TestBaseNonVirtualMethod(int n) { BaseClass childClass = new ChildClass(); Console.WriteLine("TestVirtualMethod: {0}", GetAverageTime(() => childClass.BaseNonVirtualMethod(), n)); } static void TestChildNotVirtualMethod(int n) { ChildClass childClass = new ChildClass(); Console.WriteLine("TestChildNotVirtualMethod: {0}", GetAverageTime(() => childClass.ChildNotVirtualMethod(), n)); }

Proszę zwrócić uwagę na TestVirtualMethod. Korzystamy tam z instancji ChildClass, a nie BaseClass. W przypadku BaseClass wirtualna tabela musiałaby być wykorzystywana, nawet w przypadku “zapieczętowanej” klasy. Teoretycznie teraz, kompilator ma wystarczającą wiedzę, aby wyemitować call zamiast callvirt.

Wyniki prezentującą się następująco. Klasa sealed:

image

Zwykła (unsealed):

image

Przedstawione wyniki kompletnie nic nie mówią. Zaglądając do IL, tylko dowiemy się to samo, co w poprzednim wpisie czyli, że zawsze callvirt jest emitowany:

IL_0001: ldfld class ConsoleApplication3.ChildClass ConsoleApplication3.Program/'<>c__DisplayClass1'::childClass IL_0006: callvirt instance void ConsoleApplication3.BaseClass::VirtualMethod() IL_000b: nop IL_000c: ret

Wiemy, że zawsze dla instance-methods emitowany jest callvirt. Przypuszczenie było, że callvirt dla klas zapieczętowanych może ominąć etap sprawdzania vtable. Powyższy test nie potwierdza tego, ale może być to spowodowane wersją .NET\C#, dużo bardziej skomplikowanymi regułami, które nie są udokumentowane, implementacją specyficznego języka (C#, VB,CPP)  itp. Kiedyś pisałem o wydajności metod statycznych i metod wirtualnych. Bardzo szybko udało się uzyskać znaczące różnice w wydajności. O ile w przypadku metod wirtualnych, różnica między wywołaniem metody inline a zwykłą,  mogła mieć realny wpływ na wydajność, to w przypadku modyfikatora sealed, możemy to bez problemu zignorować.

Jednym z ważnych powodów, dlaczego c# używa callvirt nawet dla niewirtualnych metod, jest sprawdzenie czy instancja nie jest NULL’em. Na przykład, poniższy kod nie wyrzuciłby wyjątku, gdyby byłby wywołany za pomocą instrukcji call, a nie callvirt:

AnyClass instance = null; instance.AnyMethod(); //OK jeśli AnyMethod nie korzysta z this i jest wywolana za pomoca call.

Tutaj kolejna ciekawostka odnośnie sealed. Załóżmy, że mamy:

new ChildClass().VirtualMethod();

W takim przypadku, mamy pewność, że nigdy nie będziemy mieli wartości NULL. Dla wirtualnej metody, wciąż niestety zostanie wyemitowany callvirt:

IL_0000: newobj instance void ConsoleApplication3.ChildClass::.ctor() IL_0005: callvirt instance void ConsoleApplication3.BaseClass::VirtualMethod()

Zmieńmy naszą deklaracje na method hiding:

class BaseClass { public virtual void VirtualMethod() { } } sealed class ChildClass : BaseClass { public new void Method() { } }

Wtedy ten sam kod, tzn:

new ChildClass().Method();

Wyemituje call, zamiast callvirt!:

IL_0000: newobj instance void ConsoleApplication3.ChildClass::.ctor() IL_0005: call instance void ConsoleApplication3.ChildClass::Method()

Jeśli tylko rozdzielimy inicjalizacje z wywołaniem:

ChildClass childClass=new ChildClass(); childClass.Method();

Kompilator wyemituje znów callvirt, ponieważ nie ma pewności czy instancja nie jest NULL:

IL_0000: newobj instance void ConsoleApplication3.ChildClass::.ctor() IL_0005: stloc.0 IL_0006: ldloc.0 IL_0007: callvirt instance void ConsoleApplication3.ChildClass::Method() IL_000c: ret

Spróbujmy jeszcze sprawdzić, co się stanie, gdy mamy instrukcję warunkową, teoretycznie gwarantującą, że wartość nigdy nie będzie NULL:

ChildClass childClass=new ChildClass(); if(childClass!=null) childClass.Method();

Kompilator tego nie rozpozna i wciąż będziemy mieli callvirt:

IL_0000: newobj instance void ConsoleApplication3.ChildClass::.ctor() IL_0005: stloc.0 IL_0006: ldloc.0 IL_0007: brfalse.s IL_000f IL_0009: ldloc.0 IL_000a: callvirt instance void ConsoleApplication3.ChildClass::Method()

Nic dziwnego, byłoby to zbyt ryzykowne przypuszczenie, biorąc pod uwagę np. wielowątkowość.

Takich przykładów można mnożyć, ale nie ma to według mnie sensu. Reguły dobierania callvirt, a call dla zapieczętowanych klas są skomplikowane i mogą w każdej chwili ulec zmianie – są to oczywiście sprawy czysto implementacje i w każdej aktualizacji framework’u, może to zmienić się. Wniosek taki, że aspekt wydajności kompletnie pomijamy i wiemy, że generalnie kompilator preferuje callvirt dla instance-method, call jest wykorzystywany wyłącznie w metodach statycznych oraz bardzo rzadko (wyłącznie w bardzo specyficznych sytuacjach), gdy korzystamy z sealed.

SpecFlow – zdarzenia

Posted December 27th, 2014 by Piotr Zieliński
Categories: Testy

Kiedyś w jednym z komentarzu ktoś zasugerował, aby opisać również zdarzenia (hooks) w specflow. Wszystko można znaleźć w dokumentacji, ale również preferuję kilka zwięzłych przykładów niż suchy opis.

Tak jak w nUnit czy jakimkolwiek innym framework’u, możemy definiować co powinno wykonać się przed lub po testach. Atrybutami BeforeTestRun oraz AfterTestRun dekorujemy metody, które mają się wykonać przed i po wszystkich testach:

[Binding] public class Hooks { [BeforeTestRun] public static void BeforeTestRun() { } [AfterTestRun] public static void AfterTestRun() { } }

Atrybut Binding jest niezbędny, gdy definiujemy zdarzenia w osobnej klasie. W przypadku BeforeTestRun ma to sens, ponieważ nie są one specyficzne dla żadnego zestawu testów. Jeśli mamy 10 scenariuszy, BeforeTestRun oraz AfterTestRun wykonają się tylko raz – przed i po wykonaniu wszystkich testów (a nie po każdym scenariuszu osobno). Metoda powinna być również statyczna.

Jeśli mamy logikę, która powinna być wykonana dla każdego scenariusza osobno, wtedy korzystamy z BeforeScenario oraz AfterScenario:

[Binding] public class Hooks { [BeforeFeature] public static void BeforeFeature() { } [AfterFeature()] public static void AfterFeature() { } }

Dla 10 scenariuszy,  metody zatem zostaną wykonane również po 10 razy. Z poprzedniego posta, pamiętamy, że mamy do dyspozycji jeszcze tagi:

@myFirstTag Feature: EditPost Jakis opis tutaj... Scenario: Edit a post Given I have logged into CMS When I press the edit a post button Then article should be updated.

W takim scenariuszu, możliwe jest powiązanie hook’a ze specyficznym tagiem:

[BeforeFeature("myFirstTag")] public static void BeforeFeature() { }

Parametr to lista tagów więc można stworzyć jeden hook dla kilku scenariuszy jednocześnie.

Before i After to aliasy dla BeforeScenario oraz AfterScenario. Moim zdaniem jednak, jeśli ktoś nie jest obeznany z SpecFlow, wtedy nazwa Before może być mało dokładna. Tak czy inaczej, odpowiednik powyższego kodu to:

[Binding] public class Hooks { [Before("myFirstTag")] public static void BeforeFeature() { } [After()] public static void AfterFeature() { } }

Jeśli chcemy wykonać kod pomiędzy Given, When, Then wtedy:

[Binding] public class Hooks { [BeforeScenarioBlock] public static void BeforeScenarioBlock() { } [AfterScenarioBlock] public static void AfterScenarioBlock() { } }

Atrybuty również wspierają tagi. Nazwa Block może być trochę myląca, ale chodzi tutaj, jak wspomniałem o GWT.

Jeśli potrzebujemy bardziej szczegółowe zdarzenia, możemy:

[Binding] public class Hooks { [BeforeStep] public static void BeforeStep() { } [AfterStep] public static void AfterStep() { } }

Pozostaje wyjaśnić czym różni się krok od bloku.  Załóżmy, że mamy zaprezentowany wcześniej test:

@myFirstTag Feature: EditPost Jakis opis tutaj... Scenario: Edit a post Given I have logged into CMS When I press the edit a post button Then article should be updated.

W tym przypadku, blok i krok stanowią to samo i można to byłoby rozdzielić w następujący sposób:

[Blok 1, Krok 1] Given I have logged into CMS

[Blok 2, Krok 2] When I press the edit a post button

[Blok 3, Krok 3] Then article should be updated.

Różnica jest, gdy mamy bloki złożone z And, tzn.:

Feature: EditPost ... Scenario: Edit a post Given I have logged into CMS And I have enough permissions When I press the edit a post button Then article should be updated and afafa

Given składa się z dwóch kroków, zatem można byłoby to rozpisać:

[Blok 1, Krok 1] Given I have logged into CMS

[Krok 2] And I have enough permissions

[Blok 2, Krok 3] When I press the edit a post button

[Blok 3, Krok 4] Then article should be updated and afafa