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.

Leave a Reply

Your email address will not be published.