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:
- Organizar/Montar
- Agir
- 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:
- Dependência de dados
- Dependência de restrição de chave
- 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:
- 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
- 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
- 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
- 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
- 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.