ScalaとSpring Boot環境に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.properties
にcom.github.maiflai.gradle-scalatest.mode = append
を設定し、ScalaTest用のタスクをbuild.gradleに追加する - ハマりポイント4 - JacksonでScalaのクラスがうまくシリアライズデシリアライズされない →
MappingJackson2HttpMessageConverter
をMockMvc
にセットする
前提となる環境
- 言語:Scala 2.x
- フレームワーク:Spring Boot
- テストフレームワーク:ScalaTest
- ビルドツール:Gradle
やりたいこと
- Spring Cloud Contractを導入し、Producerのテストをする
ハマりポイント1 - ScalaのテストクラスがSpring Cloud Contractの生成するJUnitのテストから参照出来ない
まず、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で書くことにしました。
ハマりポイント2 - JavaからScalaのコレクションを呼び出す方法が分からない
こちらに関しては、単純に僕の知識不足の話なのです。
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
java.util.List<Foo> list = ...
— がくぞ (@gakuzzzz) April 23, 2021
immutable.List<Foo> sList = CollectionConverters$.asScala(list).toList();
的な
上記を参考にして、結果的にはこんな感じにしました。
CollectionConverters
というJavaとScalaの世界を繋ぐConverterがあるのですね。
Hoge hoge = new Hoge(
CollectionConverters.collectionAsScalaIterable(
Arrays.asList(fuga)
).toList()
);
余談 - ScalaとKotlinのコレクションの違い
余談ですが、KotlinとScalaのコレクションの違いに関してはがくぞ先生が書いてるので参考になると思います。
Javaとの相互運用性とコレクションフレームワークのimmutablityなどのトレードオフ的なものがあります
(それをトレードオフと呼んでいいのか自信はない)。
あと面白いのは後発のKotlinはimmutabilityはそこまで重要視してなくて(ある程度はしてるけど)、immutable よりも read-only と mutable の分離の方をより重視してるみたいな話とか。標準のコレクションフレームワークも immutable じゃなくて read-only だったり
— がくぞ (@gakuzzzz) May 7, 2021
これScalaだとimmutable/mutableなコレクションFWを標準に持ってるのですが、Kotlinの場合はJavaの標準ライブラリを実体として使い、型としてラップする事でコレクションFWを実現してるんですよね。なのでScalaと比べてjarのサイズを小さく保てたり相互変換のコストを減らせたりできる(続 https://t.co/sxPCG8kzLO
— がくぞ (@gakuzzzz) May 7, 2021
ハマりポイント3 - ScalaTestとJUnitをGradleから両方動かす方法が分からない
元々、既存のテストコードはScalaTestで書かれていたので、
Spring Cloud ContractのJUnitとの共存でハマりました。
./gradlew test
でテスト実行すると、ScalaTestは流れるがJUnitが流れないという状況になったということです。
これはScalaTestのGradleプラグインがテストランナーを全てScalaTestで動く想定でhandleしているためでした。
解決策としては、プラグインのREADMEの## Other Frameworks
のところ
(https://github.com/maiflai/gradle-scalatest#other-frameworks)にも書いてあるんですが、
gradle.properties
にcom.github.maiflai.gradle-scalatest.mode = append
を設定し- ScalaTestプラグイン用のテストタスクを作ってtestタスクとつないであげる
ことで解決できました。こんな感じ:
task myTest(dependsOn: testClasses, type: Test, group: 'verification') {
com.github.maiflai.ScalaTestPlugin.configure(it)
}
test.dependsOn myTest
READMEのサンプルの方には、tags
というのが書いてあってタグづけられたテストケースに絞り込みが出来るんですが、
ScalaTestのタグの仕組みを理解してなくて、最初ハマりました…。
解決してくれたチームのメンバーに圧倒的謝罪と感謝🙇♂️🙇♂️🙇♂️
ハマりポイント4 - JacksonでScalaのクラスがうまくシリアライズ/デシリアライズされない
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.properties
にcom.github.maiflai.gradle-scalatest.mode = append
を設定し、ScalaTest用のタスクをbuild.gradleに追加する - ハマりポイント4 - JacksonでScalaのクラスがうまくシリアライズデシリアライズされない →
MappingJackson2HttpMessageConverter
をMockMvc
にセットする
というわけで、別に知ってればなんということはないのですが、ハマった話でした。
誤解なきように言うと、多分Scalaを使う場合のスタンダードとは少し違うのでハマりポイントが多かったのかな?というのと、
単純に僕がScalaもSpringも全然ワカラナイのでハマったというのがあります。
なので、ハマったからと言ってそこまでネガティブではなく、他の人が困ったときに役立てばなと思ってブログに書いてみました。
ScalaとSpring BootとGradleという組み合わせが結構難しいのかも。
でも、例えば慣れたSpringの技術スタックで各サービス作りたいとか繋ぎたい(Spring Cloud的な意味で)とかそういう話もあるじゃないですか?
そういう場合には、こういうポイント抑えておけば一応やりたい事は出来るので、
ハマりポイントさえ超えてしまえば、あとは困ることはないです。
特に事情がなければ、普通にScalaとPlayとsbtとかでやっとくとかいうのもありとは思います。
あとは、Jacksonをやめてjson4s使うとか?
ハマってる時は、ウゥーとなってましたが、勉強する良い機会になりました!
0 件のコメント:
コメントを投稿