Automation Driver は、流れるような (fluent) 構文を使用して、アプリケーションのユーザー入力をシミュレートできる Unreal Engine の機能です。Automation Driver は、ブラウザ入力シミュレーション用のその他の外部ライブラリと機能セットが非常によく似ており、他のライブラリと同様に、主に、ユーザーの動作をシミュレートする機能テストを作成するために使用されます。

機能
Automation Driver は入力をシミュレートします。カーソルの動き、クリック、プレス、キーボード入力、スクロール、ドラッグ アンド ドロップなどをシミュレートすることができます。Automation Driver のこの最初のバージョンでは、従来のデスクトップ入力一式、つまり、基本的にすべてのキーボードとマウス関連の入力をサポートしています。将来的には、タッチ ジェスチャー、コントローラー入力、さらにはモーション検知にも対応する予定です。
Automation Driver では、保守が容易で比較的コードの変更にも強い、流れるような可読性の高い構文により、これらの入力をシミュレートすることが特徴です。最も重要なポイントは、Automation Driver はプラットフォーム レイヤーで入力をシミュレートするため、Automation Driver のパブリック API はスレートに依存しない点です。少しの作業で、シーン アクタやその他のものを操作するように拡張できます。
この機能はプラットフォームにも依存しないため、どのようなプラットフォームでも、基本の入力タイプをシミュレートできる限り、使用できます。
しくみ
Core には、ほとんどの外部入力を受け付けるインターフェースのセットがあります。Automation Driver では、これらのインターフェースの shim 実装を作成し、基本的な依存性の注入を利用して、実際の実装を「パススルー」バージョンに置き換えることができます。この「パススルー」バージョンは、アプリケーションに渡されるプラットフォーム入力とそうでないプラットフォーム入力をデリゲートし、自身の入力を完全にシミュレートします。Automation Driver はそのようにして処理を実行します。
使い方
デフォルトでは、Automation Driver は無効になっています。有効にするには、Module API で Enable()
関数を呼び出します。無効にするには、Disable()
関数を呼び出します。
IAutomationDriverModule::Get().Enable();
//@todo simulate user behavior here
IAutomationDriverModule::Get().Disable();
有効になると、Automation Driver は、アプリケーションが受け取るプラットフォーム入力のほとんどについてブロックを開始します。この状態になったら、独自のドライバ インスタンスを作成できます。
FAutomationDriverPtr Driver = IAutomationDriverModule::Get().CreateDriver();
独自のドライバ インスタンスを作成したら、入力のシミュレートを開始できます。サインアップ フォームの入力をシミュレートする簡単な例を次に示します。
FDriverElementRef SignUpForm = Driver->FindElement(By::Id("Form"));
FDriverElementRef SubmitBtn = Driver->FindElement(By::Path("#Form//Submit"));
FDriverSequenceRef Sequence = Driver->CreateSequence();
Sequence->Actions()
.Focus(SignUpForm)
.Type(TEXT("FirstName\tLastName\[email protected]"))
.Click(SubmitBtn);
Sequence->Perform();
API を使用する
Automation Driver を操作する主な API として、同期 API と非同期 API の 2 つがあります。最も簡単にコードを作成できるのは同期 API ですが、同期 API のコードは GameThread で実行できません。同期 API は、入力シミュレーションが完了するまで、処理をブロックし待機するためです。入力シミュレーションは GameThread で実行すべき潜在的なロジックとしてキューに保持されますが、その間は同期ドライバ API が処理をブロックするためデッドロックが発生し、シミュレート対象の入力は決して処理されません。このような事態にならないようにしてください。
このドキュメントでは、GameThread でロジックを実行しないものとして、すべての例で同期 API を使用しています。
この概念がわかりにくいと思われる場合は、新しい Automation Spec テスト タイプについての説明を参考にしてください。
要素を探す
有効な入力を生成するための最初のステップは、操作対象のアプリケーションの主要なコンポーネントを特定することです。Automation Driver はロケーターを利用してこれを行います。スレートベースの要素を検出できる既存のロケーターを、流れるようなコーディングで使用できる方法がいくつかあります。
By::Id()
要素を検索する最も理想的な方法は、Id に基づいて検索することです。プログラマーは、ウィジェットに明示的な Automation Driver のメタデータ ID をタグ付けする必要があるため、検索が失敗する可能性は大幅に低くなります。
ウィジェットのタグ付けは簡単で、次のように行うことができます。
SNew(STextBlock)
.Text(InViewModel, &IViewModel::GetFirstName)
.AddMetaData(FDriverMetaData::Id("SignUpFormFirstNameField"))
ID は他の ID と競合しないように、できる限り固有の ID にすることをお勧めします。このような ID なら、はるかに参照しやすくなります。ID は、他の ID や要素によって範囲指定できるように、パスを使用して参照することもできます。ただし、コードの変更に弱いスコープ コンテキストにテストが依存しなくても済むように、一意の ID を生成することをお勧めします。
例えば、一意であることが確実な ID であれば、次のコードで目的のウィジェットを検索できます。
FDriverElementRef FirstNameField = Driver->FindElement(By::Id("SignUpFormFirstNameField"));
ID が個々の要素に一意の ID ではなく、要素のセットに対して一意の ID である場合は、次のコードを使用してセットの全要素を一度に検索できます。
FDriverElementCollectionRef SignUpFormFields = Driver->FindElements(By::Id("SignUpFormField"));
TArray<FDriverElementRef> Fields = SignUpFormFields->GetElements();
要素のコレクションに対して GetElements()
メソッドを呼び出すことで、実際に要素の検索が開始されます。目的の要素が表示されたのに消えてしまった場合は、その点に留意してください。要素が表示されるまで意識的に待機しなければならない場合があります。
By::Path()
パスで要素を検索する方法は、現在利用可能な検索の中で最もコードの変更に弱い一方、最も強力な方法でもあります。By::Path()
ロケーターを使用すると、タグ、ID、タイプの階層マッチングにより、特定の要素を取得できます。
以下に、構文の例をいくつか示します。
By::Path("#SignUpFormFirstNameField")
By::Path("FormField"))
By::Path("Documents//Tiles")
By::Path("<SAutomationDriverSpecSuite>")
By::Path("#Piano//#KeyB/<STextBlock>")
By::Path("#Suite//Form//Rows//#A1//<SEditableText>")
Path の構文
構文 | 説明 |
---|---|
#SignUpFormFirstNameField |
# は、その後に続くテキストが明示的な ID であることを表します。SWidget の場合、Automation Driver ID のメタデータでタグ付けする必要があります。 |
FormField |
プレーン テキストは一般的なタグを表します。SWidget の場合は、Tag または TagMetadata と、マッチングするプレーン テキスト値を指定する必要があります。 |
<STextBlock> |
<> はタイプを表します。SWidget の場合は、SNew 構成で使用されている明示的なタイプである必要があります。タイプを使用するパスは、Widget Reflector (ウィジェット リフレクタ) を参照すれば、簡単に作成できます。 |
/ |
階層はスラッシュで表されます。シングル スラッシュは、シングル スラッシュの前の値と一致する要素の直接の子と、シングル スラッシュの次の値が一致しなければらないことを表します。 |
// |
階層はスラッシュで表されます。ダブル スラッシュは、ダブル スラッシュの前の値と一致した要素の子孫と、ダブル スラッシュの次の値が一致しなければならないことを表します。 |
今後、さらに多くの構文オプションがパス ロケーターに追加される予定ですが、これらは現在使用可能なオプションです。
エスケープ文字はサポートされていないため、パス ロケーターでは先頭に <
または #
文字を含むタグまたは ID とのマッチングは適切に処理されません。
その他の構文の使用例を確認するには、次を参照してください。Engine/Source/Developer/AutomationDriver/Private/Specs/AutomationDriver.spec.cpp
By::Cursor()
このロケーターは、カーソルの現在位置の直下にある要素を返します。
FDriverElementRef ElementUnderCursor = Driver->FindElement(By::Cursor());
By::Delegate()
既存のロケーターでは必要な処理を実現できない場合、By::Delegate()
またはそのオーバーロードを介して、独自のデリゲートまたはラムダを渡すことができます。この場合、Automation Driver は、要素を検索する際に、そのコード ブロックをゲーム スレッド上で呼び出します。
FDriverElementRef CustomElement = Driver->FindElement(By::WidgetLambda([this](TArray<TSharedRef<SWidget>>& OutWidgets){
OutWidgets.Add(SpeciallyCachedWidget);
}));
アクションを実行する
Automation Driver でアクションを実行するには、主に 2 つの方法があります。最も簡単な方法は FDriverElementRef
で直接使用できるアクションを使用することで、これは単一の要素 (もしくは、少数の要素のセット) を操作する場合に使用します。もう一つの方法は FDriverSequenceRef
を作成することです。この方法では、長い一連のアクションをキューに入れて、多数の異なる要素に対して (または、特定の要素が全くない場合に) アクションを実行できます。
要素
ドライバの要素参照が取得されたら、そこから直接、使用可能なアクションを実行できます。たとえば、次のようになります。
Driver->FindElement(By::Id("Submit"))->Click();
この例では、#Submit
要素への参照を取得した後、デフォルトの Click
メソッドを直接呼び出しています。ドライバの要素参照から直接使用できるすべてのアクションは、そのアクションが呼び出される要素にのみ影響します。上記の Click
の例では、Automation Driver は最初にカーソルを #Submit
要素へ移動します。要素がスレート DOM に存在しない場合、Automation Driver の構成で定義されている暗黙的なタイムスパン (時間) が経過するまでは待機します。タイムアウトする前に要素が表示された場合は、要素上にカーソルを移動します。要素が画面に表示されていない場合は、ドライバは要素を画面上に移動するメソッド (要素が表示されるようにスクロールするなど) を検索します。要素が表示されると、カーソルが要素の上に移動します。そこでようやく、フル クリックがシミュレートされます。
目的の要素にのみアクションが実行されるように、ドライバのすべての要素参照メソッドはこのように動作します。
シーケンス
ドライバ シーケンスは、ドライバに対してアクションを発行する、より堅牢な方法です。シーケンスを使用すると、影響を与える可能性のある要素を気にせずにアクションを実行したり、特定の要素セットに対してアクションを実行したりできます。さらに、シーケンスは何度も呼び出すことができるため、再利用性がとても高く、ヘルパー ライブラリに最適です。
FDriverSequenceRef Sequence = Driver->CreateSequence();
Sequence->Actions()
.MoveToElement(By::Id("Submit"))
.Click(EMouseButtons::Left);
Sequence->Perform();
シーケンスは、Perform()
が呼び出されるまでアクションを実行しません。一旦アクションがシーケンスに追加されると、削除できません。シーケンスの実行中に、追加でアクションをシーケンスに含めることはできませんが、シーケンスが終了したら、追加のアクションを含めることができます。
アクションが 1 つでも失敗すると、シーケンス全体が失敗し、その時点で実行が停止します。
アクション
シミュレートできるすべてのアクション タイプの詳細については、次のファイルを参照してください。
Engine/Source/Developer/AutomationDriver/Public/IDriverElement.h
Engine/Source/Developer/AutomationDriver/Public/IDriverSequence.h
現在、対応しているアクションはキーボードとマウスの入力のみです。この入力には、一般的に次のものが含まれます。
- Mouse Move
- Mouse Wheel Scrolling
- Click
- Double Click
- Press and Hold Button
- Release Button
- Type
- TypeChord
Ctrl + Shift + S のようなキーボード ショートカットの実行に便利です。 - Press and Hold Key
- Release Key
- Focus
これには、特定の要素が表示されるようになるまでのスクロール、要素が表示しているテキスト、サイズ、または位置の取得などの補足的なインテリジェント アクションが含まれます。
この機能については現在積極的に開発していませんが、以下のアクションのサポートを追加する可能性があります。
- Scene/Actor interaction
- Controller Input
- Touch Input/Gesture
- Motion Detection
待機する
ユーザー シミュレーションのオートメーション テストを作成する場合は、通常、さまざまなイベントの発生を待機します。Automation Driver には、待機を容易にするサポートがビルトインされています。
Automation Driver のどのアクションでも、設定されている ImplicitWait
タイムスパンの時間が経過するまでは、依存するシナリオの発生を自動的に待機します。このタイムスパンの時間が経過すると、タイムアウトしてアクションは失敗します。このような例としては、要素の生成と表示を待機し、要素が表示されたらクリック イベントをシミュレートします。
ImplicitWait
のタイムスパンは動的に設定でき、必要に応じて Automation Driver の設定オプションを介して、シミュレーション中に調整できます。以下に例を示します。
現在のデフォルトの ImplicitWait
タイムスパンは 3 秒ですが、次のように明示的または条件付きの待機を実行することもできます。
Driver->Wait(FTimespan::FromSeconds(2));
FDriverSequenceRef Sequence = Driver->CreateSequence();
Sequence->Actions()
.Wait(Until::ElementExists(ElementA, FWaitTimeout::InSeconds(3)))
.Focus(ElementA);
Driver->Wait(Until::ElementIsVisible(ElementA, FWaitInterval::InSeconds(0.25), FWaitTimeout::InSeconds(1)));
Driver->Wait(Until::ElementIsInteractable(ElementA, FWaitInterval::InSeconds(0.25), FWaitTimeout::InSeconds(1)));
Driver->Wait(Until::ElementIsScrolledToBeginning(ScrollBox, FWaitTimeout::InSeconds(3)));
シミュレーションを実行する前提条件として、独自のデリゲートまたはラムダ条件を指定することもできます。また、すべての待機に必須の timeout 引数と、待機条件を再評価する頻度を定義する、オプションの interval timespan があります。
まとめ
Automation Driver API は GameThread
で実行できないため、シミュレーションを作成するのは少し難しいかもしれませんが、Automation Driver を新しい Spec テスト タイプと組み合わせると容易になります。
以下のコードは、120 種類以上ある Spec のエクスペクテーション (テスト) のうちの 1 つから抜き出したスニペットです。これらのテストは、Automation Driver 自体が正常に機能していることを確認するために使用されます。
以下のスニペットの処理の詳細については、Automation Spec テスト タイプのドキュメントを参照してください。
BEGIN_DEFINE_SPEC(FAutomationDriverSpec, "System.Automation.Driver", EAutomationTestFlags::ProductFilter| EAutomationTestFlags::ApplicationContextMask)
TSharedPtr<SWindow> SuiteWindow;
TSharedPtr<SAutomationDriverSpecSuite> SuiteWidget;
TSharedPtr<IAutomationDriverSpecSuiteViewModel> SuiteViewModel;
FAutomationDriverPtr Driver;
END_DEFINE_SPEC(FAutomationDriverSpec)
void FAutomationDriverSpec::Define()
{
BeforeEach([this]() {
if (IAutomationDriverModule::Get().IsEnabled())
{
IAutomationDriverModule::Get().Disable();
}
IAutomationDriverModule::Get().Enable();
if (!SuiteViewModel.IsValid())
{
SuiteViewModel = FSpecSuiteViewModelFactory::Create();
}
if (!SuiteWidget.IsValid())
{
SuiteWidget = SNew(SAutomationDriverSpecSuite, SuiteViewModel.ToSharedRef());
}
if (!SuiteWindow.IsValid())
{
SuiteWindow = FSlateApplication::Get().AddWindow(
SNew(SWindow)
.Title(FText::FromString(TEXT("Automation Driver Spec Suite")))
.ClientSize(FVector2D(600, 540))
[
SuiteWidget.ToSharedRef()
]);
}
SuiteWidget->RestoreContents();
SuiteWindow->BringToFront(true);
SuiteViewModel->Reset();
Driver = IAutomationDriverModule::Get().CreateDriver();
});
Describe("Element", [this]()
{
Describe("Type", [this]()
{
It("should focus the element and type the characters of the specified string", EAsyncExecution::ThreadPool, [this]()
{
FDriverElementRef Element = Driver->FindElement(By::Id("A1"));
Element->Type(TEXT("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
TEST_EQUAL(SuiteViewModel->GetFormString(EFormElement::A1), TEXT("ABCDEFGHIJKLMNOPQRSTUVWXYZ"));
});
});
});
AfterEach([this]() {
Driver.Reset();
IAutomationDriverModule::Get().Disable();
});
}
ここで重要な点は、BeforeEach()
に渡されたラムダが GameThread
上で実行されていることです。このラムダによりテストのシナリオが設定され、ウィジェットが作成され、適宜ウィンドウが配置されます。実際の入力シミュレーションは It()
ラムダによって実行されます。詳しく見ると、このラムダでは EAsyncExecution::ThreadPool
値を It()
に渡しています。これにより、ラムダは GameThread
とは別のスレッドで実行され、安全に入力をシミュレートすることができます。このため、Automation Driver のコードにブレークポイントを設定して、さまざまなアクションの実行時にステップ実行することができます。最後に、AfterEach()
が環境をクリーンアップし、GameThread
上に戻って実行されます。
最後に
Automation Driver のコードを使用する場合は、GameThread
上では実行されないこと、したがって、スレッドセーフではない SharedPtrs のコピーを作成するのは安全ではないことに常に留意してください。スレートはスレッドセーフではない SharedPtrs
しか使用しないため、これは重要な注意事項です。
スレッドセーフではない SharedPtr
を使用したアクセスが必要な場合は、スレッド間を切り替えながら動作のシミュレートやチェックを実行できるように、テスト専用の BeforeEach()
ブロックを作成します。
テストの実行中に SharedPtr
が破棄されないことがわかっている場合には、テスト クラス自体にキャッシュすることもできます。SharedPtr
はラムダからアクセスできても、複製されないため、参照カウントでの競合状態は発生しません。上記のスニペットでは、これを SuiteViewModel
を使って行っています。
通常は、すべての Automation Driver のシミュレーションを専用の BeforeEach()
ブロックで実行してから、GameThread
で It()
のエクスペクテーション チェックを実行することをお勧めします。