2021/06/03 このエントリーをはてなブックマークに追加 はてなブックマーク - Scala 2.x、Spring Boot、ScalaTest、Gradle構成のアプリケーションでSpring Cloud Contractを使ってテストを書くまでの準備

Scala 2.x、Spring Boot、ScalaTest、Gradle構成のアプリケーションでSpring Cloud Contractを使ってテストを書くまでの準備





Scala x Spring BootなプロジェクトにSpring Cloud ContractによるCDCテストを導入しようとしたら、
色々ハマりどころがあったのでブログにまとめておきます。



  • ハマりポイント1 - ScalaのテストクラスがSpring Cloud Contractの生成するJUnitのテストから参照出来ない
  • ハマりポイント2 - JavaからScalaのコレクションを呼び出す方法が分からない
  • ハマりポイント3 - ScalaTestとJUnitをGradleから両方動かす方法が分からない
  • ハマりポイント4 - JacksonでScalaのクラスがうまくシリアライズデシリアライズされない





  • ハマりポイント1 - ScalaのテストクラスがSpring Cloud Contractの生成するJUnitのテストから参照出来ない → Javaで書く
  • ハマりポイント2 - JavaからScalaのコレクションを呼び出す方法が分からない → scala.jdk.CollectionConvertersを使う
  • ハマりポイント3 - ScalaTestとJUnitをGradleから両方動かす方法が分からない → gradle.propertiescom.github.maiflai.gradle-scalatest.mode = append を設定し、ScalaTest用のタスクをbuild.gradleに追加する
  • ハマりポイント4 - JacksonでScalaのクラスがうまくシリアライズデシリアライズされない → MappingJackson2HttpMessageConverterMockMvcにセットする




  • 言語:Scala 2.x
  • フレームワーク:Spring Boot
  • テストフレームワーク:ScalaTest
  • ビルドツール:Gradle


  • Spring Cloud Contractを導入し、Producerのテストをする

まず、Spring Cloud Contractの動く流れについて(Spring Cloud Contractの役割などは割愛します。詳細は公式ドキュメントhttps://spring.pleiades.io/projects/spring-cloud-contract)。


Spring Cloud Contractは、ビルド時にGradleの test タスクで動きます。
その際、Spring Cloud Contractは、Json or Groovy DSLなどで書かれた契約情報をもとに
JavaのJUnitクラスを自動生成 → テスト実行します。

そして自動生成のテストクラスは、こちらで指定した基底クラスを継承させることが出来ます。
この基底クラスで、各コントローラーごとのモックを差し込むなど
事前準備(@BeforeEach 的なことを)するのがお作法となってます。

参考)
https://iikanji.hatenablog.jp/entry/2020/08/04/235009
https://spring.pleiades.io/projects/spring-cloud-contract


しかし、この基底クラスをScalaで書くことは出来ないようです。

なぜかというと、自動生成したテストはJavaクラスであり、このクラスのコンパイル時点は
testCompileJava フェーズだからです。これはtestScalaCompileフェーズより手前 なので、
Scalaのテストクラスは参照出来ないのです(クラスが見つかりませんのコンパイルエラーになる)。


ということでScalaで基底テストクラスを書くのは断念して、Javaで書くことにしました。


こちらに関しては、単純に僕の知識不足の話なのです。

Javaで基底クラスを書くことにしたのは良いものの、
プロダクションコードのレスポンスで使われているのはScalaのオブジェクトです。
Javaの世界からScalaのオブジェクト生成する必要があります。

よーし、、あれ?ScalaのコレクションどうやってJavaから呼び出すんや〜となりました。

(scala.collection.immutable.List[Fuga] をフィールドに持つ Hoge というcaseクラスをJavaから作りたい、
とかそんな簡単なことだったんですが…。)

自分が普段使っているKotlinだと結構無意識に呼べるというか、
そういうゆるい設計になっているので(interoperabilityともいう)、
明確にJavaとScalaのコレクションの世界で変換する必要があるのは噂には聞いていたものの、どうしたら良いのか。



Stack Overflowを漁ってみる


まず、Stack Overflowみて試行錯誤しました。

$colon$colon とかめっちゃ面白いなーと思いつつ、もうちょっと簡単に書きたいよねーとなりました。
https://stackoverflow.com/questions/6578615/how-to-use-scala-collection-immutable-list-in-a-java-code



これはちょっとツライ:

Hoge hoge = new Hoge(); 
List singletonList = $colon$colon$.MODULE$.apply(hoge, List.empty());

ツライので、方向転換。



詳しい人からありがたい情報をもらう


辛みもあり、雑にツイートしたところ、
がくぞ先生とよしださんに拾ってもらって無事やり方が分かりました
(Scalaの人たちは優しいのでツイートしたら教えてくれるんじゃないかとい打算はあった。ゴメンナサイ & アリガトウゴザイマス)。

一連のやり取りはこのあたり:
https://twitter.com/yy_yank/status/1385415796046786564


上記を参考にして、結果的にはこんな感じにしました。

CollectionConverters というJavaとScalaの世界を繋ぐConverterがあるのですね。

  Hoge hoge = new Hoge(
                CollectionConverters.collectionAsScalaIterable(
                        Arrays.asList(fuga)
                ).toList()
            );
  


余談ですが、KotlinとScalaのコレクションの違いに関してはがくぞ先生が書いてるので参考になると思います。
Javaとの相互運用性とコレクションフレームワークのimmutablityなどのトレードオフ的なものがあります
(それをトレードオフと呼んでいいのか自信はない)。





元々、既存のテストコードはScalaTestで書かれていたので、
Spring Cloud ContractのJUnitとの共存でハマりました。
./gradlew testでテスト実行すると、ScalaTestは流れるがJUnitが流れないという状況になったということです。

これはScalaTestのGradleプラグインがテストランナーを全てScalaTestで動く想定でhandleしているためでした。


解決策としては、プラグインのREADMEの## Other Frameworks のところ
(https://github.com/maiflai/gradle-scalatest#other-frameworks)にも書いてあるんですが、


  1. gradle.propertiescom.github.maiflai.gradle-scalatest.mode = append を設定し
  2. ScalaTestプラグイン用のテストタスクを作ってtestタスクとつないであげる

ことで解決できました。こんな感じ:

task myTest(dependsOn: testClasses, type: Test, group: 'verification') {
    com.github.maiflai.ScalaTestPlugin.configure(it)
}
test.dependsOn myTest


READMEのサンプルの方には、tagsというのが書いてあってタグづけられたテストケースに絞り込みが出来るんですが、
ScalaTestのタグの仕組みを理解してなくて、最初ハマりました…。

解決してくれたチームのメンバーに圧倒的謝罪と感謝🙇‍♂️🙇‍♂️🙇‍♂️



Jacksonを使ってScalaのコレクションやcaseクラスなどをシリアライズ/デシアライズする場合は、
ScalaModuleをObjectMapperに設定する必要があります(https://github.com/FasterXML/jackson-module-scala)。

あと、テスト用のObjectMapperでUnknown Fieldsは許容するようにしたい、という感じ。
ただ、Contractのテスト実行時、色々設定をいじってもController内で持っていたはずの値がJSONに変換されず 空JSON {} で返る現状でハマりました。なんでなの!


結論を言うと、Contractの基底テストに以下のように設定することで解決しました。


MappingJackson2HttpMessageConverter mappingJackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();
ObjectMapper mapper = new ObjectMapper().findAndRegisterModules();
mapper.registerModule(new DefaultScalaModule());
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mappingJackson2HttpMessageConverter.setObjectMapper(mapper);
MockMvc mockMvc = MockMvcBuilders.standaloneSetup(new Controller(mockService)).setMessageConverters(
mappingJackson2HttpMessageConverter).build();
RestAssuredMockMvc.mockMvc(mockMvc);


Spring Cloud Contractの自動生成するテストコードは
rest-assured(https://github.com/rest-assured/rest-assured)というMockライブラリの RestAssuredMockMvcを使っています。
こいつが、ちゃんとJSON変換するようにするためにはMappingJackson2HttpMessageConverter を設定する必要があると…。
これは知らないと分からないやつですね。

ハマってる人が何人かいて、以下のissueのコメントで解決方法がわかりました。
RestAssuredMockMvc.config().objectMapperConfig っていう、明らかにコレだろ!ってやつが
どスルーされて困っていたのですよね。

https://github.com/rest-assured/rest-assured/issues/1116#issuecomment-537059429



  • ハマりポイント1 - ScalaのテストクラスがSpring Cloud Contractの生成するJUnitのテストから参照出来ない → Javaで書く
  • ハマりポイント2 - JavaからScalaのコレクションを呼び出す方法が分からない → scala.jdk.CollectionConvertersを使う
  • ハマりポイント3 - ScalaTestとJUnitをGradleから両方動かす方法が分からない → gradle.propertiescom.github.maiflai.gradle-scalatest.mode = append を設定し、ScalaTest用のタスクをbuild.gradleに追加する
  • ハマりポイント4 - JacksonでScalaのクラスがうまくシリアライズデシリアライズされない → MappingJackson2HttpMessageConverterMockMvcにセットする

というわけで、別に知ってればなんということはないのですが、ハマった話でした。

誤解なきように言うと、多分Scalaを使う場合のスタンダードとは少し違うのでハマりポイントが多かったのかな?というのと、
単純に僕がScalaもSpringも全然ワカラナイのでハマったというのがあります。
なので、ハマったからと言ってそこまでネガティブではなく、他の人が困ったときに役立てばなと思ってブログに書いてみました。

ScalaとSpring BootとGradleという組み合わせが結構難しいのかも。
でも、例えば慣れたSpringの技術スタックで各サービス作りたいとか繋ぎたい(Spring Cloud的な意味で)とかそういう話もあるじゃないですか?
そういう場合には、こういうポイント抑えておけば一応やりたい事は出来るので、
ハマりポイントさえ超えてしまえば、あとは困ることはないです。


特に事情がなければ、普通にScalaとPlayとsbtとかでやっとくとかいうのもありとは思います。
あとは、Jacksonをやめてjson4s使うとか?

ハマってる時は、ウゥーとなってましたが、勉強する良い機会になりました!






0 件のコメント:

コメントを投稿

GA