LINQ: GroupJoin

Dzisiaj podstawy, ale wcześniej nie miałem potrzeby skorzystania z  funkcji GroupJoin. Myślę, że prosty przykład jest najlepszą dokumentacją.

Dosyć częstą używaną funkcją jest GroupBy. Jeśli mamy np. listę zamówień w postaci (IdCustomer, Name), wykonując GroupBy na IdCustomer otrzymamy słownik, gdzie kluczem jest identyfikator zamówienia, a wartością lista zamówień danego klienta.

GroupJoin, jak sama nazwa sugeruje jest połączeniem Join z GroupBy. Załóżmy, że mamy następujące encje:

    class CustomerInfo
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }

    class Order
    {
        public int IdCustomer { get; set; }
        public string Name { get; set; }
    }

Następnie przykładowe dane, definiujące relacje wyglądają następująco:

            var customers = new CustomerInfo[3];
            customers[0] = new CustomerInfo() { Id = 1, Name = "Piotr" };
            customers[1] = new CustomerInfo() { Id = 2, Name = "Pawel" };
            customers[2] = new CustomerInfo() { Id = 3, Name = "tertert" };

            var orderList = new Order[5];
            orderList[0] = new Order() { IdCustomer = 1, Name = "Zamowienie 1" };
            orderList[1] = new Order() { IdCustomer = 1, Name = "Zamowienie 1 a" };
            orderList[2] = new Order() { IdCustomer = 2, Name = "Zamowienie 2 a" };
            orderList[3] = new Order() { IdCustomer = 2, Name = "Zamowienie 2 b" };
            orderList[4] = new Order() { IdCustomer = 3, Name = "Zamowienie 3" };

W celu uzyskania zamówień dla konkretnego klienta wystarczy:

            var customerOrders = customers.GroupJoin(orderList, x => x.Id, x => x.IdCustomer, (customer, orders) => new
            {
                CustomerName = customer.Name,
                Orders = orders.ToArray()
            });

            foreach (var customerOrder in customerOrders)
            {
                Console.WriteLine(customerOrder.CustomerName);

                foreach (var order in customerOrder.Orders)
                {
                    Console.WriteLine("\t{0}", order.Name);
                }
            }

Wynikiem będzie lista klientów wraz z zamówieniami:
1

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.

Definiowanie własnych typów danych w C# (statyczne typowanie)

Zwykle programiści korzystają z podstawowych typów dostarczonych przez C#, takich jak String, Int32 czy Double. W świecie programowania obiektowego można jednak pójść o krok dalej i budować własne typy danych. Przeważnie programiści korzystają z nich wyłącznie, gdy do zaimplementowania jest jakaś logika. Dlaczego nie tworzyć ich nawet w sytuacjach, gdy mają one przechowywać wyłącznie dane?

Problem z podstawowymi typami takimi jak String Czy Int32 to fakt, że stanowią one wyłącznie fizyczną, a nie logiczną reprezentację danych. Int32 opisuje każdą liczbę całkowitą, co jest dość szerokim określeniem. Załóżmy, że w kodzie mamy dwie jednostki do opisania:
– prędkość (km/h)
– temperatura (C)

Standardowe rozwiązanie to użycie typów podstawowych, np. Int32:

int velocity = 85;
int temperature = 20;

Niestety powyższy kod nie będzie mógł korzystać z zalet statycznego typowania. Poniższy kod skompiluje się i o błędzie dowiemy się dopiero przy wykonywaniu jakiś operacji:

int velocity = 85;
int temperature = 20;

int result = velocity * temperature;

Prawdopodobnie nie ma sensu mnożyć prędkości przez temperaturę. Analogicznie typy podstawowe nie dostarczają żadnej walidacji logicznej reprezentacji. Co prawda w przypadku liczb, możemy zadeklarować velocity jako uint co ma większy sens ponieważ unikniemy wtedy błędów z związanych z ujemną prędkością. Problem jednak pojawi się w przypadku string:

string email="test@test.com";

W tym przypadku zdecydowanie potrzebna jest walidacja.

Zdefiniujmy zatem typy opisujące logiczną reprezentację danych:

    struct Velocity
    {
        private readonly uint _value;

        public Velocity(uint value)
        {
            _value = value;
        }
    }

    struct Temperature
    {
        private readonly int _value;

        public Temperature(int value)
        {
            _value = value;
        }
    }

Od tej pory, niemożliwe jest popełnienie następującego błędu:

var velocity = new Velocity(43);
var temperature = new Temperature(5);

var result = velocity + temperature;

Kod zakończy się błędem kompilacji: “Operator ‘+’ cannot be applied to operands of type ‘Velocity’ and ‘Temperature'”. Błąd wykryty na etapie kompilacji jest oczywiście dużo bardziej pożądany niż te spotykane już w trakcie działania aplikacji.

Oprócz korzyści z związanych z bezpiecznym typowaniem, kod jest łatwiejszy w zrozumieniu. Typ nadaje kontekst danej wartości.
Dodanie walidacji sprowadza się teraz wyłącznie do:

    struct Temperature
    {
        private readonly int _value;

        public Temperature(int value)
        {        
            _value = value;

            if (!IsValid(value))
                throw new ArgumentException("Temperature must be between -2000 and 2000.");
        }

        private bool IsValid(int value)
        {
            return value < 2000 && value > -2000;
        }
    }

Oczywiście to dopiero początek. W praktyce chcemy mieć do dyspozycji pewne operatory, np.:

            var temperature1 = new Temperature(42);
            var temperature2 = new Temperature(42);
            var temperature3 = new Temperature(41);

            Console.WriteLine(temperature1 == temperature2); // true
            Console.WriteLine(temperature1 == temperature3); // false

Jeśli chcemy wspierać ==, wtedy wystarczy napisać:

    struct Temperature
    {
        private readonly int _value;

        public Temperature(int value)
        {        
            _value = value;

            if (!IsValid(value))
                throw new ArgumentException("Temperature must be between -2000 and 2000.");
        }

        private bool IsValid(int value)
        {
            return value < 2000 && value > -2000;
        }

        public static bool operator ==(Temperature t1, Temperature t2)
        {
            return t1.Equals(t2);
        }

        public static bool operator !=(Temperature t1, Temperature t2)
        {
            return !t1.Equals(t2);
        }
    }

W praktyce warto zadeklarować bazowy typ, który implementuje najczęściej używane operacje (==,!=,>,<,+,-). Nie można również przesadać w drugą stronę i każdą zmienną owijać w typ. Moim zdaniem bardzo szybko taki kod stanie się przykładem overengineering. Jeśli jakiś typ danych występuje bardzo często w kodzie (np. money), wtedy warto o tym pomyśleć.

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.

SQL prompt – IntelliSense w SQL Management Studio

Większość czasu spędzam z C#, a nie z TSQL. Tym bardziej narzędzie SQL prompt zaskoczyło mnie pozytywnie, ponieważ dzięki niemu, mam namiastkę IntelliSense  dostępnego w Visual Studio.

Bardzo często nie znam parametrów wejściowych danej funkcji w TSQL. W Visual Studio przyzwyczajony byłem, że naciskałem CTRL+Space i wszystko stawało się jasne.

Niestety SQL Prompt to płatne narzędzie i możemy tylko za darmo korzystać z wersji trial.  Moim zdaniem ceny są dość wysokie. ale jak ktoś pracuje w firmie, która zakupiła bundle różnych narzędzi od RedGate, wtedy może cieszyć się również SQL Prompt.

Załóżmy, że mamy tabelę “Article” i chcemy napisać Insert into. Za pomocą SQL Prompt, wystarczy, że napsizemy “INSERT INTO Article”, a zostanie wygenerowany szablon.

Screen:

1

Wygenerowany szablon:

INSERT INTO dbo.Articles
        ( Id, Title, Content )
VALUES  ( 0, -- Id - int
          N'', -- Title - nchar(10)
          N''  -- Content - nchar(500)
          )

Komentarze określające typ kolumn, to również bardzo przydatna informacja, jak i dobra praktyka.
Podobnie wygląda sprawa z wywoływaniem procedur. Wystarczy podać nazwę procedury, a automatycznie zostanie wygenerowany szablon (tutaj przykład dla tSQLt.AssertEqual):

EXEC tSQLt.AssertEquals @Expected = NULL, -- sql_variant
    @Actual = NULL, -- sql_variant
    @Message = N'' -- nvarchar(max)

Inna “ukryta” funkcjonalność SQL Prompt to skrót CTRL+F12. Wystarczy, że zaznaczymy dowolną procedurę lub kolumnę, a po naciśnięciu wspomnianego skrótu klawiszowego, dany element zostanie podświetlony w Object Explorer. Zachowanie analogiczne do Shift+Alt+L w Resharper i zaznaczania aktualnego pliku w Solution Explorer.

Ponadto, korzystam często z możliwości formatowania kodu dostarczonego przez SQL Prompt i podoba mi się, że słowa kluczowe automatycznie są zamieniane na wielkie litery.

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.