Category Archives: Testy

NUnit 3 – testy wykonywane równolegle

Wersja nUnit 3.0 wspiera wykonywanie testów równolegle.  Jeśli mamy testy integracyjne albo akceptacyjne, wtedy wykonywanie ich równolegle, w znaczący sposób może przyśpieszyć  CI. W przypadku testów jednostkowych nie ma już to takiego znaczenia, bo i tak powinny one wykonywać się szybko.

Zaczynamy oczywiście od instalacji pakietu nuget NUnit 3. Załóżmy, że mamy następujące, czasochłonne do wykonania testy:

   [TestFixture]
    public class Tests1
    {
        [Test]
        public void Test1()
        {
            Console.WriteLine("Test 1 - start");
            Thread.Sleep(1000);
            Console.WriteLine("Test 1 - end");
        }
    }

    [TestFixture]
    public class Tests2
    {
        [Test]
        public void Test2()
        {
            Console.WriteLine("Test 2 - start");
            Thread.Sleep(1000);
            Console.WriteLine("Test 2 - end");
        }
    }

    [TestFixture]
    public class Tests3
    {
        [Test]
        public void Test3()
        {
            Console.WriteLine("Test 3 - start");
            Thread.Sleep(1000);
            Console.WriteLine("Test 3 - end");
        }
    }

    [TestFixture]
    public class Tests4
    {
        [Test]
        public void Test4()
        {
            Console.WriteLine("Test 4 - start");
            Thread.Sleep(1000);
            Console.WriteLine("Test 4 - end");
        }
    }

Jeśli wykonamy je teraz, zostaną one odpalone w sposób sekwencyjny:

2

Czas wykonania to około 5 sekund, ponieważ domyślnie nie są one uruchamiane równoległe.
W Assembly.cs możemy umieścić globalny atrybut, który umożliwi ich zrównoleglenie, tzn.:

[assembly: Parallelizable(ParallelScope.Fixtures)]

Wynik:

1

Widzimy, że czas ich wykonania zmalał do 2 sekund.

Jeśli jakiś test chcemy wyłączyć z zrównoleglenia, wtedy wystarczy umieścić następujący atrybut:

[Parallelizable(ParallelScope.None)]

Jak widać, atrybut możemy stosować zarówno globalnie jak i selektywnie na konkretnych testach. Dobrze napisane testy nie powinny zależeć od kolejności ich wykonywania, zatem umieszczenie atrybutu Parallelizable globalnie jest rozsądnym podejściem.
Domyślna liczba wątków służąca do odpalenia testów zależy od procesora, a konkretnie liczby rdzeni. Możliwie jest jednak ręczne sterowanie liczbą wątków za pomocą następującego atrybutu:

[assembly:LevelOfParallelism(5)]

Warto również wspomnieć, że na dzień dzisiejszy nie istnieje możliwość wykonania konkretnych testów (metod) równolegle. Innymi słowy, możliwe jest zrównoleglenie wyłącznie klas (TestFixture).

Cykl BDD\TDD

Zarówno o BDD, jak i TDD wiele razy już pisałem. Najważniejszą rzeczą w TDD jest sekwencja red-green-refactor.  Zwykle BDD\TDD przybiera następującą postać:

  1.  Zaczynamy od etapu BDD. Definiujemy wymagania za pomocą języka rozumianego zarówno przez programistów jak i jak BA\PO. Za pomocą Specflow możemy zdefiniować np. następujący test:
        Given I have entered 50 into the calculator
        And I have entered 70 into the calculator
        When I press add
        Then 120 should be displayed on the screen
    
  2. W celu zaimplementowania powyższej specyfikacji, rozpoczynamy cykl TDD. W tym celu tworzymy serie klasycznych testów jednostkowych. Cykl TDD trwa do momentu, kiedy specyfikacja z punktu pierwszego zostanie zaimplementowana.
  3. Definiujemy następną specyfikację, rozpoczynając zatem kolejny cykl TDD.

Innymi słowy, na każdą specyfikacje BDD, przypada ileś testów TDD. Wszystko jest wykonywane na zasadzie red-green-refactor. Definiując specyfikację BDD w punkcie pierwszym, oczywiście taki test będzie czerwony dopóki nie przeprowadzimy cyklu TDD.

Testy UI: Wzorzec PageObject

O testach UI, szczególnie w SpecFlow pisałem już kilka razy. Zawsze korzystałem z wzorca PageObject, chociaż nie wiedziałem, że ma on swoją nazwę. Czasami mam wrażenie, że na proste rzeczy wymyśla się “wzorce”. Muszę przyznać jednak, że  uproszcza to często komunikację między programistami. Wzorce w końcu stanowią pewnego rodzaju słownictwo dla programistów. Zamiast opisywać coś w kilku zdaniach, można powiedzieć po prostu nazwę wzorca.

PageObject pattern polega na tym, że pisząc testy jednostkowe, opakowujemy stronę w obiekty C#.  Początkujący programiści często piszą testy  w stylu:

        [Test]
        public void Query_ShouldBe_Persisted_When_SearchButtonIsClicked()
        {
            IWebDriver driver = new ChromeDriver();
            driver.Navigate().GoToUrl("http://www.bing.com");

            var textBox = driver.FindElement(By.Id("sb_form_q"));
            textBox.SendKeys("piotr zielinski c#");

            var searchButton = driver.FindElement(By.Id("sb_form_go"));
            searchButton.Click();

            string query = driver.FindElement(By.Id("sb_form_q")).GetAttribute("value");
            Assert.That(query,Is.EqualTo("piotr zielinski c#"));
        }

Test jest trudny w zrozumieniu oraz czasochłonny w utrzymaniu. Idealny test powinien być czytany jak skrypt opisujący kolejne akcje. W PageObject opakowujemy strony w obiekty c#. W naszym przypadku stworzymy dwa obiekty, SearchForm oraz SearchResults:

   public class SearchForm
    {
        private readonly IWebDriver _webDriver;

        public SearchForm(IWebDriver webDriver)
        {
            _webDriver = webDriver;
        }

        public SearchResults Search(string query)
        {
            var textBox = _webDriver.FindElement(By.Id("sb_form_q"));
            textBox.SendKeys(query);

            var searchButton = _webDriver.FindElement(By.Id("sb_form_go"));
            searchButton.Click();

            return new SearchResults(_webDriver);
        }        
    }
    public class SearchResults
    {
        private readonly IWebDriver _webDriver;

        public SearchResults(IWebDriver webDriver)
        {
            this._webDriver = webDriver;
        }

        public string GetQuery()
        {
            return _webDriver.FindElement(By.Id("sb_form_q")).GetAttribute("value");
        }
    }

Test zatem wygląda teraz następująco:

            IWebDriver driver = new ChromeDriver();
            driver.Navigate().GoToUrl("http://www.bing.com");
            const string query = "Piotr zielinski c#";
          
            var searchForm=new SearchForm(driver);
            var searchResults = searchForm.Search(query);

            Assert.That(searchResults.GetQuery(), Is.EqualTo(query));

Zwykle opakowuję również sterownik czyli interfejs IWebDriver. Można stworzyć obiekt o nazwie np. BingWebsite który będzie stanowił bramkę do wszelkich testów. Celem jest, aby jakikolwiek kod Selenium nie znajdował się bezpośrednio w teście. Dzięki temu, ten sam kod może być wykorzystywany w różnych testach. Ponadto, przy skomplikowanych testach, kod jest dużo prostszy w rozumieniu ponieważ stanowi skrypt bez magicznych zmiennych.

tSQLt – testowanie wyzwalaczy

O tSQLt pisałem już wiele razy. Dzisiaj chciałbym pokazać jak testować wyzwalacze (triggers). Załóżmy, że mamy następującą tabelę:


CREATE TABLE [dbo].[Articles](
	[Id] [int] IDENTITY(1,1) NOT NULL,
	[Price] [int] NULL,
 CONSTRAINT [PK_Articles] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

Jest to prosta tabela z kluczem głównym oraz jedną kolumną (cena). Napiszmy wyzwalacz, w którym za każdym razem, gdy dodajemy nowy wiersz, cena jest podwajana i kolejny wiersz jest dodawany:

CREATE TRIGGER [dbo].[OnArticleInserted]  
ON [dbo].[Articles]  
AFTER INSERT
AS    
  INSERT INTO Articles (Price) (SELECT Price*2 from inserted)

Nasz test z kolei będzie wyglądać następująco:

ALTER PROCEDURE [sampleTests].[testTrigger]
AS
BEGIN
  --Assemble
  exec tSQLt.FakeTable 'Articles'
  exec tSQLt.ApplyTrigger 'Articles', 'OnArticleInserted'

  create table ExpectedValues
  (
     Price integer
  )

  INSERT INTO ExpectedValues (Price) Values (6),(12)
  
  --Act
  INSERT INTO Articles (Price) Values (6)
  
  --Assert
  EXEC tSQLt.AssertEqualsTable @Expected = N'ExpectedValues',  @Actual = N'Articles'
  
END

Zawsze chcemy odizolowywać tabele. Z tego względu używamy FakeTable na “Articles”. W naszym przypadku, wyzwalacz operuje tylko na tej tabeli więc to wystarczy. W praktyce izolujemy wszystkie tabele z danego wyzwalacza.
Następnie wywołujemy ApplyTrigger. Wiemy, że FakeTable powoduje stworzenie mock’a, zatem wszystkie wyzwalacze domyślnie będą odizolowane. Wywołanie ApplyTrigger umożliwia selektywną aktywację wyzwalaczy.

Dalsza część kodu już wygląda analogicznie do innych testów. Wykonujemy INSERT INTO na Article, co spowoduje uruchomienie naszego wyzwalacza. Na końcu sprawdzamy, czy nowy wiersz faktycznie został dodany.

ASP.NET WebAPI – Pakiet Microsoft.Owin.Testing

W poprzednim wpisie pokazałem częściowo pakiet Owin do testowania. Został on użyty do testów dostawcy. Myślę, że zasługuję on na osobny wpis, ponieważ często w testach integracyjnych czy UI jest przydatny.

Owin.Testing to nic innego jak testowy serwer WWW. Zamiast odpalać proces IIS ręcznie w SetUP, można skorzystać z Owin. Stwórzmy zatem projekt dla testów i zainstalujmy kilka pakietów:

1. nUNit
2. Microsoft.Owin.Testing
3. Microsoft.Owin.Host.HttpListener
4. Restsharp

Następnie podstawowy test wygląda następująco:

   [TestFixture]
    public class WebTests
    {
        [Test]
        public void OwinAppTest()
        {
            using (WebApp.Start<Mystartup>("http://localhost:5454"))
            {
                var httpclient = new RestClient("http://localhost:5454");
                var response =  httpclient.Get(new RestRequest("/api/values"));

                string content = response.Content;

                // assert
            }
        }
    }

Widzimy, że musimy stworzyć OWIN startup w aplikacji ASP.NET WebAPI. Tworzymy zatem plik z szablonu:
1

Domyślnie zostanie wygenerowany następująca klasa:

    public class MyStartup
    {
        public void Configuration(IAppBuilder app)
        {
            // For more information on how to configure your application, visit http://go.microsoft.com/fwlink/?LinkID=316888
        }
    }

Musimy umieścić tam kod, który zwykle zostaje wykonany zamiast Global.asax. Oznacza to, że możemy usunąć Global.asax, a następnie umieścić w MyStartup następującą konfigurację:

  public class MyStartup
    {
        public void Configuration(IAppBuilder app)
        {
            var config = new HttpConfiguration();

            WebApiConfig.Register(config);

            app.UseWebApi(config);

        }
    }

Aby powyższy kod skompilował się, należy najpierw zainstalować w projekcie WebApi pakiety “Microsoft.AspNet.WebApi.OwinSelfHost” oraz “Microsoft.Owin.Host.SystemWeb”. Pierwszy pakiet jest potrzebny, aby mogło się hostować aplikację w procesie, a nie w IIS. Powyższy test, nie będzie odpalać zatem jakiejkolwiek instancji IIS.

Z kolei drugi pakiet (SystemWeb) jest wymagany w celu odpalenia Startup, w momencie hostowania aplikacji w IIS.

Za pomocą Owin, w łatwy sposób można odpalać aplikacje self-host. Analogiczne pakiety można znaleźć m.in. dla Nancy.

CONSUMER DRIVEN CONTRACTS w PACT-NET – implementacja testów dostawcy

W poprzednim wpisie przedstawiłem framework PACT.NET. Zdołaliśmy zaimplementować testy po stronie konsumentów. Wynikiem tego, był wygenerowany plik JSON zawierający nagrane testy. Dla przypomnienia, test po stronie konsumenta wyglądał następująco:

[TestFixture]
public class ConsumerTests
{
    private IMockProviderService _mockProviderService;
    private IPactBuilder _pactProvider;
 
    [OneTimeSetUp]
    public void OneTimeSetUp()
    {
        _pactProvider = new PactBuilder()
            .ServiceConsumer("ConsumerName")
            .HasPactWith("ProviderName");
 
        _mockProviderService = _pactProvider.MockService(3423,false);
    }
 
    [OneTimeTearDown]
    public void OneTimeTearDown()
    {
        _pactProvider.Build();
    }
 
    [Test]
    public void Should_Return_FirstName()
    {
        //Arrange
        _mockProviderService.Given("there are some persons data")
            .UponReceiving("a request to retrieve all persons")
            .With(new ProviderServiceRequest
            {
                Method = HttpVerb.Get,
                Path = "/api/persons"     
            })
            .WillRespondWith(new ProviderServiceResponse
            {
                Status = 200,
                Headers = new Dictionary<string, string>
                {
                    { "Content-Type", "application/json; charset=utf-8" }
                },
                Body = new[]
                {
                    new
                    {
                        FirstName = "Piotr",
                    }
                }
            });
 
        var consumer = new PersonsApiClient("http://localhost:3423");
 
        //Act
        var persons = consumer.GetAllPersons();
 
        //Assert
        CollectionAssert.IsNotEmpty(persons);
        CollectionAssert.AllItemsAreNotNull(persons.Select(x=>x.FirstName));
 
        _mockProviderService.VerifyInteractions();
    }
}

Z kolei wygenerowany JSON:

{
  "provider": {
    "name": "ProviderName"
  },
  "consumer": {
    "name": "ConsumerName"
  },
  "interactions": [
    {
      "description": "a request to retrieve all persons",
      "provider_state": "there are some persons data",
      "request": {
        "method": "get",
        "path": "/api/persons"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json; charset=utf-8"
        },
        "body": [
          {
            "FirstName": "Piotr"
          }
        ]
      }
    }
  ],
  "metadata": {
    "pactSpecificationVersion": "1.1.0"
  }
}

Przejdźmy zatem do testu po stronie dostawcy:

    [TestFixture]
    public class ProducerTests
    {
        [Test]
        public void Ensure_That_Consumer_Requirements_Are_Met()
        {
            var config = new PactVerifierConfig();
            IPactVerifier pactVerifier = new PactVerifier(() => { }, () => { }, config);

            pactVerifier
                .ProviderState(
                    "there are some persons data",
                    setUp: DataSetup,tearDown:DataTearDown);
       
            using (var testServer = TestServer.Create<Startup>())
            {
                pactVerifier
                   .ServiceProvider("Persons API", testServer.HttpClient)
                   .HonoursPactWith("Consumer")
                   .PactUri(@"consumername-providername.json")
                   .Verify();
            }
        }

        private void DataTearDown()
        {
        }


        private void DataSetup()
        {
        }

    }

W celu uruchomienia serwera, używamy Microsoft.Owin.Testing czyli klasy TestServer. W momencie odpalenia testu, zostanie zatem stworzony serwer z naszą aplikacją. Następnie “odgrywany” będzie test wygenerowany przez konsumenta. Operujemy na prawdziwych danych, a nie na mock’ach, stąd metody DataSetup oraz DataTearDown w praktyce zawierają odpowiedni kod, dodający oraz usuwający dane testowe.

Po wykonaniu testu, zostanie wygenerowany plik persons_api_verifier.log:

2016-06-01 19:54:01.894 +01:00 [Debug] Verifying a Pact between ConsumerName and ProviderName
  Given there are some persons data
    a request to retrieve all persons
      with GET /api/persons
        returns a response which
          has status code 200
          includes headers
            'Content-Type' with value application/json; charset=utf-8
          has a matching body (FAILED - 1)

Failures:

1) Expected: [
  {
    "FirstName": "Piotr"
  }
], Actual: [
  {
    "FirstName": "Piotr",
    "LastName": "Zielinski"
  },
  {
    "FirstName": "first name",
    "LastName": "last name"
  }
]

Widzimy, że test zakończył się niepowodzeniem, ponieważ oczekiwana zawartość Content jest inna niż ta zwrócona przez prawdziwy serwis.

Za pomocą PathVerifier definiujemy, które testy chcemy wykonać. W naszym przypadku jest tylko jeden. W praktyce definiuje się wiele stanów i kilka plików JSON.

Consumer driven contracts – pact-net

O Consumer-Driven-Contracts pisałem już tutaj. Sam koncept jest dosyć prosty, ale bez odpowiednich narzędzi może być mozolny w wdrożeniu.
Pack-net, jak nie trudno się domyślić jest implementacją biblioteki pact w C#. Kod źródłowy możną znaleźć na GitHub.

Wiemy, aby zaimplementować Consumer-Driven-Contracts (CDC) musimy napisać testy zarówno po stronie konsumenta jak i dostawcy. Zwykle, konsumenci nie testują prawdziwej usługi, a jedynie operują na mock’ach. Następnie dostawca, wykona wszystkie testy konsumenckie na prawdziwej usłudzę. Sztuczka polega na tym, aby w jakiś sposób przekazać do dostawcy testy zdefiniowane przez konsumenta. Nie chcemy generować biblioteki DLL i jej przekazywać. Pact wygeneruje za nas plik JSON, ze wszystkimi testami. Podsumowując za pomocą Pact:

1. Konsumenci definiują klasyczne testy jednostkowe. Wykonywane są one w izolacji na mock’u usługi.
2. Testy jednostkowe są “nagrywane” i  zapisywane w pliku JSON.
3. Plik (lub pliki w przypadku wielu konsumentów) JSON  są odczytywane przez producenta. Nagrane testy są odtwarzane, ale będą już wykonywane na prawdziwej usłudze.

Jeśli tworzymy usługę, wtedy krok trzeci gwarantuje nam, że spełniamy wszystkie wymogi konsumentów. Jeśli konsument A potrzebuje np. pole FirstName w odpowiedzi, wtedy zdefiniuje to za pomocą testu jednostkowego, a dostawca następnie zweryfikuje to.
Pact dostarcza łatwy interfejs i nie musimy się martwić np. odpaleniem serwer’a web w testach dostawcy, które zawsze wykonywane są na prawdziwej usłudze. Podobnie, dzięki Pact łatwo tworzyć mock’a usługi. Po prostu wystarczy zdefiniować oczekiwane nagłówki czy zawartość ciała.

Myślę, że najłatwiej będzie to zrozumieć na przykładzie. Załóżmy, że tworzymy usługę zwracającą listę osób. Nasza solucja zatem będzie składać się z:
1. Provider.Web – usługa
2. Provider.Web.Tests – testy dostawcy (wykonywane na prawdziwej usłudze)
3. Consumer – prosty klient łączący się z usługą
4. Consumer.Tests – testy konsumenckie (wykonywane na mock’u)

W dzisiejszym wpisie zajmiemy się punktami 1,3 oraz 4, a następnym razem zajmiemy się testami dostawcy.

Zacznijmy zatem od dostawcy (punkt 1):

    public class PersonsController : ApiController
    {
        public PersonInfo[] GetAll()
        {
            var person1 = new PersonInfo {FirstName = "Piotr", LastName = "Zielinski"};
            var person2 = new PersonInfo { FirstName = "first name", LastName = "last name" };

            return new PersonInfo[] {person1, person2};
        }
    }

    public class PersonInfo
    {
        public string FirstName { get; set; }
        public string LastName { get; set; }
    }

Zwykłe WebApi, którego odpowiedź wygląda następująco:

<ArrayOfPersonInfo xmlns:i="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://schemas.datacontract.org/2004/07/Provider.Web.Controllers">
<PersonInfo>
<FirstName>Piotr</FirstName>
<LastName>Zielinski</LastName>
</PersonInfo>
<PersonInfo>
<FirstName>first name</FirstName>
<LastName>last name</LastName>
</PersonInfo>
</ArrayOfPersonInfo>

Następnie zdefiniujemy klienta (konsumenta – punkt 3):

   public class PersonsApiClient
    {
        public string BaseUri { get; set; }

        public PersonsApiClient(string baseUri)
        {
            BaseUri = baseUri;
        }

        public PersonInfo[] GetAllPersons()
        {
            var client = new RestClient(BaseUri);
            var persons = client.Get<List<PersonInfo>>(new RestRequest("api/persons")).Data;

            return persons.ToArray();
        }
    }

Ostatni etap w dzisiejszym wpisie to zdefiniowanie testu konsumenta (punkt 4):

    [TestFixture]
    public class ConsumerTests
    {
        private IMockProviderService _mockProviderService;
        private IPactBuilder _pactProvider;

        [OneTimeSetUp]
        public void OneTimeSetUp()
        {
            _pactProvider = new PactBuilder()
                .ServiceConsumer("ConsumerName")
                .HasPactWith("ProviderName");

            _mockProviderService = _pactProvider.MockService(3423,false);
        }

        [OneTimeTearDown]
        public void OneTimeTearDown()
        {
            _pactProvider.Build();
        }

        [Test]
        public void Should_Return_FirstName()
        {
            //Arrange
            _mockProviderService.Given("there are some persons data")
                .UponReceiving("a request to retrieve all persons")
                .With(new ProviderServiceRequest
                {
                    Method = HttpVerb.Get,
                    Path = "/api/persons"      
                })
                .WillRespondWith(new ProviderServiceResponse
                {
                    Status = 200,
                    Headers = new Dictionary<string, string>
                    {
                        { "Content-Type", "application/json; charset=utf-8" }
                    },
                    Body = new[]
                    {
                        new
                        {
                            FirstName = "Piotr",
                        }
                    }
                });

            var consumer = new PersonsApiClient("http://localhost:3423");

            //Act
            var persons = consumer.GetAllPersons();

            //Assert
            CollectionAssert.IsNotEmpty(persons);
            CollectionAssert.AllItemsAreNotNull(persons.Select(x=>x.FirstName));

            _mockProviderService.VerifyInteractions();
        }
    }

Za pomocą klasy PactBuilder budujemy mock usługi czyli IMockProviderService. Następnie definiujemy wejścia i wyjścia mocka. Widzimy, że jeśli zapytanie GET przyjdzie na /api/persons wtedy zwracamy jeden obiekt, zawierający imię. Za pomocą MockProvider możemy zasymulować dowolne zachowanie usługi. W naszym przypadku jest to po prostu GET i odpowiedź. W bardziej zaawansowanych przypadkach dochodzą do tego parametry query, nagłówki, metody POST, DELETE itp.

Mock zwróci zatem tylko jeden obiekt, zawierający imię Za pomocą następnych asercji, zweryfikujemy, że faktycznie imię musi zawierać jakąś treść. Jeśli pole FirstName na tym etapie jest puste, oznacza to, że implementacja klienta jest błędną – np. deserialziacja nie działa tak jak należy. Widzimy, że port 3423 został dobrany losowo, ponieważ nie ma to znaczenia dla mock’a.
VerifyInteractions sprawdzi również tzw. interakcje. W powyższym przypadku oznacza to, że dokładnie jedno zapytanie GET “api/persons” zostało wysłane do usługi. Jeśli nasz klient zawierałby błąd, np. dwukrotne wysłanie zapytania, wtedy test również zakończyłby się błędem.

Po wykonaniu testu, zostanie on nagrany i zapisany w pliku “consumername-providername.json”:

{
  "provider": {
    "name": "ProviderName"
  },
  "consumer": {
    "name": "ConsumerName"
  },
  "interactions": [
    {
      "description": "a request to retrieve all persons",
      "provider_state": "there are some persons data",
      "request": {
        "method": "get",
        "path": "/api/persons"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json; charset=utf-8"
        },
        "body": [
          {
            "FirstName": "Piotr"
          }
        ]
      }
    }
  ],
  "metadata": {
    "pactSpecificationVersion": "1.1.0"
  }
}

W kolejnym wpisie zajmiemy się implementacją testów dostawcy, które będą wykorzystywać właśnie wygenerowany plik JSON, w celu wykonania takiego samego testu na prawdziwej usłudze.

tSQLt – jak testować “constraints”

Załóżmy, że mamy następującą tabelę:

CREATE TABLE [dbo].[Articles](
	[Id] [int] NOT NULL,
	[Price] [int] NULL,
 CONSTRAINT [PK_Articles] PRIMARY KEY CLUSTERED 
(
	[Id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

GO

ALTER TABLE [dbo].[Articles]  WITH CHECK ADD  CONSTRAINT [CK_Articles] CHECK  (([Price]>(5)))
GO

ALTER TABLE [dbo].[Articles] CHECK CONSTRAINT [CK_Articles]
GO

Oprócz tabeli, dodaliśmy walidację danych. Cena zawsze musi być większą niż 5. Jeśli spróbujemy dodać wartość mniejszą niż 5, zakończy się to błędem.

INSERT INTO Articles Values(1,4)

Błąd:

The INSERT statement conflicted with the CHECK constraint "CK_Articles". The conflict occurred in database "testdb", table "dbo.Articles", column 'Price'.

Spróbujmy teraz wykonać taką samą operację w teście:

EXEC tSQLt.FakeTable 'dbo.Articles'
INSERT INTO Articles Values(1,4)

Dodanie wartości 4 nie spowoduje żadnego błędu. Wynika to z tego, że każdy stub pozbawiony jest jakichkolwiek restrykcji. Zwykle jest to bardzo korzystne, ponieważ jeśli testujemy tylko jedną kolumnę, nie musimy martwić się o resztę wartości.
Jeśli z kolei chcemy, aby nasze testy pokryły również walidację danych (constraints), wtedy mamy do dyspozycji metodę ApplyConstraint:

EXEC tSQLt.FakeTable 'dbo.Articles'
EXEC tSQLt.ApplyConstraint @TableName='dbo.Articles', @ConstraintName="CK_Articles"

INSERT INTO Articles Values(1,4)

Dzięki ApplyConstraint możemy dodawać ograniczenia jedno po drugim, co umożliwia prawidłową implementację testów jednostkowych, gdzie chcemy testować wyłącznie pojedyncze rzeczy. Prawidłowa implementacja testu wygląda zatem następująco:

EXEC tSQLt.FakeTable 'dbo.Articles'
EXEC tSQLt.ApplyConstraint @TableName='dbo.Articles', @ConstraintName="CK_Articles"

EXEC tSQLt.ExpectException

INSERT INTO Articles Values(1,4)

Analogicznie, powinniśmy przetestować wartości, które są zgodne z wymaganiami:

EXEC tSQLt.FakeTable 'dbo.Articles'
EXEC tSQLt.ApplyConstraint @TableName='dbo.Articles', @ConstraintName="CK_Articles"

EXEC tSQLt.ExpectNoException

INSERT INTO Articles Values(1,10)

Testowanie kluczy obcych odbywa się również za pomocą ApplyContraint. Bardzo podobną metodą jest ApplyTrigger:

tSQLt.ApplyTrigger [@TableName = ] 'table name'
                    , [@TriggerName = ] 'trigger name'

BenchmarkDotNet – prosta biblioteka do testów wydajnościowych

Dzisiaj chciałbym pokazać BenchmarkDotNet. Dzięki niemu w łatwy sposób można przetestować wydajność konkretnych metod w c#. Na blogu temat wydajności poruszałem już wiele razy i wiemy,  nie jest łatwe prawidłowe zmierzenie czasu wykonania kodu. Pamiętajmy, że kod wykonany pierwszy raz zawsze musi zostać przetłumaczony do kodu maszynowego (JIT). W momencie wywołania pierwszy raz jakiejkolwiek metody, CLR sprawdzi czy dana metoda ma już kod maszynowy. Jeśli jakaś metoda w ogóle nie została wykonana, wtedy nie ma nawet potrzeby generowania kodu maszynowego. W celu rzetelnego przetestowania jakiejkolwiek metody, należy zawsze wykonać “warm-up”.

Dzięki BenchmarkDotNet nie musimy się o to martwić. Automatycznie zostaną stworzone odizolowane projekty dla każdego z testów. Framework zadba o wyliczenie statystyk takich jak m.in. mediana czy odchylenie standardowe.

Zacznijmy od instalacji pakietu NuGet:

PM> Install-Package BenchmarkDotNet

Kod, który będziemy testować wygląda następująco:

    public class SomeCode
    {
        public void CalculateSlow()
        {
            Thread.Sleep(5000);
        }

        public void CalculateFast()
        {
            Thread.Sleep(5);
        }
    }

Sam test z kolei, to nic innego jak klasa oznaczona atrybutami [Benchmark]:

    public class PerformanceTests
    {
        private readonly SomeCode _sut=new SomeCode();

        [Benchmark]
        public void CalculateSlowTest()
        {
            _sut.CalculateSlow();
        }

        [Benchmark]
        public void CalculateFastTest()
        {
            _sut.CalculateFast();
        }
    }

W celu wykonania testu, należy skorzystać z klasy BenchmarkRunner:

Summary summary = BenchmarkRunner.Run<PerformanceTests>();

Jak widzimy, w wyniku dostaniemy obiekt Summary, zawierający szczegóły z testów. Nie musimy się jednak nim przejmować teraz, ponieważ framework wygeneruje automatycznie raporty zarówno w formie plików tekstowych, jak i na wyjściu aplikacji konsolowej. Po odpaleniu powyższego kodu, na początku zobaczymy:
1

Przez następne kilka sekund\minut będą wykonywane powyższe testy. Po zakończeniu, na ekranie konsoli zobaczymy podsumowanie:
2

Oprócz tego, w folderze bin zobaczymy kilka raportów m.in. PerformanceTests-measurements.csv, PerformanceTests-report.csv, PerformanceTests-report.html jak i również w formie markdown. Jeśli nie wystarczają one, można napisać własne i odpowiednio skonfigurować BenchmarkDotNet.

tSQLt – integracja z TeamCity

Kilka postów poświęciłem już na temat pisania testów jednostkowych dla SQL Server. Tak samo jak ze zwykłymi testami w C#, chcemy je wykonywać jako etap w CI. Ostatnio zwykle pracuję z TeamCity, dlatego w tym wpisie pokażę plugin właśnie dla TC.

Integracja TeamCity z tSQLt sprowadza się do instalacji następującego plugina:

https://github.com/cprieto/tsqlt-teamcity

Plugin umiezczamy w “C:\ProgramData\JetBrains\TeamCity\plugins”. Następnie, przy tworzeniu kolejnego etapu w CI, do dyspozycji będziemy mieli tSQLt runner (screen z oficjalnej dokumentacji):

Analogicznie do testów jednostkowych nUnit, będziemy mogli przeglądać, które testy zostały wykonane. Konfiguracja zatem sprowadza się do instalacji plugina oraz podania connection string.