こんにちは
この記事では、GWTとGXTのUIコンポーネントとMVP(パッシブビュー)アーキテクチャを使用して、アプリケーションのロジックと外観を分離するGWTのユニット/統合テストについて説明します。
GWTとGXTはここではランダムに割り当てられません-GoogleはGWTでMVPパターン(より正確には、より一般的なもの-ロジックとプレゼンテーションの分離)をサポートしやすくするいくつかのフレームワークを開発しました。 これは、アプリケーションロジックをモジュールに分割するためのアクティビティとプレース、POJOオブジェクトをウィジェットに自動的にマッピングするためのGWTエディター、宣言的なインターフェースの説明のためのUiBindingです。
これはすべて、GXT UIフレームワークでもサポートされています。 T.ch. 実際、GWTまたはGXTのUIコンポーネントの使用に大きな違いはありません。
その結果、重いGWTフレームワークを持ち上げることなく、簡単にテストできるアプリケーションが得られます。
目的
機能を最大限にカバーしたGWTアプリケーションでテストを作成するだけです。 理想的には、統合テスト(アプリケーションのすべてのレイヤーを最小限のテストでテストし、レイヤーの相互接続をテストする統合テスト)の簡単な記述。 必要です、なぜなら クライアントには多くのロジックがあり、開発者のマシンですばやく実行できる簡単で実行しやすいテストで最大限にカバーしたいと思います。
使用されている技術とフレームワーク
1)パッシブビューを備えた
MVP (モデルビュープレゼンター)-UIを使用したアプリケーションのテストの基礎。 要するに、これは、Viewが最小限の役割を持つロジックとディスプレイの分離のパターンです。 Viewがシンプルになり、ミスを犯しにくくなります。 そして、それをテストするのは難しいので(このためにはGWT環境全体を上げる必要があります)、これは手元にあるだけです-テストしません。
MVPの詳細については、こちらをご覧ください。
GWTのMVP。
ホワイトペーパー:
www.gwtproject.org/articles/mvp-architecture.htmlパッシブビュー。 M.ファウラー:
martinfowler.com/eaaDev/PassiveScreen.htmlGWTのMVP:
www.javabeat.net/what-is-model-view-presenter-mvp-in-gwt-applicationさまざまなMV *パターン:
outcoldman.com/en/archive/2010/02/22/patterns-mvc-mvp--mvvm2)
DI (依存性注入)-GWTの実装-GIN、GWTを起動せずにテストする-Guice。 GWT.create()ではなくPresentersのすべての依存関係は、Injectアノテーションを使用して定義されます。 つまり 代わりにGWT RPCを使用する場合
ServiceAsync serviceAsync = GWT.create(ServiceAsync.class);
書く
@Inject ServiceAsync serviceAsync;
GWT環境を完全に上げずにテストする場合も必要です。この場合、作成できない依存関係(たとえば、View実装)はモックに置き換えられます。 さらに、GINとGuiceをペアで使用します-同じアノテーションを使用してDI(Inject、Provide)を決定するため、コードを別のDIフレームワーク用に変更する必要はなく、構成する必要はありません-依存関係を正しくバインドするだけです。
3)
モック -オブジェクトをエミュレートします。 Presenterが正しく機能するには、少なくともViewを置き換える必要があります。 モックのフレームワークとして、Mockitoを使用します。
4)
SyncProxy -JavaからGWT RPCリクエストを実行するためのフレームワーク。 GWT環境を上げることなくGWT RPC呼び出しを行う機能を提供するテストに必要です。
SyncProxy
code.google.com/p/gwt-syncproxy5)
Jetty-サーブレットコンテナー。 Webアプリケーションを実行する必要がありました。
6)
GWT Editor -POJOオブジェクトをUIコンポーネントに自動的にマッピングできるGWTフレームワーク。 ビューをマッピングからアンロードして、パッシブビューに近づける必要があります。
GWTエディター。 公式ドキュメント:
www.gwtproject.org/doc/latest/DevGuideUiEditors.htmlGXTのGWTエディター:
docs.sencha.com/gxt/3.1.0-beta/data/Editors.html7)
アクティビティと場所 -便利な履歴メカニズム(ブラウザでのページアクセスの履歴-戻るボタンと進むボタン)のためのGoogleのフレームワーク。 原則として、ここでは必須ではありませんが、ブラウザ履歴のサポートを簡単に実装できます。 また考慮してください。
アクティビティと場所。 公式ドキュメント。
www.gwtproject.org/doc/latest/DevGuideMvpActivitiesAndPlaces.html8)
UiBinder -UIの宣言的記述のためのGWTメカニズム。 本質的に、GWT UI要素はhtml / jspページのように-タグを使用して定義されます。 これも必要ではありませんが、ビューをアンロードして、イベントハンドラーと最小限のロジックのみをそこに残すことができます。
UiBinder 公式ドキュメント:
www.gwtproject.org/doc/latest/DevGuideUiBinder.html未使用の技術
1)
Selenium -
Seleniumで作成されたテストは、保守、作成、実行が困難です。 完全な統合テストは可能ですが、稼働状態でサポートする別の担当者と、迅速に起動できる別のマシンが必要です。 小規模なチームでは、サーバーを選択してSeleniumテストを実行するように構成できる場合、それらをサポートする個人を見つけることは非常に困難です。 T.ch. 他のプロジェクト(テストを作成したQAエンジニア)で使用できましたが、考慮していません。
2)
GWTTestCase -GWT JUnitランナー。これにより、JUnitテストでGWT環境を上げ、HtmlUnitを使用してブラウザーをエミュレートできます。 ここでは使用しませんでした 長時間起動し(実際、これは開発モードでのGWTの起動です)、HtmlUnitを使用しているため、一部のJavaScriptエミュレーションが正しく機能しない場合があります。 一般に、これは中途半端なソリューションです-一方ではGWTが起動しますが、他方では-長時間起動し、すべての場合に正しく動作しません。 T.ch. また、使用しませんでしたが、場合によっては適切かもしれません。
アプリケーションアーキテクチャとハイライト
繰り返しますが、GWTでのテストを容易にする主なパターンは、パッシブビューを備えたMVPです。 その本質は、すべてのロジックをPresenterに集中させ、ビューをできるだけシンプルにすることです(コードが単純になるほど、コード内のエラーが少なくなります。また、ビューはテストでカバーされないため、難しいため、これは非常に重要です)。 同時に、PresenterにはUIコードがありません。UI要素の使用、GWT.create()の呼び出し(DIを使用した依存性注入が代わりに使用されます)、JVMで実行できないものはすべてありません。 これはすべて、JSでコンパイルしたりGWTDevModeを上げたりすることなく、長い時間がかかるJVM上でPresenterのコードを実行できるようにするために必要です。
テストを実行する段階
1)アプリケーションでJettyを上げます。
2)GWT RPCサービスでSyncProxyを構成します。
3)Guiceを使用して、GWT RPCサービス、PresenterおよびViewインターフェイス(Viewモックを使用)の実装を決定します。
4)追加。 ActivityとPlaceを使用する場合は、ActivityMapper、ActivityManager、PlaceControlerを構成します。
5)プレゼンターと(ある場合)アクティビティと場所をテストします。
応用例
例として、学生とグループの編集アプリケーションを作成しました。 学生は1つのグループにのみ所属できます。 合計で4つのフォームがあります-学生のリスト、グループのリスト、学生の表示/編集、グループの表示/編集。
ソースは次の場所にあります:
github.com/TimReset/example.gwt.gxt.testMavenを使用するアプリケーション。 プロジェクトフォルダーには、SyncProxyが配置されているリポジトリフォルダー(Mavenにないため)とIDEAのannotation.jarがあります(IDEAからNotNullおよびNullableアノテーションを使用しているため)。
ベースのプレゼンターおよびビューとして、次のものを使用します。
public interface BasePresenter<V extends BasePresenter.View> { @NotNull V getWidget(); public interface View<T extends BasePresenter> extends IsWidget { void setPresenter(@NotNull T presenter); } }
基本的なプレゼンターの本質は、プレゼンターとビューを操作するための基本的な方法を決定することです:プレゼンターからビューを取得します。 メソッドが必要です Viewと直接連携することはありません(PresenterはViewの状態に責任があり、このViewにどの状態を与えるかを決定する必要があるため)が、Presenterを介してのみです。 ViewにはPresenterをインストールする方法があります。 メソッドが必要な理由は GIN / Guiceで循環依存関係を作成することは不可能です-Presenter'aのインスタンスはViewを指し、ViewはPresenterを指します。
UMLクラス図

この図は、クラス間の関係を示しています-PresenterはViewと通信し、EventBusにメッセージを送信します。 アクティビティはプレゼンターと通信し(図には1つのプレゼンターのみが表示されますが、さらに存在する場合もあります)、EventBusと連携します-プレゼンターからのメッセージやその他のメッセージをリッスンします。
オブジェクト間の相互作用の主なニュアンス:
1)Activityのプレゼンターからのメッセージは、EventBusを介して送信されます。 あるいは、Activityがプレゼンターにインストールするコールバックを使用できます。 ただし、このソリューションには1つの欠点があります。コールバックは消去することを忘れないでください。Presenterがシングルトンであり、アクティビティがないという事実に起因するリソースリークがないようにするためです。 つまり アクティビティはプレゼンターにコールバックを設定します。 新しいアクティビティに切り替えてコールバックを設定しませんでした(そしてプレゼンターがリセットするのを忘れていました)そして、古いアクティビティのコールバックへのリンクがハングするか、さらに悪いことに、このコールバックが呼び出されます。 ActivityおよびPlace GWTメカニズムでは、Activityの作成時にResettableEventBusが渡されますが、これは新しいActivityに切り替えるとクリアされます。 T.ch. アクティビティは、削除を心配することなく、必要なイベントハンドラの数を設定できます。 アクティビティを変更すると、これらのハンドラーは自動的に削除されます。 また、Presenterはコールバックの存在を心配する必要はありません-EventBusにメッセージを送信するだけです。
2)Presenterのアクティビティからのメッセージは、Presenterインターフェイスのメソッドを介して直接送信できます。
3)Viewを持つプレゼンターは、互いのインターフェイスを介して対話します。 テスト中にViewモックの実装を置き換える必要があります。
4)プレゼンターのみがViewを知っている-他の全員はプレゼンターとのみ通信します。 繰り返しになりますが、Viewモックを交換してから依存関係を追跡する場合(Viewについて誰も知らず、簡単に変更できる場合)に便利です。
テスト環境の設定
Jettyはweb.xmlと同じ方法でプログラム可能です-つまり web.xmlに記述されているものはすべて、Jettyのメソッド呼び出しに転送されます。 それだけで、web.xmlへの相対パスを使用できなかったため、web.xmlを明示的に指定しませんでした。特定のパスに合わせたテストを作成したくはありませんでした。 T.ch. web.xmlへの相対パスを使用してJettyを実行できる場合は、問題ありません。
SyncProxyを構成するために、非同期GWT RPCサービスのインターフェース、その実装(JettyでGWT RPCサーブレットを便利に実行するために必要)、およびこれらのサーブレットへのパスのリストとともにMapを使用します。
private static final Map<Class<? extends BaseRemoteService>, Pair<Class<?>, String>> gwtRpcServlets = Collections .unmodifiableMap(new HashMap<Class<? extends BaseRemoteService>, Pair<Class<?>, String>>() { private static final long serialVersionUID = -2126682232601937926L; { put(StudentsServiceImpl.class, new Pair<Class<?>, String>(StudentsServiceAsync.class, "/GxtModule/students")); put(GroupsServiceImpl.class, new Pair<Class<?>, String>(GroupsServiceAsync.class, "/GxtModule/groups")); } });
マッピング自体はこのサイクルで行われます。
for (Map.Entry<Class<? extends BaseRemoteService>, Pair<Class<?>, String>> entry : gwtRpcServlets .entrySet()) {
SyncProxyを使用するときは、次の点に注意してください。
1)非同期インターフェースのインスタンスはMap gwtRpcAsyncInstancesに保存されます。 その後、それらがバインド時に普遍的に受け取られることが必要です。
2)非同期インターフェイスの同期メカニズムが使用されます。 つまり、非同期GWT RPCメソッドが呼び出されると、サーバーが応答(com.google.gwt.user.client.rpc.AsyncCallback#onFailureまたはcom.googleメソッド)を受信するまで、次のコード行は実行されません。 .gwt.user.client.rpc.AsyncCallback#onSuccessこれにより、テストの記述が非常に簡単になります。実際、テストは段階的に同期として記述され、サーバー応答の待機を追加で処理する必要はありません。
3)テストパッケージでcom.google.gwt.user.server.rpc.impl.LegacySerializationPolicyクラスのコピーを作成し、com.google.gwt.user.server.rpc.impl.LegacySerializationPolicy#isInstantiableメソッドをオーバーライドしてクラスをサポートする必要があります。 Serializableの継承者。 T.ch. メソッドコードは次のようになります。
private boolean isInstantiable(Class<?> clazz) { if (clazz.isPrimitive()) { return true; } if (clazz.isArray()) { return isInstantiable(clazz.getComponentType()); }
これは必要です デフォルトでGWT RPCは、GWTポリシーを持つファイル(シリアル化可能なタイプを記述するファイル)がない場合、LegacySerializationPolicyを使用してオブジェクトをシリアル化できるかどうかを確認します。 また、このクラスでは、Serializableインターフェイスはサポートされていません。 T.ch. 手動で追加する必要があります。 このメソッドのオーバーライドは(クラスを完全に置き換えて)使用されます。 GWT RPCはこのクラスのインスタンスを直接作成し、GWT RPCはSerializable Policyを指定できません。
そして、カスタマイズの最後の瞬間は、Guiceでバインドを決定することです。 マップは、Presenterクラス、Presenterの実装クラス、およびこのPresenterのViewクラスとともに、Presenterを定義するためにも使用されます。
private static final Map<Class<? extends BasePresenter>, Pair<Class<? extends BasePresenter>, Class<? extends BasePresenter.View>>> presenters = Collections.unmodifiableMap(new HashMap<Class<? extends BasePresenter>, Pair<Class<? extends BasePresenter>, Class<? extends BasePresenter.View>>>() { private static final long serialVersionUID = 3512350621073004110L; private <T extends BasePresenter> void addPresenter(Class<T> presenterInterface, Class<? extends T> implementPresenter, Class<? extends BasePresenter.View<T>> viewClass) { if (!presenterInterface.isInterface()) { throw new IllegalArgumentException("Should be interface " + presenterInterface.getName()); } if (implementPresenter.isInterface()) { throw new IllegalArgumentException("Should be class " + implementPresenter.getName()); } put(presenterInterface, new Pair<Class<? extends BasePresenter>, Class<? extends BasePresenter.View>>(implementPresenter, viewClass)); } { addPresenter(StudentPresenter.class, StudentPresenterImpl.class, StudentPresenter.View.class); addPresenter(StudentsListPresenter.class, StudentsListPresenterImpl.class, StudentsListPresenter.View.class); addPresenter(MainWindowPresenter.class, MainWindowPresenterImpl.class, MainWindowPresenter.View.class); } });
次に、SyncProxy、Presentersによって作成されたGWT RPC非同期インターフェイスオブジェクトを、View、EventBusのモックの自動作成にバインドし、この場合、ActivityおよびPlaceが動作するようにオブジェクトを作成します。 ここでのすべてがシングルトンとして定義されているという事実に注目します。 プレゼンターとビューにとってこれは重要ではありません。アクティビティとプレースに関連するイベントバスとクラスはそのようにすべきです-なぜなら それらは多くの場所で使用され、相互にリンクしています。
injector = Guice.createInjector(new AbstractModule() { @Override protected void configure() {
プレゼンターStudentPresenterの例
public interface StudentPresenter extends BasePresenter<StudentPresenter.View> { void init(@NotNull Mode mode, @Nullable Integer studentId, @NotNull EventBus eventBus, @NotNull Command onReady); void saveStudent(Student student); boolean isEdit(); void onEditStudent(); Mode getMode(); boolean isDirty(); enum Mode { VIEW, EDIT, CREATE; } interface View extends BasePresenter.View<StudentPresenter> { void setStudent(Student student); boolean isDirty(); } }
ここでは、ViewおよびPresenterが実装する必要があるメソッドについて説明します。
PresenterからViewへのメッセージは、StudentPresenter.Viewインターフェイスを通過します。 これは一般的なMVPスキームです。 ViewからPresenterへのメッセージは、StudentPresenterインターフェイスを経由します。 多くの例では、MVPはこれを行いません。通常、Viewインターフェイスでメソッドを実行してコールバック(setEditClick(Callback c)など)を設定し、PresenterはViewを初期化するときにコールバックを設定します(たとえば、view.setEditClick(new Callback(){/ *ハンドラー関数* /})))。 ただし、Viewは、コールバックを設定するメソッドの実装でそれらを保存し、適切なタイミングで呼び出す必要があります。 コールバックを保存するための同じタイプのコードがたくさんあり、Viewにはこれらのコールバックを持つ多くのフィールドがあるという事実のため、私はこのソリューションが好きではありませんでした。 そして、Presenterには、これらのコールバックのコードを持つ多くの匿名クラスがありますが、これらはあまり読まれません。
したがって、Viewに発表者のインターフェイスへのリンクがあり、ViewからPresenterにメッセージを転送する必要がある場合、Viewは発表者のメソッドを呼び出すだけのオプションを選択しました。 この実装では、このソリューションのマイナス点は、プレゼンターのインターフェイスに実際に2種類のメソッドが含まれていることです-ビューとプレゼンター間の対話に必要なメソッド(上記の例では、これらはsaveStudent、onEditStudent、getMode、isEditメソッド) Presenterと外部の世界との相互作用に必要です(これらはinit、isDirtyメソッドです)。 このマイナスを回避するには、ViewインタラクションメソッドをPresenterと別のインターフェイスに転送し、Viewでこのインターフェイスのインスタンスを転送する必要があります。 実際、このインターフェイスは、StudentPresenterインターフェイスと同じクラスで実装できます。
オプションのインターフェースの例
public interface StudentPresenter extends BasePresenter<StudentPresenter.View> { void init(@NotNull Mode mode, @Nullable Integer studentId, @NotNull EventBus eventBus, @NotNull Command onReady); boolean isDirty(); enum Mode { VIEW, EDIT, CREATE; } interface ViewPresenter extends BasePresenter.ViewPresenter{ void saveStudent(Student student); boolean isEdit(); void onEditStudent(); Mode getMode(); } interface View extends BasePresenter.View<StudentPresenter.ViewPresenter> { void setStudent(Student student); boolean isDirty(); } }
この場合、Presenterコンシューマーには、Viewを操作するための内部メソッドではなく、それらのユーザー向けのメソッドのみが表示されます。
私が理解しているように、C#では、ViewからPresenterにメッセージを送信するタスクはイベントの助けを借りて十分に解決されています。試験例
学生の
リストと学生の編集フォームをテストします学生のリスト学生の
編集フォームテストでは、次のメソッドが追加で使用されます。 void goToWithAssert(Place newPlace) void assertWhere(Place expectedWhere); void assertWhere(BasePresenter presenter);
これらのメソッドは、現在の場所を確認し、現在画面に表示されているものを理解するために必要です。ここで、学生のリストをチェックするテストの例を考えてみましょう。学生のリストにプレゼンターの例を挙げません。フルネームru.timreset.example.gxt.client.presenter.StudentPresenterで見つけることができます @Test public void listStudent() {
これは取るに足らない例ですが、すでにメインメニューと生徒のリストの操作を確認しています。次に、より複雑な例を考えてみましょう-学生を作成します。生徒を作成するとき、完全なチェーンを確認します1)メニューの「生徒のリスト」をクリックします。2)学生のリストで[学生の作成]をクリックします。3)学生作成ウィンドウのフィールドに入力します。3)生徒を保存します。4)保存した生徒がリストに載っていることを確認します。同時に、各ステップで、目的のフォームとリストが表示されていることを確認します。 @Test public void editTest() {
ここでは、最初のテストと同様に、学生のリストに切り替えます。リストに切り替えたことを確認します。そこで、「生徒を作成」をクリックします。「生徒作成」ウィンドウに切り替えたことを確認します。ウィンドウ自体の存在(目的のビューが表示されることを確認する)と場所の存在の両方を確認します。次に、フィールドに入力して生徒を作成します。そして、作成された生徒がリストに載っていることを確認します。おわりに
行われたことを要約するために、基本的なインターフェースとそれらの間の相互作用の説明を備えたMVPアーキテクチャの例が開発されました。さらに、GWT Editor、UiBinder、Activity、およびPlaceが使用されます。テスト環境を作成しました。このアプローチを実証するために、いくつかのテストが作成されています。残念ながら、時間の不足(および怠)のため、グループを作成し、グループに追加する学生を選択するためのリストとして学生の発表者リストを使用する完全な例を実装しませんでした。このアプローチの長所と制限を取り消す
長所:
1)コールチェーンカバレッジ「UIロジック-サーバーとの相互作用-サーバーロジック-DBMSとの相互作用」でテストをすばやく実行できます。2)テストの書きやすさ(ボリュームに驚かないでください。簡単に順番に書きます)。3)一般に、ロジックと表現の分離は、大きなUIアプリケーションを作成するときに役立ちます。そしてここ制限事項:
1)UI要素をテストすることはできません。たとえば、フォームにボタンがある場合、クリックしても目的のアクションが発生することを確認できません。このボタンのクリックハンドラーとPresenterの対応するメソッドの呼び出しとの間に接続があることを確認できません。これらのエラーは、最大シンプルビューを使用することで部分的に防止されますが、除外することはできません。2)MVPは、複雑なロジックを持つ大規模なアプリケーションにより適しています-シンプルなロジックを持つフォームが数個しかない場合、MVPを使用するとコードとクラス数に多くのオーバーヘッドが生じます。一般に、私にとっては、このアプローチを使用することにしました。クラス間の義務の明確な分離(MVPによる)、コードのパフォーマンスの迅速なチェック(重いGWT環境を持ち上げる必要がないため)。さらに読む
GWT Mockitowww.objectpartners.com/2013/11/07/testing-gwt-with-gwtmockitoGWTアプリケーションの単体テストと統合テストwww.infoq.com/articles/gwt_unit_testing