CLOVER🍀

That was when it all began.

Quarkusでのテストを曞いおみる

これは、なにをしたくお曞いたもの

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ずいう䞍思議なパッケヌゞがありたすね。

こういったファむルが生成される元は、こちらのようです。

https://github.com/quarkusio/quarkus/tree/1.12.1.Final/devtools/platform-descriptor-json/src/main/resources/codestarts/quarkus

䞭身を芋たしたが、今回は芁らない気がするので削陀。

$ rm -rf src/main/java/org/littlewings/resteasyjackson

なお、削陀したファむルの元はこちらにありたす。

https://github.com/quarkusio/quarkus/tree/1.12.1.Final/devtools/platform-descriptor-json/src/main/resources/codestarts/quarkus/examples/resteasy-jackson-example/java/src/main/java/org/acme/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のサむト、以前からだいぶ雰囲気が倉わりたしたね 。

Redirecting

テスト内で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向けのテストラむブラリです。

REST Assured

䜿い方は、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。

https://github.com/quarkusio/quarkus/blob/1.12.1.Final/core/runtime/src/main/java/io/quarkus/runtime/configuration/ApplicationPropertiesConfigSource.java#L57

META-INF/microprofile-config.propertiesファむルずしお登録した堎合、240。

https://github.com/quarkusio/quarkus/blob/1.12.1.Final/core/runtime/src/main/java/io/quarkus/runtime/configuration/ApplicationPropertiesConfigSource.java#L80

ファむルシステム䞊の堎合、260。

https://github.com/quarkusio/quarkus/blob/1.12.1.Final/core/runtime/src/main/java/io/quarkus/runtime/configuration/ApplicationPropertiesConfigSource.java#L100

なので、新しく䜜成した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、テストの前に別のサヌビスを動かす、あたりはそのうち䜿うこずになるかもなぁず思ったり。

今回は、こんなずころでおしたい。