これは、なにをしたくて書いたもの?
Quarkusでのテストのやり方、書き方を覚えてみようかなということで。
こちらのガイドに沿って、見ていきます。
Testing Your Application - Quarkus
環境
今回の環境は、こちらです。
$ java --version openjdk 11.0.10 2021-01-19 OpenJDK Runtime Environment (build 11.0.10+9-Ubuntu-0ubuntu1.20.04) OpenJDK 64-Bit Server VM (build 11.0.10+9-Ubuntu-0ubuntu1.20.04, mixed mode, sharing) $ mvn --version Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 11.0.10, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-66-generic", arch: "amd64", family: "unix"
Quarkusは、1.12.1.Finalを使用します。
お題
以下のお題で行います。
- SmallRye Mutinyを使う
- CDI管理Beanのテストをする
- JAX-RSリソースクラスのテストをする
- 設定ファイルの項目を読み込み、かつテスト時に値を切り替える
- ネイティブイメージは対象外とする
なので、テストのガイド以外にも次のようなガイドも参照して書いています。
Getting Started With Reactive - Quarkus
Writing JSON REST Services - Quarkus
Configuring Your Application - Quarkus
Configuration Reference Guide - Quarkus
プロジェクトを作成する
では、まずはプロジェクトを作成します。
$ mvn io.quarkus:quarkus-maven-plugin:1.12.1.Final:create \ -DprojectGroupId=org.littlewings \ -DprojectArtifactId=resteasy-testing \ -DprojectVersion=0.0.1-SNAPSHOT \ -Dextensions="resteasy-mutiny,resteasy-jackson"
Extensionは、resteasy-mutiny
、resteasy-jackson
の2つにしました。
----------- selected extensions: - io.quarkus:quarkus-resteasy-jackson - io.quarkus:quarkus-resteasy-mutiny applying codestarts... 🔠 java 🧰 maven 🗃 quarkus 📜 config-properties 🛠 dockerfiles 🛠 maven-wrapper 🐒 resteasy-jackson-example
作成されたディレクトリ内に移動。
$ cd resteasy-testing
pom.xml
に書かれている依存関係は、こちらです。
pom.xml
<dependencies> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-jackson</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-mutiny</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-arc</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5</artifactId> <scope>test</scope> </dependency> </dependencies>
テスト回りは、quarkus-junit5
が書かれているのみです。
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5</artifactId> <scope>test</scope> </dependency>
ガイドを見ると、REST-assuredというものも使うようなので、dependency
に追加します。
Testing Your Application / Recap of HTTP based Testing in JVM mode
<dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency>
REST-assuredはQuarkusのBOMに入っているので、バージョンの指定は不要です。
https://github.com/quarkusio/quarkus/blob/1.12.1.Final/bom/application/pom.xml#L2681-L2704
src
ディレクトリの中を見てみます。
$ find src -type f src/main/docker/Dockerfile.native-distroless src/main/docker/Dockerfile.jvm src/main/docker/Dockerfile.native src/main/docker/Dockerfile.legacy-jar src/main/resources/META-INF/resources/index.html src/main/resources/application.properties src/main/java/org/littlewings/resteasyjackson/JacksonResource.java src/main/java/org/littlewings/resteasyjackson/MyObjectMapperCustomizer.java
resteasyjackson
という不思議なパッケージがありますね。
こういったファイルが生成される元は、こちらのようです。
中身を見ましたが、今回は要らない気がするので削除。
$ rm -rf src/main/java/org/littlewings/resteasyjackson
なお、削除したファイルの元はこちらにあります。
気になる方は、中身をどうぞ。
アプリケーションを書く
では、テスト対象となるアプリケーションを書きましょう。
お題は書籍ということで。
src/main/java/org/littlewings/testing/entity/Book.java
package org.littlewings.testing.entity; public class Book { String isbn; String title; int price; public static Book create(String isbn, String title, int price) { Book book = new Book(); book.setIsbn(isbn); book.setTitle(title); book.setPrice(price); return book; } // getter/setterは省略 }
こちらを永続化して保持するものを持ちたいところですが、今回はConcurrentHashMap
で持つことにします。
このクラスは、CDI管理Beanとして定義します。
src/main/java/org/littlewings/testing/repository/InMemoryBookRepository.java
package org.littlewings.testing.repository; import java.util.Comparator; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.stream.Collectors; import javax.enterprise.context.ApplicationScoped; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; import org.littlewings.testing.entity.Book; @ApplicationScoped public class InMemoryBookRepository { ConcurrentMap<String, Book> books = new ConcurrentHashMap<>(); public Uni<Book> insert(Book book) { return Uni.createFrom().item(books.put(book.getIsbn(), book)).map(b -> book); } public Uni<Book> findByIsbn(String isbn) { return Uni.createFrom().item(books.get(isbn)); } public Multi<Book> findAll() { return Multi .createFrom() .iterable(books.values().stream().sorted(Comparator.comparingInt(Book::getPrice).reversed()).collect(Collectors.toList())); } public Uni<Integer> size() { return Uni.createFrom().item(books.size()); } public Uni<Book> delete(String isbn) { return Uni.createFrom().item(books.remove(isbn)); } public Uni<Void> clear() { return Uni.createFrom().voidItem().onItem().invoke(() -> books.clear()); } }
2つのクラスを使用する、JAX-RSリソースクラス。簡単な読み書きができるだけですね。
src/main/java/org/littlewings/testing/rest/BookResource.java
package org.littlewings.testing.rest; import javax.inject.Inject; import javax.ws.rs.Consumes; import javax.ws.rs.DELETE; import javax.ws.rs.GET; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; import org.littlewings.testing.entity.Book; import org.littlewings.testing.repository.InMemoryBookRepository; @Path("book") public class BookResource { @Inject InMemoryBookRepository bookRepository; @GET @Path("{isbn}") @Produces(MediaType.APPLICATION_JSON) public Uni<Book> find(@PathParam("isbn") String isbn) { return bookRepository.findByIsbn(isbn); } @GET @Produces(MediaType.APPLICATION_JSON) public Multi<Book> findAll() { return bookRepository.findAll(); } @PUT @Path("{isbn}") @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Uni<Book> put(@PathParam("isbn") String isbn, Book book) { return bookRepository.insert(book); } @DELETE @Path("{isbn}") @Produces(MediaType.APPLICATION_JSON) public Uni<Response> delete(@PathParam("isbn") String isbn) { return bookRepository .delete(isbn) .onItem() .transform(b -> b != null ? Response.Status.NO_CONTENT : Response.Status.NOT_FOUND) .onItem() .transform(status -> Response.status(status).build()); } }
設定ファイルに項目も定義しましょう。
src/main/resources/application.properties
app.config.message1=Hello World!! app.config.message2=Hello Quarkus!! app.config.message3=Wow!!
定義した項目を返すJAX-RSリソースクラス。
src/main/java/org/littlewings/testing/rest/ConfigResource.java
package org.littlewings.testing.rest; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; import org.eclipse.microprofile.config.inject.ConfigProperty; @Path("config") public class ConfigResource { @ConfigProperty(name = "app.config.message1") String message1; @ConfigProperty(name = "app.config.message2") String message2; @ConfigProperty(name = "app.config.message3") String message3; @GET @Path("message1") @Produces(MediaType.TEXT_PLAIN) public String message1() { return message1; } @GET @Path("message2") @Produces(MediaType.TEXT_PLAIN) public String message2() { return message2; } @GET @Path("message3") @Produces(MediaType.TEXT_PLAIN) public String message3() { return message3; } }
軽く、動作確認しましょう。
$ mvn package $ java -jar target/quarkus-app/quarkus-run.jar
データの登録。
$ curl -XPUT -H 'Content-Type: application/json' localhost:8080/book/978-4295008477 -d '{ "isbn": "978-4295008477", "title": "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]", "price": 2860 }' {"isbn":"978-4295008477","title":"新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]","price":2860}k
取得。
$ curl localhost:8080/book/978-4295008477 {"isbn":"978-4295008477","title":"新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]","price":2860} $ curl localhost:8080/book [{"isbn":"978-4295008477","title":"新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]","price":2860}]
設定ファイルの項目取得。
$ curl localhost:8080/config/message1 Hello World!! $ curl localhost:8080/config/message2 Hello Quarkus!! $ curl localhost:8080/config/message3 Wow!!
OKですね。では、これらのテストを書いていきましょう。
Quakusでのテストを書く
Quarkusでのテストに関するガイドは、こちらになります。
Testing Your Application - Quarkus
Maven依存関係としてquarkus-junit5
は必須で、rest-assured
はHTTPに関するテストを行う場合に必要に応じて追加、という感じですね。
また、Maven Surefire Pluginの設定として、JBoss Log Managerの設定を入れるようにします。といってもMavenプロジェクトを
作った段階でおの設定は入っていますが。
<plugin> <artifactId>maven-failsafe-plugin</artifactId> <version>${surefire-plugin.version}</version> <executions> <execution> <goals> <goal>integration-test</goal> <goal>verify</goal> </goals> <configuration> <systemPropertyVariables> <native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path> <java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager> <maven.home>${maven.home}</maven.home> </systemPropertyVariables> </configuration> </execution> </executions> </plugin>
実際にテストを書いていく際に基本となるのは、@QuarkusTest
というアノテーションのようです。
CDI管理Beanのテストを書く
ガイドに書かれている順とは異なりますが、最初にCDI管理Beanのテストを書いてみましょう。
Testing Your Application / Injection into tests
作成したテストコードは、こちら。
src/test/java/org/littlewings/testing/repository/InMemoryBookRepositoryTest.java
package org.littlewings.testing.repository; import java.util.List; import javax.inject.Inject; import io.quarkus.test.junit.QuarkusTest; import io.smallrye.mutiny.helpers.test.AssertSubscriber; import io.smallrye.mutiny.helpers.test.UniAssertSubscriber; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.littlewings.testing.entity.Book; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; @QuarkusTest public class InMemoryBookRepositoryTest { @Inject InMemoryBookRepository bookRepository; @BeforeEach public void setup() { List.of( Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]", 2860), Book.create("978-4295008583", "マイクロサービスパターン[実践的システムデザインのためのコード解説]", 5280), Book.create("978-1492062653", "Quarkus Cookbook: Kubernetes-optimized Java Solutions", 6376), Book.create("978-4295007753", "クラウドネイティブ・アーキテクチャ 可用性と費用対効果を極める次世代設計の原則", 4290) ).forEach(b -> bookRepository.insert(b).subscribe().with(v -> { })); } @AfterEach public void teardown() { bookRepository.clear().subscribe().with(v -> { }); } @Test public void findByIsbnTest() { UniAssertSubscriber<Book> subscriber = bookRepository.findByIsbn("978-4295008477").subscribe().withSubscriber(UniAssertSubscriber.create()); Book book = subscriber .assertCompleted() .getItem(); assertThat(book.getIsbn(), is("978-4295008477")); assertThat(book.getTitle(), is("新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]")); assertThat(book.getPrice(), is(2860)); } @Test public void findAllTest() { UniAssertSubscriber<List<String>> isbnSubscriber = bookRepository .findAll() .map(Book::getIsbn) .collect() .asList() .subscribe() .withSubscriber(UniAssertSubscriber.create()); isbnSubscriber .assertCompleted() .assertItem( List.of( "978-1492062653", // price: 6379 "978-4295008583", // price: 5280 "978-4295007753", // price: 4290 "978-4295008477" // price: 2860 ) ); AssertSubscriber<String> subscriber = bookRepository .findAll() .map(Book::getIsbn) .subscribe() .withSubscriber(AssertSubscriber.create(10)); subscriber .assertCompleted() .assertItems( "978-1492062653", // price: 6379 "978-4295008583", // price: 5280 "978-4295007753", // price: 4290 "978-4295008477" // price: 2860 ); } @Test public void putTest() { bookRepository .insert(Book.create("978-4295009795", "Kubernetes完全ガイド 第2版", 4400)) .subscribe() .withSubscriber(UniAssertSubscriber.create()); UniAssertSubscriber<Integer> subscriber = bookRepository.size().subscribe().withSubscriber(UniAssertSubscriber.create()); subscriber.assertCompleted().assertItem(5); } @Test public void deleteTest() { bookRepository .delete("978-4295007753") .subscribe() .withSubscriber(UniAssertSubscriber.create()) .assertCompleted(); UniAssertSubscriber<Integer> subscriber = bookRepository.size().subscribe().withSubscriber(UniAssertSubscriber.create()); subscriber.assertCompleted().assertItem(3); } }
テストクラスには、@QuarkusTest
アノテーションを付与します。
@QuarkusTest public class InMemoryBookRepositoryTest {
この状態で、CDI管理Beanをふつうに@Inject
することができます。
@Inject
InMemoryBookRepository bookRepository;
InMemoryBookRepository
クラスで持つデータは、テストの度に初期データ登録、削除するようにしています。
@Inject InMemoryBookRepository bookRepository; @BeforeEach public void setup() { List.of( Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]", 2860), Book.create("978-4295008583", "マイクロサービスパターン[実践的システムデザインのためのコード解説]", 5280), Book.create("978-1492062653", "Quarkus Cookbook: Kubernetes-optimized Java Solutions", 6376), Book.create("978-4295007753", "クラウドネイティブ・アーキテクチャ 可用性と費用対効果を極める次世代設計の原則", 4290) ).forEach(b -> bookRepository.insert(b).subscribe().with(v -> { })); } @AfterEach public void teardown() { bookRepository.clear().subscribe().with(v -> { }); }
この範囲だと、基本的にはJUnit 5を使ったテストの話なのですが、SmallRye Mutiniyに関するテストに関してだけ少し書いて
おきましょう。
SmallRye Mutinyにテストに関する情報は、こちらに書かれています。
How can I write unit / integration tests?
Uni
に対するテストの場合は、UniAssertSubscriber
を使います。
@Test public void findByIsbnTest() { UniAssertSubscriber<Book> subscriber = bookRepository.findByIsbn("978-4295008477").subscribe().withSubscriber(UniAssertSubscriber.create()); Book book = subscriber .assertCompleted() .getItem(); assertThat(book.getIsbn(), is("978-4295008477")); assertThat(book.getTitle(), is("新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]")); assertThat(book.getPrice(), is(2860)); }
Multi
の場合は、AssertSubscriber
を使います。以下は、findAll
の結果を1度Uni
に変換してアサーションしているものと、
Multi
のままアサーションしているものです。
@Test public void findAllTest() { UniAssertSubscriber<List<String>> isbnSubscriber = bookRepository .findAll() .map(Book::getIsbn) .collect() .asList() .subscribe() .withSubscriber(UniAssertSubscriber.create()); isbnSubscriber .assertCompleted() .assertItem( List.of( "978-1492062653", // price: 6379 "978-4295008583", // price: 5280 "978-4295007753", // price: 4290 "978-4295008477" // price: 2860 ) ); AssertSubscriber<String> subscriber = bookRepository .findAll() .map(Book::getIsbn) .subscribe() .withSubscriber(AssertSubscriber.create(10)); subscriber .assertCompleted() .assertItems( "978-1492062653", // price: 6379 "978-4295008583", // price: 5280 "978-4295007753", // price: 4290 "978-4295008477" // price: 2860 ); }
AssertSubscriber
を使う時は、create
の引数にリクエストする数を書いておかないと、省略すると0
を指定したことになって
一切Subscribeしてくれません。最初、これにハマりました…。
AssertSubscriber<String> subscriber =
bookRepository
.findAll()
.map(Book::getIsbn)
.subscribe()
.withSubscriber(AssertSubscriber.create(10));
CDI管理Beanのテストは、こんな感じですね。
ちなみに、@Transactional
アノテーションをテストで使うこともできるようですが、自分はSmallRye Mutinyを中心に扱う予定なので、
この機能の出番はなさそうです。
※それとも、MicroProfile Context Propagationを使えばいいんでしょうか?
Testing Your Application / Tests and Transactions
それにしても、SmallRye Mutinyのサイト、以前からだいぶ雰囲気が変わりましたね…。
テスト内でQuarkusにアクセスするURLを取得する
続いては、こちらです。
Testing Your Application / Injecting a URI
Testing Your Application / TestHTTPResource
@TestHTTPResource
というアノテーションを使用すると、QuarkusへアクセスするためのURLをインジェクションできます。
使い方は、こんな感じです。
src/test/java/org/littlewings/testing/rest/InjectUrlTest.java
package org.littlewings.testing.rest; import java.net.URL; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.common.http.TestHTTPResource; import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @QuarkusTest public class InjectUrlTest { @TestHTTPResource URL rootUrl; @TestHTTPResource("book") URL pathSpecificUrl; @TestHTTPEndpoint(BookResource.class) @TestHTTPResource URL resourceClassSpecificUrl; @Test public void injectedUrlTest() { Assertions.assertEquals("http://localhost:8083/", rootUrl.toString()); Assertions.assertEquals("http://localhost:8083/book", pathSpecificUrl.toString()); Assertions.assertEquals("http://localhost:8083/book", resourceClassSpecificUrl.toString()); } }
@QuarkusTest
アノテーションを付与したクラスに対して
@QuarkusTest public class InjectUrlTest {
@TestHTTPResource
アノテーションを付与したURLを宣言すると、QuarkusへアクセスできるURL(http://localhost:[port]
)が
取得できます。
@TestHTTPResource
URL rootUrl;
パスを指定することもできます。
@TestHTTPResource("book") URL pathSpecificUrl;
@TestHTTPEndpoint
アノテーションにJAX-RSリソースクラスを指定すると、JAX-RSリソースクラスに指定された
@Path
アノテーションの値も埋めてくれます。
@TestHTTPEndpoint(BookResource.class) @TestHTTPResource URL resourceClassSpecificUrl;
このため、この機能は@Path
アノテーションに依存しており、JAX-RSリソースクラスに@Path
アノテーションを付与しなかった場合は
//@Path("book") public class BookResource {
テスト実行時にエラーになります。
Caused by: java.lang.RuntimeException: Could not determine the endpoint path for class org.littlewings.testing.rest.BookResource to inject java.net.URL org.littlewings.testing.rest.UrlBookResourceTest.resourceClassSpecificUrl at io.quarkus.test.common.http.TestHTTPResourceManager.inject(TestHTTPResourceManager.java:78)
この3パターンで、@TestHTTPResource
アノテーションを使ってインジェクションしたURLの結果は以下になります。
@Test public void injectedUrlTest() { Assertions.assertEquals("http://localhost:8083/", rootUrl.toString()); Assertions.assertEquals("http://localhost:8083/book", pathSpecificUrl.toString()); Assertions.assertEquals("http://localhost:8083/book", resourceClassSpecificUrl.toString()); }
通常は、このURLを使ってテストコードを書いていくわけですが、今回はやりません。
ところで、Quakursでのテスト時に使われるデフォルトのポートは8081
なのですが、今回こちらの設定を使って変更しています。
Testing Your Application / Controlling the test port
今回は以下のように定義しているのですが、これをどこで定義しているかはまた後で書きます。
quarkus.http.test-port=8083
とりあえず、今回のテストの間は、テストにおけるQuarkusのリッスンポートは8083
となります。
REST-assuredを使ってテストする
先ほどは@TestHTTPResource
アノテーションを使って、QuarkusにアクセスするためのURLを取得しましたが、この方法だと
HTTPのテストに関するサポートがなにもありません。
もっと抽象度の高いテストの方法として、QuarkusではREST-assuredを使うことができます。
Testing Your Application / RESTassured
@TestHTTPEndpoint
と組み合わせて使うことで、JAX-RSリソースクラスへアクセスするテストを書きやすくなります。
REST-assured自体は独立したREST API向けのテストライブラリです。
使い方は、RestAssured
のJavadoc、Wikiを見るとだいたいわかります。
RestAssured - rest-assured 4.3.3 javadoc
Usage · rest-assured/rest-assured Wiki · GitHub
作成したテストコードは、こちら。
src/test/java/org/littlewings/testing/rest/BookResourceTest.java
package org.littlewings.testing.rest; import java.util.List; import javax.inject.Inject; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import org.apache.http.entity.ContentType; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.littlewings.testing.entity.Book; import org.littlewings.testing.repository.InMemoryBookRepository; import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.Matchers.hasSize; @QuarkusTest @TestHTTPEndpoint(BookResource.class) public class BookResourceTest { @Inject InMemoryBookRepository bookRepository; @BeforeEach public void setup() { List.of( Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]", 2860), Book.create("978-4295008583", "マイクロサービスパターン[実践的システムデザインのためのコード解説]", 5280), Book.create("978-1492062653", "Quarkus Cookbook: Kubernetes-optimized Java Solutions", 6376), Book.create("978-4295007753", "クラウドネイティブ・アーキテクチャ 可用性と費用対効果を極める次世代設計の原則", 4290) ).forEach(b -> bookRepository.insert(b).subscribe().with(v -> { })); } @AfterEach public void teardown() { bookRepository.clear().subscribe().with(v -> { }); } @Test public void findByIsbnTest() { given() .pathParam("isbn", "978-4295008477") .when() .get("{isbn}") .then() .assertThat() .statusCode(200) .body("title", is("新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]")) .body("price", is(2860)); } @Test public void findAllTest() { given() .when() .get() .then() .assertThat() .statusCode(200) .body("price", is(List.of(6376, 5280, 4290, 2860))); } @Test public void putTest() { given() .pathParam("isbn", "978-4295009795") .contentType(ContentType.APPLICATION_JSON.getMimeType()) .body(Book.create("978-4295009795", "Kubernetes完全ガイド 第2版", 4400)) .when() .put("{isbn}") .then() .assertThat() .statusCode(200); given() .when() .get() .then() .assertThat() .statusCode(200) .body("price", hasSize(5)); // 4 + 1 } @Test public void deleteTest() { given() .pathParam("isbn", "978-4295008583") .when() .delete("{isbn}") .then() .assertThat() .statusCode(204); given() .when() .get() .then() .assertThat() .statusCode(200) .body("price", hasSize(3)); // 4 - 1 } }
テストクラスに、@QuarkusTest
アノテーションと、テスト対象のJAX-RSリソースクラスを指定して@TestHTTPEndpoint
アノテーションを付与します。
@QuarkusTest @TestHTTPEndpoint(BookResource.class) public class BookResourceTest {
テストデータは、CDI管理Beanのテストの時と同様、テストごとに登録、削除するようにしています。
@Inject InMemoryBookRepository bookRepository; @BeforeEach public void setup() { List.of( Book.create("978-4295008477", "新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]", 2860), Book.create("978-4295008583", "マイクロサービスパターン[実践的システムデザインのためのコード解説]", 5280), Book.create("978-1492062653", "Quarkus Cookbook: Kubernetes-optimized Java Solutions", 6376), Book.create("978-4295007753", "クラウドネイティブ・アーキテクチャ 可用性と費用対効果を極める次世代設計の原則", 4290) ).forEach(b -> bookRepository.insert(b).subscribe().with(v -> { })); } @AfterEach public void teardown() { bookRepository.clear().subscribe().with(v -> { }); }
あとは、こんな感じでJAX-RSリソースクラスへHTTPリクエストを実行し、アサーションすることができます。
@Test public void findByIsbnTest() { given() .pathParam("isbn", "978-4295008477") .when() .get("{isbn}") .then() .assertThat() .statusCode(200) .body("title", is("新世代Javaプログラミングガイド[Java SE 10/11/12/13と言語拡張プロジェクト]")) .body("price", is(2860)); }
REST-assuredは初めて使ったのですが、body
でJSONのパスを指定してアサーションしたりできて便利ですね。
パスにマッチする要素が複数あった場合は、コレクションとしてアサーションできます。
@Test public void findAllTest() { given() .when() .get() .then() .assertThat() .statusCode(200) .body("price", is(List.of(6376, 5280, 4290, 2860))); }
テスト時に設定を変える
最後は、テスト時に設定ファイルで指定したプロパティの値を変更してみましょう。
もともと、Quarkusのデフォルトの設定ファイルに以下の定義を書いていました。
src/main/resources/application.properties
app.config.message1=Hello World!! app.config.message2=Hello Quarkus!! app.config.message3=Wow!!
テスト時はこの値の一部を変更したい、というシチュエーションを考えてみます。
ドキュメントを見ていると、どうやらProfileというものがあるようです。
Testing Your Application / Testing Different Profiles
テストを実行してみると
$ mvn test
どうやらtest
というProfileになっているようです。
2021-03-07 00:42:24,190 INFO [io.quarkus] (main) Quarkus 1.12.1.Final on JVM started in 2.173s. Listening on: http://localhost:8083 2021-03-07 00:42:24,196 INFO [io.quarkus] (main) Profile test activated. 2021-03-07 00:42:24,197 INFO [io.quarkus] (main) Installed features: [cdi, mutiny, resteasy, resteasy-jackson, resteasy-mutiny, smallrye-context-propagation
ここで、以下のドキュメントを見てみます。
Configuring Your Application / Configuration Profiles
デフォルトでは、3つのProfileがあるようです。
- dev - Activated when in development mode (i.e. quarkus:dev)
- test - Activated when running tests
- prod - The default profile when not running in development or test mode
そして、%Profile名.[プロパティ]
という記載で対象のProfileで動作している時に有効なプロパティを設定できるようです。
たとえば、こんな感じですね。
※実際には、このようには変更していません(例です)
src/main/resources/application.properties
app.config.message1=Hello World!! app.config.message2=Hello Quarkus!! app.config.message3=Wow!! %test.app.config.message2=Test Hello Quarkus!!
これでもいいのですが、テスト用の設定をsrc/main/resources
の方に書くのはちょっとな、と。
なので、テスト用の設定ファイルを作成したいと思います。Configuration for MicroProfileのAPIで、カスタムの設定ファイルを
追加することができます。
Configuration Reference / Custom configuration sources
GitHub - eclipse/microprofile-config: MicroProfile Configuration Feature
こんな設定ファイルにしました。
src/test/resources/test-application.properties
config_ordinal = 300 quarkus.http.test-port=8083 %test.app.config.message2=Test Hello Quarkus!! app.config.message3=Oops!!
この設定ファイルを読むようなConfigSourceProvider
インターフェースの実装クラスを作成して
src/test/java/org/littlewings/testing/config/TestApplicationPropertiesConfigSourceProvider.java
package org.littlewings.testing.config; import java.io.IOException; import java.io.UncheckedIOException; import java.util.List; import java.util.Objects; import java.util.stream.Collectors; import io.smallrye.config.PropertiesConfigSource; import org.eclipse.microprofile.config.spi.ConfigSource; import org.eclipse.microprofile.config.spi.ConfigSourceProvider; public class TestApplicationPropertiesConfigSourceProvider implements ConfigSourceProvider { @Override public Iterable<ConfigSource> getConfigSources(ClassLoader forClassLoader) { return List .of("test-application.properties") .stream() .map(forClassLoader::getResource) .filter(Objects::nonNull) .map(path -> { try { return new PropertiesConfigSource(path); } catch (IOException e) { throw new UncheckedIOException(e); } }) .collect(Collectors.toList()); } }
以下のファイルを作り、作成したConfigSourceProvider
インターフェースの実装クラス名を書いておきます。
src/test/resources/META-INF/services/org.eclipse.microprofile.config.spi.ConfigSourceProvider
org.littlewings.testing.config.TestApplicationPropertiesConfigSourceProvider
また、追加した設定ファイルに書いているconfig_ordinal
という値は、優先度です。
src/test/resources/test-application.properties
config_ordinal = 300 quarkus.http.test-port=8083 %test.app.config.message2=Test Hello Quarkus!! app.config.message3=Oops!!
今回、%test.app.config.message2
というProfile別の指定と、application.properties
での定義とまったく同じキーである
app.config.message3
を使い、両方ともapplication.properties
より優先されることを確認します。
application.properties
自体がどのような優先度になっているかですが、これはソースコードを見るとわかります。
JARファイルの中の場合、250。
META-INF/microprofile-config.properties
ファイルとして登録した場合、240。
ファイルシステム上の場合、260。
なので、新しく作成したtest-application.properties
ファイルの優先度はconfig_ordinal = 300
とし、application.propreties
よりも
高く設定しています。
あとは、テストを書くだけです。こちらも、REST-assuredを使ってテストを書きます。
src/test/java/org/littlewings/testing/rest/ConfigResourceTest.java
package org.littlewings.testing.rest; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.is; @QuarkusTest @TestHTTPEndpoint(ConfigResource.class) public class ConfigResourceTest { @Test public void message1Test() { given() .when() .get("message1") .then() .assertThat() .statusCode(200) .body(is("Hello World!!")); } @Test public void message2Test() { given() .when() .get("message2") .then() .assertThat() .statusCode(200) .body(is("Test Hello Quarkus!!")); } @Test public void message3Test() { given() .when() .get("message3") .then() .assertThat() .statusCode(200) .body(is("Oops!!")); } }
このテストに書いた通り、test-application.properties
で定義したプロパティについては、そちらの方が優先されていることが
確認できます。
今回確認した内容は、こんな感じです。
テストに関する話題で、扱わなかったこと
テストのガイドに載っていて、今回扱わなかったのはこのあたりです。
Testing Your Application / Applying Interceptors to Tests
Testing Your Application / Tests and Transactions
Testing Your Application / Enrichment via QuarkusTest*Callback
Testing Your Application / Testing Different Profiles
Testing Your Application / Mock Support
Testing Your Application / Starting services before the Quarkus application starts
Testing Your Application / Native Executable Testing
Testing Your Application / Running @QuarkusTest from an IDE
一部、少し触れたものもありますが、だいたいこんな感じです。
モック、トランザクション、Profile、テストの前に別のサービスを動かす、あたりはそのうち使うことになるかもなぁと思ったり。
今回は、こんなところでおしまい。