既存の自動化テスト フレームワークに新しいタイプの自動化テストが追加されました。この新しいタイプは Spec と呼ばれています。「Spec」とは、ビヘイビア駆動開発 (BDD) 手法に従って構築されたテストを表す用語です。これは Web 開発のテストで非常によく使用される手法であり、Unreal Engine の C++ フレームワークに導入されました。
Spec での記述をお勧めするのには次のような理由があります。
- 自己文書化コードである
- 流れるように (fluent) 記述でき、より DRY なコードになることが多い
DRY (繰り返しを避ける) - スレッド化されたテスト コードや Latent テスト コードを記述するのが格段に容易である
- エクスペクテーション (テスト) を分離できる
- ほとんどの種類のテスト (機能、統合、ユニット) に使用できる
Spec を設定する方法
Spec のヘッダを定義する方法には 2 通りあり、どちらもテスト タイプの定義に従来使用している方法に似ています。
最も簡単な方法は、その他すべてのテスト定義マクロとまったく同じパラメータを取る DEFINE_SPEC マクロを使用することです。
DEFINE_SPEC(MyCustomSpec, "MyGame.MyCustomSpec", EAutomationTestFlags::ProductFilter | EAutomationTestFlags::ApplicationContextMask)
void MyCustomSpec::Define()
{
//@todo write my expectations here
}
その他の方法としては、BEGIN_DEFINE_SPEC マクロと END_DEFINE_SPEC マクロを使用することだけです。これらのマクロを使用すると、テストの一部として独自のメンバーを定義できます。次のセクションで説明しますが、このポインターを使って相対的に内容を指定すると便利です。
BEGIN_DEFINE_SPEC(MyCustomSpec, "MyGame.MyCustomSpec", EAutomationTestFlags::ProductFilter | EAutomationTestFlags::ApplicationContextMask)
TSharedPtr<FMyAwesomeClass> AwesomeClass;
END_DEFINE_SPEC(MyCustomSpec)
void MyCustomSpec::Define()
{
//@todo write my expectations here
}
その他に注意が必要なことは、他のテスト タイプのように RunTests() メンバーを使用するのではなく、Spec クラスの Define() メンバーの実装を記述する必要があるということだけです。
Spec を定義するファイルは拡張子を .spec.cpp とし、名前に「Test」という語は含めません。たとえば、FItemCatalogService クラスであれば、ItemCatalogService.h、ItemCatalogService.cpp、ItemCatalogService.spec.cpp といったファイルを使用します。
これは推奨されるガイドラインであり、技術的な制限ではありません。
エクスペクテーションを定義する方法
BDD の大半は、個々の実装のテストではなく、パブリック API のエクスペクテーションのテストを行います。これにより、テストの堅牢性が大幅に向上するため、保守が容易になります。また、同じ API の複数の実装が作成されても、問題なく動作する可能性が高まります。
Spec では、2 つの主要な関数、Describe() と It() を使用してエクスペクテーションを定義します。
Describe
Describe() は、より読みやすく、より DRY なものになるように、複雑なエクスペクテーションの内容を指定する手段として使用されます。後述しますが、Describe() を使用すると、BeforeEach() や AfterEach() などの他の補助関数とのやり取りに基づいて、より DRY なコードを作成できます。
void Describe(const FString& Description, TFunction<void()> DoWork)
Describe() は、テストに含まれるエクスペクテーションのスコープを記述する文字列と、そのエクスペクテーションを定義するラムダを取ります。
Describe() は、Describe() の中に他の Describe() をネストしてカスケードできます。
Describe() はテストではなく、実際のテスト中には実行されないことに注意してください。Spec 内でエクスペクテーション (またはテスト) が初めて定義されるときに一度だけ実行されます。
It
It() は、Spec での実際のエクスペクテーションを定義するコードです。It() は、ルートの Define() メソッドから呼び出すことも、任意の Describe() ラムダ内から呼び出すこともできます。It() は、エクスペクテーションをアサートするためだけに使用することが理想的ですが、テストするシナリオの最終設定に使用することもできます。
通常、It() を呼び出す記述文字列は「should」という単語で始めるのがベスト プラクティスです。これは「it should ~ (~であるはずである)」という意味になります。
基本的なエクスペクテーションを定義する
次の例では、前述の関数を組み合わせて、非常にシンプルなエクスペクテーションを定義しています。
BEGIN_DEFINE_SPEC(MyCustomSpec, "MyGame.MyCustomClass", EAutomationTestFlags::ProductFilter | EAutomationTestFlags::ApplicationContextMask)
TSharedPtr<FMyCustomClass> CustomClass;
END_DEFINE_SPEC(MyCustomSpec)
void MyCustomSpec::Define()
{
Describe("Execute()", [this]()
{
It("should return true when successful", [this]()
{
TestTrue("Execute", CustomClass->Execute());
});
It("should return false when unsuccessful", [this]()
{
TestFalse("Execute", CustomClass->Execute());
});
});
}
特に、複数のエクスペクテーションを結合することなく、プログラマーがエクスペクテーションを正しく記述する場合は、ご覧のように、自己文書化されたテストを作成できます。これは、すべての Describe() 呼び出しと It() 呼び出しを組み合わせると、おおよそ人間が読める文になることを想定しています。たとえば、次のようになります。
Execute() should return true when successful
Execute() should return false when unsuccessful
以下に示しているのは、現在の自動化テストの UI に表示されるような、より複雑な Spec の例です。
この例では、Driver、Element、Click はそれぞれ Describe() 呼び出しであり、It() 呼び出しによってさまざまな「should...」メッセージが定義されています。
それぞれの It() 呼び出しが、実行される個々のテストになります。したがって、テストのいずれか 1 つだけが失敗する場合は、そのテストを個別に実行できます。これにより、テストをデバッグする手間が軽減されるため、テストの保守が容易になります。また、各テストは自己文書化され、独立しているため、テストの 1 つが失敗した場合にテスト レポートを参照すれば、単に Core という非常に大きなバケットが失敗したというのではなく、より具体的に何に問題があるのかを把握することもできます。そのため、適切な担当者に問題を速やかにアサインでき、問題の調査にかかる時間が短縮されます。
そして、上記のテストのいずれかをクリックすると、そのテストが定義されている It() 文に直接移動できます。
Spec のエクスペクテーションがテストに変換されるしくみ
ここでは詳しく説明します。説明は細かくなりますが、Spec テスト タイプの基本的な動作を理解しておくと、この後で紹介する複雑な機能の一部については理解しやすくなるでしょう。
Spec テスト タイプでは、ルートの Define() 関数が、必要になったときに初めて、一度だけ実行されます。これが実行されると、全ての Describe ではないラムダを収集します。Define() が終了すると、収集したラムダやコード ブロックをすべて調べて、それぞれの It() に対して Latent コマンドの配列を生成します。
したがって、すべての BeforeEach()、It() と、AfterEach() ラムダ コード ブロックが、単一のテスト用の一連の実行にまとめられます。特定のテストを実行するように求められると、Spec テスト タイプは、その特定のテストのすべてのコマンドをキューに入れて実行に備えます。キューに入れられると、各ブロックは、前のブロックから実行終了の通知があるまで待機します。
その他の機能
Spec テスト タイプには、複雑なテストの記述を容易にする機能が他にもいくつかあります。それらを使用すると、強力でありながら扱いづらい、自動化テスト フレームワークの Latent コマンド システムを直接使用する必要がなくなります。
Spec テスト タイプでサポートされている、より複雑なシナリオで役立つ機能の一覧を次に示します。
BeforeEach と AfterEach
BeforeEach() と AfterEach() は、ごく基本的な Spec 以上のコードを記述するためのコア関数です。BeforeEach() を使用すると、後続の It() コードが実行される前にコードを実行できます。同様に、AfterEach() は It() コードが実行された後にコードを実行します。
各「テスト」は、単一の It() 呼び出しのみで構成されていることに注意してください。
次に例を示します。
BEGIN_DEFINE_SPEC(AutomationSpec, "System.Automation.Spec", EAutomationTestFlags::SmokeFilter | EAutomationTestFlags::ApplicationContextMask)
FString RunOrder;
END_DEFINE_SPEC(AutomationSpec)
void AutomationSpec::Define()
{
Describe("A spec using BeforeEach and AfterEach", [this]()
{
BeforeEach([this]()
{
RunOrder = TEXT("A");
});
It("will run code before each spec in the Describe and after each spec in the Describe", [this]()
{
TestEqual("RunOrder", RunOrder, TEXT("A"));
});
AfterEach([this]()
{
RunOrder += TEXT("Z");
TestEqual("RunOrder", RunOrder, TEXT("AZ"));
});
});
}
この例では、BeforeEach()、It()、AfterEach() の順に定義されているため、コード ブロックは上から下の順序で実行されます。 必須ではありませんが、呼び出しのこの論理的な順序を維持することをお勧めします。ただし、上記の 3 つの呼び出しの順序を変えても、結果としては常に同じテストが生成されます。
また、上記の例では、AfterEach() でエクスペクテーションをチェックしていますが、これは非常に変則的であり、Spec テスト タイプ自体をテストすることに伴う副作用です。そのため、クリーンアップ以外で AfterEach() を使用することはお勧めしません。
また、複数の BeforeEach() と AfterEach() を呼び出すこともできます。その場合は、定義されている順序で呼び出されます。最初の BeforeEach() 呼び出しは、2 つ目の BeforeEach() 呼び出しの前に実行されます。同様に、fterEach() は、最初の呼び出しが実行された後に、後続の呼び出しが実行されます。
BeforeEach([this]()
{
RunOrder = TEXT("A");
});
BeforeEach([this]()
{
RunOrder += TEXT("B");
});
It("will run code before each spec in the Describe and after each spec in the Describe", [this]()
{
TestEqual("RunOrder", RunOrder, TEXT("AB"));
});
AfterEach([this]()
{
RunOrder += TEXT("Y");
TestEqual("RunOrder", RunOrder, TEXT("ABY"));
});
AfterEach([this]()
{
RunOrder += TEXT("Z");
TestEqual("RunOrder", RunOrder, TEXT("ABYZ"));
});
さらに、BeforeEach() と AfterEach() は、呼び出し元の Describe() のスコープによって範囲が制限されます。どちらも、呼び出し元のスコープ内にある It() 呼び出しに対してのみ実行されます。
以下の複雑な例では、呼び出しの順序が推奨どおりではありませんが、すべて正しく機能します。
BEGIN_DEFINE_SPEC(AutomationSpec, "System.Automation.Spec", EAutomationTestFlags::SmokeFilter | EAutomationTestFlags::ApplicationContextMask)
FString RunOrder;
END_DEFINE_SPEC(AutomationSpec)
void AutomationSpec::Define()
{
Describe("A spec using BeforeEach and AfterEach", [this]()
{
BeforeEach([this]()
{
RunOrder = TEXT("A");
});
AfterEach([this]()
{
RunOrder += TEXT("Z");
// 結果は
// TestEqual("RunOrder", RunOrder, TEXT("ABCYZ"));
// または this になります (どちらの It() が実行されているかに応じて)
// TestEqual("RunOrder", RunOrder, TEXT("ABCDXYZ"));
});
BeforeEach([this]()
{
RunOrder += TEXT("B");
});
Describe("while nested inside another Describe", [this]()
{
AfterEach([this]()
{
RunOrder += TEXT("Y");
});
It("will run all BeforeEach blocks and all AfterEach blocks", [this]()
{
TestEqual("RunOrder", RunOrder, TEXT("ABC"));
});
BeforeEach([this]()
{
RunOrder += TEXT("C");
});
Describe("while nested inside yet another Describe", [this]()
{
It("will run all BeforeEach blocks and all AfterEach blocks", [this]()
{
TestEqual("RunOrder", RunOrder, TEXT("ABCD"));
});
AfterEach([this]()
{
RunOrder += TEXT("X");
});
BeforeEach([this]()
{
RunOrder += TEXT("D");
});
});
});
});
}
AsyncExecution
Spec テスト タイプを使用すると、単一のコード ブロックがどのように実行されるかを簡単に定義できます。オーバーロードされたバージョンの BeforeEach()、It()、AfterEach() に適切な EAsyncExecution タイプを渡すだけです。
次に例を示します。
BeforeEach(EAsyncExecution::TaskGraph, [this]()
{
// 何かを設定します
));
It("should do something awesome", EAsyncExecution::ThreadPool, [this]()
{
// 何かを行います
});
AfterEach(EAsyncExecution::Thread, [this]()
{
// 何かを分解します
));
上記のコード ブロックは、それぞれどのように実行されるかは異なりますが、確実に決められた順序で実行されます。BeforeEach() ブロックは TaskGraph のタスクとして実行され、It() はスレッド プール内のオープン スレッドで実行され、AfterEach() はコード ブロックを実行するためだけの専用のスレッドを作成します。
これらのオプションは、Automation Driver を使用するような、スレッドセーフかどうかが問題になるシナリオをシミュレートする必要がある場合に非常に便利です。
AsyncExecution 機能は Latent Completion 機能と組み合わせることができます。
Latent Completion
クエリの実行時など、複数のフレームを取り込むアクションを実行する必要があるテストを記述することが必要になることがあります。このようなシナリオでは、LatentBeforeEach()、LatentIt()、LatentAfterEach() メンバーのオーバーロードを使用できます。オーバーロードされたメンバーはいずれも、ラムダが Done と呼ばれる単純なデリゲートを取るという点を除けば、非潜在的なメンバーと同じです。
Latent バリエーションを使用すると、Spec テスト タイプは、アクティブに実行中の Latent コード ブロックが Done デリゲートを呼び出すまで、テスト シーケンスの次のコード ブロックへの実行を保留します。
LatentIt("should return available items", [this](const FDoneDelegate& Done)
{
BackendService->QueryItems(this, &FMyCustomSpec::HandleQueryItemComplete, Done);
});
void FMyCustomSpec::HandleQueryItemsComplete(const TArray<FItem>& Items, FDoneDelegate Done)
{
TestEqual("Items.Num() == 5", Items.Num(), 5);
Done.Execute();
}
この例でわかるように、他のコールバックにペイロードとして Done デリゲートを渡して、Latent コードにアクセスできるようにすることができます。したがって、上記のテストを実行すると、It() コード ブロックの実行がすでに終了していても、Done デリゲートが実行されるまでは、It() の AfterEach() コード ブロックは実行されません。
Latent Completion 機能は AsyncExecution 機能と組み合わせることができます。
パラメータ化されたテスト
データ駆動型手法でテストを作成することが必要になることがあります。これは、ファイルから入力を読み取り、その入力からテストを生成することになる場合もあれば、コードの重複を削減する手段として理想的な手法になる場合もあります。どちらの場合でも、Spec テスト タイプでは、パラメータ化されたテストを非常に自然な方法で実現できます。
Describe("Basic Math", [this]()
{
for (int32 Index = 0; Index < 5; Index++)
{
It(FString::Printf(TEXT("should resolve %d + %d = %d"), Index, 2, Index + 2), [this, Index]()
{
TestEqual(FString::Printf(TEXT("%d + %d = %d"), Index, 2, Index + 2), Index + 2, Index + 2);
});
}
});
上記の例からわかるように、パラメータ化されたテストは、パラメータ化されたデータをラムダ ペイロードの一部として渡す別の Spec 関数を動的に呼び出し、そのテスト固有の記述を生成するだけで作成できます。
場合によっては、パラメータ化されたテストを使用すると、テストが肥大化することがあります。シンプルに単一のテストで、入力からすべてのシナリオを実行することが妥当である場合があります。入力の数と生成されるテスト結果を考慮してください。パラメータ化された手段でデータ駆動型テストを作成する主な利点は、各テストを個別に実行でき、再現が容易になることです。
Redefine
パラメータ化されたテストを使用する場合は、入力を制御している外部ファイルにランタイム時に変更を加え、テストを自動的に更新すると便利な場合があります。Redefine() は Spec テスト タイプのメンバーであり、これを呼び出すと Define() プロセスが再度実行されます。これにより、テストのすべてのコード ブロックが再収集され、特定の順序でまとめられます。
上記のことを行う最も便利な方法は、入力ファイルの変更をリッスンし、必要に応じてテストに対して Redefine() を呼び出すコードを作成することです。
テストの無効化
Spec テスト タイプのすべての Describe()、BeforeEach()、It()、AfterEach() メンバーには、先頭に「x」が付いているバリエーションがあります。例えば、xDescribe()、xBeforeEach()、xIt()、xAfterEach() です。これらのバリエーションを使うと、よりシンプルにコード ブロックや Describe() を無効にできます。xDescribe() を使用すると、xDescribe() 内のすべてのコードが無効になります。
これは、繰り返しを必要とするエクスペクテーションのコメントアウトよりも簡単です。
完成した例
Spec テスト タイプの完成した例は、「Engine/Plugins/Tests/AutomationDriverTests/Source/AutomationDriverTests/Private/AutomationDriver.spec.cpp」にあります。この Spec には現時点で 120 以上のエクスペクテーションが含まれており、高度な機能のほとんどがどこかで利用されています。
Launcher チームも、Spec フレームワークの完成した使用例をいくつも作成しています。最も完成した使用例の 1 つは BuildPatchServices 関連の Spec です。