Scalaのsbtなどでテストを実行していて、特定のテストだけ実行したりしなかったり、ということをやりたい、という状況は多い。その方法を解説する。
ここではテストフレームワークとしてScalaTestを利用する。
この記事のコードの完全版は以下のリポジトリで閲覧できる:
復習: Scalaでのテスト
ScalaTestでテストを実行するには、まずbuild.sbtにScalaTestへの依存性を追加する:
val scala3Version = "3.8.2" lazy val root = project .in(file(".")) .settings( name := "scalatest-tagging-exercise", version := "0.1.0-SNAPSHOT", scalaVersion := scala3Version, libraryDependencies += "org.scalactic" %% "scalactic" % "3.2.19", // added libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.19" % Test // added )
ここではテストスタイルとしてFunSuite styleを利用する。
import org.scalatest.funsuite.AnyFunSuite class MySuite extends AnyFunSuite { test("example test") { assert(1 + 1 == 2) } test("another example test") { assert(2 + 2 == 4) } test("heavy test") { assert(3 + 3 == 6) } }
これをsrc/test/scala/MySuite.scalaとして保存して、sbt testを実行するとテストが実行される:
[info] MySuite: [info] - example test [info] - another example test [info] - heavy test [info] Run completed in 219 milliseconds. [info] Total number of tests run: 3 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 3, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [success] Total time: 5 s, completed 2026/03/01 0:47:34
さて、なぜsbt testでテストが実行できるのだろうか?まずはそこを考えておきたい。
- テストファイルをコンパイルするのはsbt(から呼び出されたscalac)の責務である。
- 具体的にテストファイルを実行して結果を返すのはsbtではなくテストフレームワーク(ここではScalaTest)の責務である。
- sbtは
sbt/test-interfaceというインターフェイスを定義しており、sbtに対応しているフレームワークはすべてこれを実装している。 - sbtは
sbt/test-interfaceを実装しているクラスを発見し、ユーザがテストを要求したときに規定の方法でそれを実行する。 - このため、sbtは
testだとかtestOnlyといった共通のインターフェイスを介してユーザにテスト機能を提供できる。
ここまで理解できれば、特定のタグがついたテストを実行するにはどうするか、分かりやすい。
テストにタグをつける
ユーザによってテストにつけられたタグをタグとして認識するのは、テストフレームワークの責務である。つまり、sbtもScalaコンパイラも、ユーザが設置したタグについて関知しない。テストのタグ付けはテストフレームワークのいち機能に過ぎない。
タグの定義
ScalaTestは、テストにタグを付ける機能を提供している。まずはタグを定義しよう。
まず、javaファイルとしてアノテーションも定義する:
package dev.capslock.scalatesttaggingexercise.tags; import java.lang.annotation.*; import org.scalatest.TagAnnotation; @TagAnnotation @Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD, ElementType.TYPE }) public @interface Heavy { }
こちらはsrc/test/java/dev/capslock/scalatesttaggingexercise/tags/Heavy.javaに配置する。
次に、Scalaのobjectを定義する:
package dev.capslock.scalatesttaggingexercise.tagobjects import org.scalatest.Tag object Heavy extends Tag("dev.capslock.scalatesttaggingexercise.tags.Heavy")
これをsrc/test/scala/dev/capslock/scalatesttaggingexercise/tagobjects/Tags.scalaという名前でテストファイルから読める場所に保存しておく。
Javaのアノテーションはファイル名と名前が揃っている必要がある。また、objectに渡す文字列はJavaのアノテーションを完全修飾名で正確に指し示していなければならない。
タグをつける
次に、テストにタグをつける。タグを付ける方法はテストスタイルによって異なる。FunSuite styleでは、testの第二引数にタグを渡す。これはvarargsなのでいくらでも渡してよい:
import org.scalatest.funsuite.AnyFunSuite import dev.capslock.scalatesttaggingexercise.tagobjects.Heavy import org.scalatest.tagobjects.Slow import org.scalatest.tagobjects.CPU class MySuite extends AnyFunSuite { test("example test", CPU) { // Added! assert(1 + 1 == 2) } test("another example test", Slow) { // Added! assert(2 + 2 == 4) } test("heavy test", Heavy, Slow) { // Added! assert(3 + 3 == 6) } }
また、アノテーションを定義している場合はテストスイート単位でタギングできる:
import org.scalatest.funsuite.AnyFunSuite import org.scalatest.tags.Slow // Added! @Slow class SlowSuite extends AnyFunSuite { test("very slow test") { assert(1 + 1 == 2) } test("another very slow test") { assert(2 + 2 == 4) } test("heavy test") { assert(3 + 3 == 6) } }
ScalaTestは標準でorg.scalatest.tags/org.scalatest.tagobjectsを提供してくれるが、これは前者がアノテーションを置いている場所で、後者が個々のテストに渡すオブジェクトを置いている場所だ。ユーザがカスタムでタグを用意する場合も、この規約に則ってtagsとtagobjectsを分けるとよさそう。
さて、これでテストとタグが関連づけられた。
ちなみに、タグのつけかたはstyleを定義しているtraitのScaladocのtagging testsのコーナーに記載されている。たとえば、FunSuite styleの場合はorg.scalatest.funsuite.AnyFunSuiteのscaladocを読むと:

みっちりと使い方が書いてある。Scalaの場合、まともなライブラリにはたいていちゃんとしたScaladocがついてくるので、とりあえず読んでみることをおすすめする。
実行するテストを選別する
さて、タグがいくら付いていようと、それがついたテストをうまいこと捌くのはテストランナーの責務だ。ScalaTestでは、org.scalatest.tools.Runnerがこれを引き受けている。
そして、タグによるテストの選別はここで定義されているため、我々はここに引数を渡せばよい。
-n
specifies a tag to include (Note: only one tag name allowed per -n) -n UnitTests -n FastTests -l
specifies a tag to exclude (Note: only one tag name allowed per -l) -l SlowTests -l PerfTests
また、sbtの機能として、testOptionsを指定する、もしくはsbt testOnly -- ...で フラグを渡すことでそのままランナーにフラグを渡せるようになっている。
sbt:scalatest-tagging-exercise> testOnly -- -n "dev.capslock.scalatesttaggingexercise.tags.Heavy" [info] MySuite: [info] - heavy test [info] Run completed in 67 milliseconds. [info] Total number of tests run: 1 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [success] Total time: 0 s, completed 2026/03/01 1:29:35 sbt:scalatest-tagging-exercise> testOnly -- -n "org.scalatest.tags.Slow" [info] MySuite: [info] - another example test [info] - heavy test [info] Run completed in 72 milliseconds. [info] Total number of tests run: 2 [info] Suites: completed 1, aborted 0 [info] Tests: succeeded 2, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [success] Total time: 0 s, completed 2026/03/01 1:32:08
-nフラグで特定のタグがついたテストを実行できることがわかった。タグは、完全修飾名で正確にタグアノテーションを指定する必要がある(ここでは、dev.capslock.scalatesttaggingexercise.tagobjects.Heavyではなく、dev.capslock.scalatesttaggingexercise.tags.Heavyを書く、ということ)特定のタグがついたテストを除外するには、-lを利用する。また、ここでScalaTestに最初からあるタグを指定するには、org.scalatest.tagsを利用する。
複数のタグを指定するには"Foo Bar"のように二重引用符で括るか、-n Foo -n Barのように多重に定義すればよい。
カスタムタスクを作成する
ここまで来れば、sbtで便利にテストを定義できるところまでもう少しだ。build.sbtをいじって、カスタムタスクを定義しよう:
lazy val testOnlyHeavy = taskKey[Unit]("Run only the heavy tests") testOnlyHeavy := (Test / testOnly) .toTask( " -- -n dev.capslock.scalatesttaggingexercise.tags.Heavy" // DO NOT FORGET TO INSERT INITIAL SPACE!!! ) .value lazy val testOnlySlow = taskKey[Unit]("Run only the slow tests") testOnlySlow := (Test / testOnly) .toTask( " -- -n org.scalatest.tags.Slow" ) .value
最初のlazy valでは、testOnlyHeavy/testOnlySlowという名前のタスクがあることを宣言する。ここでは名前だけ定義しているというわけ。
次に中身を定義している。:=の右辺でそのタスクが返すはずの処理を組み立ててやればよい。ただし直接計算するのではなく、他のタスクから組み立てたりする。ここではTest / testOnlyを再利用する。
toTask は「CLIから渡されるはずだった引数文字列を、コンパイル時に埋め込む」操作だと思ってもらったらいい。元々のtestOnlyはInputTask[Unit]であり、ユーザ入力を受け取ることを想定したタスクだ。これ単体では実行結果にすることができない。なんと、toTaskはInputTaskに引数を渡すことでTaskに変形できる。Taskはもう値を取り出せる形になっているから、.valueでtestOnlyHeavyの結果として値を割り当てて完成だ。
sbtのタスクについてもそのうち書きたいね。
実行
あとはタスクを実行するだけだ。
sbt:scalatest-tagging-exercise> testOnlyHeavy [info] SlowSuite: [info] MySuite: [info] - heavy test [info] Run completed in 66 milliseconds. [info] Total number of tests run: 1 [info] Suites: completed 2, aborted 0 [info] Tests: succeeded 1, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [success] Total time: 0 s, completed 2026/03/01 2:05:45 sbt:scalatest-tagging-exercise> testOnlySlow [info] SlowSuite: [info] - very slow test [info] - another very slow test [info] - heavy test [info] MySuite: [info] - another example test [info] - heavy test [info] Run completed in 76 milliseconds. [info] Total number of tests run: 5 [info] Suites: completed 2, aborted 0 [info] Tests: succeeded 5, failed 0, canceled 0, ignored 0, pending 0 [info] All tests passed. [success] Total time: 0 s, completed 2026/03/01 2:05:47
やったね。
まとめ
- sbtは、あらかじめ定められたインターフェイスに従ってテストツールのランナーを呼び出す。
- ScalaTestはこの規約を定義しており、呼び出されることができる。
- ScalaTestはテストに対してタグを定義する能力を有しており、ランナーに
-nや-lといったフラグを渡すことで実行するテストを制御できる。 - ScalaTestでは、タグはJavaアノテーションとScala上のオブジェクトとして2つ同時に定義する必要がある*1。
- sbtのカスタムタスクを定義することで、特定のタグがついたテストを実行する専用のタスクを定義できる。
*1:厳密にはそうではないのだが、そのほうが使いやすくて都合がよい