Database
 sql >> Base de Dados >  >> RDS >> Database

Arte de isolar dependências e dados em testes de unidade de banco de dados


Todos os desenvolvedores de banco de dados mais ou menos escrevem testes de unidade de banco de dados que não apenas ajudam na detecção de bugs antecipadamente, mas também economizam muito tempo e esforços quando o comportamento inesperado de objetos de banco de dados se torna um problema de produção.

Atualmente, existem várias estruturas de teste de unidade de banco de dados, como tSQLt, juntamente com ferramentas de teste de unidade de terceiros, incluindo dbForge Unit Test.

Por um lado, o benefício de usar ferramentas de teste de terceiros é que a equipe de desenvolvimento pode criar e executar instantaneamente testes de unidade com recursos adicionais. Além disso, usar uma estrutura de teste diretamente oferece mais controle sobre os testes de unidade. Portanto, você pode adicionar mais funcionalidades à própria estrutura de teste de unidade. No entanto, neste caso, sua equipe deve ter tempo e um certo nível de conhecimento para fazer isso.

Este artigo explora algumas práticas padrão que podem nos ajudar a melhorar a maneira como escrevemos testes de unidade de banco de dados.



Primeiro, vamos passar por alguns conceitos-chave de teste de unidade de banco de dados.

O que é teste de unidade de banco de dados


De acordo com Dave Green, os testes de unidade de banco de dados garantem que pequenas unidades do banco de dados, como tabelas, exibições, procedimentos armazenados, etc., estejam funcionando conforme o esperado.

Os testes de unidade de banco de dados são escritos para verificar se o código atende aos requisitos de negócios.

Por exemplo, se você receber um requisito como “Um bibliotecário (usuário final) deve ser capaz de adicionar novos livros à biblioteca (Sistema de Informações de Gerenciamento)”, você precisa pensar em aplicar testes de unidade para o procedimento armazenado para verificar se ele pode adicionar um novo livro ao Livro tabela.



Às vezes, uma série de testes de unidade garante que o código atenda aos requisitos. Portanto, a maioria das estruturas de teste de unidade, incluindo tSQLt, permite agrupar testes de unidade relacionados em uma única classe de teste, em vez de executar testes individuais.

Princípio AAA


Vale a pena mencionar o princípio de 3 etapas do teste de unidade, que é uma prática padrão para escrever testes de unidade. O princípio AAA é a base para o teste de unidade e consiste nas seguintes etapas:
  1. Organizar/Montar
  2. Agir
  3. Afirmar

O Organizar seção é o primeiro passo para escrever testes de unidade de banco de dados. Ele orienta através da configuração de um objeto de banco de dados para teste e configuração dos resultados esperados.

A Lei seção é quando um objeto de banco de dados (em teste) é chamado para produzir a saída real.

A Afirmação A etapa lida com a correspondência da saída real com a esperada e verifica se o teste é aprovado ou reprovado.

Vamos explorar esses métodos em exemplos específicos.

Se criarmos um teste de unidade para verificar se o AddProduct procedimento armazenado pode adicionar um novo produto, configuramos o Produto e Produto Esperado tabelas após a adição do produto. Nesse caso, o método vem na seção Organizar/Montar.

Chamar o procedimento AddProduct e colocar o resultado na tabela Product é coberto pela seção Act.

A parte Assert simplesmente corresponde à tabela Product com a tabela ExpectedProduct para ver se o procedimento armazenado foi executado com êxito ou falhou.


Compreendendo as dependências no teste de unidade


Até agora, discutimos os fundamentos do teste de unidade de banco de dados e a importância do princípio AAA (Assemble, Act e Assert) ao criar um teste de unidade padrão.

Agora, vamos nos concentrar em outra peça importante do quebra-cabeça – dependências em testes de unidade.

Além de seguir o princípio AAA e focar apenas em um objeto de banco de dados específico (em teste), também precisamos conhecer as dependências que podem afetar os testes de unidade.

A melhor maneira de entender as dependências é observar um exemplo de teste de unidade.

FuncionáriosAmostra de Configuração do Banco de Dados


Para seguir em frente, crie um banco de dados de amostra e chame-o de EmployeesSample :
-- Create the Employees sample database to demonstrate unit testing

CREATE DATABASE EmployeesSample;
GO

Agora, crie o Funcionário tabela no banco de dados de exemplo:
-- Create the Employee table in the sample database

USE EmployeesSample

CREATE TABLE Employee
  (EmployeeId INT PRIMARY KEY IDENTITY(1,1),
  NAME VARCHAR(40),
  StartDate DATETIME2,
  Title VARCHAR(50)
  );
GO

Preenchendo dados de amostra


Preencha a tabela adicionando alguns registros:
-- Adding data to the Employee table
INSERT INTO Employee (NAME, StartDate, Title)
  VALUES 
  ('Sam','2018-01-01', 'Developer'),
  ('Asif','2017-12-12','Tester'),
  ('Andy','2016-10-01','Senior Developer'),
  ('Peter','2017-11-01','Infrastructure Engineer'),
  ('Sadaf','2015-01-01','Business Analyst');
GO

A tabela fica assim:
-- View the Employee table

  SELECT e.EmployeeId
        ,e.NAME
        ,e.StartDate
        ,e.Title FROM  Employee e;
GO



Observe que estou usando o dbForge Studio para SQL Server neste artigo. Assim, a aparência da saída pode ser diferente se você executar o mesmo código no SSMS (SQL Server Management Studio). Não há diferença quando se trata de scripts e seus resultados.

Requisito para adicionar novo funcionário


Agora, se um requisito para adicionar um novo funcionário foi recebido, a melhor maneira de atender ao requisito é criar um procedimento armazenado que possa adicionar um novo funcionário à tabela com êxito.

Para fazer isso, crie o procedimento armazenado AddEmployee da seguinte maneira:
-- Stored procedure to add a new employee 

CREATE PROCEDURE AddEmployee @Name VARCHAR(40),
@StartDate DATETIME2,
@Title VARCHAR(50)
AS
BEGIN
  SET NOCOUNT ON
    INSERT INTO Employee (NAME, StartDate, Title)
  VALUES (@Name, @StartDate, @Title);
END

Teste de unidade para verificar se o requisito é atendido


Vamos escrever um teste de unidade de banco de dados para verificar se o procedimento armazenado AddEmployee atende ao requisito de adicionar um novo registro à tabela Employee.

Vamos nos concentrar em entender a filosofia de teste de unidade simulando um código de teste de unidade em vez de escrever um teste de unidade com uma estrutura de teste ou ferramenta de teste de unidade de terceiros.

Simulando teste de unidade e aplicando o princípio AAA em SQL


A primeira coisa que precisamos fazer é imitar o princípio AAA no SQL, já que não vamos usar nenhum framework de teste de unidade.

A seção Assemble é aplicada quando as tabelas reais e esperadas são normalmente configuradas junto com a tabela esperada sendo preenchida. Podemos fazer uso de variáveis ​​SQL para inicializar a tabela esperada nesta etapa.

A seção Act é usada quando o procedimento armazenado real é chamado para inserir dados na tabela real.

A seção Assert é quando a tabela esperada corresponde à tabela real. Simular a parte Assert é um pouco complicado e pode ser feito pelas seguintes etapas:
  • Contando as linhas comuns (correspondentes) entre duas tabelas que devem ser 1 (já que a tabela esperada tem apenas um registro que deve corresponder à tabela real)
  • A exclusão dos registros reais da tabela dos registros esperados da tabela deve ser igual a 0 (se o registro na tabela esperada também existir na tabela real, a exclusão de todos os registros reais da tabela esperada deve retornar 0)

O script SQL é o seguinte:

[expandir título=”Código”]
-- Simulating unit test to test the AddEmployee stored procedure

CREATE PROCEDURE TestAddEmployee
AS
BEGIN
  -- (1) Assemble

  -- Set up new employee data
  DECLARE @EmployeeId INT = 6
         ,@NAME VARCHAR(40) = 'Adil'
         ,@StartDate DATETIME2 = '2018-03-01'
         ,@Title VARCHAR(50) = 'Development Manager'


  -- Set up the expected table
  CREATE TABLE #EmployeeExpected (
    EmployeeId INT PRIMARY KEY IDENTITY (6, 1) 
    -- the expected table EmployeeId should begin with 6 
    -- since the actual table has already got 5 records and 
    -- the next EmployeeId in the actual table is 6
   ,NAME VARCHAR(40)
   ,StartDate DATETIME2
   ,Title VARCHAR(50)
  );

  -- Add the expected table data
  INSERT INTO #EmployeeExpected (NAME, StartDate, Title)
    VALUES (@NAME, @StartDate, @Title);

  -- (2) Act

  -- Call AddEmployee to add new employee data to the Employee table
  INSERT INTO Employee
  EXEC AddEmployee @NAME
                  ,@StartDate
                  ,@Title



  -- (3) Assert

  -- Match the actual table with the expected table
  DECLARE @ActualAndExpectedTableCommonRecords INT = 0 -- we assume that expected and actual table records have nothing in common

  SET @ActualAndExpectedTableCommonRecords = (SELECT
      COUNT(*)
    FROM (SELECT
        e.EmployeeId
       ,e.NAME
       ,e.StartDate
       ,e.Title
      FROM Employee e
      INTERSECT
      SELECT
        ee.EmployeeId
       ,ee.NAME
       ,ee.StartDate
       ,ee.Title
      FROM #EmployeeExpected ee) AS A)


  DECLARE @ExpectedTableExcluldingActualTable INT = 1 -- we assume that expected table has records which do not exist in the actual table

  SET @ExpectedTableExcluldingActualTable = (SELECT
      COUNT(*)
    FROM (SELECT
        ee.EmployeeId
       ,ee.NAME
       ,ee.StartDate
       ,ee.Title
      FROM #EmployeeExpected ee
      EXCEPT
      SELECT
        e.EmployeeId
       ,e.NAME
       ,e.StartDate
       ,e.Title
      FROM Employee e) AS A)


  IF @ActualAndExpectedTableCommonRecords = 1
    AND @ExpectedTableExcluldingActualTable = 0
    PRINT '*** Test Passed! ***'
  ELSE
    PRINT '*** Test Failed! ***'

END

[/expandir]

Executando teste de unidade simulado


Após a criação do procedimento armazenado, execute-o com o teste de unidade simulado:
-- Running simulated unit test to check the AddEmployee stored procedure
EXEC TestAddEmployee

A saída é a seguinte:



Parabéns! O teste de unidade de banco de dados foi aprovado.

Identificando problemas na forma de dependências no teste de unidade


Podemos detectar algo errado no teste de unidade que criamos, apesar de ter sido escrito e executado com sucesso?

Se observarmos atentamente a configuração do teste de unidade (a parte Assemble), a tabela esperada tem uma ligação desnecessária com a coluna identity:



Antes de escrever um teste de unidade, já adicionamos 5 registros à tabela real (Employee). Assim, na configuração de teste, a coluna de identidade para a tabela esperada começa com 6. No entanto, isso significa que sempre esperamos que 5 registros estejam na tabela real (Employee) para combiná-la com a tabela esperada (#EmployeeExpected).

Para entender como isso pode afetar o teste de unidade, vamos dar uma olhada na tabela real (Employee) agora:



Adicione outro registro à tabela Employee:
-- Adding a new record to the Employee table

INSERT INTO Employee (NAME, StartDate, Title)
  VALUES ('Mark', '2018-02-01', 'Developer');

Dê uma olhada na tabela Employee agora:



Exclua EmpoyeeId 6 (Adil) para que o teste de unidade possa ser executado em sua própria versão de EmployeeId 6 (Adil) em vez do registro armazenado anteriormente.
-- Deleting the previously created EmployeeId: 6 (Adil) record from the Employee table

DELETE FROM Employee
  WHERE EmployeeId=6



Execute o teste unitário simulado e veja os resultados:
-- Running the simulated unit test to check the AddEmployee stored procedure

EXEC TestAddEmployee



O teste falhou desta vez. A resposta está no conjunto de resultados da tabela Employee, conforme mostrado abaixo:



A vinculação de ID de funcionário no teste de unidade, conforme mencionado acima, não funciona quando reexecutamos o teste de unidade após adicionar um novo registro e excluir o registro de funcionário adicionado anteriormente.

Existem três tipos de dependências no teste:
  1. Dependência de dados
  2. Dependência de restrição de chave
  3. Dependência da coluna de identidade

Dependência de dados


Em primeiro lugar, este teste de unidade depende dos dados no banco de dados. De acordo com Dave Green, quando se trata do banco de dados de testes unitários, os dados em si são uma dependência.

Isso significa que seu teste de unidade de banco de dados não deve depender dos dados do banco de dados. Por exemplo, seu teste de unidade deve conter os dados reais a serem inseridos no objeto de banco de dados (tabela) em vez de depender dos dados já existentes no banco de dados que podem ser excluídos ou modificados.

No nosso caso, o fato de cinco registros já terem sido inseridos na tabela Employee real é uma dependência de dados que deve ser evitada, pois não devemos violar a filosofia de teste de unidade que diz que apenas a unidade do código é testada.

Em outras palavras, os dados de teste não devem depender dos dados reais do banco de dados.


Dependência de restrição de chave


Outra dependência é uma dependência de restrição de chave, o que significa que a coluna de chave primária EmployeeId também é uma dependência. Ele deve ser evitado para escrever um bom teste de unidade. No entanto, um teste de unidade separado é necessário para testar uma restrição de chave primária.

Por exemplo, para testar o procedimento armazenado AddEmployee, a chave primária da tabela Employee deve ser removida para que um objeto possa ser testado sem a preocupação de violar uma chave primária.

Dependência da coluna de identidade


Assim como uma restrição de chave primária, a coluna de identidade também é uma dependência. Assim, não há necessidade de testar a lógica de incremento automático da coluna de identidade para o procedimento AddEmployee; deve ser evitado a qualquer custo.

Isolando dependências no teste de unidade


Podemos evitar todas as três dependências removendo temporariamente as restrições da tabela e não depender dos dados do banco de dados para o teste de unidade. É assim que os testes de unidade de banco de dados padrão são escritos.

Nesse caso, pode-se perguntar de onde vieram os dados da tabela Employee. A resposta é que a tabela é preenchida com dados de teste definidos no teste de unidade.

Procedimento armazenado de teste de unidade de alteração


Vamos agora remover as dependências em nosso teste de unidade:

[expandir título=”Código”]
-- Simulating dependency free unit test to test the AddEmployee stored procedure
ALTER PROCEDURE TestAddEmployee
AS
BEGIN
  -- (1) Assemble

  -- Set up new employee data
  DECLARE @NAME VARCHAR(40) = 'Adil'
         ,@StartDate DATETIME2 = '2018-03-01'
         ,@Title VARCHAR(50) = 'Development Manager'

  -- Set actual table
  DROP TABLE Employee -- drop table to remove dependencies

  CREATE TABLE Employee -- create a table without dependencies (PRIMARY KEY and IDENTITY(1,1))
  (
    EmployeeId INT DEFAULT(0)
   ,NAME VARCHAR(40)
   ,StartDate DATETIME2
   ,Title VARCHAR(50)
  )

  -- Set up the expected table without dependencies (PRIMARY KEY and IDENTITY(1,1)
  CREATE TABLE #EmployeeExpected (
    EmployeeId INT DEFAULT(0)
   ,NAME VARCHAR(40)
   ,StartDate DATETIME2
   ,Title VARCHAR(50)
  )

  -- Add the expected table data
  INSERT INTO #EmployeeExpected (NAME, StartDate, Title)
    VALUES (@NAME, @StartDate, @Title)

  -- (2) Act

  -- Call AddEmployee to add new employee data to the Employee table
  EXEC AddEmployee @NAME
                  ,@StartDate
                  ,@Title
 
  -- (3) Assert

  -- Match the actual table with the expected table
  DECLARE @ActualAndExpectedTableCommonRecords INT = 0 -- we assume that the expected and actual table records have nothing in common

  SET @ActualAndExpectedTableCommonRecords = (SELECT
      COUNT(*)
    FROM (SELECT
        e.EmployeeId
       ,e.NAME
       ,e.StartDate
       ,e.Title
      FROM Employee e
      INTERSECT
      SELECT
        ee.EmployeeId
       ,ee.NAME
       ,ee.StartDate
       ,ee.Title
      FROM #EmployeeExpected ee) AS A)


  DECLARE @ExpectedTableExcluldingActualTable INT = 1 -- we assume that the expected table has records which donot exist in actual table

  SET @ExpectedTableExcluldingActualTable = (SELECT
      COUNT(*)
    FROM (SELECT
        ee.EmployeeId
       ,ee.NAME
       ,ee.StartDate
       ,ee.Title
      FROM #EmployeeExpected ee
      EXCEPT
      SELECT
        e.EmployeeId
       ,e.NAME
       ,e.StartDate
       ,e.Title
      FROM Employee e) AS A)


  IF @ActualAndExpectedTableCommonRecords = 1
    AND @ExpectedTableExcluldingActualTable = 0
    PRINT '*** Test Passed! ***'
  ELSE
    PRINT '*** Test Failed! ***'

  -- View the actual and expected tables before comparison
    SELECT e.EmployeeId
          ,e.NAME
          ,e.StartDate
          ,e.Title FROM Employee e

      SELECT    ee.EmployeeId
               ,ee.NAME
               ,ee.StartDate
               ,ee.Title FROM #EmployeeExpected ee
  
  -- Reset the table (Put back constraints after the unit test)
  DROP TABLE Employee
  DROP TABLE #EmployeeExpected

  CREATE TABLE Employee (
    EmployeeId INT PRIMARY KEY IDENTITY (1, 1)
   ,NAME VARCHAR(40)
   ,StartDate DATETIME2
   ,Title VARCHAR(50)
  );

END

[/expandir]

Executando teste de unidade simulado sem dependência


Execute o teste de unidade simulado para ver os resultados:
-- Running the dependency-free simulated unit test to check the AddEmployee stored procedure

EXEC TestAddEmployee



Execute novamente o teste de unidade para verificar o procedimento armazenado AddEmployee:
-- Running the dependency-free simulated unit test to check the AddEmployee stored procedure

EXEC TestAddEmployee



Parabéns! As dependências do teste de unidade foram removidas com sucesso.

Agora, mesmo se adicionarmos um novo registro ou conjunto de novos registros à tabela Employee, isso não afetará nosso teste de unidade, pois removemos os dados e as dependências de restrição do teste com sucesso.

Criando teste de unidade de banco de dados usando tSQLt


O próximo passo é criar um teste de unidade de banco de dados real baseado no teste de unidade simulado.

Se você estiver usando o SSMS (SQL Server Management Studio), precisará instalar o framework tSQLt, criar uma classe de teste e habilitar o CLR antes de escrever e executar o teste de unidade.

Se você estiver usando o dbForge Studio for SQL Server, você pode criar o teste de unidade clicando com o botão direito do mouse no procedimento armazenado AddEmployee e clicando em “Unit Test” => “Add New Test…” como mostrado abaixo:



Para adicionar um novo teste, preencha as informações de teste de unidade necessárias:



Para escrever o teste de unidade, use o seguinte script:
--  Comments here are associated with the test.
--  For test case examples, see: http://tsqlt.org/user-guide/tsqlt-tutorial/
CREATE PROCEDURE [BasicTests].[test if new employee can be added]
AS
BEGIN
  --Assemble
  DECLARE @NAME VARCHAR(40) = 'Adil'
         ,@StartDate DATETIME2 = '2018-03-01'
         ,@Title VARCHAR(50) = 'Development Manager'


  EXEC tSQLt.FakeTable "dbo.Employee" -- This will create a dependency-free copy of the Employee table
  
  CREATE TABLE BasicTests.Expected -- Create the expected table
  (
    EmployeeId INT 
    ,NAME VARCHAR(40)
   ,StartDate DATETIME2
   ,Title VARCHAR(50)
  )


  -- Add the expected table data
  INSERT INTO BasicTests.Expected (NAME, StartDate, Title)
    VALUES (@NAME, @StartDate, @Title)

  --Act
  EXEC AddEmployee @Name -- Insert data into the Employee table
                  ,@StartDate 
                  ,@Title 
  

  --Assert 
  EXEC tSQLt.AssertEqualsTable @Expected = N'BasicTests.Expected'
                              ,@Actual = N'dbo.Employee'
                              ,@Message = N'Actual table matched with expected table'
                              ,@FailMsg = N'Actual table does not match with expected table'

END;
GO

Em seguida, execute o teste de unidade do banco de dados:



Parabéns! Criamos e executamos com sucesso o teste de unidade do banco de dados que está livre de dependências.

Coisas para fazer


É isso. Você está pronto para isolar dependências de testes de unidade de banco de dados e criar um teste de unidade de banco de dados livre de dependências de dados e restrições depois de ler este artigo. Como resultado, você pode melhorar suas habilidades realizando as seguintes coisas:
  1. Tente adicionar o procedimento armazenado Excluir funcionário e crie um teste de unidade de banco de dados simulado para Excluir funcionário com dependências para ver se ele falha em determinadas condições
  2. Tente adicionar o procedimento armazenado Excluir funcionário e crie um teste de unidade de banco de dados livre de dependências para ver se um funcionário pode ser excluído
  3. Tente adicionar o procedimento armazenado Pesquisar funcionário e crie um teste de unidade de banco de dados simulado com dependências para ver se um funcionário pode ser pesquisado
  4. Tente adicionar o procedimento armazenado Pesquisar funcionário e crie um teste de unidade de banco de dados livre de dependências para ver se um funcionário pode ser pesquisado
  5. Tente requisitos mais complexos criando procedimentos armazenados para atender aos requisitos e, em seguida, escrevendo testes de unidade de banco de dados livres de dependências para ver se eles passam no teste ou falham. No entanto, certifique-se de que o teste seja repetível e focado em testar a unidade do código

Ferramenta útil:


dbForge Unit Test – uma GUI intuitiva e conveniente para implementar testes de unidade automatizados no SQL Server Management Studio.