AnyStub、Java接続スタブライブラリ

多くのプラットフォームとは異なり、Javaには接続スタブライブラリが不足しています。 この世界に長い間住んでいるなら、おそらくWireMock、Betamax、またはSpockに精通しているはずです。 テストの多くの開発者は、Mockitoを使用してオブジェクトの動作、ローカルh2データベースを使用したDataJpaTest、Cucumberテストを記述します。 今日は、これらのアプローチを使用して発生する可能性のあるさまざまな問題に対処するのに役立つ軽量の代替手段に会います。 特に、anyStubは次の問題を解決しようとします。









anyStubとは何ですか?



AnyStubは関数呼び出しをラップし、すでに記録されている一致する呼び出しを見つけようとします。 これにより、次の2つのことが起こります。









すぐに使用できるanyStubは、httpリクエストのスタブとjavax.sqlからのいくつかのインターフェースを作成するためのApache HttpClientからhttpクライアントのラッパーを提供します。 他の接続用のスタブを作成するためのAPIも提供されます。







AnyStubは単純なクラスライブラリであり、環境の特別な構成は必要ありません。 このライブラリは、スプリングブートアプリケーションを使用することを目的としており、このパスに従うことで最大限のメリットが得られます。 Springの外部、プレーンなJavaアプリケーションで使用できますが、間違いなく追加の作業が必要になります。 以下の説明は、スプリングブートアプリケーションのテストに焦点を当てています。







統合テストを見てみましょう。 これは、システムをテストする最もエキサイティングで包括的な方法です。 実際、魔法の注釈を書くとき、spring-bootとJUnitはほとんどすべてを行います。







@RunWith(SpringRunner.class) @SpringBootTest
      
      





現在、統合テストは過小評価されており、限られた範囲で使用されており、一部の開発者はそれらを避けています。 これは主に、テストの準備とメンテナンスに時間がかかるため、またはビルドサーバーで環境を特別に構成する必要があるためです。







anyStubを使用すると、スプリングコンテキストを無効にする必要はありません。 代わりに、コンテキストを本番構成に近づけることは簡単で簡単です。







この例では、Pivo​​talのマニュアルからanyStubをConsuming a RESTful Web Serviceに接続する方法を説明します。







pom.xmlを介してライブラリを接続する







  <dependency> <groupId>org.anystub</groupId> <artifactId>anystub</artifactId> <version>0.2.27</version> <scope>test</scope> </dependency>
      
      





次のステップは、スプリングコンテキストを変更することです。







 package hello; import org.anystub.http.StubHttpClient; import org.apache.http.client.HttpClient; import org.apache.http.impl.client.HttpClientBuilder; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.boot.web.client.RestTemplateCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestTemplate; @Configuration public class TestConfiguration { @Bean public RestTemplateBuilder builder() { RestTemplateCustomizer restTemplateCustomizer = new RestTemplateCustomizer() { @Override public void customize(RestTemplate restTemplate) { HttpClient real = HttpClientBuilder.create().build(); StubHttpClient stubHttpClient = new StubHttpClient(real); HttpComponentsClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(); requestFactory.setHttpClient(stubHttpClient); restTemplate.setRequestFactory(requestFactory); } }; return new RestTemplateBuilder(restTemplateCustomizer); } }
      
      





この変更により、アプリケーション内のコンポーネントの関係は変更されませんが、単一のインターフェースの実装のみが置き換えられます。 これにより、 Barbara Lisk Substitution Principleに送られます。 アプリケーションの設計がそれを侵害しない場合、この置換は機能を侵害しません。







すべて準備完了です。 このプロジェクトにはすでにテストが含まれています。







 @RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTest { @Autowired private RestTemplate restTemplate; @Test public void contextLoads() { assertThat(restTemplate).isNotNull(); } }
      
      





このテストは空ですが、すでにアプリケーションコンテキストを実行しています。 ここから楽しみが始まります 。 上で述べたように、テストのアプリケーションコンテキストは、外部システムへのhttpリクエストが実行されるCommandLineRunnerが作成される作業コンテキストと一致します。







 @SpringBootApplication public class Application { private static final Logger log = LoggerFactory.getLogger(Application.class); public static void main(String args[]) { SpringApplication.run(Application.class); } @Bean public RestTemplate restTemplate(RestTemplateBuilder builder) { return builder.build(); } @Bean public CommandLineRunner run(RestTemplate restTemplate) throws Exception { return args -> { Quote quote = restTemplate.getForObject( "https://gturnquist-quoters.cfapps.io/api/random", Quote.class); log.info(quote.toString()); }; } }
      
      





これは、ライブラリの動作を示すのに十分です。 初めてテストを開始すると、新しいcomplete/src/test/resources/anystub/stub.yml









 request0: exception: [] keys: [GET, HTTP/1.1, 'https://gturnquist-quoters.cfapps.io/api/random'] values: [HTTP/1.1, '200', OK, 'Content-Type: application/json;charset=UTF-8', 'Date: Thu, 25 Apr 2019 23:04:49 GMT', 'X-Vcap-Request-Id: 5ffce9f3-d972-4e95-6b5c-f88f9b0ae29b', 'Content-Length: 177', 'Connection: keep-alive', '{"type":"success","value":{"id":3,"quote":"Spring has come quite a ways in addressing developer enjoyment and ease of use since the last time I built an application using it."}}']
      
      





どうした spring-bootは、テスト構成からアプリケーションにRestTemplateBuilderを構築しました。 これにより、httpクライアントのスタブ実装を介してアプリケーションが実行されました。 StubHttpClientはリクエストをインターセプトし、スタブファイルを見つけられず、リクエストを実行し、結果をファイルに保存し、ファイルから回復した結果を返しました。







これ以降、インターネットに接続せずにこのテストを実行でき、このリクエストは成功します。 restTemplate.getForObject()



は同じ結果を返します。 将来のテストでこの事実に頼ることができます。







説明されいるすべての変更はGitHubで見つけることができます。







実際、まだ単一のテストを作成していません。 テストを作成する前に、データベースでどのように機能するかを見てみましょう。







この例では、Pivo​​talマニュアルのSpringとJDBCを使用したリレーショナルデータへのアクセスに統合テストを追加します







この場合のテスト構成は次のようになります。







 package hello; import org.anystub.jdbc.StubDataSource; import org.h2.jdbcx.JdbcDataSource; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; @Configuration public class TestConfiguration { @Bean public DataSource dataSource() { JdbcDataSource ds = new JdbcDataSource(); ds.setURL("jdbc:h2:./test"); return new StubDataSource(ds); } }
      
      





ここでは、外部データベースに対して通常のデータソースが作成され、スタブ実装-StubDataSourceクラスでラップされます。 Spring-bootはそれをコンテキストに埋め込みます。 また、テストでスプリングコンテキストを実行するには、少なくとも1つのテストを作成する必要があります。







 package hello; import org.anystub.AnyStubId; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import static org.junit.Assert.*; @RunWith(SpringRunner.class) @SpringBootTest public class ApplicationTest { @Test @AnyStubId public void test() { } }
      
      





これも空のテストです-その唯一のタスクはアプリケーションコンテキストを実行することです。 ここでは、非常に重要なアノテーション@AnystubId



が表示されますが、まだ含まれていません。







最初の実行後、すべてのデータベース呼び出しを含む新しいsrc/test/resources/anystub/stub.yml



src/test/resources/anystub/stub.yml



。 データベースを使用すると、春が舞台裏でどのように機能するかに驚くでしょう。 テストを新たに実行しても、データベースへの実際のアクセスは行われないことに注意してください。 test.mv.dbを削除すると、テストを繰り返し実行しても表示されません。 変更の完全なセットはGitHubで表示できます。







まとめると。 anyStubの場合:









おそらく疑問があります。データベースがまだ存在しない場合、どのようにこれをカバーするのか、否定的なテストと例外処理をどうするのか。 これに戻りますが、最初に、簡単なテストの作成に取り組みます。







現在、RESTful Webサービスの使用を実験しています 。 このプロジェクトには、テスト可能なコンポーネントは含まれていません。 アーキテクチャ設計の2つの層を表す2つのクラスが下に作成されます。







 package hello; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; @Component public class DataProvider { private final RestTemplate restTemplate; public DataProvider(RestTemplate restTemplate) { this.restTemplate = restTemplate; } Quote provideData() { return restTemplate.getForObject( "https://gturnquist-quoters.cfapps.io/api/random", Quote.class); } }
      
      





DataProviderは、データへのアクセスを提供します 揮発性 外部システム。







 package hello; import org.springframework.stereotype.Component; @Component public class DataProcessor { private final DataProvider dataProvider; public DataProcessor(DataProvider dataProvider) { this.dataProvider = dataProvider; } int processData() { return dataProvider.provideData().getValue().getQuote().length(); } }
      
      





DataProcessorは、外部システムからのデータを処理します。







DataProcessor



をテストするDataProcessor



です。 処理アルゴリズムの正確性をテストし、将来の変更による劣化からシステムを保護する必要があります。







これらの目標を達成するには、データセットを使用してDataProviderモックオブジェクトを作成し、テストでDataProcessorコンストラクターに渡すことを検討します。 別の方法として、DataProcessorを分解してQuoteクラスの処理を強調表示することもできます。 次に、このようなクラスは、単体テストを使用して簡単にテストできます(これは、クリーンコードに関する著書で推奨される方法です)。 コードの変更とテストデータの発明を避け、テストを作成してみましょう。







 @RunWith(SpringRunner.class) @SpringBootTest public class DataProcessorTest { @Autowired private DataProcessor dataProcessor; @Test @AnyStubId(filename = "stub") public void processDataTest() { assertEquals(131, dataProcessor.processData()); } }
      
      





@AnystubIdアノテーションについて説明します。 この注釈は、テストでスタブファイルを管理および制御するのに役立ちます。 テストクラスまたはそのメソッドで使用できます。 この注釈は、対応する領域に個別のスタブファイルを設定します。 いずれかの領域がクラスおよびメソッドレベルのアノテーションで同時にカバーされている場合、メソッドアノテーションが優先されます。 この注釈には、スタブファイルの名前を定義するfilenameパラメーターがあります。 省略した場合、拡張子「.yml」が自動的に追加されます。 このテストを実行しても、新しいファイル見つかりません。 src/test/resources/anystub/stub.yml



はすでに以前に作成されており、このテストはそれを再利用します。 クエリの結果を分析することにより、このスタブから131番を取得しました。







  @Test @AnyStubId public void processDataTest2() { assertEquals(131, dataProcessor.processData()); Base base = getStub(); assertEquals(1, base.times("GET")); assertTrue(base.history().findFirst().get().matchEx_to(null, null, ".*gturnquist-quoters.cfapps.io.*")); }
      
      





このテストでは、@ AnyStubIdアノテーションがfilenameパラメーターなしで表示されます。 この場合、 src/test/resources/anystubprocessDataTest2.yml



ます。 ファイル名は、関数の名前(クラス)+ ".yml"から作成されます。 anyStubがこのテスト用の新しいファイルを作成したら、実際のシステムコールを行う必要があります。 そして、新しい見積もりの​​長さが同じであることは幸運です。 最後の2つのチェックは、アプリケーションの動作をテストする方法を示しています。 パラメータまたはパラメータの一部によってクエリを選択し、クエリの数をカウントします。 ドキュメントには、時間と一致関数のバリエーションがいくつかあります







  @Test @AnyStubId(requestMode = RequestMode.rmTrack) public void processDataTest3() { assertEquals(79, dataProcessor.processData()); assertEquals(79, dataProcessor.processData()); assertEquals(168, dataProcessor.processData()); assertEquals(79, dataProcessor.processData()); Base base = getStub(); assertEquals(4, base.times("GET")); }
      
      





このテストでは、新しいrequestModeパラメーターとともに@AnyStubIdが表示されます。 スタブファイルの権限を管理できます。 制御する2つの側面があります:ファイルによる検索と外部システムを呼び出す許可。







RequestMode.rmTrack



は、次のルールを設定します:ファイルが作成されたばかりの場合、すべての要求は外部システムに送信され、ファイルに同一の要求があるかどうかに関係なく、回答とともにファイルに書き込まれます(ファイル内の重複は許可されます)。 テストを実行する前にスタブファイルが存在する場合、外部システムへのリクエストは禁止されています。 呼び出しはまったく同じ順序で行われます。 次の要求がファイル内の要求と一致しない場合、例外がスローされます。







RequestMode.rmNew



このモードはデフォルトでアクティブになっています。 各要求はスタブファイルで検索されます。 一致する要求が見つかった場合-対応する結果がファイルから復元され、外部システムへの要求は延期されます。 要求が見つからない場合、外部システムが要求され、結果がファイルに保存されます。 ファイル内の重複したリクエスト-発生しません。







RequestMode.rmNone



各リクエストはスタブファイルで検索されます。 一致するクエリが見つかった場合、その結果はファイルから復元されます。 テストがファイルにない要求を生成すると、例外がスローされます。







RequestMode.rmAll



は、最初の要求の前に、スタブファイルが消去されます。 すべての要求はファイルに書き込まれます(ファイル内の重複は許可されます)。 接続の動作を監視する場合は、このモードを使用できます。







RequestMode.rmPassThrough



すべての要求は、実装スタブをバイパスして、外部システムに直接送信されます。







これらの変更はGitHubで入手できます。







他に何?



anyStubが応答を保存する方法を見ました。 外部システムへのアクセス時に例外がスローされた場合、anyStubはそれを保存し、後続のリクエストでそれを再現します。







多くの場合、例外はトップレベルのクラスによってスローされますが、接続クラスは有効な応答を取得します(おそらくエラーコードで)。 この場合、anyStubはエラーコードを使用して答えを再現し、上位クラスもテストの例外をスローします。







リポジトリにスタブファイルを追加します。







スタブファイルを削除して上書きすることを恐れないでください。







スタブファイルを賢く管理します。 1つのファイルを複数のテストで再利用するか、各テストに個別のファイルを提供できます。 必要に応じてこの機会を利用してください。 ただし、通常、異なるアクセスモードで単一のファイルを使用することはお勧めできません。







これらはすべてanyStubの主な機能です。








All Articles