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 パターンを使用することについてブログを書きました。