sql >> Base de Datos >  >> RDS >> Database

Arte de aislar dependencias y datos en pruebas unitarias de bases de datos

Todos los desarrolladores de bases de datos escriben más o menos pruebas unitarias de bases de datos que no solo ayudan a detectar errores temprano, sino que también ahorran mucho tiempo y esfuerzos cuando el comportamiento inesperado de los objetos de la base de datos se convierte en un problema de producción.

Hoy en día, hay una serie de marcos de prueba de unidad de base de datos, como tSQLt, junto con herramientas de prueba de unidad de terceros, incluida dbForge Unit Test.

Por un lado, el beneficio de usar herramientas de prueba de terceros es que el equipo de desarrollo puede crear y ejecutar instantáneamente pruebas unitarias con funciones adicionales. Además, el uso directo de un marco de prueba le brinda más control sobre las pruebas unitarias. Por lo tanto, puede agregar más funciones al propio marco de pruebas unitarias. Sin embargo, en este caso, su equipo debe tener tiempo y cierto nivel de experiencia para hacer esto.

Este artículo explora algunas prácticas estándar que pueden ayudarnos a mejorar la forma en que escribimos pruebas unitarias de bases de datos.

Primero, repasemos algunos conceptos clave de las pruebas unitarias de bases de datos.

¿Qué es la prueba de unidad de base de datos?

Según Dave Green, las pruebas unitarias de la base de datos garantizan que las unidades pequeñas de la base de datos, como tablas, vistas, procedimientos almacenados, etc., funcionen como se espera.

Las pruebas unitarias de la base de datos se escriben para verificar si el código cumple con los requisitos comerciales.

Por ejemplo, si recibe un requisito como "Un bibliotecario (usuario final) debería poder agregar nuevos libros a la biblioteca (Sistema de información de gestión)", debe pensar en aplicar pruebas unitarias para el procedimiento almacenado para verificar si puede agregar un nuevo libro al Libro mesa.

A veces, una serie de pruebas unitarias aseguran que el código cumpla con los requisitos. Por lo tanto, la mayoría de los marcos de pruebas unitarias, incluido tSQLt, permiten agrupar pruebas unitarias relacionadas en una sola clase de prueba en lugar de ejecutar pruebas individuales.

Principio AAA

Vale la pena mencionar el principio de 3 pasos de las pruebas unitarias, que es una práctica estándar para escribir pruebas unitarias. El principio AAA es la base de las pruebas unitarias y consta de los siguientes pasos:

  1. Organizar/ensamblar
  2. Actuar
  3. Afirmar

El arreglar La sección es el primer paso para escribir pruebas unitarias de bases de datos. Guía a través de la configuración de un objeto de base de datos para probar y configurar los resultados esperados.

El acto La sección es cuando se llama a un objeto de base de datos (bajo prueba) para producir la salida real.

La afirmación El paso trata de hacer coincidir la salida real con la esperada y verifica si la prueba pasa o falla.

Exploremos estos métodos en ejemplos particulares.

Si creamos una prueba unitaria para verificar que el AddProduct procedimiento almacenado puede agregar un nuevo producto, configuramos el Producto y Producto Esperado tablas después de agregar el producto. En este caso, el método viene en la sección Arrange/Assemble.

Llamar al procedimiento AddProduct y colocar el resultado en la tabla Product está cubierto por la sección Act.

La parte Assert simplemente hace coincidir la tabla Product con la tabla ExpectedProduct para ver si el procedimiento almacenado se ejecutó con éxito o falló.

Comprensión de las dependencias en las pruebas unitarias

Hasta ahora, hemos discutido los conceptos básicos de las pruebas unitarias de bases de datos y la importancia del principio AAA (ensamblar, actuar y afirmar) al crear una prueba unitaria estándar.

Ahora, centrémonos en otra pieza importante del rompecabezas:las dependencias en las pruebas unitarias.

Además de seguir el principio AAA y centrarnos solo en un objeto de base de datos en particular (bajo prueba), también necesitamos conocer las dependencias que pueden afectar las pruebas unitarias.

La mejor manera de comprender las dependencias es ver un ejemplo de una prueba unitaria.

EmpleadosConfiguración de la base de datos de muestra

Para continuar, cree una base de datos de muestra y llámela EmployeesSample :

-- Create the Employees sample database to demonstrate unit testing

CREATE DATABASE EmployeesSample;
GO

Ahora, crea el Empleado tabla en la base de datos de ejemplo:

-- 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

Poblando datos de muestra

Complete la tabla agregando algunos 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

La tabla se ve así:

-- View the Employee table

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

Tenga en cuenta que estoy usando dbForge Studio para SQL Server en este artículo. Por lo tanto, el aspecto de salida puede diferir si ejecuta el mismo código en SSMS (SQL Server Management Studio). No hay diferencia cuando se trata de scripts y sus resultados.

Requisito para agregar un nuevo empleado

Ahora, si se ha recibido un requisito para agregar un nuevo empleado, la mejor manera de cumplir con el requisito es crear un procedimiento almacenado que pueda agregar con éxito un nuevo empleado a la tabla.

Para hacer esto, cree el procedimiento almacenado AddEmployee de la siguiente manera:

-- 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

Prueba unitaria para verificar si se cumple el requisito

Vamos a escribir una prueba de unidad de base de datos para verificar si el procedimiento almacenado AddEmployee cumple con el requisito de agregar un nuevo registro a la tabla Employee.

Centrémonos en comprender la filosofía de prueba unitaria simulando un código de prueba unitaria en lugar de escribir una prueba unitaria con un marco de prueba o una herramienta de prueba unitaria de terceros.

Simulación de pruebas unitarias y aplicación del principio AAA en SQL

Lo primero que tenemos que hacer es imitar el principio AAA en SQL ya que no vamos a utilizar ningún marco de pruebas unitarias.

La sección Ensamblar se aplica cuando las tablas reales y esperadas normalmente se configuran junto con la tabla esperada que se llena. Podemos hacer uso de variables SQL para inicializar la tabla esperada en este paso.

La sección Actuar se utiliza cuando se llama al procedimiento almacenado real para insertar datos en la tabla real.

La sección Afirmar es cuando la tabla esperada coincide con la tabla real. Simular la parte Assert es un poco complicado y se puede lograr siguiendo los siguientes pasos:

  • Contar las filas comunes (coincidencias) entre dos tablas que deberían ser 1 (dado que la tabla esperada tiene solo un registro que debería coincidir con la tabla real)
  • Excluir los registros de la tabla real de los registros de la tabla esperada debe ser igual a 0 (si el registro en la tabla esperada también existe en la tabla real, entonces excluir todos los registros de la tabla real de la tabla esperada debe devolver 0)

El script SQL es el siguiente:

[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]

Ejecución de prueba unitaria simulada

Después de crear el procedimiento almacenado, ejecútelo con la prueba unitaria simulada:

-- Running simulated unit test to check the AddEmployee stored procedure
EXEC TestAddEmployee

La salida es la siguiente:

¡Felicidades! La prueba unitaria de la base de datos pasó.

Identificación de problemas en forma de dependencias en la prueba unitaria

¿Podemos detectar algún problema en la prueba unitaria que creamos a pesar de que se escribió y ejecutó correctamente?

Si observamos de cerca la configuración de la prueba unitaria (la parte Ensamblar), la tabla esperada tiene un enlace innecesario con la columna de identidad:

Antes de escribir una prueba unitaria, ya hemos agregado 5 registros a la tabla real (Empleado). Por lo tanto, en la configuración de prueba, la columna de identidad para la tabla esperada comienza con 6. Sin embargo, esto significa que siempre esperamos que haya 5 registros en la tabla real (Empleado) para que coincida con la tabla esperada (#EmployeeExpected).

Para entender cómo esto puede afectar la prueba unitaria, echemos un vistazo a la tabla real (Empleado) ahora:

Agregue otro registro a la tabla Empleado:

-- Adding a new record to the Employee table

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

Eche un vistazo a la tabla de empleados ahora:

Elimine EmpoyeeId 6 (Adil) para que la prueba unitaria pueda ejecutarse en su propia versión de EmployeeId 6 (Adil) en lugar del registro almacenado anteriormente.

-- Deleting the previously created EmployeeId: 6 (Adil) record from the Employee table

DELETE FROM Employee
  WHERE EmployeeId=6

Ejecute la prueba unitaria simulada y vea los resultados:

-- Running the simulated unit test to check the AddEmployee stored procedure

EXEC TestAddEmployee

La prueba ha fallado esta vez. La respuesta se encuentra en el conjunto de resultados de la tabla Empleado como se muestra a continuación:

El enlace de ID de empleado en la prueba de unidad como se mencionó anteriormente no funciona cuando volvemos a ejecutar la prueba de unidad después de agregar un nuevo registro y eliminar el registro de empleado agregado anteriormente.

Hay tres tipos de dependencias en la prueba:

  1. Dependencia de datos
  2. Dependencia de restricciones clave
  3. Dependencia de la columna de identidad

Dependencia de datos

En primer lugar, esta prueba unitaria depende de los datos de la base de datos. Según Dave Green, cuando se trata de la base de datos de pruebas unitarias, los datos en sí son una dependencia.

Esto significa que la prueba unitaria de su base de datos no debe basarse en los datos de la base de datos. Por ejemplo, su prueba de unidad debe contener los datos reales que se insertarán en el objeto de la base de datos (tabla) en lugar de depender de los datos que ya existen en la base de datos que se pueden eliminar o modificar.

En nuestro caso, el hecho de que ya se hayan insertado cinco registros en la tabla Empleado real es una dependencia de datos que debe evitarse porque no debemos violar la filosofía de la prueba unitaria que dice que solo se prueba la unidad del código.

En otras palabras, los datos de prueba no deben basarse en los datos reales de la base de datos.

Dependencia de restricciones clave

Otra dependencia es una dependencia de restricción de clave, lo que significa que la columna de clave principal EmployeeId también es una dependencia. Debe prevenirse para escribir una buena prueba unitaria. Sin embargo, se requiere una prueba de unidad separada para probar una restricción de clave principal.

Por ejemplo, para probar el procedimiento almacenado AddEmployee, la clave principal de la tabla Employee debe eliminarse para que un objeto pueda probarse sin la preocupación de violar una clave principal.

Dependencia de la columna de identidad

Al igual que una restricción de clave principal, la columna de identidad también es una dependencia. Por lo tanto, no hay necesidad de probar la lógica de incremento automático de la columna de identidad para el procedimiento AddEmployee; debe evitarse a toda costa.

Aislamiento de dependencias en pruebas unitarias

Podemos evitar las tres dependencias eliminando las restricciones de la tabla temporalmente y luego no depender de los datos en la base de datos para la prueba unitaria. Así es como se escriben las pruebas de unidad de base de datos estándar.

En este caso, uno podría preguntarse de dónde provienen los datos de la tabla Empleado. La respuesta es que la tabla se llena con datos de prueba definidos en la prueba unitaria.

Alteración del procedimiento almacenado de prueba de unidad

Ahora eliminemos las dependencias en nuestra prueba unitaria:

[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]

Ejecución de pruebas unitarias simuladas sin dependencia

Ejecute la prueba unitaria simulada para ver los resultados:

-- Running the dependency-free simulated unit test to check the AddEmployee stored procedure

EXEC TestAddEmployee

Vuelva a ejecutar la prueba unitaria para comprobar el procedimiento almacenado AddEmployee:

-- Running the dependency-free simulated unit test to check the AddEmployee stored procedure

EXEC TestAddEmployee

¡Felicidades! Las dependencias de la prueba unitaria se han eliminado correctamente.

Ahora, incluso si agregamos un nuevo registro o un conjunto de nuevos registros a la tabla de empleados, no afectará nuestra prueba unitaria ya que hemos eliminado las dependencias de datos y restricciones de la prueba con éxito.

Creación de prueba de unidad de base de datos usando tSQLt

El siguiente paso es crear una prueba de unidad de base de datos real basada en la prueba de unidad simulada.

Si está utilizando SSMS (SQL Server Management Studio), deberá instalar el marco tSQLt, crear una clase de prueba y habilitar CLR antes de escribir y ejecutar la prueba unitaria.

Si está utilizando dbForge Studio para SQL Server, puede crear la prueba unitaria haciendo clic con el botón derecho en el procedimiento almacenado AddEmployee y luego haciendo clic en "Prueba unitaria" => "Agregar nueva prueba..." como se muestra a continuación:

Para agregar una nueva prueba, complete la información requerida de la prueba unitaria:

Para escribir la prueba unitaria, use el siguiente 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

Luego, ejecute la prueba unitaria de la base de datos:

¡Felicidades! Hemos creado y ejecutado con éxito la prueba unitaria de la base de datos que está libre de dependencias.

Cosas que hacer

Eso es todo. Está listo para aislar las dependencias de las pruebas unitarias de la base de datos y crear una prueba unitaria de la base de datos libre de dependencias de datos y restricciones después de leer este artículo. Como resultado, puede mejorar sus habilidades realizando las siguientes cosas:

  1. Intente agregar el procedimiento almacenado Eliminar empleado y cree una prueba de unidad de base de datos simulada para Eliminar empleado con dependencias para ver si falla bajo ciertas condiciones
  2. Intente agregar el procedimiento almacenado Eliminar empleado y cree una prueba de unidad de base de datos libre de dependencias para ver si se puede eliminar un empleado
  3. Intente agregar el procedimiento almacenado Buscar empleado y cree una prueba de unidad de base de datos simulada con dependencias para ver si se puede buscar un empleado
  4. Intente agregar el procedimiento almacenado Buscar empleado y cree una prueba de unidad de base de datos libre de dependencias para ver si se puede buscar un empleado
  5. Pruebe con requisitos más complejos creando procedimientos almacenados para cumplir con los requisitos y luego escribiendo pruebas de unidad de base de datos sin dependencias para ver si pasan la prueba o fallan. Sin embargo, asegúrese de que la prueba sea repetible y se centre en probar la unidad del código

Herramienta útil:

Prueba unitaria de dbForge:una GUI intuitiva y conveniente para implementar pruebas unitarias automatizadas en SQL Server Management Studio.