Spring Cloud Contractで遊んでみた #1

Spring Cloud Contractを触ってみようと思ったきっかけは、会社での SwaggerでAPI仕様書を書く -> レビュー -> 実装 という開発フロー問題を感じているからです。 このフローには 1.実装とAPI仕様書で乖離してしまう恐れがある2.いちいちAPI仕様書を書くのがめんどくさい3.実装が仕様を満たしている保証はできない という問題があります。

そこで、実装とAPI仕様書が乖離しないいい感じのライブラリを探してたら、「Spring Fox」と「Spring REST Docs」が見つかりました。しかし、どちらもコードからAPI仕様書を生成してくれるライブラリなのですが、微妙だなと感じました。 Spring Foxは、公式でないしプロダクトコードに手を加えないといけないです。Spring REST Docsは理想に近かったのですが、TDD向きで今から始めるには辛いかなと。 このような相談を知り合いのエンジニアに話したら、「Spring Cloud Contract」があるよと教えてくれました。

Spring Cloud Contractとは

Spring Cloud Contractは、CDC(Consumer Driven Contracts)をサポートするためのプロジェクトでマイクロサービス化されたアプリケーションに嬉しいプロジェクトです。

Consumer Driven Contracts testingとは

Consumer Driven Contracts(CDC) testingは主にマイクロサービスに役立つテスト手法です。 Consumer(サービスを使う側)が定義したContract(契約)をProvider(サービスを提供する側)が守らなければなりません。

サンプルアプリケーション

ソースコードGitHubに置いてあります。ProducerConsumer

サンプルの内容

  • ProducerとConsumerを実装
  • Spring Cloud Contractを使ってCDC testingできるようにする

サンプルは、Java13、Spring Boot2.2.0で作っています。

Producer

Controllerの実装

BrandController.java

@RestController
@RequestMapping("/brands")
public class BrandController {
  @GetMapping
  public BrandListDto list() {
      Brand stof = new Brand();
      stof.setName("STOF");
      stof.setDesigner("Tanita");

      BrandListDto listDto = new BrandListDto();
      listDto.setBrands(List.of(stof));
      return listDto;
  }
}

Spring Cloud Contractを依存関係に追加

pom.xml

<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-verifier</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

<dependencyManagement>
  <dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>${spring-cloud.version}</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<build>
  <plugins>
    <plugin>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-contract-maven-plugin</artifactId>
        <version>${spring-cloud-contract-maven-plugin.verison}</version>
        <extensions>true</extensions>
        <configuration>
        <!-- 生成するテストがextendsするクラス -->
            <baseClassForTests>
                com.b1a9idps.springcloudcontractsample.producer.TestBase
            </baseClassForTests>
        <!-- JUnit5で利用できるように(デフォルトはJUnit4) -->
            <testFramework>JUNIT5</testFramework>
        </configuration>
    </plugin>
  </plugins>
</build>

TestBase.java

@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)
// DIコンテナを破棄するタイミングをコントロールする
@DirtiesContext
public abstract class TestBase {
  @BeforeEach
  public void setup(WebApplicationContext context) {
      RestAssuredMockMvc.mockMvc(
              MockMvcBuilders.webAppContextSetup(context).build());
  }
}

Contractを追加

GroovyかYAMLで記述します。デフォルトのパスは、 $rootDir/src/test/resources/contracts です。

brand.yml

request:
  method: GET
  url: /brands
  headers:
    Content-Type: application/json
response:
  status: 200
  body:
    brands:
      - name: "STOF"
        designer:: "Tanita"
  headers:
    Content-Type: application/json

期待するリクエストとレスポンスをそれぞれ記述します。bodyを別でjsonファイルを定義することもできます。

brand.yml

request:
  method: GET
  url: /brands
  headers:
    Content-Type: application/json
response:
  status: 200
  bodyFromFile: brand_response.json
  headers:
    Content-Type: application/json

テストクラスを生成する

./mvnw clean install を実行することでテストクラスが生成して、スタブを作ってローカルレポジトリにインストールします。

...

[INFO] Generating server tests source code for Spring Cloud Contract Verifier contract verification
[INFO] Will use contracts provided in the folder [/Users/uchitate/study/producer/src/test/resources/contracts]
[INFO] Directory with contract is present at [/Users/uchitate/study/producer/src/test/resources/contracts]
[INFO] Test Source directory: /Users/uchitate/study/producer/target/generated-test-sources/contracts added.
[INFO] Using [com.b1a9idps.springcloudcontractsample.producer.TestBase] as base class for test classes, [null] as base package for tests, [null] as package with base classes, base class mappings []
[INFO] Creating new class file [/Users/uchitate/study/producer/target/generated-test-sources/contracts/com/b1a9idps/springcloudcontractsample/producer/ContractVerifierTest.java]
[INFO] Generated 1 test classes.
[INFO]
[INFO] --- spring-cloud-contract-maven-plugin:2.1.3.RELEASE:convert (default-convert) @ producer ---
[INFO] Will use contracts provided in the folder [/Users/uchitate/study/producer/src/test/resources/contracts]
[INFO] Copying Spring Cloud Contract Verifier contracts to [/Users/uchitate/study/producer/target/stubs/META-INF/com.b1a9idps.spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/contracts]. Only files matching [.*] pattern will end up in the final JAR with stubs.
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 2 resources
[INFO] Converting from Spring Cloud Contract Verifier contracts to WireMock stubs mappings
[INFO]      Spring Cloud Contract Verifier contracts directory: /Users/uchitate/study/producer/src/test/resources/contracts
[INFO] Stub Server stubs mappings directory: /Users/uchitate/study/producer/target/stubs/META-INF/com.b1a9idps.spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/mappings
[INFO] Creating new stub [/Users/uchitate/study/producer/target/stubs/META-INF/com.b1a9idps.spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/mappings/brand.json]
[INFO]
[INFO] --- maven-resources-plugin:3.1.0:testResources (default-testResources) @ producer ---
[INFO] Using 'UTF-8' encoding to copy filtered resources.
[INFO] Copying 2 resources
[INFO] skip non existing resourceDirectory /Users/uchitate/study/producer/target/generated-test-resources/contracts
[INFO]
[INFO] --- maven-compiler-plugin:3.8.1:testCompile (default-testCompile) @ producer ---
[INFO] Changes detected - recompiling the module!
[INFO] Compiling 2 source files to /Users/uchitate/study/producer/target/test-classes
[INFO]
[INFO] --- maven-surefire-plugin:2.22.2:test (default-test) @ producer ---

...
// テスト実行
...

[INFO] --- spring-cloud-contract-maven-plugin:2.1.3.RELEASE:generateStubs (default-generateStubs) @ producer ---
[INFO] Files matching this pattern will be excluded from stubs generation []
[INFO] Building jar: /Users/uchitate/study/producer/target/producer-0.0.1-SNAPSHOT-stubs.jar
[INFO]
[INFO] --- maven-jar-plugin:3.1.2:jar (default-jar) @ producer ---
[INFO] Building jar: /Users/uchitate/study/producer/target/producer-0.0.1-SNAPSHOT.jar
[INFO]
[INFO] --- spring-boot-maven-plugin:2.2.0.RELEASE:repackage (repackage) @ producer ---
[INFO] Replacing main artifact with repackaged archive
[INFO]
[INFO] --- maven-install-plugin:2.5.2:install (default-install) @ producer ---
[INFO] Installing /Users/uchitate/study/producer/target/producer-0.0.1-SNAPSHOT.jar to /Users/uchitate/.m2/repository/com/b1a9idps/spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/producer-0.0.1-SNAPSHOT.jar
[INFO] Installing /Users/uchitate/study/producer/pom.xml to /Users/uchitate/.m2/repository/com/b1a9idps/spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/producer-0.0.1-SNAPSHOT.pom
[INFO] Installing /Users/uchitate/study/producer/target/producer-0.0.1-SNAPSHOT-stubs.jar to /Users/uchitate/.m2/repository/com/b1a9idps/spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/producer-0.0.1-SNAPSHOT-stubs.jar

生成されたテストクラス(ContractVerifierTest.java

public class ContractVerifierTest extends TestBase {

    @Test
    public void validate_brand() throws Exception {
        // given:
            MockMvcRequestSpecification request = given()
                    .header("Content-Type", "application/json");

        // when:
            ResponseOptions response = given().spec(request)
                    .get("/brands");

        // then:
            assertThat(response.statusCode()).isEqualTo(200);
            assertThat(response.header("Content-Type")).isEqualTo("application/json");
        // and:
            DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
            assertThatJson(parsedJson).array("['brands']").contains("['designer']").isEqualTo("Tanita");
            assertThatJson(parsedJson).array("['brands']").contains("['name']").isEqualTo("STOF");
    }

}

Consumer

BrandServiceの実装

BrandServiceImpl.java

@Service
public class BrandServiceImpl implements BrandService {
  private final RestTemplate restTemplate;
  private final URI producerUrl;

  public BrandServiceImpl(RestTemplateBuilder restTemplateBuilder, @Value("${producer.url}") String producerUrl) {
      this.restTemplate = restTemplateBuilder.build();
      this.producerUrl = UriComponentsBuilder.fromHttpUrl(producerUrl).build().toUri();
  }

  @Override
  public BrandListDto list() {
      HttpHeaders httpHeaders = new HttpHeaders();
      httpHeaders.add(HttpHeaders.CONTENT_TYPE, APPLICATION_JSON_VALUE);

      ResponseEntity<BrandListDto> responseEntity = restTemplate.exchange(
              producerUrl,
              HttpMethod.GET,
              new HttpEntity<>(httpHeaders),
              new ParameterizedTypeReference<>() {});
      return responseEntity.getBody();
  }
}

Spring Cloud Contractを依存関係に追加

Producer側とほぼ同じなので説明は省略します。

pom.xml

<dependencies>
  <dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

<dependencyManagement>
  <dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-dependencies</artifactId>
        <version>${spring-cloud.version}</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

<build>
  <plugins>
    <plugin>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-contract-maven-plugin</artifactId>
        <version>${spring-cloud-contract-maven-plugin.verison}</version>
        <configuration>
            <testFramework>JUNIT5</testFramework>
        </configuration>
    </plugin>
  </plugins>
</build>

テストを書く

/test/resources/application.yaml

producer:
  url: http://localhost:8080/brands

BrandServiceImplTest.java

// StubRunnnerExtension.classを登録する
@ExtendWith({SpringExtension.class, StubRunnerExtension.class})
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@AutoConfigureStubRunner(ids = {"com.b1a9idps.spring-cloud-contract-sample:producer:+:stubs:8080"}, stubsMode = StubsMode.LOCAL)
class BrandServiceImplTest {
  @Autowired
  private BrandService brandService;

  @Test
  void list() {
      Assertions.assertThat(brandService.list().getBrands())
              .extracting(Brand::getName, Brand::getDesigner)
              .containsExactly(Tuple.tuple("STOF", "Tanita"));
  }
}

@AutoConfigureStubRunner を付与してスタブ生成の対象を指定します。idsに指定する値は、 [groupId]:artifactId[:version][:classifier][:port] です。.m2レポジトリに .m2/repository/com/b1a9idps/spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/producer-0.0.1-SNAPSHOT-stubs.jar とjarがインストールされているので、 com.b1a9idps.spring-cloud-contract-sample:producer:+:stubs:8080 と指定します。バージョンを + で指定すると最新バージョンを設定します。

テストを実行

2019-11-19 00:07:15.872  INFO 24517 --- [           main] o.s.c.c.s.AetherStubDownloaderBuilder    : Will download stubs and contracts via Aether
2019-11-19 00:07:15.874  INFO 24517 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Remote repos not passed but the switch to work offline was set. Stubs will be used from your local Maven repository.
2019-11-19 00:07:15.984  INFO 24517 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Desired version is [+] - will try to resolve the latest version
2019-11-19 00:07:15.997  INFO 24517 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved version is [0.0.1-SNAPSHOT]
2019-11-19 00:07:16.007  INFO 24517 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Resolved artifact [com.b1a9idps.spring-cloud-contract-sample:producer:jar:stubs:0.0.1-SNAPSHOT] to /Users/uchitate/.m2/repository/com/b1a9idps/spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/producer-0.0.1-SNAPSHOT-stubs.jar
2019-11-19 00:07:16.010  INFO 24517 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacking stub from JAR [URI: file:/Users/uchitate/.m2/repository/com/b1a9idps/spring-cloud-contract-sample/producer/0.0.1-SNAPSHOT/producer-0.0.1-SNAPSHOT-stubs.jar]
2019-11-19 00:07:16.016  INFO 24517 --- [           main] o.s.c.c.stubrunner.AetherStubDownloader  : Unpacked file to [/var/folders/0z/n7t2zxm566zfmzbtg7qdc6_80000gp/T/contracts17319340210365511943]
2019-11-19 00:07:16.705  INFO 24517 --- [           main] wiremock.org.eclipse.jetty.util.log      : Logging initialized @4424ms
2019-11-19 00:07:16.770  INFO 24517 --- [           main] w.org.eclipse.jetty.server.Server        : jetty-9.2.z-SNAPSHOT
2019-11-19 00:07:16.784  INFO 24517 --- [           main] w.o.e.j.server.handler.ContextHandler    : Started w.o.e.j.s.ServletContextHandler@5c60c08{/__admin,null,AVAILABLE}
2019-11-19 00:07:16.785  INFO 24517 --- [           main] w.o.e.j.server.handler.ContextHandler    : Started w.o.e.j.s.ServletContextHandler@212e39ee{/,null,AVAILABLE}
2019-11-19 00:07:16.810  INFO 24517 --- [           main] w.o.e.j.s.NetworkTrafficServerConnector  : Started NetworkTrafficServerConnector@173e960b{HTTP/1.1}{0.0.0.0:8080}
2019-11-19 00:07:16.811  INFO 24517 --- [           main] w.org.eclipse.jetty.server.Server        : Started @4532ms
2019-11-19 00:07:16.812  INFO 24517 --- [           main] o.s.c.contract.stubrunner.StubServer     : Started stub server for project [com.b1a9idps.spring-cloud-contract-sample:producer:0.0.1-SNAPSHOT:stubs] on port 8080
2019-11-19 00:07:17.082  INFO 24517 --- [tp2142521143-31] /__admin                                 : RequestHandlerClass from context returned com.github.tomakehurst.wiremock.http.AdminRequestHandler. Normalized mapped under returned 'null'
2019-11-19 00:07:17.106  INFO 24517 --- [tp2142521143-31] WireMock                                 : Admin request received:
127.0.0.1 - POST /mappings

Connection: [keep-alive]
User-Agent: [Apache-HttpClient/4.5.5 (Java/13)]
Host: [localhost:8080]
Content-Length: [225]
Content-Type: [text/plain; charset=UTF-8]
{
  "id" : "a18b18d9-bf53-478a-be02-c4b05d417911",
  "request" : {
    "url" : "/ping",
    "method" : "GET"
  },
  "response" : {
    "status" : 200,
    "body" : "OK"
  },
  "uuid" : "a18b18d9-bf53-478a-be02-c4b05d417911"
}

2019-11-19 00:07:17.180  INFO 24517 --- [tp2142521143-32] WireMock                                 : Admin request received:
127.0.0.1 - POST /mappings

Connection: [keep-alive]
User-Agent: [Apache-HttpClient/4.5.5 (Java/13)]
Host: [localhost:8080]
Content-Length: [227]
Content-Type: [text/plain; charset=UTF-8]
{
  "id" : "d4c408f0-344e-4994-9118-f3844404a3fc",
  "request" : {
    "url" : "/health",
    "method" : "GET"
  },
  "response" : {
    "status" : 200,
    "body" : "OK"
  },
  "uuid" : "d4c408f0-344e-4994-9118-f3844404a3fc"
}

2019-11-19 00:07:17.246  INFO 24517 --- [tp2142521143-33] WireMock                                 : Admin request received:
127.0.0.1 - POST /mappings

Connection: [keep-alive]
User-Agent: [Apache-HttpClient/4.5.5 (Java/13)]
Host: [localhost:8080]
Content-Length: [493]
Content-Type: [text/plain; charset=UTF-8]
{
  "id" : "3e1ac4f8-4565-42b5-946a-e136e1822941",
  "request" : {
    "url" : "/brands",
    "method" : "GET",
    "headers" : {
      "Content-Type" : {
        "equalTo" : "application/json"
      }
    }
  },
  "response" : {
    "status" : 200,
    "body" : "{\"brands\":[{\"name\":\"STOF\",\"designer\":\"Tanita\"}]}",
    "headers" : {
      "Content-Type" : "application/json"
    },
    "transformers" : [ "response-template" ]
  },
  "uuid" : "3e1ac4f8-4565-42b5-946a-e136e1822941"
}

2019-11-19 00:07:17.313  INFO 24517 --- [           main] o.s.c.c.stubrunner.StubRunnerExecutor    : All stubs are now running RunningStubs [namesAndPorts={com.b1a9idps.spring-cloud-contract-sample:producer:0.0.1-SNAPSHOT:stubs=8080}]
2019-11-19 00:07:17.358  INFO 24517 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 53672 (http) with context path ''
2019-11-19 00:07:17.362  INFO 24517 --- [           main] c.b.s.c.s.impl.BrandServiceImplTest      : Started BrandServiceImplTest in 3.981 seconds (JVM running for 5.083)
2019-11-19 00:07:17.365  WARN 24517 --- [           main] o.s.c.c.stubrunner.StubRunnerFactory     : No stubs to download have been passed. Most likely you have forgotten to pass them either via annotation or a property

2019-11-19 00:07:17.735  INFO 24517 --- [tp2142521143-31] /                                        : RequestHandlerClass from context returned com.github.tomakehurst.wiremock.http.StubRequestHandler. Normalized mapped under returned 'null'
2019-11-19 00:07:17.801  INFO 24517 --- [tp2142521143-31] WireMock                                 : Request received:
127.0.0.1 - GET /brands

Connection: [keep-alive]
User-Agent: [Apache-HttpClient/4.5.9 (Java/13)]
Host: [localhost:8080]
Accept-Encoding: [gzip,deflate]
Accept: [application/json, application/*+json]
Content-Type: [application/json]

Matched response definition:
{
  "status" : 200,
  "body" : "{\"brands\":[{\"name\":\"STOF\",\"designer\":\"Tanita\"}]}",
  "headers" : {
    "Content-Type" : "application/json"
  },
  "transformers" : [ "response-template" ]
}

Response:
HTTP/1.1 200
Content-Type: [application/json]
Matched-Stub-Id: [3e1ac4f8-4565-42b5-946a-e136e1822941]

2019-11-19 00:07:17.913  WARN 24517 --- [           main] .StubRunnerWireMockTestExecutionListener : You've used fixed ports for WireMock setup - will mark context as dirty. Please use random ports, as much as possible. Your tests will be faster and more reliable and this warning will go away
2019-11-19 00:07:17.923  INFO 24517 --- [           main] w.o.e.j.s.NetworkTrafficServerConnector  : Stopped NetworkTrafficServerConnector@173e960b{HTTP/1.1}{0.0.0.0:8080}
2019-11-19 00:07:17.924  INFO 24517 --- [           main] w.o.e.j.server.handler.ContextHandler    : Stopped w.o.e.j.s.ServletContextHandler@212e39ee{/,null,UNAVAILABLE}
2019-11-19 00:07:17.924  INFO 24517 --- [           main] w.o.e.j.server.handler.ContextHandler    : Stopped w.o.e.j.s.ServletContextHandler@5c60c08{/__admin,null,UNAVAILABLE}
2019-11-19 00:07:17.925  WARN 24517 --- [           main] w.o.e.j.util.thread.QueuedThreadPool     : qtp2142521143{STOPPING,8<=8<=10,i=3,q=6} Couldn't stop Thread[qtp2142521143-28,5,]
2019-11-19 00:07:17.925  WARN 24517 --- [           main] w.o.e.j.util.thread.QueuedThreadPool     : qtp2142521143{STOPPING,8<=8<=10,i=0,q=4} Couldn't stop Thread[qtp2142521143-29,5,]
2019-11-19 00:07:17.926  INFO 24517 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Shutting down ExecutorService 'applicationTaskExecutor'
Class transformation time: 0.040030372s for 10526 classes or 3.8029994299828998E-6s per class

Process finished with exit code 0

これがテストを実行時に吐き出されるログです。テストが実行されるまでに「1. ローカルレポジトリからstubのjarをインストール」「2. 8080番ポートでモックサーバを起動」が行われています。

まとめ

Producer側では、Contractに基づいたテストが生成され、Consumer側で利用するためのスタブを生成してローカルレポジトリにインストールします。Consumer側では、生成されたスタブを使ってテストを実行します。 Spring Cloud Contractを使えば、Consumerが定義したContractをProviderが守っていることが担保できます。また、Consumerも実サービスに近いテストを行うことができます。とてもよいですね!!!

長くなりすぎたので、別記事でSpring REST Docsとの連携は書きます!

ユニットテストで@ConfigurationPropertiesが有効になるようにしたい!

ここ数年で何回かぶち当たってたことが解決したので備忘録として。内容はタイトルの通りです。 @SpringBootTest を使ってテスト書けば設定ファイルを読み込んでくれるので悩む必要はないんですけど、大人の事情でそういうことができない場合もありますよね。そうすると当然、設定ファイルを読み込んでくれないわけです。

今回、@SpringBootTest を使ってテストを書かなくても設定ファイルを読み込めるようなテストを書きました。

app.properties

app.domain=b1a9idps.com

AppProperties.java

@ConfigurationProperties("app")
public class AppProperties {
    private String domain;

    // getterとsetterは省略
}

TestConfig.java

@TestConfiguration
// @EnableConfigurationProperties は、@ConfigurationProperties が付与されたクラスに設定ファイルの値をセットする
@EnableConfigurationProperties(value = AppProperties.class)
public class TestConfig {}

ConfigurationPropertyTest.java

@ExtendWith(SpringExtension.class)
@TestPropertySource(properties = "spring.config.name=app")
// TestConfigクラスを適用して、設定ファイルを読み込む。 https://docs.spring.io/spring-boot/docs/2.2.0.RELEASE/reference/html/spring-boot-features.html#boot-features-configfileapplicationcontextinitializer-test-utility
@ContextConfiguration(classes = {TestConfig.class}, initializers = ConfigFileApplicationContextInitializer.class)
class ConfigurationPropertyTest {

    @Autowired
    private AppProperties appProperties;

    @Test
    void getValue() {
        assertEquals("b1a9idps.com", appProperties.getDomain());
    }
}

おまけ(メソッドに @ConfigurationProperties を付与している場合)

app.properties

app.app.name=uchitate

AppAppProperties.java

public class AppAppProperties {
    private String name;

    // getterとsetterは省略
}

AppAppConfig.java

@Configuration
public class AppAppConfig {

    @Bean
    @ConfigurationProperties("app.app")
    public AppAppProperties appAppProperties() {
        return new AppAppProperties();
    }
}

TestConfig.java

@TestConfiguration
// ConfigクラスをBean登録する
@Import(AppAppConfig.class)
@EnableConfigurationProperties
public class TestConfig {}

ConfigurationPropertiesTest.java

@ExtendWith(SpringExtension.class)
@TestPropertySource(properties = "spring.config.name=app")
@ContextConfiguration(classes = {TestConfig.class}, initializers = ConfigFileApplicationContextInitializer.class)
class ConfigurationPropertyTest {

    @Autowired
    private AppAppProperties appAppProperties;

    @Test
    void getValue() {
        assertEquals("uchitate", appAppProperties.getName());
    }
}

テストクラスは、AppProperties.javaのテストとほぼ同じです

今回の環境

Java11
Spring Boot 2.2.RELEASE

今回のソースコードこちら

@DataJpaTestでRepositoryのテスト

テスト書いてますか?私は好きです!嫌いな人は多いみたいですけど...

Springのテスト部分のリファレンス読んで忘れないうちにまとめよう精神が働いたので書きます。 今回は、@DataJpaTestというレポジトリのテストを書くためのアノテーションを紹介します。

@DataJpaTestアノテーションは、デフォルトでインメモリDBの設定をしたり、@EntityがついたクラスをBean登録したり、@RepositoryついたクラスをBean登録するなどSpring Data JPAレポジトリの設定してくれたりします。 @DataJpaTestのソースコードをのぞいてみるとこんな感じの設定が行われているようです。

...
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
...

Data JPAテストは、トランザクショナルで各テストの終わりにロールバックします。 もし、こうしたくない場合は、次のように書いてください。

@Transactional(propagation = Propagation.NOT_SUPPORTED)

また、data JPAテストでは、標準的なJPA EntityManagerの代わりにTestEntityManagerのBeanがインジェクトされます。

ソースコード

GitHub

pom.xml

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
    <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.junit.platform</groupId>
    <artifactId>junit-platform-launcher</artifactId>
    <version>1.2.0</version>
    <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <version>5.2.0</version>
    <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-params</artifactId>
    <version>1.2.0</version>
    <scope>test</scope>
    </dependency>
</dependencies>
  • Java8
  • SpringBoot 2.0.3.RELEASE
  • H2
  • JUnit5

Brand.java

@Entity
public class Brand implements Serializable {
    enum Gender {
        MAN, WOMAN, UNISEX
    }
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer id;
    @Column(nullable = false)
    private String name;
    @Enumerated(EnumType.STRING)
    private Gender gender;
    public Brand(String name, Gender gender) {
        this.name = name;
        this.gender = gender;
    }
    ... getter, setter
}

id、ブランド名、ブランドの対象性別をもつBrandエンティティで話を進めていきます。

BrandRepository.java(今回のテスト対象)

@Repository
public interface BrandRepository extends JpaRepository<Brand, Integer> {
    List<Brand> findByGender(Gender gender);
    Integer countByGender(Gender gender);
}

性別でブランドを検索する findByGenderメソッドと性別ごとのブランド数を検索する countByGenderメソッドを準備しました。

BrandRepositoryTest.java

// SpringExtention.classは、Junit 5上でSpring TestContext Frameworkを使えるようにしている
@ExtendWith(SpringExtension.class)
class BrandRepositoryTest {

    @Nested
    @DataJpaTest
    class FindByGender {
        @Autowired
    private TestEntityManager entityManager;
        @Autowired
        private BrandRepository brandRepository;

    @BeforeEach
    void beforeEach() {
            entityManager.persist(new Brand("STOF", Gender.UNISEX));
        entityManager.persist(new Brand("ETHOSENS", Gender.MAN));
        entityManager.persist(new Brand("dulcamara", Gender.UNISEX));
    }

    @Test
    void man() {
        List<Brand> brands = brandRepository.findByGender(Gender.MAN);
        org.assertj.core.api.Assertions.assertThat(brands)
            .extracting(Brand::getName, Brand::getGender)
            .containsExactly(Tuple.tuple("ETHOSENS", Gender.MAN));
        }

    @Test
    void woman() {
            List<Brand> brands = brandRepository.findByGender(Gender.WOMAN);
        org.assertj.core.api.Assertions.assertThat(brands)
                    .hasSize(0);
    }
    }

    @Nested
    @DataJpaTest
    class CountByGender {
        @Autowired
    private TestEntityManager entityManager;

    @Autowired
    private BrandRepository brandRepository;

    @BeforeEach
    void beforeEach() {
        entityManager.persist(new Brand("STOF", Gender.UNISEX));
        entityManager.persist(new Brand("ETHOSENS", Gender.MAN));
        entityManager.persist(new Brand("dulcamara", Gender.UNISEX));
        }

    @Test
    void man() {
        int count = brandRepository.countByGender(Gender.MAN);
        Assertions.assertEquals(1, count);
    }

    @Test
    void woman() {
        int count = brandRepository.countByGender(Gender.WOMAN);
        Assertions.assertEquals(0, count);
    }
    }
}

検証には一部AssertJを使ってます。AssertJについては、こちらで簡単に触れてます。

BrandRepositoryTest.java実行する

実行時のログを抜粋して紹介します。

...
2018-07-05 00:20:30.440  INFO 5456 --- [           main] o.s.j.d.e.EmbeddedDatabaseFactory        : Starting embedded database: url='jdbc:h2:mem:393e83d1-908e-4fb0-91d2-8becd045235b;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa'
...

H2データベースを使ってることがわかります。

...
Hibernate: drop table brand if exists
Hibernate: drop sequence if exists hibernate_sequence
Hibernate: create sequence hibernate_sequence start with 1 increment by 1
Hibernate: create table brand (id integer not null, gender varchar(255), name varchar(255) not null, primary key (id))
...

brandテーブルを作ってます。

...
Hibernate: insert into brand (gender, name, id) values (?, ?, ?)
Hibernate: insert into brand (gender, name, id) values (?, ?, ?)
Hibernate: insert into brand (gender, name, id) values (?, ?, ?)
...

beforeEachの処理が行われている様子です。

...
Hibernate: select count(brand0_.id) as col_0_0_ from brand brand0_ where brand0_.gender=?
...
Hibernate: select brand0_.id as id1_0_, brand0_.gender as gender2_0_, brand0_.name as name3_0_ from brand brand0_ where brand0_.gender=?
...

countByGenderとfindByGenderが実行されていることがわかります。

...
Hibernate: drop table brand if exists
Hibernate: drop sequence if exists hibernate_sequenceでテーブルとhibernate_sequenceが削除されていることがわかります。

テストが終わるとテーブルとhibernate_sequenceを削除しています。 簡単にレポジトリのテストができることがわかっていただけたかと思います。

hibernate_sequenceは最後にしか消されないので、ずっと連番が振られる続けるので、自動採番のIDの値の検証する場合はお気をつけてください。テストごとに初期データをinsertするの美しくないのでうまい方法見つけたいです。

参考

43.3.11 Auto-configured Data JPA Tests

追記

2018/7/12 create tableやdrop tableをFlywayを使って行なっているため、application.propertiesに以下のように書いているとテーブルが作成されずに「xxxテーブルがありません」みたいに怒られますので、Flywayは使えるようにしてください。

spring.flyway.enabled=false

Javaパフォーマンス本を読んで #2

第3章 Javaパフォーマンスのツールボックス

パフォーマンス分析は可視化と表裏一体です。アプリケーションの内部や、その実行環境で起こっていることを知るのがパフォーマンス分析で、可視化にはツールが不可欠です。

オペレーティングシステム付属のツールと分析

プログラムの分析の手始めになるツールは、Javaとの直接の関係はまったくなく、OSに付属している、基本的な監視のツールを利用します。 パフォーマンステストを行う際には、OSからもCPUとメモリ、ディスクの使用率は最低限収集すべきです。ネットワークを利用するプログラムでは、ネットワークの使用率も必要です。

CPUの使用率

CPUの使用率の値はある一定の期間(1秒 or 5秒 or 30秒)の平均値を表しています。CPUの使用率とは、プログラムがどの程度効率的にCPUを使えているかということを表しているので、この値が大きければ大きいほど望ましい状態になります。

ディスクの使用率

ディスクの使用率を監視することには、2つの大きな意味があります。1つ目は、アプリケーション自体に関わるものです。2つ目は、システムがスワッピングを行なっているかどうかわかるという点です。

ネットワークの使用率

ネットワークを利用するアプリケーションでは、ネットワークのトラフィックも監視しなければなりません。UNIXシステムでの基本的なネットワーク監視ツールとしては、netstatがよく使われます。

Javaの監視ツール

JVM自体に関する情報を得るには、Java向けの監視ツールが必要です。以下のようなツールがJDKに付属しています。

  • jcmd 指定されたJavaのプロセスについて、クラスやスレッドそしてJVMの基本的な情報を出力します。
  • jconsole JVMのアクティビティをグラフィカルに表現します。スレッドやクラスの利用状況、ガベージコレクションの内容などがわかります。
  • jhat メモリのヒープダンプを分析しやすい形で出力します。事後的な分析に利用されます。
  • jmap ヒープダンプや、JVMでのメモリの使用に関する情報を出力します。
  • jinfo JVMのシステムプロパティを表示します。一部のシステムプロパティについては、あたいの変更も行えます。
  • jstack Javaのプロセスのスタックをダンプします。
  • jstat ガベージコレクションやクラスローディングのアクティビティに関する情報を出力します。
  • jvisualvm JVMの監視、実行中のアプリケーションに対するプロファイリング、JVMのヒープダンプの分析のためのGUIツールです。

JVMの基本的な情報

  • 実行時間 JVMが実行されている時間を取得します。 jcmd [PID] vm.uptime
  • システムプロパティSystem.getProperties() を通じて得られるのと同じ情報が出力されます。 jcmd [PID] vm.system_properties jinfo -sysprops [PID]
  • JVMのバージョン `jcmd [PID] VM.version
  • JVMコマンドライン引数 jcmd [PID] VM.command_line
  • JVMのチューニングフラグ アプリケーションの中で有効かされているチューニングフラグは以下のようにして取得します。 jcmd [PID] VM.flags [-all]

スレッドの情報

jconsoleやjvisualvmを使うと、アプリケーション内で実行されているスレッドの数をリアルタイムに表示できます。スレッドの実行がブロックされているかどうかを知りたい場合には、スレッドスタックを確認するとよいでしょう。これにはjstackを利用します。

jstack [PID]jcmd

[PID] Thread.print

クラスの情報

アプリケーションの中で利用されているクラスの数を調べるには、jconsoleやjstatを使います。jstatではクラスのコンパイルに関する情報を得ることもできます。

ガベージコレクションの動的な分析

jconsoleはヒープの使用率をグラフとしてリアルタイムに表示でき、jcmdはガベージコレクションを実行させることができます。

プロファイリングツール

パフォーマンスアナリストにとって、プロファイラは最も重要なツールの1つです。 プロファイラはソケットなどの通信手段を使って対象のアプリケーションに接続し、アプリケーションのふるまいに関する情報を取得します。したがって、プロファイリング対象のアプリケーションと同じくらいプロファイラ自身についてもチューニングを行わなければなりません。

Java Mission Control

商用版のJava 7(7u40以降)とJava 8には、Java Mission Controlと呼ばれる新しい監視と制御の仕組みが含まれています。利用には商用ライセンスが必要です。 Java Mission Controlのプログラム(jmc)を起動すると、マシン上でのJVMのプロセスがウインドウ内に表示されます。

Javaパフォーマンス本を読んで #3

第5章 ガベージコレクションの基礎

ガベージコレクションの概要

Java言語の最も魅力的な特徴の1つに、開発者がオブジェクトのライフサイクルを管理しなくてもよいという点があります。ガベージコレクションとは基本的に、使われなくなったオブジェクトを探すこととそのオブジェクトに関連づけられたメモリを解放することで成り立っています。 メモリの断片化を防ぐために、不使用のメモリを解放して、メモリを領域を結合します。 簡単に言うと、ガベージコレクションのパフォーマンスは未使用のオブジェクトの発見とメモリの解放そしてヒープのコンパクト化という3つの要因によって決まります。

すべてのガベージコレクターは基本的に、オブジェクトを複数の領域に分類して管理し、それぞれのために別のヒープ領域を用意しています。これらの領域は、old(または、終身在職を意味するtenured)領域そしてyoung領域と呼ばれます。young領域はさらに、edenとsurvivorという2つの空間に分けられます。

利用するガベージコレクターの判断基準

どのガベージコレクターを利用するべきか判断する際には、全体としてパフォーマンスの目標を定めるとよいでしょう。

個々のリクエストに対するレスポンスタイムが重要な場合では、次のような点について検討が必要です。

  • スレッド停止によって、一部のリクエストが影響を受ける可能性があります。この影響を最小化するのが目標なら、コンカレント型ガベージコレクタを利用するのがよいでしょう。
  • 停止の影響を受けた外れ値よりも、レスポンスタイムの平均値が重要なら、スループット型ガベージコレクターの方がよい結果を得られるでしょう。
  • コンカレント型ガベージコレクターでは長い停止を避けられますが、その代償としてCPUの使用量は増加します。

バッチ処理型のアプリケーションでは、次のようなトレードオフを考慮しながらガベージコレクターを選択してください。

  • CPUの使用率が余裕があるなら、コンカレント型ガベージコレクターを使いましょう。フルガベージコレクションに伴う停止を回避でき、迅速に処理を終えることができます。
  • CPUリソースが限られている場合にコンカレント型ガベージコレクターを使うと、さらにCPUの使用量が増加してしまい、処理の完了までの時間はかえって延びるでしょう。

ガベージコレクションアルゴリズム

シリアル型ガベージコレクター

クライアントマシンのマシンでは、これがデフォルトのガベージコレクターになります。ヒープの処理を行うスレッドは1つです。マイナーガベージコレクションとフルガベージコレクションの両方で、アプリケーションスレッドをすべて停止した上で処理が行われます。

スループット型ガベージコレクター

サーバークラスのマシンでは、これがデフォルトのガベージコレクターになります。young領域の処理に複数のスレッドを利用します。したがって、マイナーガベージコレクションはシリアル型よりも大幅に高速です。また、old領域の処理に複数のスレッドを利用することもでき、Java 7u4以降ではこれがデフォルトの設定になっています。 ここでも、マイナーガベージコレクションとフルガベージコレクションの双方でアプリケーションスレッドはすべて停止します。

CMSガベージコレクター

スループット型やシリアル型のガベージコレクターで行われているような、フルガベージコレクションに伴う長い停止を回避するためにCMS(Concurrent Mark Sweep)ガベージコレクターは設計されました。フルガベージコレクションの際にアプリケーションスレッドを止めずにすむよう、CMSガベージコレクターでは1つまたは複数のスレッドを使ってバックグラウンドでold領域を探索し、使われていないオブジェクトを破棄します。バックグラウンドのスレッドによるold領域のスキャンが行われます。   アプリケーションスレッドと並行してバックグラウンドでヒープを検索するので、CPU使用率は増加します。

G1ガベージコレクター

大きなヒープ(およそい4GB以上)を最大限の停止時間で処理することを目標としています。アプリケーションスレッドの実行中にold領域の処理を並行して行えます。CMSを利用する場合よりも、フルガベージコレクションが必要になる可能性を低くできます。

Javaパフォーマンス本を読んで #4

第7章 ヒープのベストプラクティス

一般的には、オブジェクトを生成する回数を減らし、必要なくなったらすぐに廃棄すべきというルールがある。一方、同じ種類のオブジェクトをなんども生成するのは全体としてのパフォーマンスの悪化に繋がります。このようなオブジェクトを破棄せずに再利用するようにすれば、パフォーマンスは大きく向上します。

ヒープの分析

ヒープヒストグラム

どの型のオブジェクトが多くのメモリを消費しているかという点について、分析が必要です。このための最も簡単な方法が、ヒープヒストグラムを利用するというものです。ヒープヒストグラムを使えば、アプリケーション内で使われているオブジェクトの数を手早く確認できます。 ヒープヒストグラムのデータはとても小さいので、自動化されたテストの中で毎回出力されるようにするとよいでしょう。

ヒープダンプ

ヒープヒストグラムよりも詳細な分析が必要な場合には、ヒープダンプが必要になります。 ヒープの分析では、まず保持メモリ量(retained memory)について調べるのが一般的です。保持メモリ量とは、該当するオブジェクトがガーベジコレクションの対象になった場合に解放されるメモリ量を表します。 ヒープの中で保持メモリ量の多いオブジェクトはドミネータ(dominator)と呼ばれます。 一般的な指針としては、コレクションのエントリではなくコレクション自体のオブジェクトから探索を初めて、最大のコレクションを探すようにしましょう。

OutOfMemoryError

OutOfMemoryErrorは次のような状況で発生します。

  • JVMから利用できるネイティブメモリが少ない場合
  • permanent領域(Java7まで)やメタスペース(Java8)がいっぱいになった場合
  • ヒープ自体がいっぱいになった場合(使用中のオブジェクトが多すぎ、ヒープとして設定された量を上回っています) ヒープ自体がいっぱいの場合、このようなエラーメッセージが表示されます。Exception in thread "main" java.lang.OutOfMemoryError: Java heap space このエラーが発生するのは、主にヒープメモリの不足が原因です。単に、使用中のオブジェクトが多すぎてヒープに収まらなくなったか、メモリリークが発生している可能性があるかのどちらかが考えられます。 どちらの場合でも、どのオブジェクトがメモリを最も多く消費しているかを知るにはヒープダンプの分析が必要です。
[ヒープダンプの自動取得]
OutOfMemoryErrorの発生は予測ができないため、いつヒープダンプを取得するべきか判断するのは困難です。そこで、JVMには次のようなフラグが用意されています。

-XX:+HeapDumpOnOutOfMemoryError
  OutOfMemoryErrorの発生時にヒープダンプを生成
-XX:HeapDumpPath=パス
  ヒープダンプの出力先を指定
-XX:+HeapDumpAfterFullGC
  フルガベージコレクションの後にヒープダンプを生成
-XX:+HeapDumpBeforeFullGC
  フルガベージコレクションの前にヒープダンプを生成

アプリケーションはオブジェクトを追加するだけで解放はしないため、コレクションは、メモリリークを最も発生しやすいです。不必要になったオブジェクトは積極的にコレクションから削除しましょう。

メモリの使用量を減らす

Javaでメモリを効率的に利用するには、まずヒープメモリの使用量を減らすべきです。ここでは、メモリの使用量を減らすための3つの方法(オブジェクトトのサイズを減らす、オブジェクトの初期化を遅らせる、canonicalオブジェクトの利用)を紹介します。

オブジェクトのサイズを減らす

オブジェクトはヒープ上の一定の領域を消費するので、メモリの使用量を減らすにはオブジェクトを小さくするのが一番の近道です。 オブジェクトのサイズを減らすには、インスタンス変数の数を減らすという方法とインスタンス変数のサイズを減らすという方法があります。nullのインスタンス変数も、一定のメモリを消費します。

オブジェクトの初期化を遅らせる

対象の処理の頻度が低い場合に、このように初期化を遅らせると効果的です。一方、処理が頻繁に行われる場合にはメモリが節約されず、パフォーマンスをはわずかに悪化する可能性があります。

事前の解放

変数の値としてnullをセットすると、ガベージコレクターがより早く街灯のオブジェクトを解放できます。しかし、これが効果的なケースは限られています。 ローカル変数は、メソッドの終了後に有効範囲から外れるため自動的にガベージコレクターによる解放の対象になります。

immutableオブジェクトとcanonicalオブジェクト

Javaでは多くの型のオブジェクトはimmutable(変更不能の意)です。Booleanクラスのコンストラクタはprivateなものだけにし、パラメータの値に応じてBoolean.TRUEとBoolean.FALSEのいずれかを返すようなstaticメソッドを用意するというのがより良い設計です。 このように、同じ意味を持つ複数のimmutableオブジェクトを代表した表現をcanonicalオブジェクトと呼びます。

canonicalオブジェクトの生成

Stringクラスで行われているように、オブジェクトをcanonical化することによって、同一のimmutableオブジェクトが繰り返し生成されるのを防ぎます。その結果、アプリケーションが使用するヒープの量を大幅に削減できます。

Javaパフォーマンス本を読んで #1

第2章 パフォーマンステストのアプローチ

実アプリケーションでテストする

パフォーマンステストに利用できるコードは、マイクロベンチマーク、マクロベンチマーク、メゾベンチマークの3種類にカテゴライズできる。

マイクロベンチマーク

これは、とても小さな単位でパフォーマンスを測定するためのテストです。マイクロベンチマーク用の正しいコードを書くことはとても難しいです。

  • 処理結果を利用しなければならない 処理結果を変数に書き込むだけでは、コンパイラが処理が不要と判断して処理自体を削除する可能性があるので、変数に書き込んだあとは読み出す必要があります。
  • 不必要な処理を含めてはならない テスト用に乱数を生成する処理など、ベンチマーク用の処理を計測時間に含んでしまうと、意図したベンチマークが取れないので気をつける必要があります。
  • 正しい入力に基づいて測定しなければならない テストように生成する乱数が入力値の範囲内にない場合など、使われ方を反映したとは言えません。適切な範囲の値を使うようにする必要があります。

なお、ベンチマークを取る際は、ウォームアップを行って置かないとコード本体だけでなくコンパイルのパフォーマンスも測定してしまうので、気をつける必要があります。 マイクロベンチマークの作成は容易ではなく、役に立つケースは限られます。落とし穴について理解して、適切なマイクロベンチマークの作成メリットがあるのか(あるいは、よりマクロなレベルのテストに注力する方がよいのか)どうかを理解しましょう。

スループット、バッチ、レスポンスタイムを理解する

バッチ(一括)処理の測定

パフォーマンス測定するための最も簡単な方法は、ある作業にどの程度の時間がかかっているかを調べるというものです。 Javaは、JIT(just-in-time)コンパイルという厄介者がいて、コードが完全に最適化され十分なパフォーマンスを発揮するためには数分またはそれ以上の時間が必要です。このため、Javaのパフォーマンスに関する調査ではウォームアップの時間がとても重視されています。

スループットの計測

スループットとは、ある一定の時間内にどの程度の寮の処理を行えるかということを表します。クライアント-サーバー型のテストでスループットを計測する際には、クライアント側にシンクタイム(think time。何も処理を行わずに待つ時間)があってはなりません。 通常は、一定時間でのリクエストの総数ではなく1秒間あたりのリクエストの数がスループットとして扱われます。この値は、TPS(transactions per second)、RPS(requests per second)、OPS(operations per second)と呼ばれます。 スループットを計測するテストでは、レスポンスタイムを測る場合よりも少数のクライアントを使うようにして負荷を下げるのが一般的です。 固定的な一連の処理を測定するというわけではないため、スループットの測定ではほぼ必ず十分なウォームアップの時間が用意されます。

レスポンスタイムのテスト

レスポンスタイムとは、クライアントがリクエストを送信してからレスポンスを受け取るまでの経過時間を表します。レスポンスタイムのテストでは、ユーザの振る舞いをより正確に模倣することが目指されているため、クライアントのスレッドが処理の間に一定時間スリープするシンクタイムを設けます。 レスポンスタイムの算出方法は、2つあります。1つは平均です。もう1つは、レスポンスタイムをリクエストのパーセンタイル値として表現する方法です。例えば、90パーセンタイルあたいは1.5秒といったように表現されます。この場合、90%のリクエストでレスポンスタイムは1.5秒以下で、残りの10%では1.5秒以上という意味になります。 この2つの値の違いは、外れ値の扱いに現れます。平均値の計算にはすべての値が含まれるため、他と大きく異なる値があると平均値に影響します。