1. Introduction

SBT is one of the most popular build tools in the Scala ecosystem. It has many features to help Scala programmers package, run, and test their applications.

In this tutorial, we’ll look at a feature that has changed in SBT 1.9.0: writing and running integration tests. We’ll briefly see how we used to set integration tests up before SBT 1.9.0 and what the recommended way is nowadays.

2. Integration Testing

In software engineering, integration testing is the phase of software testing where we combine two or more software modules and test them as a whole. Generally speaking, integration testing happens after unit testing, where we validate the behavior of units of code in isolation, and before system testing, where we verify that the entire system fulfills its requirements.

Integration testing is a crucial phase nowadays, as it allows developers to test the interaction between small groups of modules before doing so with the system as a whole. It helps uncover issues that are more challenging to detect with unit tests, such as incorrect error handling, interactions with third-party services, and data formatting.

3. Integration Testing Before SBT 1.9.0

Until version 1.9.0, SBT considered integration tests as an additional test configuration. This means we could have a directory named it containing separate test source files with dedicated tasks and settings, along with main (for the application code) and test (for the unit tests).

Let’s see how to enable integration testing before SBT 1.9.0:

lazy val root = (project in file("."))
  .configs(IntegrationTest)
  .settings(
    Defaults.itSettings,
    libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.17" % "it"
  )

The code snippet above shows three interesting things:

  • configs(IntegrationTest) adds the default SBT integration test configuration. By default, most settings are equal to the standard Test configuration;
  • settings(Default.itSettings) adds compilation, packaging, and testing tasks and settings to the IntegrationTest configuration;
  • libraryDependencies += “org.scalatest” %% “scalatest” % “it” adds the ScalaTest library to the IntegrationTest configuration. As we just saw, we can refer to it with it. If we wanted to add ScalaTest to the standard test configuration, we could have used “test,it”.

After setting up the build, we can create the source directory as usual. For example, src/it/scala will contain the test source files, whereas src/it/resources will host the test resources.

Before diving into the test execution, let’s add a sample test in src/it/scala/com/baeldung/it:

class SampleTest extends AnyFlatSpec {
  "The sample test" should "pass" in {
    assert(1 + 1 == 2)
  }
}

If we configure SBT as we saw above, we can use the test or testOnly tasks to run the integration tests. For example, to run all tests, we can run IntegrationTest/test:

sbt:sbt-integration-tests-pre-1-9-0> IntegrationTest/test
[info] compiling 1 Scala source to ...
[info] SampleTest:
[info] The sample test
[info] - should pass
[info] Run completed in 254 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.

IntegrationTest in SBT is an example of the definition of a custom configuration. If we set up the build described above, the integration test configuration inherits all the tasks and settings available for the standard test configuration.

Since SBT 1.9.0, the maintainers decided to deprecate the custom configuration mechanism to simplify the build tool. Therefore, IntegrationTest was also deprecated. In the next section, we’ll see the new recommended approach.

4. Integration Testing Since SBT 1.9.0

Since SBT 1.9.0, the configuration above triggers deprecation warnings:

.../integration-tests-pre-1-9-0/build.sbt:7: warning: value IntegrationTest in trait LibraryManagementSyntax is deprecated (since 1.9.0): Create a separate subproject for testing instead
  .configs(IntegrationTest)
           ^
.../integration-tests-pre-1-9-0/build.sbt:9: warning: lazy value itSettings in object Defaults is deprecated (since 1.9.0): Create a separate subproject instead of using IntegrationTest and in addition avoid using itSettings
    Defaults.itSettings,

Since SBT 1.9.0, the recommended way to set up integration tests is to create a subproject named “integration” and treat integration tests as standard tests. Let’s see how to set up the build:

lazy val root = (project in file("."))

lazy val integration = (project in file("integration"))
  .dependsOn(root)
  .settings(
    publish / skip := true,
    libraryDependencies += "org.scalatest" %% "scalatest" % "3.2.17" % Test
  )

First, we define the root project. In a real application, this would likely contain the code to test and the unit tests. Secondly, we define the integration subproject, depending on root. Its settings are now very simple; we’ll use the standard SBT test configuration for our integration tests. We just added publish / skip := true to avoid publishing the test project.

With this setup, we’ll define the test classes in integration/src/test/scala/com/baeldung/it rather than in src/it/scala/com/baeldung/it (as we did before). Similarly, we’ll run them with integration / test, where integration represents the name of the subproject:

sbt:sbt-integration-tests-post-1-9-0> integration / test
[info] compiling 1 Scala source to ...
[info] SampleTest:
[info] The sample test
[info] - should pass
[info] Run completed in 259 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.

This way, SBT treats integration tests as unit tests, only in a different sub-project. Therefore, we’ll still have access to all the settings and tasks available in the standard test configuration.

5. Conclusion

In this article, we saw how to set up, write, and run integration tests in SBT. Since the recommended setup changed in SBT 1.9.0, we first looked at the “legacy” way and compared it with the most recent one.

As usual, you can find the code over on GitHub.