GETメソッドの実装(Lab) Spring Boot REST API ー Spring Certified Professional

Spring Certified ProfessionalのBuilding a REST API with Spring Bootコースを引き続き進めています。

今日はGETメソッドの実装のLabからです。
今回受講中のコースはこちらから↓

Building a REST API with Spring Boot

GETエンドポイントのSpring Bootテストを書く

前回のLabで学習した通りで、今回もテスト駆動開発REST APIの実装を進めていく。
REST APIの実装はSpringWebの機能を使用するため、テスト実行時にもSpringWebの機能を使えるようにする必要がある。
その際に使用するのが、「@SpringBootTest」アノテーションである。

「CashcardApplicationTests.java」を編集していく。このテストクラス自体はSpringInitializrでプロジェクトを作成した際に自動で作成されている。

修正前のソースはこちら。

package example.cashcard;

import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class CashcardApplicationTests {

    @Test
    void contextLoads() {
    }

}

上記ソースを編集して、下記のソースにする。

package example.cashcard;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class CashCardApplicationTests {
    @Autowired
    TestRestTemplate restTemplate;

    @Test
    void shouldReturnACashCardWhenDataIsSaved() {
        ResponseEntity<String> response = restTemplate.getForEntity("/cashcards/99", String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    }
}

ソースについて、ポイントをまとめる。

以下のアノテーションを付与することで、テスト実行時にSpringBootアプリケーションが起動してリクエストを実行できるようになる。

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)

ローカルで実行されているアプリケーションに HTTP リクエストを送信できるようにするテスト ヘルパーを DIしている。
今回DIする際に「@Autowired」アノテーションを使用しているが、これを使用したDIはあまり推奨されないのでテストのみでの使用にとどめておくべき。

@Autowired
TestRestTemplate restTemplate;

下記はリクエストを送信している実際の処理である。
今回の場合、HTTPメソッドが「GET」でリクエストエンドポイントが「/cashcards/99」のリクエストとなっている。
なお、第二引数で指定しているのは、レスポンスボディの型である。

メソッドの戻り値で使用されている「ResponseEntity 」には、レスポンスボディの他、ステータスコードやヘッダなどレスポンスに関する様々な情報が格納されている。

ResponseEntity<String> response = restTemplate.getForEntity("/cashcards/99", String.class);

現時点でこのテストを実行すると失敗する。まだプロジェクト内に1つもREST APIを実装していないためである。
ここからAPIの実装に着手していく。


RESTコントローラを作成する

「CashCardController」クラスを追加し、ハンドラメソッドを1つ追加する。

package example.cashcard;

import org.springframework.http.ResponseEntity;

public class CashCardController {

    private ResponseEntity<String> findById() {
        return ResponseEntity.ok("{}");
    }
}

こちらの実装完了後にテストを再実行してもテストは失敗のまま…

失敗の原因として、先ほど追加したコントローラクラスは、Springが正式にコントローラだと認識できていないという点が挙げられる。
Springにきちんと認識してもらうために、もう少しコードを追加する必要がある。


GETエンドポイントをコントローラに追加する

コントローラクラスの実装を下記のように変更する。
変更点はクラス・メソッドにそれぞれアノテーションが追加されたことである。

package example.cashcard;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/cashcards")
class CashCardController {

    @GetMapping("/{requestedId}")
    private ResponseEntity<String> findById() {
        return ResponseEntity.ok("{}");
    }
}

追加したアノテーションについて

@RestController

このアノテーションを付与することで、Springにこのクラスが RestControllerのコンポーネントであり、HTTP リクエストを処理できることを認識させることができる。

@RequestMapping("/cashcards")

このコントローラーのハンドラメソッドにアクセスするために必要なエンドポイントを記載している。

@GetMapping("/{requestedId}")

このアノテーションが付与されたメソッドをハンドラーメソッドと認識する。
今回の定義の場合、「cashcards/{requestedID}」に一致する GET リクエストは、このメソッドによって処理される。

修正完了後にテストを再実行すると、テストが成功する!
ただし、今のテストの実装だとレスポンスステータスしか確認していないため、レスポンスの内容も確認するテストに書き換えていく。

テストメソッドに以下の処理を追加する。
APIを呼び出したときにレスポンスとしてIDが含まれていることを期待しているので、IDがnullでないことを確認するための処理である。

DocumentContext documentContext = JsonPath.parse(response.getBody());
Number id = documentContext.read("$.id");
assertThat(id).isNotNull();

これを追加した状態でテストを再実行すると失敗する。
現状レスポンスは空のJsonのため、そもそもIDという項目自体見つからないよ、というエラーが発生している。

コントローラクラスに戻ってレスポンスとして返却している値を修正する。

@GetMapping("/{requestedId}")
private ResponseEntity<CashCard> findById() {
    CashCard cashCard = new CashCard(1000L, 0.0);
    return ResponseEntity.ok(cashCard);
}

テストを再実行すると成功する…が、リクエストとして渡している値(99)とレスポンスとして返却される値(1000)が等しくないので、この結果は正しいとは言えない。
テストのアサーションがあまり良くなかったので修正する。また、合わせてamountに対するアサーションも追加する。

DocumentContext documentContext = JsonPath.parse(response.getBody());
Number id = documentContext.read("$.id");
assertThat(id).isEqualTo(99);

Double amount = documentContext.read("$.amount");
assertThat(amount).isEqualTo(123.45);

コントローラクラス側もレスポンスオブジェクト作成部分を以下に修正する。

CashCard cashCard = new CashCard(99L, 123.45);

テストを再実行して成功することを確認する。


@PathVariableアノテーションを使用する

ここまでハンドラーメソッドの「requestId」を無視して実装していたが、ここでコントローラでこのパス変数を使用して、正しい「Cash Card」を返すように実装を修正していく。

まずはテストクラスに以下のメソッドを追加する。

@Test
void shouldNotReturnACashCardWithAnUnknownId() {
  ResponseEntity<String> response = restTemplate.getForEntity("/cashcards/1000", String.class);

  assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
  assertThat(response.getBody()).isBlank();
}

コントローラクラスの修正に入っていく。ハンドラメソッドに引数を追加し「@PathVariable」アノテーションでリクエスト時に指定されたrequestIdを取得できるようにする。

@GetMapping("/{requestedId}")
private ResponseEntity<CashCard> findById(@PathVariable Long requestedId) {
}

ハンドラメソッドの処理も修正して、引数で受け取ったrequestIdによって処理が変わるようにする。

@GetMapping("/{requestedId}")
private ResponseEntity<CashCard> findById(@PathVariable Long requestedId) {
    if (requestedId.equals(99L)) {
        CashCard cashCard = new CashCard(99L, 123.45);
        return ResponseEntity.ok(cashCard);
    } else {
        return ResponseEntity.notFound().build();
    }
}

この状態でテストを実行すると成功する。

今回はここまで。