不要忘记您还可以在 Dennis 的排列过程中利用输出参数
另一种稍微复杂的方法是利用 Test Data Builder 模式,这是一种在编译代码世界中建立已久的方法,但似乎不太常用于数据库。
这里的原则是您创建许多测试助手来移交创建有效密钥实体的责任。每个构建器过程都应该能够创建一个有效的对象(即行),包括可选的任何依赖项。然后可以在许多单元测试中使用它,仅提供或检索该测试所需的值。
在下面的示例中,InvoiceBuilder 将向 dbo.Invoice 表添加一个有效行,甚至在需要的地方创建一个新客户(从 Invoice 到 Customer 有一个外键)。InvoiceBuilder 然后提供所有这些值作为输出。
这意味着单元测试可以创建一个或多个发票,仅提供该测试所需的详细信息和/或收集测试所需的任何结果值。
这可能看起来像很多代码,但是当您有 20 个或 30 个或更多单元测试都需要创建发票作为“安排”步骤的一部分时,这可以节省大量时间。它还增加了一个真正的优势,例如,如果我们向 dbo.Invoice 表添加一个新的 NOT NULL 列,我们只需要重构 InvoiceBuilder 而不是无数测试。诚然,tSQLt.FakeTable
这意味着我们可以避免一些重构,但情况并非总是如此。
与原始问题相比,我在实际测试中使用了一点艺术许可,以更好地说明我的想法。我们有一个称为标量函数dbo.InvoiceTotalOutstanding()
,它返回给定客户的所有发票的未结总金额。这可以很容易地成为过程或视图的结果集中的一列,但更容易用标量值演示测试。
因此,在下面的示例中,我们[TestHelpers].[InvoiceBuilder]
将保证有效的 Invoice 行(包括在必要时创建相关的 Customer 行)。
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
我们有一个[InvoiceManagerTests].[ArrangeMultipleInvoices]
创建客户和多张发票的安排程序。
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
该类InvoiceManagerTests
有一个非常简单的 Setup 方法,它只是隔离受此测试示例影响的表。
create procedure [InvoiceManagerTests].[Setup]
as
begin
exec tSQLt.FakeTable 'dbo.Customer'
exec tSQLt.FakeTable 'dbo.Invoice'
end
go
我们的第一个测试是[Test InvoiceTotalOutstanding for all invoices]
检查在多张发票的情况下,返回的值是否正确相加。请注意,当我们调用时,[InvoiceManagerTests].[ArrangeMultipleInvoices]
我们只输入两个发票金额并收集客户 ID 作为输出,然后我们将其用作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
在我们的第二个测试中,[Test InvoiceTotalOutstanding excludes settled invoices]
我们检查总数中是否仅包含未结发票。我们提供的输入[ArrangeMultipleInvoices]
是相同的,只是我们指定其中一张发票应标记为已结算。
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
测试数据构建器和类排列器(带有输出)的这种组合是我广泛使用的一种模式,并且在同一组表周围有很多测试的情况下,我在创建和维护测试时节省了大量时间。
几年前,我在博客上写过使用测试数据构建器模式进行数据库单元测试。