tSQLt:在SetUp和测试之间共享数据

时间:2015-03-24 11:18:53

标签: sql-server unit-testing tdd tsqlt

我正在使用tSQLt来测试t-sql代码。

测试的安排部分经常非常广泛,我试图将大量的测试推到SetUp程序中,以便在类中的测试中重用。

如果安装和测试程序能够“知道”,那将非常有用。相同的信息,即有一些共享数据。例如,假设安装程序会创建测试发票并将发票ID设置为已知的内容:

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

然后在测试中我们想对测试发票做一些事情,如下:

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

能够在两个(和更多)程序中替换重复@ TestId的值,只需将其推入某些"类变量"在SetUp过程中,然后从测试中使用它。任何想法如何以紧凑的方式实现它?我可以想象在[InvoiceManager]模式中创建一个表并在测试中从中读取它。这样的任何机会都存在,只是我无法在文档中找到它?谢谢!

2 个答案:

答案 0 :(得分:4)

一种方法是更改​​您的设置方式。而不是定义'安排'在SetUp过程中的数据,您可以在测试模式上创建一个新过程。例如,InvoiceManager.Arrange。此过程可以将@TestId作为输入参数。然后,您将从每个测试过程中调用InvoiceManager.Arrange。我经常使用这种技术,而不是使用SetUp,发现它工作得很好。即使我需要从每个测试过程中明确地调用它,我发现如果它很复杂,我可以将我的编排步骤分解为多个命名良好的存储过程。

以下是一个示例,说明我将如何解决您的问题:

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

答案 1 :(得分:2)

不要忘记你也可以利用丹尼斯的安排程序

中的输出参数

另一种稍微复杂一点的方法是利用Test Data Builder模式,这种模式在编译的代码世界中是一种长期建立的方法,但似乎不常用于数据库。

这里的原则是您创建了许多测试助手来交付创建有效密钥实体的责任。每个构建器过程应该能够创建有效对象(即行),可选地包括任何依赖性。然后,可以在许多单元测试中使用它,仅提供或检索该测试所需的值。

在下面的示例中,InvoiceBuilder将向dbo.Invoice表添加一个有效行,甚至在需要时创建一个新客户(从Invoice到Customer有一个外键)。 InvoiceBuilder然后将所有这些值作为输出。

这意味着单元测试可以创建一个或多个发票,仅提供该测试所需的详细信息和/或收集测试所需的任何结果值。

这可能看起来像很多代码,但是当你有20或30个或更多单元测试时,所有人都需要创建发票作为"安排"的一部分。这可以节省很多时间。它还增加了一个真正的优势,例如,如果我们在dbo.Invoice表中添加一个新的NOT NULL列,我们只需要重新构造InvoiceBuilder而不是umpteen测试。无可否认,tSQLt.FakeTable意味着我们可以避免某些重构,但情况并非总是如此。

与原始问题相比,我在实际测试中使用了一点艺术许可,以更好地说明我的想法。我们有一个名为dbo.InvoiceTotalOutstanding()的标量函数,它返回给定客户的所有发票的未结清总金额。这可以很容易地成为过程或视图的结果集中的列,但更容易使用标量值来演示测试。

因此,在下面的示例中,我们有[TestHelpers].[InvoiceBuilder],它将保证有效的发票行(如果需要,包括创建相关的客户行)。

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

测试数据构建器和类编排器(带输出)的这种组合是我广泛使用的模式,并且在同一组表中有大量测试可以节省我在创建和维护测试时的大量时间

几年前我在博客上写了using the Test Data Builder pattern for database unit testing