1. Introduction

There are several testing libraries that can be used in Scala. Some of the most popular are ScalaTest, MUnit, ScalaCheck, and Specs2.

In this tutorial, we’ll look at one of the simplest testing libraries, uTest.

2. Why uTest?

There are some advantages of uTest over other libraries:

  • coloured, clearly formatted, and readable test results
  • uniform syntax for defining simple and nested tests
  • uniform syntax for executing tests
  • support for Scala, Scala JS, and Scala Native

3. Setup

To use uTest, we can first add the dependency to build.sbt:

libraryDependencies += "com.lihaoyi" %% "utest" % "0.8.1" % "test"

To make the tests detectable by SBT, we need to add a line to the build configuration:

testFrameworks += new TestFramework("utest.runner.Framework")

4. Writing a Simple Test

Now that we have set up uTest, we can write our first test case.

We define the test by extending it from TestSuite. Then, we define the necessary tests by overriding the method tests():

import utest._
object SimpleUTest extends TestSuite {
  override def tests: Tests = Tests {
    test("str") {
      val name = "Baeldung"
      assert(name.length == 8)
    }
  }
}

We should note that the test class is defined as an object and not a class. Otherwise, the test runner won’t detect them as tests.

We can define the individual tests by using the test() block. We need to import the entire utest package to access all the methods.

5. Executing the Tests

We can execute the tests by simply running the sbt command:

sbt test

Here’s how we can run only the tests under a particular sub-module (for e.g.: scala_libraries_4) in a multi-module build:

sbt 'scala_libraries_4/test'

This executes all the tests and generates a simple report:

-------------------------------- Running Tests --------------------------------
+ com.baeldung.scala.utest.SimpleUTest.str 18ms  

We can also execute a single test instead of all the tests by providing the fully qualified path of the test:

sbt 'scala_libraries_4/testOnly -- com.baeldung.scala.utest.SimpleUTest.str'

If we only provide the path till SimpleUTest, then all the test cases within this suite will get executed.

If a test fails, uTest shows a nicely-formatted error message. Let’s make a slight change in the assert statement in the above code to make the test fail:

assert(name.length == 7)

Now, when we run the same test, we’ll get an error message: uTest Failure Message

IntelliJ IDEA already has integration with uTest. As a result, we can run the tests in IntelliJ IDEA just like any other testing library. Similarly, we can also run the tests in VSCode using the Scala Metals extension.

6. Asynchronous Test

We can also write asynchronous test suites in the same way as synchronous tests.

The test runner automatically handles these tests. We only need to return a Future value from the test. If the Future is a failure, the test fails, or else the test passes.

Let’s look at a simple asynchronous test:

object AsyncTest extends TestSuite {
  def getFromDB(): Future[Int] = Future { 42 }
  override def tests: Tests = Tests {
    test("get something from database") {
      getFromDB().map(v => assert(v == 500))
    }
  }
}

Now, we can execute this test in SBT:

testOnly -- com.baeldung.scala.utest.AsyncTest

7. Additional Features

uTest provides many additional features. Let’s look at some of the most useful ones in this section.

7.1. Arrow Assert

uTest provides a syntactic sugar for the assert statement. We can use the ==> symbol to write assert:

test("arrow assert") {
  val name = "Baeldung"
  name.length ==> 8
}

7.2. Before and After Execution

Sometimes, we need to execute some code before and after the test. In uTest, we can simply write the code within the test object to execute it before all the tests.

We can also override the method utestAfterAll() to execute some code, like cleanup logic, after all the tests.

Now, let’s see the before and after execution in action:

object BeforeAfterTest extends TestSuite {
  println("This is executed before the tests")
  override def utestAfterAll() = {
    println("This method will be executed after all the tests")
  }
  override def tests: Tests = Tests {
    test("simple test") {
      assert(1 == 1)
    }
    test("simple test 2") {
      assert(2 == 2)
    }
  }
}

Here’s the output when we execute this test in SBT:

------------ Running Tests com.baeldung.scala.utest.BeforeAfterTest ------------
This is executed before the tests
+ com.baeldung.scala.utest.BeforeAfterTest.simple test 23ms  
+ com.baeldung.scala.utest.BeforeAfterTest.simple test 2 0ms  
This method will be executed after all the tests

7.3. Testing Exception

We can use intercept() to verify that an exception is thrown. It verifies that the expected exception is thrown within the block.

Additionally, the intercept block returns the thrown exception so that we can write more detailed tests on the exception instance:

override def tests: Tests = Tests {
  def funnyMethod: String = throw new RuntimeException("Uh oh...")
  test("Handle an exception") {
    val ex = intercept[RuntimeException] {
      funnyMethod
    }
    assert(ex.getMessage == "Uh oh...")
  }
}

7.4. Retry

Another very useful feature of uTest is the ability to retry the tests multiple times. There may be some flaky tests that fail occasionally. However, due to these flaky tests, the entire test pipeline might fail and we need to re-run the whole CI pipeline again.

We can use the retry() method to provide the maximum number of retries before the test is considered a failure:

def flakyMethod: Int = Random.nextInt(4)
test("retry flaky test") - retry(3) {
  val value = flakyMethod
  assert(value > 2)
}

Now, uTest retries the test case at most three more times, and it reports the test as a failure only if the test fails all four times.

We can also configure retry for the entire test suite. For that, we need to mix in with the trait TestSuite.Retries. This forces us to override the retry count which will then be applied to all the tests within the test suite:

object RetryTestSuite extends TestSuite with TestSuite.Retries {
  override def utestRetryCount: Int = 3
  override def tests: Tests = Tests {
    test("retryable test 1") {
      assert(true)
    }
    test("retryable test 2") {
      assert(true)
    }
  }
}

7.5. Nested Tests

We can also write nested test blocks in uTest. This is very useful in sharing code between different tests.

uTest supports writing any level of the nested test block.

Let’s look at nested test blocks in action:

override def tests: Tests = Tests {
  test("outer test") - {
    val list = List(1,2)
    println("This is an outer level of the test.")
    test("inner test 1") - {
      val list2 = List(10,20)
      list.zip(list2) ==> List((1,10), (2,20))
    }
    test("inner test 2") - {
      val str = List("a", "b")
      list.zip(str) ==> List((1,"a"), (2,"b"))
    }
  }
  test("outer test 2") - {
    println("there is no nesting level here") 
    assert(true)
  }
}

When we execute this test, we get the output:

-------------------------------- Running Tests --------------------------------
This is an outer level of the test.
+ com.baeldung.scala.utest.NestedTest.outer test.inner test 1 52ms  
This is an outer level of the test.
+ com.baeldung.scala.utest.NestedTest.outer test.inner test 2 0ms  
there is no nesting level here
+ com.baeldung.scala.utest.NestedTest.outer test 2 25ms  
Execution took 77ms
3 tests, 3 passed
All tests in com.baeldung.scala.utest.NestedTest passed
===============================================

We should note that only the leaf nodes(innermost test blocks) of nested tests are considered tests and executed. The inner blocks are just fixtures to share reusable code.

8. Conclusion

In this article, we looked at uTest and some of its major features. Libraries such as Ammonite and Fansi use uTest as the testing framework.

As always, the sample code used in this tutorial is available over on GitHub.