mike-neckのブログ

Java or Groovy or Swift or Golang

Spring Rest Repositories + JPA で @ManyToOne で指定されているフィールドを用いて検索するリクエストを投げる

ドキュメント探しても、ググってもstack overflowを探しても明確に書かれていなかったっぽいので(本来的には hal の仕様を見るのが正しいかもしれない)

次のような二つのエンティティが存在するものとする。

@Entity
class Book {
  @Id
  private Integer id;
  private String title;
  // getter/setter 省略
}
class Review {
  @Id
  private Integer id;
  @ManyToOne(optional = false)
  private Book book;
  private String text;
  private LocalDateTime created;
  // getter/setter 省略
}

この Review クラスのレポジトリーを次のように作る

@Repository
interface ReviewRepository extends PagingAndSortingRepository<Review, Integer> {
  @RestResource(path = "by_book")
  Page<Review> findByBookOrderByCreatedDesc(@Param("book") final Book book, final Pageable pageable);
}

Spring Rest Repository で 関連するオブジェクトのレコードはオブジェクトそのものではなく、 link(URI) で表されるので、オブジェクトについても URI で指定できる。したがって Review リポジトリーを Book で検索する(上記findByBookOrderByCreatedDesc を呼び出す)ためのリクエストは次のようになる(アプリケーションはポート8080、コンテキストパスは book-repo とする)。

$ curl "http://localhost:8080/book-repo/reviews/search/by_book?book=http://localhost:8080/book-repo/books/2"

SpringのRestTemplate で Rest Repository が返す application/hal+json からオブジェクトを取得する

何か日本語でなかったため。

実行するのは spring-web が jar にある Java SE 環境(Spring context は存在しない)。


  • 必要なライブラリー
    • org.springframework.hateoas:spring-hateoas とその依存ライブラリー
  • 使ったバージョン
    • spring-core: 4.3.13.RELEASE
    • spring-hateoas: 0.23.0.RELEASE
    • (spring-boot: 1.5.9.RELEASE)

public static void main(String[] args) {
    final ObjectMapper objectMapper = new ObjectMapper().registerModule(new Jackson2HalModule())
            .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES)
            .setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
    final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter(objectMapper);
    converter.setSupportedMediaTypes(Collections.singletonList(MediaTypes.HAL_JSON));
    final RestTemplate restTemplate = new RestTemplate(Collections.singletonList(converter));
    final ResponseEntity<PagedResources<Resource<SomeData>>> entity =
            restTemplate.exchange("http://localhost:5000/app/some-data",
                    HttpMethod.GET,
                    null,
                    new ParameterizedTypeReference<PagedResources<Resource<SomeData>>>() {});
    final List<SomeData> list = entity.getBody().getContent().stream()
            .map(Resource::getContent)
            .collect(toList());
    System.out.println(list);
}

Spring Data REST(+ Spring Data JPA)でPOSTする時のメモ

Spring Data REST + Spring Data JPA で こんなエンティティを作る。

@Entity
public class User {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  private Long id;
  private String name;
}
@Entity
public class Message {
  @Id
  @GeneratedValue(strategy = IDENTITY)
  private Long id;
  @ManyToOne(optional = false)
  private User user;
  private String text;
}

このとき、新しい User を作る場合はこんな感じになる。

$ curl -X POST http://localhost:8080/user -H "Content-Type:application/json" -d "{\"name\":\"foo\"}"
{
  "name" : "foo",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/user/1"
    },
    "user" : {
      "href" : "http://localhost:8080/user/1"
    }
  }
}

Message を作る場合、 User の参照をどのように指定するかわからなかったのでググった。

user には userhref URL を指定するらしい。

$ curl -X POST http://localhost:8080/message -H "Content-Type:application/json" -d "{\"text\":\"hello\",\"user\":\"http://localhost:8080/user/1\"}"
{
  "text" : "hello",
  "_links" : {
    "self" : {
      "href" : "http://localhost:8080/messages/1"
    },
    "message" : {
      "href" : "http://localhost:8080/messages/1"
    },
    "user" : {
      "href" : "http://localhost:8080/messages/1/user"
    }
  }
}

JUnit5のNestedクラスを使ってテスト条件をクラスの構造に閉じ込める(テスト小ネタ Advent Calendar 2017 6日目)

f:id:mike_neck:20171116211230p:plain

JUnit4まででは増えすぎたテストを整理するために、スタティックなメンバークラスを作って整理してきたかと思います。

@RunWith(Enclosed.class)
public class Foo {
  public static class Bar {
    @Test
    public void baz() {}
  }
}

JUnit5 ではネストしたクラスを用いる場合、 スタティックでないメンバークラスを用いるようになりました。

class Foo {
  class Bar {
    @Test
    void baz() {}
  }
}

このような構造のクラスの場合、メンバークラスからルートにあるクラスのフィールドにアクセスできます。

class Foo {
  private String name;
  @BeforeEach
  void setup() {
    this.name = "foo";
  }
  @Nested
  class Bar {
    @Test
    void test() {
      assertEquals("foo", name); // succeed
    }
  }
}

これを応用して、テストの条件をクラス構造として表現します。

例えば、次のようなサービスクラスのメソッドがあって、モックを用いたテストを行います。

public class ReportServiceImpl imlements ReportService {
  private final TimeRepository timeRepository;
  private final GameDao gameDao;
  private final UserExternalRepository userExternalRepository;

  // これをテストしたい
  public List<UserScore> getDailyRanking(final LocalDate date) {
    if (!timeRepository.isBeforeToday(date)) {
      return Collections.emptyMap();
    }
    final List<UserScore> userScoreList = gameDao.dailyUserScoreList(date);
    if (userScoreList.isEmpty()) {
      return Collections.emptyMap();
    }
    final Set<UserId> users = userScore.stream().map(UserScore::getUserId).collect(toSet());
    final Collection<UserId> premiumUsers = userExternalRepository.findPremiumUsers(users);
    if (premiumUsers.isEmpty()) {
      return Collections.emptyMap();
    }
    return userScoreList.stream()
        .filter(s -> premiumUsers.contains(s.getUserId()))
        .sorted((l,r) -> Long.compare(l.getScore(), r.getScore()))
        .collect(toList());
  }
}

JUnit4であると、このようなメソッドのテストは次のように条件の記述が占めるようになります。

@RunWith(MockitoJUnitRunner.class)
public class ReportServiceGetDailyRankingTest {
  @Mock TimeRepository timeRepository;
  @Mock GameDao gameDao;
  @Mock UserExternalRepository userExternalRepository;
  @InjectMock ReportServiceImpl service;

  @Test //1
  public void 指定日のスコアがない場合の結果は0件() {
    when(timeRepository.isBeforeToday(any())).thenReturn(true);
    when(gameDao.dailyUserScoreList(any())).thenReturn(Collections.emptyList());

    final List<UserScore> actual = service.getDailyRanking(LocalDate.now());

    assertTrue(actual.isEmpty());
  }

  @Test //2
  public void 指定日が実行日と同じ場合の結果は0件() {
    when(timeRepository.isBeforeToday(any())).thenReturn(false);

    final List<UserScore> actual = service.getDailyRanking(LocalDate.now());

    assertTrue(actual.isEmpty());
  }

  @Test //3
  public void プレミアムユーザーがいない場合の結果は0件() {
    when(timeRepository.isBeforeToday(any())).thenReturn(true);
    when(gameDao.dailyUserScoreList(any())).thenReturn(Arrays.asList(score1, score2, score3));
    when(userExternalRepository.findPremiumUsers(anyIterable())).thenReturn(Collections.emptySet());

    final List<UserScore> actual = service.getDailyRanking(LocalDate.now());

    assertTrue(actual.isEmpty());
  }
}

メソッドやクラスの設計が適切な状態であれば、メソッドが依存するモジュールの数も少なく、モックの記述量も少なくなるため、これでも良いかもしれません。しかし、現実的にはメソッドの分割が不十分で依存するモジュールも多いため、モックの記述量もばかにならないようなテストがあったりします。また、幾つかのモックの記述が重複するため、共通できるものを @Before で定義しすぎた結果、 UnnecessaryStubbingException という例外が出て、 MockitoJunitRunner ではなく MockitoJunitRunner.Silent を使わざるを得なくなるのではないかと思います。基本的な使い方が変わらないJUnit5でも言えてくるのではないかと思います。

そこで、最初に書いた メンバークラスがスタティックでないことを利用します。

ルートにあるクラスから、条件を積み重ねていった入れ子クラスを作っていって、それをテストの事前条件にします。

例えば、先の例では 1個目のテストと3個目のテストは timeRepository の条件を共有しているので、それらのテストを同じクラスのメンバークラスとして記述するようにします。また、2個目のテストの timeRepository の条件は 1個目と3個目とは異なるので、それらとは異なるクラスのメンバークラスに記述します。また、1個目と3個目のテストは gameDao については異なる条件ですので、これらは別々のクラスに所属します。

@DisplayName("getDailyRankingのパラメーターが")
class ReportServiceGetDailyRankingTest {
  private TimeRepository timeRepository = mock(TimeRepository.class);

  @DisplayName("本日の場合")
  @Nested
  class TodayOrAfter {
    private ReportService service;

    @BeforeEach
    void setup() {
      when(timeRepository.isBeforeToday(any())).thenReturn(true);
      service = new ReportServiceImpl(timeRepository, mock(GameDao.class, mock(UserExternalRepository.class)))
    }
    @DisplayName("件数は0件")
    @Test
    void test() {
      final List<UserScore> actual = service.getDailyReport(LocalDate.now());
      assertTrue(actual.isEmpty());
    }
  }

  @DisplayName("前日の場合に")
  @Nested
  class BeforeToday {
    private GameDao gameDao = mock(GameDao.class);

    @BeforeEach
    void setup() {
      when(timeRepository.isBeforeToday(any())).thenReturn(false);
    }

    @DisplayName("プレイしたプレイヤーがいないときは")
    @Nested
    class NoPlayer {
    private ReportService service;

      @BeforeEach
      void setup() {
        when(gameDao.dailyUserScoreList(anyIterable())).thenReturn(Collecitons.emptySet());
        service = new ReportServiceImpl(timeRepository, mock(GameDao.class, mock(UserExternalRepository.class)))
      }

      @DisplayName("件数は0件")
      @Test
      void test() {
        final List<UserScore> actual = service.getDailyReport(LocalDate.now());
        assertTrue(actual.isEmpty());
      }
    }

    // 以下略
  }
}

このテストの書き方のメリットはテストクラスの構造によってテスト条件が明確になるので、テストのメンテナンスなどの際に条件が理解しやすく修正箇所が小さくなるということです。デメリットはネストが深くなってしまってIDEやスタイルの設定によっては読みづらくなるということです。ただし、このネストの深さについては、ここでの議論の入り口にある、クラス・メソッドの分割ができていないという状態を改善することで修正できますし、そうするべきだと思っています。