Database
 sql >> Database >  >> RDS >> Database

L'arte di isolare le dipendenze e i dati nell'unità di test del database

Tutti gli sviluppatori di database scrivono più o meno unit test di database che non solo aiutano a rilevare i bug in anticipo, ma fanno anche risparmiare molto tempo e sforzi quando il comportamento imprevisto degli oggetti del database diventa un problema di produzione.

Al giorno d'oggi, esistono numerosi framework di unit test di database come tSQLt insieme a strumenti di unit test di terze parti tra cui dbForge Unit Test.

Da un lato, il vantaggio dell'utilizzo di strumenti di test di terze parti è che il team di sviluppo può creare ed eseguire istantaneamente unit test con funzionalità aggiuntive. Inoltre, l'utilizzo di un framework di test offre direttamente un maggiore controllo sugli unit test. Pertanto, puoi aggiungere più funzionalità al framework di unit test stesso. Tuttavia, in questo caso, il tuo team deve avere tempo e un certo livello di esperienza per farlo.

Questo articolo esplora alcune pratiche standard che possono aiutarci a migliorare il modo in cui scriviamo gli unit test del database.

Per prima cosa, esaminiamo alcuni concetti chiave del test delle unità di database.

Che cos'è l'unità di test del database

Secondo Dave Green, gli unit test del database assicurano che piccole unità del database, come tabelle, viste, stored procedure e così via, funzionino come previsto.

Gli unit test del database vengono scritti per verificare se il codice soddisfa i requisiti aziendali.

Ad esempio, se ricevi un requisito come "Un bibliotecario (utente finale) dovrebbe essere in grado di aggiungere nuovi libri alla biblioteca (Sistema informativo di gestione)", devi pensare di applicare degli unit test per la stored procedure per verificare se può aggiungere un nuovo libro al Libro tabella.

A volte, una serie di unit test garantisce che il codice soddisfi i requisiti. Pertanto, la maggior parte dei framework di unit test, incluso tSQLt, consente di raggruppare i relativi unit test in un'unica classe di test piuttosto che eseguire singoli test.

Principio AAA

Vale la pena menzionare il principio in 3 fasi del test unitario che è una pratica standard per scrivere test unitari. Il principio AAA è la base per i test unitari e consiste nei seguenti passaggi:

  1. Disporre/Assemblare
  2. Agire
  3. Afferma

Disponi la sezione è il primo passaggio nella scrittura di unit test del database. Guida attraverso la configurazione di un oggetto di database per il test e l'impostazione dei risultati attesi.

La legge la sezione è quando un oggetto database (in prova) viene chiamato per produrre l'output effettivo.

La affermazione step si occupa di abbinare l'output effettivo a quello previsto e verifica se il test ha superato o meno.

Esploriamo questi metodi su esempi particolari.

Se creiamo uno unit test per verificare che AddProduct stored procedure può aggiungere un nuovo prodotto, impostiamo il Prodotto e Prodotto previsto tabelle dopo l'aggiunta del prodotto. In questo caso, il metodo rientra nella sezione Disponi/Assembla.

La chiamata alla procedura AddProduct e l'inserimento del risultato nella tabella Product è trattata dalla sezione Act.

La parte Assert abbina semplicemente la tabella Product con la tabella ExpectedProduct per vedere se la procedura memorizzata è stata eseguita correttamente o non è riuscita.

Capire le dipendenze nei test unitari

Finora abbiamo discusso le basi del test unitario del database e l'importanza del principio AAA (Assemble, Act e Assert) durante la creazione di un test unitario standard.

Ora, concentriamoci su un altro pezzo importante del puzzle:le dipendenze negli unit test.

Oltre a seguire il principio AAA e concentrarci solo su un particolare oggetto di database (in fase di test), abbiamo anche bisogno di conoscere le dipendenze che possono influenzare gli unit test.

Il modo migliore per comprendere le dipendenze è guardare un esempio di unit test.

Impostazione database di esempio dipendenti

Per andare avanti, crea un database di esempio e chiamalo EmployeesSample :

-- Create the Employees sample database to demonstrate unit testing

CREATE DATABASE EmployeesSample;
GO

Ora crea il Dipendente tabella nel database di esempio:

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

Popolazione dei dati di esempio

Popolare la tabella aggiungendo alcuni record:

-- 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 tabella si presenta così:

-- View the Employee table

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

Tieni presente che in questo articolo sto usando dbForge Studio per SQL Server. Pertanto, l'aspetto dell'output potrebbe differire se si esegue lo stesso codice in SSMS (SQL Server Management Studio). Non c'è differenza quando si tratta di script e dei loro risultati.

Requisito per aggiungere un nuovo dipendente

Ora, se è stato ricevuto un requisito per aggiungere un nuovo dipendente, il modo migliore per soddisfare il requisito è creare una stored procedure in grado di aggiungere correttamente un nuovo dipendente alla tabella.

A tale scopo, creare la stored procedure AddEmployee come segue:

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

Unit Test per verificare se il requisito è soddisfatto

Scriveremo uno unit test del database per verificare se la stored procedure AddEmployee soddisfa i requisiti per aggiungere un nuovo record alla tabella Employee.

Concentriamoci sulla comprensione della filosofia degli unit test simulando un codice di unit test piuttosto che scrivere uno unit test con un framework di test o uno strumento di unit test di terze parti.

Simulare il test unitario e applicare il principio AAA in SQL

La prima cosa che dobbiamo fare è imitare il principio AAA in SQL poiché non utilizzeremo alcun framework di unit test.

La sezione Assembla viene applicata quando le tabelle effettive e previste vengono normalmente impostate insieme alla tabella prevista che viene popolata. Possiamo utilizzare le variabili SQL per inizializzare la tabella prevista in questo passaggio.

La sezione Act viene utilizzata quando viene chiamata la procedura memorizzata effettiva per inserire dati nella tabella effettiva.

La sezione Assert è quando la tabella prevista corrisponde alla tabella effettiva. La simulazione della parte Assert è un po' complicata e può essere ottenuta con i seguenti passaggi:

  • Conteggio delle righe comuni (corrispondenti) tra due tabelle che dovrebbero essere 1 (poiché la tabella prevista ha un solo record che dovrebbe corrispondere alla tabella effettiva)
  • L'esclusione dei record di tabella effettivi dai record di tabella previsti dovrebbe essere uguale a 0 (se il record nella tabella prevista esiste anche nella tabella effettiva, l'esclusione di tutti i record di tabella effettivi dalla tabella prevista dovrebbe restituire 0)

Lo script SQL è il seguente:

[expand title=”Codice”]

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

[/espandi]

Esecuzione del test unitario simulato

Dopo aver creato la procedura memorizzata, eseguirla con lo unit test simulato:

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

L'output è il seguente:

Congratulazioni! Il test dell'unità di database è stato superato.

Identificazione dei problemi sotto forma di dipendenze in Unit Test

Possiamo rilevare qualcosa di sbagliato nello unit test che abbiamo creato nonostante sia stato scritto ed eseguito correttamente?

Se osserviamo da vicino l'impostazione dell'unità di test (la parte Assemble), la tabella prevista ha un'associazione non necessaria con la colonna identity:

Prima di scrivere uno unit test abbiamo già aggiunto 5 record alla tabella effettiva (Impiegato). Pertanto, durante l'impostazione del test, la colonna dell'identità per la tabella prevista inizia con 6. Tuttavia, ciò significa che nella tabella effettiva (Dipendente) ci aspettiamo sempre che 5 record corrispondano alla tabella prevista (#EmployeeExpected).

Per capire in che modo ciò può influire sul test unitario, diamo un'occhiata alla tabella effettiva (Dipendente):

Aggiungi un altro record alla tabella Dipendente:

-- Adding a new record to the Employee table

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

Dai un'occhiata alla tabella Dipendenti ora:

Elimina EmpoyeeId 6 (Adil) in modo che lo unit test possa essere eseguito sulla propria versione di EmployeeId 6 (Adil) anziché sul record precedentemente archiviato.

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

DELETE FROM Employee
  WHERE EmployeeId=6

Esegui lo unit test simulato e guarda i risultati:

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

EXEC TestAddEmployee

Il test questa volta è fallito. La risposta si trova nel set di risultati della tabella Dipendenti come mostrato di seguito:

Il binding dell'ID dipendente nello unit test di cui sopra non funziona quando rieseguiamo lo unit test dopo aver aggiunto un nuovo record ed eliminato il record del dipendente aggiunto in precedenza.

Ci sono tre tipi di dipendenze nel test:

  1. Dipendenza dai dati
  2. Dipendenza dai vincoli chiave
  3. Dipendenza colonna identità

Dipendenza dai dati

Prima di tutto, questo unit test dipende dai dati nel database. Secondo Dave Green, quando si tratta del database di unit test, i dati stessi sono una dipendenza.

Ciò significa che il test dell'unità del database non dovrebbe basarsi sui dati nel database. Ad esempio, il tuo unit test dovrebbe contenere i dati effettivi da inserire nell'oggetto database (tabella) piuttosto che basarsi sui dati già esistenti nel database che possono essere eliminati o modificati.

Nel nostro caso, il fatto che cinque record siano già stati inseriti nella tabella Dipendente effettiva è una dipendenza dai dati che deve essere prevenuta perché non dovremmo violare la filosofia dello unit test che dice che viene testata solo l'unità del codice.

In altre parole, i dati del test non dovrebbero basarsi sui dati effettivi nel database.

Dipendenza dai vincoli chiave

Un'altra dipendenza è una dipendenza dal vincolo della chiave, il che significa che anche la colonna della chiave primaria EmployeeId è una dipendenza. Deve essere prevenuto per scrivere un buon unit test. Tuttavia, è necessario uno unit test separato per testare un vincolo di chiave primaria.

Ad esempio, per testare la procedura memorizzata AddEmployee, la chiave primaria della tabella Employee deve essere rimossa in modo che un oggetto possa essere testato senza doversi preoccupare di violare una chiave primaria.

Dipendenza colonna identità

Proprio come un vincolo di chiave primaria, anche la colonna Identity è una dipendenza. Pertanto, non è necessario testare la logica di incremento automatico della colonna di identità per la procedura AddEmployee; deve essere evitato ad ogni costo.

Isolamento delle dipendenze negli unit test

Possiamo prevenire tutte e tre le dipendenze rimuovendo temporaneamente i vincoli dalla tabella e quindi non dipendiamo dai dati nel database per lo unit test. Ecco come vengono scritti gli unit test standard del database.

In questo caso, ci si potrebbe chiedere da dove provengono i dati per la tabella Employee. La risposta è che la tabella viene popolata con i dati di test definiti nello unit test.

Modifica della procedura memorizzata del test dell'unità

Ora rimuoviamo le dipendenze nel nostro unit test:

[expand title=”Codice”]

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

[/espandi]

Esecuzione del test unitario simulato senza dipendenze

Esegui lo unit test simulato per vedere i risultati:

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

EXEC TestAddEmployee

Eseguire nuovamente lo unit test per controllare la procedura memorizzata AddEmployee:

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

EXEC TestAddEmployee

Congratulazioni! Le dipendenze dallo unit test sono state rimosse correttamente.

Ora, anche se aggiungiamo un nuovo record o un insieme di nuovi record alla tabella Employee, ciò non influirà sul nostro unit test poiché abbiamo rimosso i dati e le dipendenze dai vincoli dal test con successo.

Creazione di unit test di database utilizzando tSQLt

Il passaggio successivo consiste nel creare un vero e proprio unit test di database basato sullo unit test simulato.

Se stai usando SSMS (SQL Server Management Studio) dovrai installare il framework tSQLt, creare una classe di test e abilitare CLR prima di scrivere ed eseguire lo unit test.

Se si utilizza dbForge Studio per SQL Server è possibile creare lo unit test facendo clic con il pulsante destro del mouse sulla procedura memorizzata AddEmployee e quindi facendo clic su "Unit Test" => "Aggiungi nuovo test..." come mostrato di seguito:

Per aggiungere un nuovo test, inserisci le informazioni sul test dell'unità richieste:

Per scrivere lo unit test, utilizzare il seguente 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

Quindi, esegui lo unit test del database:

Congratulazioni! Abbiamo creato ed eseguito con successo un test unitario del database che è privo di dipendenze.

Cose da fare

Questo è tutto. Dopo aver esaminato questo articolo, sei pronto per isolare le dipendenze dagli unit test di database e creare unit test di database privi di dipendenze di dati e vincoli. Di conseguenza, puoi migliorare le tue abilità eseguendo le seguenti cose:

  1. Prova ad aggiungere la procedura memorizzata Elimina dipendente e crea uno unit test di database simulato per Elimina dipendente con dipendenze per vedere se non riesce in determinate condizioni
  2. Prova ad aggiungere la procedura memorizzata Elimina dipendente e crea un test unitario del database privo di dipendenze per vedere se un dipendente può essere eliminato
  3. Prova ad aggiungere la procedura memorizzata Cerca dipendente e crea uno unit test di database simulato con dipendenze per vedere se è possibile cercare un dipendente
  4. Prova ad aggiungere la procedura memorizzata Cerca dipendente e crea uno unit test di database privo di dipendenze per vedere se è possibile cercare un dipendente
  5. Si prega di provare requisiti più complessi creando procedure memorizzate per soddisfare i requisiti e quindi scrivendo unit test del database privi di dipendenze per vedere se superano il test o falliscono. Tuttavia, assicurati che il test sia ripetibile e incentrato sul test dell'unità del codice

Strumento utile:

dbForge Unit Test:una GUI intuitiva e conveniente per l'implementazione di unit test automatizzati in SQL Server Management Studio.