3

我正在使用 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 过程中的某个“类变量”中,然后在测试中使用它,就能够在两个(以及更多)过程中替换重复的 @TestId 值,这将是一件好事。任何想法如何以紧凑的方式实现它?我可以想象在 [InvoiceManager] 模式中创建一个表并在测试中读取它。任何可能存在这样的事情只是我在文档中找不到它?谢谢!

4

2 回答 2

6

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

这是一个示例来说明我将如何解决您的问题:

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
于 2015-03-24T13:14:43.117 回答
2

不要忘记您还可以在 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

测试数据构建器和类排列器(带有输出)的这种组合是我广泛使用的一种模式,并且在同一组表周围有很多测试的情况下,我在创建和维护测试时节省了大量时间。

几年前,我在博客上写过使用测试数据构建器模式进行数据库单元测试

于 2015-03-25T10:48:10.573 回答