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 プロシージャの「クラス変数」にプッシュするだけで、両方 (およびそれ以上) のプロシージャで重複する値を置き換えて、テストから使用できると便利です。コンパクトな方法でそれを達成する方法はありますか? [InvoiceManager] スキーマでテーブルを作成し、テストでそれを読み取ることを想像できます。ドキュメントで見つからないだけで、このようなものが存在する可能性はありますか?ありがとう!

4

2 に答える 2

2

Dennis の Arrange プロシージャで出力パラメータを活用できることも忘れないでください。

もう 1 つの少し複雑なアプローチは、コンパイル済みコードの世界では長い間確立されているアプローチですが、データベースではあまり使用されていないように見える Test Data Builder パターンを利用することです。

ここでの原則は、多数のテスト ヘルパーを作成して、有効なキー エンティティを作成する責任を引き継ぐことです。各ビルダ プロシージャは、必要に応じて任意の依存関係を含む有効なオブジェクト (行) を作成できる必要があります。これは、そのテストに必要な値のみを提供または取得する多くの単体テストで使用できます。

以下の例では、InvoiceBuilder は dbo.Invoice テーブルに有効な行を追加し、必要に応じて新しい Customer を作成します (Invoice から Customer への外部キーがあります)。InvoiceBuilder は、これらすべての値を出力として提供します。

つまり、単体テストでは、そのテストに必要な詳細のみを提供する 1 つ以上の請求書を作成したり、テストに必要な結果の値を収集したりできます。

これは最初は大量のコードのように見えるかもしれませんが、「手配」ステップの一部としてすべての請求書を作成する必要がある 20 または 30 以上の単体テストが作成される頃には、これにより多くの時間を節約できます。また、たとえば新しい NOT NULL 列を dbo.Invoice テーブルに追加した場合、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]、2 つの請求金額のみを入力し、顧客 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

2 番目のテストでは、[Test InvoiceTotalOutstanding excludes settled invoices]未払いの請求書のみが合計に含まれていることを確認します。提供する入力は[ArrangeMultipleInvoices]同じですが、請求書の 1 つを決済済としてマークする必要があることを指定する点が異なります。

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

このテスト データ ビルダとクラス アレンジャー (出力付き) の組み合わせは、私が広く使用しているパターンであり、同じテーブル セットに多数のテストがある場合、テストの作成と保守の両方で多くの時間を節約できます。

数年前に、データベースの単体テストに Test Data Builder パターンを使用することについてブログを書きました。

于 2015-03-25T10:48:10.573 に答える