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.