
こんにちは!私の名前はユーリSkvortsov、私たちのチームは、ロスバンクの自動テストに従事しています。私たちのタスクの1つは、機能テストを自動化するツールを開発することです。
この記事では、他の問題を解決するための小さな補助ユーティリティとして考案されたが、最終的にはスタンドアロンツールに変わったソリューションについて説明したいと思います。 Fast-Unitフレームワークについて説明します。これにより、宣言スタイルでユニットテストを記述し、ユニットテストの開発をコンポーネントコンストラクターに変えることができます。このプロジェクトは主に、主力製品であるTladiantaをテストするために開発されました。これは、デスクトップ、Web、モバイル、レストの4つのプラットフォームをテストするための統合BDDフレームワークです。
そもそも、自動化フレームワークのテストは一般的なタスクではありません。しかし、この場合、それはテストプロジェクトの一部ではなく、独立した製品であったため、ユニットの必要性をすぐに認識しました。
最初の段階では、assertJやMockitoなどの既製のツールを使用しようとしましたが、すぐにプロジェクトの技術的機能のいくつかに遭遇しました。
- TladiantaはすでにJUnit4を依存関係として使用しているため、別のバージョンのJUnitを使用することが難しくなり、Beforeでの作業が難しくなります。
- Tladiantaには、さまざまなプラットフォームで動作するためのコンポーネントが含まれています。機能的には「非常に近い」が、階層や動作が異なる多くのエンティティがあります。
- «» ( ) ;
- , , , , ;
- - (, Appium , , , );
- , : Mockito .
最初に、ドライバーの交換方法、偽のSelenium要素の作成方法、およびテストハーネスの基本アーキテクチャーの作成方法を学習したとき、テストは次のようになりました。
@Test
public void checkOpenHint() {
ElementManager.getInstance().register(xpath,ElementManager.Condition.VISIBLE,
ElementManager.Condition.DISABLED);
new HintStepDefs().open(("");
assertTrue(TestResults.getInstance().isSuccessful("Open"));
assertTrue(TestResults.getInstance().isSuccessful("Click"));
}
@Test
public void checkCloseHint() {
ElementManager.getInstance().register(xpath);
new HintStepDefs().close("");
assertTrue(TestResults.getInstance().isSuccessful("Close"));
assertTrue(TestResults.getInstance().isSuccessful("Click"));
}
またはこのようにさえ:
@Test
public void fillFieldsTestOld() {
ElementManager.getInstance().register(ElementManager.Type.CHECK_BOX,"//check-box","",
ElementManager.Condition.NOT_SELECTED);
ElementManager.getInstance().register(ElementManager.Type.INPUT,"//input","");
ElementManager.getInstance().register(ElementManager.Type.RADIO_GROUP,
"//radio-group","");
DataTable dataTable = new Cucumber.DataTableBuilder()
.withRow("", "true")
.withRow("", "not selected element")
.withRow(" ", "text")
.build();
new HtmlCommonSteps().fillFields(dataTable);
assertEquals(TestResults.getInstance().getTestResult("set"),
ElementProvider.getInstance().provide("//check-box").force().getAttribute("test-id"));
assertEqualsTestResults.getInstance().getTestResult("sendKeys"),
ElementProvider.getInstance().provide("//input").force().getAttribute("test-id"));
assertEquals(TestResults.getInstance().getTestResult("selectByValue"),
ElementProvider.getInstance().provide("//radio-group").force().getAttribute("test-id"));
}
上記のコードでテストされているものを見つけたり、チェックを理解したりすることは難しくありませんが、膨大な量のコードがあります。エラーをチェックして説明するためのソフトウェアを含めると、読みにくくなります。そして、チェックの実際のロジックは非常に原始的ですが、メソッドが目的のオブジェクトで呼び出されたことをチェックしようとしています。このようなテストを作成するには、ElementManager、ElementProvider、TestResults、TickingFuture(特定の時間内の要素の状態の変更を実装するラッパー)について知っておく必要があります。これらのコンポーネントはプロジェクトごとに異なり、変更を同期する時間がありませんでした。
もう1つの課題は、いくつかの標準の開発でした。私たちのチームにはオートマトンの利点があり、私たちの多くはユニットテストの開発に十分な経験がありません。一見簡単ですが、お互いのコードを読むのは非常に面倒です。私たちは技術的負債を十分に迅速に清算しようとしましたが、そのようなテストが何百も発生すると、維持が困難になりました。さらに、コードが構成で過負荷になり、実際のチェックが失われ、ストラップが太いため、フレームワークの機能をテストする代わりに、独自のストラップがテストされたことが判明しました。
そして、あるモジュールから別のモジュールに開発を移そうとしたとき、一般的な機能を引き出す必要があることが明らかになりました。その瞬間、ベストプラクティスを備えたライブラリを作成するだけでなく、このツール内に単一ユニットの開発プロセスを作成するというアイデアが生まれました。
変化する哲学
コード全体を見ると、コードの多くのブロックが「意味なしに」繰り返されていることがわかります。メソッドをテストしますが、常にコンストラクターを使用します(何らかのエラーがキャッシュされる可能性を回避するため)。最初の変換-テストされたインスタンスのチェックと生成を注釈に移動しました。
@IExpectTestResult(errDesc = " set", value = "set",
expected = "//check-box", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = " sendKeys", value = "sendKeys",
expected = "//input", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = " selectByValue", value = "selectByValue",
expected = "//radio-group", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@Test
public void fillFieldsTestOld() {
ElementManager.getInstance().register(ElementManager.Type.CHECK_BOX, "//check-box", "",
ElementManager.Condition.NOT_SELECTED);
ElementManager.getInstance().register(ElementManager.Type.INPUT, "//input", "");
ElementManager.getInstance().register(ElementManager.Type.RADIO_GROUP,
"//radio-group", "");
DataTable dataTable = new Cucumber.DataTableBuilder()
.withRow("", "true")
.withRow("", "not selected element")
.withRow(" ", "text")
.build();
runTest("fillFields", dataTable);
}
何が変わったの?
- チェックは別のコンポーネントに委任されています。これで、アイテムの保存方法やテスト結果について知る必要がなくなりました。
- : errDesc , .
- , , , – runTest, , .
- .
- - , .
この形式の表記法が気に入ったので、同じ方法で別の複雑なコンポーネント、つまり要素の生成を単純化することにしました。ほとんどのテストは既成の手順に専念しており、正しく機能することを確認する必要がありますが、そのようなチェックを行うには、偽のアプリケーションを完全に「起動」して要素で埋める必要があります(Web、デスクトップ、モバイルについて話していることを思い出してください。かなり大きく異なります)。
@IGenerateElement(type = ElementManager.Type.CHECK_BOX)
@IGenerateElement(type = ElementManager.Type.RADIO_GROUP)
@IGenerateElement(type = ElementManager.Type.INPUT)
@Test
@IExpectTestResult(errDesc = " set", value = "set",
expected = "//check-box", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = " sendKeys", value = "sendKeys",
expected = "//input", convertedBy = Converters.XpathToIdConverter.class, soft = true)
@IExpectTestResult(errDesc = " selectByValue", value = "selectByValue",
expected = "//radio-group", convertedBy = Converters.XpathToIdConverter.class, soft = true)
public void fillFieldsTest() {
DataTable dataTable = new Cucumber.DataTableBuilder()
.withRow("", "true")
.withRow("", "not selected element")
.withRow(" ", "text")
.build();
runTest("fillFields", dataTable);
}
これで、テストコードが完全にテンプレートになり、パラメータが明確に表示され、すべてのロジックがテンプレートコンポーネントに移動しました。デフォルトのプロパティにより、空の行を削除することが可能になり、オーバーロードの十分な機会が提供されました。このコードは、BDDアプローチ、前提条件、チェック、アクションとほぼ一致しています。さらに、すべてのバインディングがテストのロジックから外れているため、マネージャーやテスト結果のストレージについて知る必要がなくなり、コードはシンプルで読みやすくなっています。 Javaの注釈はほとんどカスタマイズできないため、文字列から最終結果を受け取ることができるコンバーターのメカニズムを導入しました。このコードは、メソッドを呼び出したという事実だけでなく、それを実行した要素のIDもチェックします。当時存在していたほぼすべてのテスト(200ユニット以上)は、すぐにこのロジックに移行し、単一のテンプレートにまとめました。テストは本来あるべき姿になりました-ドキュメント、コードではないので、宣言性になりました。 Fast-Unitの基礎を形成したのはこのアプローチです-宣言性、自己文書化テスト、およびテストされた機能の分離。テストは1つのテスト方法のチェックに完全に専念しています。
私たちは開発を続けています
ここで、プロジェクトのフレームワーク内でそのようなコンポーネントを独立して作成する機能を追加し、それらの操作のシーケンスを制御する機能を追加する必要がありました。これを行うために、フェーズの概念を開発しました。Junitとは異なり、これらのフェーズはすべて各テスト内に独立して存在し、テストの実行時に実行されます。デフォルトの実装として、次のライフサイクルを定めています。
- Package-generate-package-infoに関連する注釈を処理します。これらに関連するコンポーネントは、構成のダウンロードと一般的なハーネスの準備を提供します。
- Class-generate-テストクラスに関連付けられた注釈を処理します。フレームワークに関連する構成アクションがここで実行され、準備されたバインディングに適合します。
- 生成-テストメソッド自体(エントリポイント)に関連付けられた注釈を処理します。
- テスト-インスタンスを準備し、テストされたメソッドを実行します。
- アサート-チェックを実行します。
処理される注釈は、次のように記述されます。
@Target(ElementType.PACKAGE) //
@IPhase(value = "package-generate", processingClass = IStabDriver.StabDriverProcessor.class,
priority = 1) // ( )
public @interface IStabDriver {
Class<? extends WebDriver> value(); // ,
class StabDriverProcessor implements PhaseProcessor<IStabDriver> { //
@Override
public void process(IStabDriver iStabDriver) {
//
}
}
}
Fast-Unit機能は、ライフサイクルを任意のクラスでオーバーライドできることです。これは、テスト対象のクラスとフェーズを示すように設計されたITestClassアノテーションによって記述されます。フェーズのリストは、単純に文字列配列として指定されるため、構成の変更とフェーズの順序が可能になります。フェーズを処理するメソッドも注釈を使用して検出されるため、クラスで必要なハンドラーを作成してマークを付けることができます(さらに、クラス内でのオーバーライドも可能です)。大きな利点は、この分離により、テストをレイヤーに分割できることでした。パッケージの生成または生成フェーズで終了したテストでエラーが発生した場合、テストハーネスが損傷します。 class-generateの場合-フレームワークの構成メカニズムに問題があります。テストのフレームワーク内で、テストされた機能にエラーがある場合。テストフェーズでは、バインディングとテスト対象の機能の両方で技術的にエラーが発生する可能性があるため、発生する可能性のあるバインディングエラーを特別なタイプであるInnerExceptionでラップしました。
各フェーズは分離されています。他のフェーズに依存せず、直接相互作用しません。フェーズ間で渡されるのはエラーだけです(前のフェーズでエラーが発生した場合、ほとんどのフェーズはスキップされますが、これは必要ありません。たとえば、アサートフェーズはとにかく機能します)。
ここで、おそらく、テストインスタンスはどこから来ているのかという疑問がすでに生じています。コンストラクターが空の場合、それは明らかです。ReflectionAPIを使用して、テスト対象のクラスのインスタンスを作成するだけです。しかし、コンストラクターが起動した後、このコンストラクトでパラメーターを渡すか、インスタンスを構成するにはどうすればよいですか?オブジェクトがビルダーによって構築されている場合、または一般的に静的なテストに関するものである場合はどうなりますか?このために、コンストラクターの複雑さを隠すプロバイダーのメカニズムが開発されました。
デフォルトのパラメーター化:
@IProvideInstance
CheckBox generateCheckBox() {
return new CheckBox((MobileElement) ElementProvider.getInstance().provide("//check-box")
.get());
}
パラメータなし-問題ありません(CheckBoxクラスをテストし、インスタンスを作成するメソッドを登録しています)。ここではデフォルトのプロバイダーがオーバーライドされるため、テスト自体に何も追加する必要はありません。テスト自体がこのメソッドをソースとして自動的に使用します。この例は、Fast-Unitロジックを明確に示しています。複雑で不要なものを非表示にしています。テストの観点からは、CheckBoxクラスでラップされたモバイル要素がどこからどのように取得されるかはまったく問題ではありません。重要なのは、指定された要件を満たすCheckBoxオブジェクトがあることだけです。
自動引数インジェクション:次のようなコンストラクターがあると仮定しましょう:
public Mask(String dataFormat, String fieldFormat) {
this.dataFormat = dataFormat;
this.fieldFormat = fieldFormat;
}
次に、引数インジェクションを使用したこのクラスのテストは次のようになります。
Object[] dataMask={"_:2_:2_:4","_:2/_:2/_:4"};
@ITestInstance(argSource = "dataMask")
@Test
@IExpectTestResult(errDesc = " ", value = FAST_RESULT,
expected = "12/10/2012")
public void convert() {
runTest("convert","12102012");
}
指名プロバイダー
最後に、複数のプロバイダーが必要な場合は、名前バインディングを使用して、コンストラクターの複雑さを隠すだけでなく、その本当の意味も示します。同じ問題は次のように解決できます。
@IProvideInstance("")
Mask createDataMask(){
return new Mask("_:2_:2_:4","_:2/_:2/_:4");
}
@ITestInstance("")
@Test
@IExpectTestResult(errDesc = " ", value = FAST_RESULT,
expected = "12/10/2012")
public void convert() {
runTest("convert","12102012");
}
IProvideInstanceとITestInstanceは関連付けられた注釈であり、テスト対象のインスタンスを取得する場所をメソッドに指示できます(静的の場合、このインスタンスは最終的にReflection APIを介して使用されるため、nullが返されます)。プロバイダーのアプローチでは、テストで実際に何が発生するかについてより多くの情報が得られ、コンストラクターの呼び出しが前提条件を説明するテキストのパラメーターに置き換えられるため、コンストラクターが突然変更された場合は、プロバイダーを修正するだけで済みますが、実際の機能が変更されるまで、テストは変更されません。レビュー中に複数のプロバイダーが表示された場合、それらの違いに気付くでしょう。したがって、テストされたメソッドの動作の特殊性に気付くでしょう。フレームワークをまったく知らなくても、Fast-Unit操作の原則を知っているだけです。開発者はテストコードを読み、テストされたメソッドが何をするかを理解することができます。
結論と結果
私たちのアプローチには多くの利点があることが判明しました。
- 簡単なテストの移植性。
- バインディングの複雑さを隠し、テストを中断せずにそれらをリファクタリングする機能。
- 後方互換性が保証されています-メソッド名の変更はエラーとして記録されます。
- テストは、各メソッドのかなり詳細なドキュメントになりました。
- 検査の質が大幅に向上しました。
- ユニットテストの開発はパイプラインプロセスになり、開発とレビューの速度が大幅に向上しました。
- 開発されたテストの安定性-フレームワークとFast-Unit自体は活発に開発されていますが、テストの劣化はありません。
明らかな複雑さにもかかわらず、このツールを迅速に実装することができました。現在、ほとんどのユニットはその中に書かれており、かなり複雑で大量の移行で信頼性をすでに確認しており、かなり複雑な欠陥を特定することができました(たとえば、要素やテキストチェックの待機中)。技術的な負債を迅速に解消し、ユニットとの効果的な作業を確立することができ、ユニットを開発の不可欠な部分にしました。現在、チーム外の他のプロジェクトでこのツールをより積極的に実装するためのオプションを検討しています。
現在の問題と計画:
- , . , ( - ).
- .
- .
- , -.
- Fast-Unit junit4, junit5 testng