テスト駆動開発サンプル(Lab) ー Spring Certified Professional

Spring Certified ProfessionalのBuilding a REST API with Spring Bootコースを引き続き進めています。
前回の投稿から半年以上経ってしまった…

今日はテスト駆動開発の実践からです。
今回受講中のコースはこちらから↓

Building a REST API with Spring Boot

失敗するテストを作成する

Javaのテストクラスは、「src/test」配下に作成する。「src/main」配下に作成しないように注意する。
テストクラスのパッケージは「src/main」に作成されているテスト対象クラスと同じにする。

テスト駆動開発の場合、「src/main」配下にプログラムを作成するよりも先にテストクラスを作成することになる。


テストクラス作成

テストクラス名には一般的には「Test」というサフィックスを付けて、このクラスがテストクラスであることが分かるようにする。

Eclipseでテストクラスを作成する方法

CashCardJsonTest.javaクラスを作成する

作成されたテストクラスを少し書き換えて、下記のようにする。

「@Test」アノテーションは、付与されたメソッドがテストであることをJunitが認識するために必要なアノテーションである。
メソッドの中にテスト処理を記載していても、「@Test」アノテーションが付与されていなかった場合、Junitはテストとして認識せず実行してくれないので注意する。

package example.cashcard;

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

import org.junit.jupiter.api.Test;

class CashCardJsonTest {

    @Test
    void myFirstTest() {
        assertThat(1).isEqualTo(42);
    }
}

テスト実行

このテストを実行すると、「1=42」という検証をしていることもあり、当然失敗する。

Junitで特定のテストを実行する

Junit実行結果

アサーションの記述を下記のように修正することでテストは成功する

assertThat(42).isEqualTo(42);

テスト実行結果


テストクラスの書き換え

Cashcard REST APIを作成する、という目標に即したテスト内容に書き換えていく。
具体的には以下のような内容に書き換える。
「@JsonTest」はこのテストクラスがJacksonを使用するテストであることを宣言している。

package example.cashcard;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;

import java.io.IOException;

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

@JsonTest
class CashCardJsonTest {

    @Autowired
    private JacksonTester<CashCard> json;

    @Test
    void cashCardSerializationTest() throws IOException {
        CashCard cashCard = new CashCard(99L, 123.45);
        assertThat(json.write(cashCard)).isStrictlyEqualToJson("expected.json");
        assertThat(json.write(cashCard)).hasJsonPathNumberValue("@.id");
        assertThat(json.write(cashCard)).extractingJsonPathNumberValue("@.id")
                .isEqualTo(99);
        assertThat(json.write(cashCard)).hasJsonPathNumberValue("@.amount");
        assertThat(json.write(cashCard)).extractingJsonPathNumberValue("@.amount")
             .isEqualTo(123.45);
    }
}

このテストクラスは、Eclipseで開発している場合は目で見て分かる通りでコンパイルエラーが出ているため、実行すると確実に失敗する。
コンパイルエラーの原因は16行目等で登場している「CashCard」クラスがプロジェクト内に定義されていないことである。

コンパイルエラー

コンパイルエラーを解決するためにプロジェクト内に新規で「CashCard」クラスを作成する。
このクラスはテストのためだけではなく、メイン処理の中で利用されていくものなので「src/test」ではなく「src/main」配下に作成する。

package example.cashcard;

record CashCard(Long id, Double amount) {
}

クラスの作成が完了した後で、改めてテストクラスを見ると、コンパイルエラーが解消されていることが分かる。

コンパイルエラー解消

この状態であればテストが成功するかもしれないので、テストを実行してみる。
が、なんか失敗する。。

テスト失敗

障害トレースをよく読むと、「FileNotFindException」が発生していて[example/cashcard/expected.json]がないよって言っていることが分かるので、このファイルを追加する必要がある、と判断できる。

テストのどこでファイルなんて必要としているのか?というのをテストクラスと障害トレースで確認する。
障害トレースの内容から上記の例外はテストクラスの21行目で発生していることが分かるので、テストクラスの21行目を見てみると以下のような処理が記載されている。

assertThat(json.write(cashCard)).isStrictlyEqualToJson("expected.json");

isStrictlyEqualToJsonメソッドは、引数に渡したファイル名のファイルの内容と「json.write(cashCard)」で作成されるJson文字列の内容が正しいかどうかを検証しているメソッドのため、ただファイルを適当に追加すればよいというわけではないということが分かる。

json.write(cashCard)」の内容って?と次はなるので、writeメソッドの引数として渡されている「cashcard」に設定されている値を理解する必要がある。
このインスタンスはテストクラス20行目で宣言されていて、そこから内容を把握することができる。

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

Json文字列ではkey-valueの関係性が必要なので、「99」と「123.45」それぞれのキーを知りたい。
キーは「CashCard」クラスを参照すると確認出来て、第一引数が「id」第二引数が「amount」であることが分かるので、

ここまで調べた内容から、作成するべきJsonファイルの内容は以下であることが分かる。

{
  "id": 99,
  "amount": 123.45
}

Jsonファイルの作成、配置が完了後にテストを再実行するとテストが成功する。

テスト再実行結果


シリアライズのテスト

シリアライズとはシリアライズの逆操作のことであり、ファイルやバイト配列のデータをアプリケーションのオブジェクトの変換する操作のことを指す。
これにより、あるプラットフォームでシリアライズされたデータを別プラットフォームでデシリアライズすることができるようになる。
データをシリアライズするのに最も一般的なデータ形式は「JSON」。

先ほどまで作成していたテストがJavaオブジェクト→Jsonに変換するテストだったので、逆のJsonJavaオブジェクトに変換のテストを作成していく。

package example.cashcard;

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

import java.io.IOException;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;

@JsonTest
class CashCardJsonTest {

    @Autowired
    private JacksonTester<CashCard> json;

    @Test
    void cashCardSerializationTest() throws IOException {
        CashCard cashCard = new CashCard(99L, 123.45);
        assertThat(json.write(cashCard)).isStrictlyEqualToJson("expected.json");
        assertThat(json.write(cashCard)).hasJsonPathNumberValue("@.id");
        assertThat(json.write(cashCard)).extractingJsonPathNumberValue("@.id")
                .isEqualTo(99);
        assertThat(json.write(cashCard)).hasJsonPathNumberValue("@.amount");
        assertThat(json.write(cashCard)).extractingJsonPathNumberValue("@.amount")
                .isEqualTo(123.45);
    }

    // こっちのテストを追加
    @Test
    void cashCardDeserializationTest() throws IOException {
        String expected = """
                {
                    "id":99,
                    "amount":123.45
                }
                """;
        assertThat(json.parse(expected))
                .isEqualTo(new CashCard(1000L, 67.89));
        assertThat(json.parseObject(expected).id()).isEqualTo(1000);
        assertThat(json.parseObject(expected).amount()).isEqualTo(67.89);
    }
}

テストメソッド追加後、追加テストを実行してみると失敗する。
失敗原因を見てみるとアサーションエラーであることが分かる。

デシリアライズテスト実行結果

改めてテストメソッドの内容を確認すると、Json文字列を定義しているところの内容と、アサーションで書いている値が異なっている。
アサーションで指定している値をJson文字列のものと合わせた後でテストを再実行すると成功する。


所感

テスト駆動開発をラボのカリキュラムに沿って初めて体験してみたけど、作りたいものが明確で、かつ開発者が設計内容をしっかり理解できていないと難しいしかなり面倒で時間かかるな…という印象だった。
でもちゃんとやれば設計外の実装を書いてしまうみたいなことを未然に防げそうで良いのかなとも思った。
けどこの開発方法に慣れるまでは1機能実装するのにすごい時間がかかりそう(スケジュールゆるゆるな案件じゃないと難しいかも?)