2015-03-24 1 views
1

J'utilise tSQLt pour tester le code t-sql. Très souvent, la partie Arrangement des tests est assez étendue et j'essaie d'en faire passer une grande partie à la procédure SetUp pour la réutiliser parmi les tests de la classe.tSQLt: partage de données entre SetUp et les tests

Il serait très utile que les procédures de configuration et de test puissent "connaître" la même information, c'est-à-dire avoir des données partagées. Par exemple, supposons que la configuration crée une facture de test et fixe ID facture à quelque chose de connu:

CREATE PROCEDURE [InvoiceManager].[SetUp] 
AS 
    DECLARE @TestId INT = 10; 

    EXEC tsqlt.FakeTable @SchemaName='dbo', @TableName='Invoice'; 
    INSERT INTO dbo.Invoice (Id, Amount) VALUES (@TestId, 20.50); 
GO 

Et puis dans le test que nous voulons faire quelque chose à la facture de test, comme ceci:

CREATE PROCEDURE [InvoiceManager].[Test_InvoiceHandler] 
AS 
    DECLARE @TestId INT = 10; -- duplication I would like to eliminate 

    -- Action 
    EXEC dbo.InvoiceHandler @InvoiceId = @TestId; 

    -- Assert 
    -- ... some assertions 
GO 

Serait agréable de remplacer la duplication de la valeur de @ TestId dans les deux procédures (et plus) en le poussant simplement dans une certaine "variable de classe" dans la procédure SetUp, puis de l'utiliser à partir des tests. Des idées pour y parvenir de manière compacte? Je pourrais imaginer créer une table dans le schéma [InvoiceManager] et en lire des tests. Y a-t-il une chance que quelque chose comme ça existe juste que je ne peux pas le trouver dans le docu? Merci!

Répondre

4

Une approche serait de changer la façon dont vous faites votre installation. Au lieu de définir les données «Réorganiser» dans la procédure SetUp, vous pouvez créer une nouvelle procédure sur votre schéma de test. Par exemple, InvoiceManager.Arrange. Cette procédure pourrait prendre votre @TestId comme paramètre d'entrée. Vous appelez ensuite InvoiceManager.Arrange de chacune de vos procédures de test. J'utilise cette technique assez souvent au lieu d'utiliser SetUp et je trouve que ça marche plutôt bien. Même si je dois l'appeler explicitement à partir de chaque procédure de test, je trouve que je peux décomposer mon étape d'organisation en plusieurs procédures stockées bien nommées si elle est complexe.

Voici un exemple pour illustrer comment je résoudre votre question:

CREATE PROCEDURE [InvoiceManager].[Arrange] 
    @TestId INT 
AS 
    EXEC tsqlt.FakeTable @SchemaName='dbo', @TableName='Invoice'; 
    INSERT INTO dbo.Invoice (Id, Amount) VALUES (@TestId, 20.50); 
GO 

CREATE PROCEDURE [InvoiceManager].[Test_InvoiceHandler] 
AS 
    DECLARE @TestId INT = 10; 
    EXEC InvoiceManager.Arrange @TestId; 

    -- Action 
    EXEC dbo.InvoiceHandler @InvoiceId = @TestId; 

    -- Assert 
    -- ... some assertions 
GO 
2

N'oubliez pas que vous pouvez également tirer parti des paramètres de sortie dans Organiser la procédure de Dennis

Une autre approche un peu plus impliqué est de prendre Avantage du modèle Test Data Builder qui est une approche établie de longue date dans le monde du code compilé, mais qui semble moins utilisé pour les bases de données. Le principe ici est que vous créez un certain nombre d'aides de test pour transférer la responsabilité de la création d'entités clés valides. Chaque procédure de générateur devrait être capable de créer un objet valide (c'est-à-dire une ligne) incluant, éventuellement, n'importe quelles dépendances. Ceci peut ensuite être utilisé pour de nombreux tests unitaires fournissant ou récupérant uniquement les valeurs requises pour ce test. Dans mon exemple ci-dessous, InvoiceBuilder ajoutera une ligne valide à la table dbo.Invoice, créant même un nouveau client si nécessaire (il existe une clé étrangère de Invoice to Customer). InvoiceBuilder fournit ensuite toutes ces valeurs en tant que sorties.

Cela signifie qu'un test unitaire peut créer une ou plusieurs factures ne fournissant que les informations nécessaires pour ce test et/ou collecter toutes les valeurs résultantes requises pour le test. Cela peut ressembler à beaucoup de code au début, mais au moment où vous avez 20 ou 30 tests unitaires ou plus qui ont tous besoin de créer des factures dans le cadre de l'étape "arranger" cela peut faire gagner beaucoup de temps . Cela ajoute également un réel avantage où si, par exemple, nous ajoutons une nouvelle colonne NOT NULL à la table dbo.Invoice, nous avons seulement besoin de refacturer InvoiceBuilder et non de multiplier les tests. Certes, tSQLt.FakeTable signifie que nous pouvons être en mesure d'éviter une partie de ce refactoring, mais ce n'est pas toujours le cas.

J'ai utilisé une petite licence artistique en termes de tests réels par rapport à la question originale pour mieux illustrer mes pensées. Nous avons une fonction scalaire appelée dbo.InvoiceTotalOutstanding() qui renvoie le montant total impayé pour toutes les factures pour un client donné. Cela pourrait tout aussi bien être une colonne dans le jeu de résultats d'une procédure ou d'une vue, mais il est plus facile de démontrer les tests avec une valeur scalaire. Dans l'exemple ci-dessous, nous avons [TestHelpers].[InvoiceBuilder] qui garantira une ligne de facture valide (y compris la création de la ligne client dépendante si nécessaire).

create procedure [TestHelpers].[InvoiceBuilder] 
(
    @InvoiceDate datetime = null out 
, @InvoiceName varchar(max) = null out 
, @InvoiceAmount decimal(18,4) = null out 
, @InvoiceIsSettled bit = null out 
, @CustomerId int = null out 
, @InvoiceId int = null out 
, @DoBuildDependencies bit = 1 
) 
as 
begin 
    --! If an Invoice ID has been supplied and exists just return those values 
    if exists (select 1 from dbo.Invoice where InvoiceId = @InvoiceId) 
     begin 
      select 
        @InvoiceDate = InvoiceDate 
       , @InvoiceName = InvoiceName 
       , @InvoiceAmount = InvoiceAmount 
       , @InvoiceIsSettled = InvoiceIsSettled 
       , @CustomerId = CustomerId 
      from 
       dbo.Invoice 
      where 
       InvoiceId = @InvoiceId 

      goto EndEx; 
     end 

    --! If we get here, there is no invoice so create one making sure any required values are valid 

    --! Always use the supplied values where present 
    set @InvoiceDate = coalesce(@InvoiceDate, '20101010 10:10:10') ; -- use some standard fixed date 
    set @InvoiceName = coalesce(@InvoiceName, '') -- use the simplest value to meet any domain constraints 
    set @InvoiceAmount = coalesce(@InvoiceAmount, 1.0) -- use the simplest value to meet any domain constraints 
    set @InvoiceIsSettled = coalesce(@InvoiceIsSettled, 0) ; 

    --! We use other Test Data Builders to create any dependencies 
    if @DoBuildDependencies = 1 
     begin 
      --! CustomerBuilder will ensure that the specified customer exists 
      --! or create one if @CustomerId is not specified or present. 
      --! Use an output parameter to ensure @CustomerId is valid 
      exec TestDataBuilders.CustomerBuilder @CustomerId = @CustomerId out ; 
     end 

    --! Now we are ready to create our new invoice with a set of valid values 
    --! NB: For this example we assume that the real Invoice.InvoiceId has IDENTITY() property 

    --! At this point in the code, we don't know whether we are inserting to the real table 
    --! which auto-increments or a mocked table created with tSQLt.FakeTable without IDENTITY 
    if objectproperty(object_id(N'[dbo].[Invoice]'), N'TableHasIdentity') = 1 
     begin 
      insert dbo.Invoice 
      (
       InvoiceDate 
      , InvoiceName 
      , InvoiceAmount 
      , InvoiceIsSettled 
      , CustomerId 
      ) 
      values 
      (
       @InvoiceDate 
      , @InvoiceName 
      , @InvoiceAmount 
      , @InvoiceIsSettled 
      , @CustomerId 
      ) 

      set @InvoiceId = scope_identity(); 
     end 
    else 
     begin 
      --! Get a valid Invoice ID that isn't already in use 
      set @InvoiceId = coalesce(@InvoiceId, (select max (InvoiceId) from dbo.Invoice) + 1, 1); 

      insert dbo.Invoice 
      (
       InvoiceId 
      , InvoiceDate 
      , InvoiceName 
      , InvoiceAmount 
      , InvoiceIsSettled 
      , CustomerId 
      ) 
      values 
      (
       @InvoiceId 
      , @InvoiceDate 
      , @InvoiceName 
      , @InvoiceAmount 
      , @InvoiceIsSettled 
      , @CustomerId 
      ) 
     end 

--///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 
EndEx: 
--///////////////////////////////////////////////////////////////////////////////////////////////////////////////////// 

    return; 
end 
go 

Nous avons une procédure qui crée Arranger [InvoiceManagerTests].[ArrangeMultipleInvoices] un client et plusieurs factures.

create procedure [InvoiceManagerTests].[ArrangeMultipleInvoices] 
(
    @CustomerId int = null out 
, @InvoiceIdA int = null out 
, @InvoiceDateA datetime = null out 
, @InvoiceNameA varchar(max) = null out 
, @InvoiceAmountA decimal(18,4) = null out 
, @InvoiceIsSettledA bit = null out 
, @InvoiceIdB int = null out 
, @InvoiceDateB datetime = null out 
, @InvoiceNameB varchar(max) = null out 
, @InvoiceAmountB decimal(18,4) = null out 
, @InvoiceIsSettledB bit = null out 
) 
as 
begin 
    --! Create/validate our Customer 
    exec TestDataBuilders.CustomerBuilder @CustomerId = @CustomerId out ; 

    --! Create the Invoices 
    --! Using the Test Data Builder pattern means that our tests only need to specify 
    --! the values of interest 
    exec TestHelpers.InvoiceBuilder 
      @InvoiceDate = @InvoiceDateA out 
     , @InvoiceName = @InvoiceNameA out 
     , @InvoiceAmount = @InvoiceAmountA out 
     , @InvoiceIsSettled = @InvoiceIsSettledA out 
     , @CustomerId = @CustomerIdA out 
     , @InvoiceId = @InvoiceIdA out 

    exec TestHelpers.InvoiceBuilder 
      @InvoiceDate = @InvoiceDateB out 
     , @InvoiceName = @InvoiceNameB out 
     , @InvoiceAmount = @InvoiceAmountB out 
     , @InvoiceIsSettled = @InvoiceIsSettledB out 
     , @CustomerId = @CustomerIdB out 
     , @InvoiceId = @InvoiceIdB out 
end 
go 

La classe InvoiceManagerTests a une méthode de configuration très simple qui isole uniquement les tables affectées par cet exemple de test.

create procedure [InvoiceManagerTests].[Setup] 
as 
begin 
    exec tSQLt.FakeTable 'dbo.Customer' 
    exec tSQLt.FakeTable 'dbo.Invoice' 
end 
go 

Notre premier test, [Test InvoiceTotalOutstanding for all invoices] vérifie que dans le cas de plusieurs factures la valeur retournée est sommée correctement. Notez que lorsque nous appelons le [InvoiceManagerTests].[ArrangeMultipleInvoices], nous saisissons uniquement les deux montants de la facture et collectons l'ID client en tant que sortie que nous utilisons ensuite comme entrée pour la fonction dbo.InvoiceTotalOutstanding().

create procedure [InvoiceManagerTests].[Test InvoiceTotalOutstanding for all invoices] 
as 
begin 
    --! To test that Invoice values are correctly aggregated 
    --! we only need to specify each invoice value and let 
    --! [InvoiceManagerTests].[ArrangeMultipleInvoices] take care of the rest 

    --! Arrange 
    declare @CustomerId int 
    declare @InvoiceAmountA decimal(18,4) = 5.50; 
    declare @InvoiceAmountB decimal(18,4) = 6.70; 
    --! Expected value should be Amount A + Amount B 
    declare @ExpectedInvoiceAmount decimal(18,4) = 12.20; 

    exec InvoiceManagerTests.ArrangeMultipleInvoices 
      @CustomerId = @CustomerId out 
     , @InvoiceAmountA = @InvoiceAmountA out 
     , @InvoiceAmountB = @InvoiceAmountB out 

    --! Act 
    declare @ActualValue decimal(18,2) = dbo.InvoiceTotalOutstanding(@CustomerId) 

    --! Assert that InvoiceTotalOutstanding column returned by module 
    --! matches the expected values 
    exec tSQLt.AssertEquals @ExpectedInvoiceAmount, @ActualValue ; 
end 
go 

Dans notre second test, nous vérifions [Test InvoiceTotalOutstanding excludes settled invoices] inclus dans le total des factures en suspens. Les entrées que nous fournissons à [ArrangeMultipleInvoices] sont les mêmes sauf que nous spécifions que l'une des factures doit être marquée comme réglée.

create procedure [InvoiceManagerTests].[Test InvoiceTotalOutstanding excludes settled invoices] 
as 
begin 
    --! To test that Invoice Total excludes Settled invoices 
    --! we only need to specify each invoice value and set one invoice as Settled 
    --! then let [InvoiceManagerTests].[ArrangeMultipleInvoices] take care of the rest 

    --! Arrange 
    declare @CustomerId int 
    declare @InvoiceAmountA decimal(18,4) = 5.50; 
    declare @InvoiceAmountB decimal(18,4) = 6.70; 
    --! Expected value should be Amount A only as Invoice B is Settled 
    declare @ExpectedInvoiceAmount decimal(18,4) = 5.5; 

    exec InvoiceManagerTests.ArrangeMultipleInvoices 
      @CustomerId = @CustomerId out 
     , @InvoiceAmountA = @InvoiceAmountA out 
     , @InvoiceAmountB = @InvoiceAmountB out 
     , @InvoiceIsSettledB = 1 

    --! Act 
    declare @ActualValue decimal(18,2) = dbo.InvoiceTotalOutstanding(@CustomerId) 

    --! Assert that InvoiceTotalOutstanding column returned by module 
    --! matches the expected values 
    exec tSQLt.AssertEquals @ExpectedInvoiceAmount, @ActualValue ; 
end 
go 

Cette combinaison des constructeurs de données de test et arrangeurs de classe (avec des sorties) est un modèle que je l'utilise beaucoup et où il y a beaucoup de tests dans le même ensemble de tables me permet de gagner beaucoup de temps à la fois dans la création et maintenir mes tests.

J'ai blogué sur using the Test Data Builder pattern for database unit testing il y a quelques années.