社のSlackでこういう発言を見た。
Go みたいにプログラムとテストを同じ dir に入れたい。
そういえば、テストとコードを同じ場所に配置する言語をよく見かける。こうした、テストとコードを同じ位置に置くという思想を(テストの)コロケーションと呼ぶようだ。
この考え方は特に最近の言語やフレームワークでよく見る気がする(一長一短あると思うのでその是非についてはいったん措く)。
さて、Scalaではどうかというと、一般的にソースコードはsrc/mainに、テストはtest/mainに置く。厳密には、これを決めているのはScalaの仕様ではなく、デファクトなビルドツールsbtのデフォルト設定だ。
Scalaでテストのコロケーションを実現する
では、Scala(のsbt)ではこれができないのかというと、そんなことはない。筆者がScalaのDiscordで訊いてきたところ、特にトリッキーな方法を使わずにsbtの設定で実現できるとのことだった。以下にbuild.sbtを示す:
// sbt 1.10.2 val scala3Version = "3.5.0" // Thanks to https://discord.com/channels/632150470000902164/922600050989875282/1285830933030633557 // make compile sources directory and test sources directory be the same directory Compile / unmanagedSourceDirectories := Seq(baseDirectory.value / "src") Test / unmanagedSourceDirectories := (Compile / unmanagedSourceDirectories).value // `*.test.scala` are test sources; all other `*.scala` files are compile sources Compile / unmanagedSources / fileInputIncludeFilter := "**/*.scala" Test / unmanagedSources / fileInputIncludeFilter := "**/*.test.scala" Compile / unmanagedSources / fileInputExcludeFilter := (Test / unmanagedSources / fileInputIncludeFilter).value Test / unmanagedSources / fileInputExcludeFilter := sbt.nio.file.PathFilter .fromFileFilter(sbt.io.NothingFilter) lazy val root = project .in(file(".")) .settings( // ... )
このbuild.sbtでは、以下のような設定を行っている:
- プロジェクトルートから見た
srcディレクトリはソースディレクトリであると指示する - プロジェクトルートから見た
srcディレクトリはテストディレクトリであるとも指示する - ソースディレクトリのうち、
**/*.scalaにマッチするもののみがソースである - テストディレクトリ(ソースと同じ)のうち、
**/*.test.scalaにマッチするもののみがテストである - ソースディレクトリのうち、テストに合致するものはソースではない
- テストディレクトリは特にフィルタしない
ここでunmanagedSourcesを利用しているのは、通常のscalaSourceを利用するとその内部でmainやtestを認識してしまうためだろう。
サンプル
実際にこの手法で素朴なプロジェクトを作成してみた。srcディレクトリのストラクチャは以下の通りだ:
src
└── dev
└── capslock
└── collocationexample
├── Main.scala
└── util
├── Fib.scala
└── Fib.test.scala
特に問題なくsbt runやsbt testに成功したし、VSCodeでは正しくコードを認識した(IntelliJではソースからテストを呼べそうに見えてしまうことがあるようだ)。
この先コロケーションさせたいかどうかはまだわからないが、とにかくやろうと思えばできることが分かった。Scala.jsなどとのクロスビルドへの影響は確認していないことに注意されたい(例えば、js固有の処理を記述したい場合など)。
リソースについて
ちなみにこの手法を採用してもリソースは従来通りのsrc/main/resourcesから読まれる。この設定も変更可能だ。
Compile / resourceDirectory := baseDirectory.value / "resources" Test / resourceDirectory := baseDirectory.value / "testResources"
こうするとリソースはプロジェクトルートのresourcesとtestResourcesから読まれるようになる(詳しくはリポジトリを参照)。