Category Archives: Testy

tSQLt – asercja błędów

W nUnit możemy sprawdzać, czy funkcja wyrzuciła dany wyjątek. W tSQLt analogicznie istnieje taka możliwość za pomocą procedur
EXPECTEXCEPTION oraz EXPECTNOEXCEPTION. SQL Server oczywiście nie ma jako takich wyjątków, znanych ze świata C#. Mamy za to pojęcie Severity Level. Określa one jak bardzo dany błąd jest poważny. Stwórzmy procedurę, która próbuje użyć nieistniejącej tabeli:

CREATE PROCEDURE DoSomething
AS
BEGIN
		SELECT * FROM NOT_EXISTING_TABLE;

END
GO

Jeśli wykonamy ją w tSQLt, test zakończy się oczywiście niepowodzeniem. Jeśli chcemy zweryfikować, że dowolny błąd został wyrzucony wtedy:

EXEC tSQLt.ExpectException 
EXEC DoSomething;

Metodę ExpectException musimy wykonać przed SUT. Może wydawać się to trochę mało intuicyjne ponieważ w C# zwykle DoSomething wywołujemy jako wyrażenie lambda.
Powyższy kod nie jest jednak dobrą praktyką ponieważ wyłapuje wszystkie błędy. Metoda ExpectException przyjmuje kilka interesujących, opcjonalnych parametrów:

tSQLt.ExpectException 
                     [  [@ExpectedMessage= ] 'expected error message']
                     [, [@ExpectedSeverity= ] 'expected error severity']
                     [, [@ExpectedState= ] 'expected error state']
                     [, [@Message= ] 'supplemental fail message']
                     [, [@ExpectedMessagePattern= ] 'expected error message pattern']
                     [, [@ExpectedErrorNumber= ] 'expected error number']

Zwykle chcemy ustalić przynajmniej ExpectedSeverity, aby wiedzieć jakiego typu błędy są spodziewane i akceptowane.
Druga, analogicnza metoda to ExpectNoException:

EXEC tSQLt.ExpectNoException 
EXEC DoSomething;

Nie trudno domyślić się, że używamy ją w sytuacjach, gdy nie spodziewamy się wyjątku.

tSQLt – izolacja funkcji

Izolacja funkcji w tSQLt niestety wygląda trochę inaczej niż miało miejsce to w przypadku izolacji procedur. Do dyspozycji mamy następującą funkcję:

tSQLt.FakeFunction [@FunctionName = ] 'function name'
                 , [@FakeFunctionName = ] 'fake function name'

Jak widzimy z sygnatury, musimy dostarczyć nazwę funkcji, która ma wykonać się zamiast oryginalnego kodu. Załóżmy, że chcemy stworzyć stub GetDate, który zwraca zawsze stałą wartość. Niestety, GetDate jest funkcją systemową i takich nie można bezpośrednio modyfikować. Stwórzmy zatem wrapper:

CREATE FUNCTION GetDateWrapper
(
)
RETURNS DATETIME
AS
BEGIN
	RETURN GETDATE()

END

Następnie nasz stub będzie zwracał zawsze stałą datę:

CREATE FUNCTION sampleTests.GetDateFake
(
)
RETURNS DATETIME
AS
BEGIN
	RETURN DATETIMEFROMPARTS(2015,1,1,12,10,14,4)

END

GetDateFake zwraca 2015-01-01. Przykładowy test może wyglądać następująco:

EXEC tsqlt.FakeFunction @FunctionName = N'dbo.GetDateWrapper', -- nvarchar(max)
    @FakeFunctionName = N'sampleTests.GetDateFake' -- nvarchar(max)

SELECT dbo.GetDateWrapper();

W przypadku GetDate nie jest to najlepsza praktyka. W zależności od scenariusza, lepszym rozwiązaniem może okazać się przekazywanie czasu jako parametr wejściowy. FakeFunction zdecydowanie jest praktyczny, gdy w kodzie wywołujemy skomplikowane funkcje i nie chcemy ich akurat weryfikować w danym teście.

tSQLt – izolacja procedur

W ostatnich wpisach pokazałem jak izolować dane w formie tabel. W skomplikowanych przypadkach jednak, chcemy również izolacji procedur. Klasyczny przykład to operacje bazujące na czasie lub losowych wartościach. Załóżmy, że mamy następującą procedurę:

CREATE PROCEDURE GetCurrentDate
@outputDate DATETIME output
AS
BEGIN
	SELECT @outputDate=GETDATE();
END

Następnie chcemy przetestować procedurę, która korzysta z powyższej metody. Oczywiście będzie to bardzo trudne, ponieważ wynik prawdopodobnie zależy od aktualnego czasu. Nie trudno wyobrazić sobie zapytanie typu:

SELECT * FROM Table where EndDate > GetDate();

Wynika z tego, że musimy odizolować GetCurrentDate i wstrzyknąć zawartość, którą możemy kontrolować. Izolacja procedur odbywa się za pomocą metody SpyProcedure:

EXEC tSQLt.SpyProcedure 'dbo.GetCurrentDate', 'SET @outputDate = DATEFROMPARTS(2015,01,5)';

Od tego momentu, jakiekolwiek wywołanie GetCurrentDate, wykona kod przekazany jako drugi parametr. W powyższym przypadku, GetCurrentDate zawsze zwraca 2015-01-05. Całość można przetestować wykonując następujący kod:

DECLARE @date datetime;

EXEC tSQLt.SpyProcedure 'dbo.GetCurrentDate', 'SET @outputDate = DATEFROMPARTS(2015,01,5)';

EXEC dbo.GetCurrentDate @date OUTPUT;

SELECT @date;

Niestety, możliwości są dużo mniejsze niż te co mamy w przypadku C#+moq. Język TSQL jest jednak dość prosty. Moim zdaniem jest to wystarczające, szczególnie, że procedury T-SQL są dużo prostsze niż logika zaimplementowana w kodzie Java\C#.

tSQLt – testowanie procedur, sortowanie

W poprzednim wpisie pokazałem jak testować wiersze zwracane przez procedury.  Niestety nie brały one pod uwagę kolejności wierszy, co może wydawać się nienaturalne. Załóżmy, że mamy następującą procedurę, zwracającą artykuły posortowane po tytule:

CREATE PROCEDURE [dbo].[GetArticles]
AS
BEGIN

	SELECT * FROM dbo.Articles  ORDER BY title asc;
END

Następnie analogicznie do poprzedniego wpisu, piszemy test jednostkowy:

  --Assemble
  EXEC tSQLt.FakeTable 'dbo.Articles'

  INSERT INTO dbo.Articles (Title) VALUES('C'),('B'),('A')

  CREATE TABLE ActualValues
  (
  		Id INTEGER,
		Title nvarchar(10),
		Content nchar(500)
  )

    CREATE TABLE ExpectedValues
  (
		Title nvarchar(10)
  )
  
 INSERT INTO dbo.ExpectedValues ( Title) VALUES  ( 'A'),('B'),('C')

 INSERT INTO ActualValues EXEC dbo.GetArticles;
  
  --Assert
  EXEC tSQLt.AssertEqualsTable @Expected = N'ExpectedValues',  @Actual = N'ActualValues'

Wstrzykujemy do naszego “mocka” wiersze w kolejności “C,B,A” i sprawdzamy, że na wyjściu pojawią się one w kolejności rosnącej ponieważ w procedurze mamy order by Title Asc. Odpalamy test i wszystko wydaje się w porządku. Niestety jest to błędny wniosek. Spróbujmy zmienić test do następującej postaci:

  --Assemble
  EXEC tSQLt.FakeTable 'dbo.Articles'

  INSERT INTO dbo.Articles (Title) VALUES('C'),('B'),('A')

  CREATE TABLE ActualValues
  (
  		Id INTEGER,
		Title nvarchar(10),
		Content nchar(500)
  )

    CREATE TABLE ExpectedValues
  (
		Title nvarchar(10)
  )
  
 INSERT INTO dbo.ExpectedValues ( Title) VALUES  ('C'),('B'),('A')

 INSERT INTO ActualValues EXEC dbo.GetArticles;
  
  --Assert
  EXEC tSQLt.AssertEqualsTable @Expected = N'ExpectedValues',  @Actual = N'ActualValues'

Zmieniliśmy kolejność oczekiwanych artykułów na błędną. Po uruchomieniu testu, przekonamy się, że wciąż zakończy się on sukcesem, czego oczywiście nie spodziewaliśmy się!

Niestety kolejność nie jest sprawdzana, co jak się dłużej zastanowimy ma sens, ale na pierwszy rzut oka jest mylące.

Musimy stworzyć kolumnę, która będzie identyfikowała kolejność. Naturalnym wyborem jest zatem “identity”:

  --Assemble
  EXEC tSQLt.FakeTable 'dbo.Articles'

  INSERT INTO dbo.Articles (Title) VALUES('C'),('B'),('A')

  CREATE TABLE ActualValues
  (
  		Id INTEGER,
		Title nvarchar(10),
		Content nchar(500),
		OrderSequence INT IDENTITY(1,1)
  )

    CREATE TABLE ExpectedValues
  (
		OrderSequence INT IDENTITY(1,1),
		Title nvarchar(10)
  )
  
 INSERT INTO dbo.ExpectedValues ( Title) VALUES  ('C'),('B'),('A')

 INSERT INTO ActualValues EXEC dbo.GetArticles;
  
  --Assert
  EXEC tSQLt.AssertEqualsTable @Expected = N'ExpectedValues',  @Actual = N'ActualValues'

Proszę zauważyć, że dodaliśmy OrderSequence do ExpectedValues oraz ActualValues. Po odpaleniu testu, dostaniemy błąd, ponieważ kolejność nie zgadza się:
1

Jeśli zmienimy ExpectedValues z powrotem do sekwencji rosnącej (A,B,C), wtedy test oczywiście zakończy się sukcesem.

Jak zatem to działa? Identity jest zwiększane automatycznie w momencie dodawania danych. Zatem ExpectedValues będzie miał wartości (1,A),(2,B) oraz (3,B). Analogicznie sprawa wygląda z ActualValues. Wynik GetArticles jest dodawany do tabeli ActualValues, która z kolei zawiera pole identity – za każdym razem OrderSequence jest zwiększane. Innymi słowy, dane musza być dodawane w tej samej kolejności, zarówno do ActualValues jak i ExpectedValues.

tSQLt – testowanie procedur

Dzisiaj kolejny scenariusz, w którym przetestujemy procedurę zwracającą dane. Tak jak w poprzednim wpisie, mamy następującą tabelę

CREATE TABLE [dbo].[Articles](
	[Id] [INT] NOT NULL,
	[Title] [NCHAR](10) NULL,
	[Content] [NCHAR](500) NULL,
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]

Załóżmy również, że mamy procedurę GetArticlesByTitle:

CREATE PROCEDURE GetArticlesByTitle
	@title NVARCHAR(10)
AS
BEGIN

	SELECT * FROM dbo.Articles WHERE Title=@Title;
END
GO

Metoda bardzo prosta, ale większość testów będzie miała taki sam szablon Zaczynamy od izolacji tabel, w tym przypadku “Articles”:

  EXEC tSQLt.FakeTable 'dbo.Articles'

Następnie musimy jakieś dane wstrzyknąć:

  INSERT INTO dbo.Articles (Title,Content) VALUES('art1','content1'),('est','content2')

W powyższym przykładzie, dane są dodawane do mock’a, a nie realnej tabeli ponieważ na początku mamy tSQLt.FakeTable.

Kolejnym etapem jest stworzenie dwóch pomocniczych tabel – ExpectedValues oraz ActualValues:

  CREATE TABLE ActualValues
  (
  		Id INTEGER,
		Title nvarchar(10),
		Content nchar(500)
  )

    CREATE TABLE ExpectedValues
  (
		Title nvarchar(10),
		Content nchar(500)
  )
  
  INSERT INTO dbo.ExpectedValues ( Title,Content) VALUES  ( 'art1','content1')

Całość testu wygląda tak:

ALTER PROCEDURE [sampleTests].[test when there is an article with the requested name it should be returned]
AS
BEGIN
  --Assemble
  EXEC tSQLt.FakeTable 'dbo.Articles'

  INSERT INTO dbo.Articles (Title,Content) VALUES('art1','content1'),('est','content2')

  CREATE TABLE ActualValues
  (
  		Id INTEGER,
		Title nvarchar(10),
		Content nchar(500)
  )

    CREATE TABLE ExpectedValues
  (
		Title nvarchar(10),
		Content nchar(500)
  )
  
  INSERT INTO dbo.ExpectedValues ( Title,Content) VALUES  ( 'art1','content1')
  --Act
 INSERT INTO ActualValues EXEC dbo.GetArticlesByTitle 'art1'
  
  --Assert
  EXEC tSQLt.AssertEqualsTable @Expected = N'ExpectedValues',  @Actual = N'ActualValues'
  
END;

Za pomocą AssertEqualsTable porównujemy, że ActualValues (wartości zwrócone z procedury) są równe naszym oczekiwaniom (ExpectedValues). Po wykonaniu testu, jeśli któraś z wartości nie jest taka sama, dostaniemy tabelkę porównującą wyniki:
1

Jak widzimy, testowanie jest dość proste. Najpierw dodajemy potrzebne dane,  a potem definiujemy nasze oczekiwania.

tSQLt – izolacja tabel

Jedną z największych korzyści z tSQLt jest moim zdaniem izolacja danych. Załóżmy, że mamy na następującą tabelę:

USE [testdb]
GO

/****** Object:  Table [dbo].[Articles]    Script Date: 2016-02-20 2:24:25 PM ******/
SET ANSI_NULLS ON
GO

SET QUOTED_IDENTIFIER ON
GO

CREATE TABLE [dbo].[Articles](
	[Id] [int] NOT NULL,
	[Title] [nchar](10) NULL,
	[Content] [nchar](500) NULL,
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

Następnie w naszym teście chcemy przetestować jakąś metodę operującą właśnie na tabeli Article. Z tego względu, musimy umieścić w niej jakieś dane. Przykład testu:

USE [testdb];
GO
SET ANSI_NULLS ON;
GO
SET QUOTED_IDENTIFIER ON;
GO
--  Comments here are associated with the test.
--  For test case examples, see: http://tsqlt.org/user-guide/tsqlt-tutorial/
ALTER PROCEDURE [samples].[test tableIsolation]
AS
    BEGIN

        INSERT  INTO dbo.Articles
                ( Id ,
                  Title ,
                  Content
                )
        VALUES  ( 1 , -- Id - int
                  N'test title' , -- Title - nchar(10)
                  N' test content'  -- Content - nchar(500)
                );
    END;

Widzimy, że nie tworzymy mock’a. Każde odpalenie testu spowoduje dodanie danych do tabeli. Na szczęście wszystkie testy odpalane są transakcjach, zatem po wykonaniu testu, nastąpi rollback.

Może wydawać się to w porządku, ale operowanie na prawdziwej tabeli jest złe, ponieważ co prawda wszelkie zmiany dokonane przez test zostaną anulowane, to test będzie widział dane, które już istniały w tabeli przed odpaleniem testu. Z tego względu stworzenie “fake” tabeli jest lepszym rozwiązaniem:

ALTER PROCEDURE [samples].[test tableIsolation]
AS
    BEGIN

	EXEC tSQLt.FakeTable 'dbo.Articles'

        INSERT  INTO dbo.Articles
                ( Id ,
                  Title ,
                  Content
                )
        VALUES  ( 1 , -- Id - int
                  N'test title' , -- Title - nchar(10)
                  N' test content'  -- Content - nchar(500)
                );
     END;

Za pomocą FakeTable, tworzymy odizolowany mock tabeli. Efekt będzie taki, że:
– dane w tabeli Articles zapisane przed wykonaniem testu, nie są widocznie w trakcie wykonywania testu
– dane zapisane w tabeli Articles w trakcie testu, nie są widocznie na zewnątrz po wykonaniu testu.

Bez fake, wyłącznie drugi punkt byłby prawdziwy, ponieważ rollback by anulował wszystkie nowo dodane wiersze. W testach jednostkowych chcemy mieć zawsze pełną izolacje tak, że dane dodane do tabel poza testem, nie mają wpływu na wynik wykonania testów.
Jak zatem działa FakeTable? To zwykła procedura T-SQL, zatem możemy ją sprawdzić w kodzie:

ALTER PROCEDURE [tSQLt].[FakeTable]
    @TableName NVARCHAR(MAX),
    @SchemaName NVARCHAR(MAX) = NULL, --parameter preserved for backward compatibility. Do not use. Will be removed soon.
    @Identity BIT = NULL,
    @ComputedColumns BIT = NULL,
    @Defaults BIT = NULL
AS
BEGIN
   DECLARE @OrigSchemaName NVARCHAR(MAX);
   DECLARE @OrigTableName NVARCHAR(MAX);
   DECLARE @NewNameOfOriginalTable NVARCHAR(4000);
   DECLARE @OrigTableFullName NVARCHAR(MAX); SET @OrigTableFullName = NULL;
   
   SELECT @OrigSchemaName = @SchemaName,
          @OrigTableName = @TableName
   
   IF(@OrigTableName NOT IN (PARSENAME(@OrigTableName,1),QUOTENAME(PARSENAME(@OrigTableName,1)))
      AND @OrigSchemaName IS NOT NULL)
   BEGIN
     RAISERROR('When @TableName is a multi-part identifier, @SchemaName must be NULL!',16,10);
   END

   SELECT @SchemaName = CleanSchemaName,
          @TableName = CleanTableName
     FROM tSQLt.Private_ResolveFakeTableNamesForBackwardCompatibility(@TableName, @SchemaName);
   
   EXEC tSQLt.Private_ValidateFakeTableParameters @SchemaName,@OrigTableName,@OrigSchemaName;

   EXEC tSQLt.Private_RenameObjectToUniqueName @SchemaName, @TableName, @NewNameOfOriginalTable OUTPUT;

   SELECT @OrigTableFullName = S.base_object_name
     FROM sys.synonyms AS S 
    WHERE S.object_id = OBJECT_ID(@SchemaName + '.' + @NewNameOfOriginalTable);

   IF(@OrigTableFullName IS NOT NULL)
   BEGIN
     IF(COALESCE(OBJECT_ID(@OrigTableFullName,'U'),OBJECT_ID(@OrigTableFullName,'V')) IS NULL)
     BEGIN
       RAISERROR('Cannot fake synonym %s.%s as it is pointing to %s, which is not a table or view!',16,10,@SchemaName,@TableName,@OrigTableFullName);
     END;
   END;
   ELSE
   BEGIN
     SET @OrigTableFullName = @SchemaName + '.' + @NewNameOfOriginalTable;
   END;

   EXEC tSQLt.Private_CreateFakeOfTable @SchemaName, @TableName, @OrigTableFullName, @Identity, @ComputedColumns, @Defaults;

   EXEC tSQLt.Private_MarkFakeTable @SchemaName, @TableName, @NewNameOfOriginalTable;
END

Możemy zobaczyć, że oryginalna tabela ma zmienianą nazwę, a potem tworzymy fake o takiej samej nazwie jak oryginalna tabela.

Na zakończenie bardzo ważna uwaga odnośnie FakeTable. Wszelkie “constraints” nie są wymuszane. W powyższym przypadku, klucz główny jest wymaganym polem i następujący kod normalnie spowoduje wyjątek.

INSERT  INTO dbo.Articles (Title) VALUES('test')

Wyjątek:
“Cannot insert the value NULL into column ‘Id’, table ‘testdb.dbo.Articles’; column does not allow nulls. INSERT fails.”

Z kolei w teście powyższy kod nie spowoduje żadnych błędów. Jak wiemy, FakeTable to nowa tabela ponieważ stara ma zmienianą nazwę. Ta nowa tabela, nie ma dodanych żadnych “constraints”. W praktyce jest to bardzo korzystne. Testy jednostkowe powinny skupiać się wyłącznie na jednej funkcjonalności. Z tego względu, jeśli tabela ma 20 wymaganych kolumn, a my testujemy wyłącznie jedną, nie musimy pozostałych 19 wypełniać danymi.

tSQLt- narzędzie SQL Test

W ostatnim wpisie, przedstawiłem prosty test wykonywany po stronie bazy danych. Zanim przejdę do omawiania bardziej zaawansowanych tematów, warto przyjrzeć się narzędziu SQL Test. Jest to proste narzędzie od RedGate, umożliwiające odpalenie i tworzenie testów. Zamiast np. ręcznie wywoływać “NewTestClass” czy “RunAll”, odpalamy testy tak samo jak w Visual Studio.

Po zainstalowaniu wersji trial, zobaczymy w SQL Management Studio następujące okno:

1

Jak widać, możemy uruchomić dowolny test. Jeśli chcemy dodać nowy test, pojawi się okno gdzie możemy podać nazwę testu oraz test fixture:

2

Narzędzie stworzy schema oraz szablon procedury reprezentującej test:

USE [testdb]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
--  Comments here are associated with the test.
--  For test case examples, see: http://tsqlt.org/user-guide/tsqlt-tutorial/
ALTER PROCEDURE [divideTests].[test anotherTest]
AS
BEGIN
  --Assemble
  --  This section is for code that sets up the environment. It often
  --  contains calls to methods such as tSQLt.FakeTable and tSQLt.SpyProcedure
  --  along with INSERTs of relevant data.
  --  For more information, see http://tsqlt.org/user-guide/isolating-dependencies/
  
  --Act
  --  Execute the code under test like a stored procedure, function or view
  --  and capture the results in variables or tables.
  
  --Assert
  --  Compare the expected and actual values, or call tSQLt.Fail in an IF statement.  
  --  Available Asserts: tSQLt.AssertEquals, tSQLt.AssertEqualsString, tSQLt.AssertEqualsTable
  --  For a complete list, see: http://tsqlt.org/user-guide/assertions/
  EXEC tSQLt.Fail 'TODO:Implement this test.'
  
END;

Narzędzie jest bardzo proste i dość znacząco ułatwia posługiwanie się tSQLt. Moim zdaniem jednak cena zdecydowanie nie jest adekwatna to możliwości produktu.

tSQLt – testy jednostkowe bazy danych w SQL Server

Często logika zawarta w procedurach jest dość skomplikowana. W zależności od projektu, może okazać się,  że potrzebujemy testów jednostkowych. Dzięki tSQLt możemy  testować tSQL w analogiczny sposób do nUnit+moq, czyli:

  • Dane po wykonaniu testu są usuwane. Każdy test jest wykonywany w transakcji. Nie musimy się zatem martwić, że testując coś będziemy zaśmiecać bazę danych.
  • Każdy element może być odizolowany, czyli możemy stworzyć mock dla tabeli, procedury lub funkcji.
  • Testy wykonywane są w pełnej izolacji.
  • Łatwa integracja z CI board.

tSQLt to zespól procedur, które umożliwiają wykonywanie testów bezpośrednio w bazie danych. Musimy zatem najpierw zainstalować je tak jak w przypadku c# był to NuGet. Pakiet możemy znaleźć tutaj. W zestawie znajdują się następujące skrypty:

  • SetClrEnabled.sql – musimy najpierw włączyć CLR na bazie.
  • tSQLt.class.sql – zestaw procedur potrzebny do wykonywania testów (m.in. asercje).
  • Example.sql – przykłady testów. Oczywiście nie musimy tego instalować, ale stanowi to znakomitą dokumentację.

W celu napisania naszego pierwszego testu, uaktywniamy CLR i odpalamy następnie tSQL.class.sql. Po instalacji, zobaczymy, że wiele nowych procedur zostało dodanych:

1

Stwórzmy teraz prostą funkcję w TSQL:


CREATE FUNCTION DivideNumbers
(
@a integer,@b integer
)
RETURNS integer
AS
BEGIN
declare @result integer;
set @result = @a/@b;

RETURN @result;

END

Oczywiście przykład typowo teoretyczny, ale nie ma teraz to znaczenia.

Tak jak w nUnit zaczynamy od TestFixture:


EXEC tSQLt.NewTestClass 'divideTests';

Z punktu widzenia SQL Server, divideTests stanowi schema:

2

W tSQLt “TestClass” to po prostu testFixture. To w nim będziemy przechowywać nasze testy. Stwórzmy zatem pierwszy test:

create PROCEDURE divideTests.[testDivideNumbers]	
AS
BEGIN

	declare @actualResult int
	declare @expectedValue int

	set @expectedValue=12

	select @actualResult=dbo.DivideNumbers(144,12)

	exec tSQLt.AssertEquals  @expected=@expectedValue, @actual=@actualResult
END

Jak widzimy, jest to zwykła procedura, która została stworzona w odpowiednim schema, który jest skojarzony z TestClass (divideTests). Każdy test to standardowe etapy Arrange, Act oraz Assert. Ten ostatni wykonujemy za pomocą szeregu procedur dostarczonych przez tSQLt – w powyższym przypadku jest to AssertEquals.

Jeśli chcemy uruchomić wszystkie testy naraz wtedy wykonujemy:

exec tSQLt.RunAll

W rezultacie zobaczymy raport:

3

Zmieńmy teraz naszą funkcję, tak aby zawierała błąd:

ALTER FUNCTION [dbo].[DivideNumbers]
(
	@a integer,@b integer
)
RETURNS integer
AS
BEGIN
	declare @result integer;
	set @result = @a/@b+1; -- blad

	RETURN @result;

END

Po odpaleniu testów, dostaniemy tym razem błąd asercji:

4

Jeśli chcemy uruchomić wyłącznie jeden zestaw testów, wtedy lepiej użyć:

EXEC tSQLt.Run ‘divideTests’;

Oczywiście tworzenie lub testowanie powyższych funkcji mija się z celem. W przyszłych postach pokaże jak testować procedury, które bardzo często zawierają skomplikowaną logikę (co nierzadko jest złą praktyką, ale to inny już temat).

Prawdziwe korzyści z tSQLt zauważa się, gdy należy odizolować pewne tabele i operacje. Powyższy przykład to funkcja, która nie ma żadnych zależności więc tSQLt oprócz asercji nie wniósł nic nowego. Wpis miał na celu pokazać wyłącznie instalacje tSQLt, stworzenie test fixture (“TestClass”) oraz uruchamianie testów.

HTTP – testy wydajnościowe w JMeter

JMeter jest darmową aplikacją bardzo przydatną podczas  wykonywania “load testing”. Interfejs użytkownika co prawda jest bardzo mało intuicyjny, ale po pewnym czasie można przyzwyczaić się. Aplikacja, po uruchomieniu prezentuje się następująco:
1

Oczywiście na oficjalnej stronie można znaleźć pełną dokumentację, więc moim celem nie jest opisywanie każdego elementu. Jako próbkę, po prostu spróbujmy stworzyć test, który będzie łączył się z jakąś stroną (np. Google) i symulował zapytania wykonywane przez użytkownika. W tym celu, najpierw dodajemy tzw. Thread Group:

2

Thread group służy do symulacji ruchu. Po dodaniu elementu, będziemy mogli skonfigurować częstotliwość zapytań:

3

Number of threads oraz ramp-up period to chyba najważniejsze parametry. Pierwszy z nich to liczba użytkowników (wątków). Drugi z kolei to czas w którym zapytania mają zostać wykonane. Jeśli zatem ustalimy Number of threads na 1000, a ramp-up period na 30, wtedy zostanie wykonanych 1000 akcji (np. zapytań) w ciągu 30 sekund.

Kolejny przydatny element to “Sampler”. W naszym przypadku skorzystamy z HTTP Request:

4

Po dodaniu elementu zobaczymy,  że możemy skonfigurować nazwę serwera, jak i przekazywane parametry. Załóżmy, że chcemy wykonać proste zapytanie GET do Google:

5

Z powyższego screena widać, że praktycznie każdą składową można skonfigurować. Nie ma zatem problemu z wysłaniem POST z bardziej zaawansowanymi parametrami.

Po wykonaniu zapytania, warto coś zrobić z dostarczoną odpowiedzią. Pierwszym krokiem może być wyświetlenie wyników np. w formie drzewa:

6

Przydatne jest również wygenerowanie Summary Report:

7

Niezbędna jest również walidacja odpowiedzi. Musimy wiedzieć, kiedy nasz test powinien zakończyć się sukcesem albo błędem. Z tego względu dodajmy asercję odpowiedzi:

8

Załóżmy, że jeśli odpowiedź przyjdzie z kodem HTTP 200, wtedy test zakończyć powinien się sukcesem:

9

Mamy już wszystkie elementy. Wystarczy, że skonfigurujemy Thread Group (liczbę użytkowników oraz ramp-up) i możemy uruchomić testy. Po uruchomieniu i wykonaniu testów, przejdźmy do dodanego wcześniej Results Tree:

10

Każdą odpowiedź możemy zobaczyć, zarówno w formie dokumentu HTML jak i czystego tekstu. Z kolei Summary Report zawiera informacje takie jak średni czas wykonania zapytania czy procent błędów:

11

Z powyższego screena wiemy, że wszystkie zapytania zakończyły się sukcesem, a średni czas wykonania to 481 milisekund.

Oczywiście to tylko namiastka możliwości JMeter. W praktyce będziemy musieli użyć więcej elementów. Zwykle serwisy mają mechanizm autoryzacji i wtedy będziemy musieli przechowywać ciasteczka pomiędzy różnymi zapytaniami. Zamiast wysyłać pojedyncze zapytanie, być możemy będziemy chcieli symulować pewną sekwencję zapytań.

Myślę, że przydatny skrót to CTRL+E, który powoduje wykasowanie aktualnych raportów.

Inną przydatną opcją jest możliwość nagrywania sekwencji za pomocą przeglądarki. Wtedy JMeter będzie służył jako serwer proxy i wszelkie zapytania wykonane przez użytkownika w przeglądarce, będą tworzyć odpowiednie elementy w JMeter.

nUnit–Wykonywanie testów w osobnych AppDomain

Testy jednostkowe z natury muszą być wykonywane w izolacji. Wykonanie np. pierwszego testu nie powinno mieć żadnego znaczenia dla pozostałych. Analogicznie, kolejność ich wykonywania nie ma znaczenia. Zwykle jest to bardzo proste i osiąga się to poprzez np. mock’i.

Czasami jednak może zajść potrzeba całkowitej izolacji poprzez wykonywanie każdego testu w osobnej AppDomain. Myślę, że w 99% przypadków jednak, można bez tego obyć się. Ostatnio jednak, pisząc pewne narzędzie do Visual Studio, musiałem odizolować od siebie całkowicie to co znajduje się w pamięci. Na szczęście wystarczyło zainstalować poniższy pakiet NuGet:

Install-Package NUnit.ApplicationDomain

Następnie jakiejkolwiek testy wystarczy oznaczać odpowiednim atrybutem:

[Test, RunInApplicationDomain] public void MyTest() { }

Jeszcze raz zaznaczam, w większości przypadków fakt, że potrzebujemy wykonać test w osobnej AppDomain powinien sygnalizować, że coś złego jest z naszym kodem. W przypadku jednak niektórych testów integracyjnych ma to sens – np. kiedy w teście musimy załadować zewnętrzny DLL, który potem chcemy usunąć z pamięci ponieważ nie powinien wpływać na następne testy. W CLR jedynym sposobem na usunięcie DLL z pamięci, jest zniszczenie AppDomain. W bardzo specyficznych scenariuszach testów integracyjnych, powyższe rozwiązanie ma sens.