What's new in ScalaTest 3
ScalaTest 3.0.0 was recently released. Being a major release it brings lots of features, changes (some of them potentially breaking) and new deprecations.
In this post I will focus more on the main additions and enhancements, followed by some code examples.
Installation
Add this line to your sbt build file:
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.0" % "test"
If you use maven add a dependency to your pom.xml file:
<dependency>
<groupId>org.scalatest</groupId>
<artifactId>scalatest_2.11</artifactId>
<version>3.0.0</version>
<scope>test</scope>
</dependency>
For Scala 2.10 use scalatest_2.10 artifact id.
Main improvements
Many improvements are added in this version. Here's a short list:
- Full support for Scala.js
- Added async styles for testing
Future:
AsyncWordSpec,AsyncFunSuite,AsyncFunSpec,AsyncFlatSpec,AsyncFreeSpecandAsyncFeatureSpec. The trait versions are added as well:AsyncWordSpecLike,AsyncFunSuiteLike,AsyncFunSpecLike,AsyncFlatSpecLike,AsyncFreeSpecLikeandAsyncFeatureSpecLike. The result type of tests isFuture[Assertion]. A few examples can be found in the next section. - Added
org.scalatest.compatible.Assertionmarker trait, required for the new async testing styles. Also,org.scalatest.Assertiontype alias is defined in theorg.scalatestpackage object. - Enhanced
Matchersby adding more matcher words for testing containers:oneElementOf,noElementsOf,atLeastOneElementOf,atMostOneElementOf,allElementsOfandinOrderElementsOf, which must followcontain(examples in the next section). - Changed the result type of assertions and matcher expressions from
UnittoAssertion. - Changed the result type of styles to be
Anyinstead ofUnit. Therefore, compiling with-Ywarn-value-discardwill not result in warnings for tests that end with an assertion or matcher expression because their result type is notUnitanymore.
Examples
Async styles
Probably one of the most expected features in Scalatest 3. Async styles allow you to test futures in an async way, as you would do in your implementation code. All async styles expect tests to have the result type of Future[Assertion].
Suppose we have a simple user service:
import scala.concurrent.Future
import scala.util.{Failure, Success}
//Don't even dare to implement this way, it's just for showcase purposes.
class UserService {
private var users: Seq[User] = Seq(User("11111", "johndoe", 30))
def findUser(id: String): Future[User] = Future.fromTry {
Thread.sleep(3000)
users.find(_.id == id) match {
case Some(user) => Success(user)
case _ => Failure(UserNotFoundException(s"User with id: $id not found"))
}
}
def addUser(user: User): Future[User] = Future.fromTry {
if (users.exists(_.username == user.username))
Failure(UserAlreadyExistsException(s"User with username: ${user.username} already exists!"))
else {
users :+= user
Success(user)
}
}
}
case class User(id: String, username: String, age: Int)
case class UserNotFoundException(message: String) extends Exception(message)
case class UserAlreadyExistsException(message: String) extends Exception(message)
And the test case:
import org.scalatest.{AsyncWordSpecLike, Matchers}
import scala.concurrent.ExecutionContext.Implicits.global
class UserServiceSpec extends AsyncWordSpecLike with Matchers {
val userService = new UserService
"UserService" should {
"return a User" when {
"searching by a valid id" in {
val id = "11111"
userService.findUser(id).map { user =>
user.id shouldBe id
user.username shouldBe "johndoe"
user.age shouldBe 30
}
}
}
}
}
If we change user.age shouldBe 30 to user.age shouldBe 20 the test will fail:
30 was not equal to 20
ScalaTestFailureLocation: com.ted.playground.scalatest.service.UserServiceSpec$$anonfun$1$$anonfun$apply$mcV$sp$1$$anonfun$apply$mcV$sp$2$$anonfun$apply$1 at (UserServiceSpec.scala:19)
org.scalatest.exceptions.TestFailedException: 30 was not equal to 20
at org.scalatest.MatchersHelper$.indicateFailure(MatchersHelper.scala:340)
at org.scalatest.Matchers$AnyShouldWrapper.shouldBe(Matchers.scala:6864)
at com.ted.playground.scalatest.service.UserServiceSpec$$anonfun$1$$anonfun$apply$mcV$sp$1$$anonfun$apply$mcV$sp$2$$anonfun$apply$1.apply(UserServiceSpec.scala:19)
at com.ted.playground.scalatest.service.UserServiceSpec$$anonfun$1$$anonfun$apply$mcV$sp$1$$anonfun$apply$mcV$sp$2$$anonfun$apply$1.apply(UserServiceSpec.scala:16)
at scala.util.Success$$anonfun$map$1.apply(Try.scala:236)
at scala.util.Try$.apply(Try.scala:191)
at scala.util.Success.map(Try.scala:236)
at scala.concurrent.Future$$anonfun$map$1.apply(Future.scala:235)
at scala.concurrent.Future$$anonfun$map$1.apply(Future.scala:235)
at scala.concurrent.impl.CallbackRunnable.run(Promise.scala:32)
at scala.concurrent.impl.ExecutionContextImpl$AdaptedForkJoinTask.exec(ExecutionContextImpl.scala:121)
at scala.concurrent.forkjoin.ForkJoinTask.doExec(ForkJoinTask.java:260)
at scala.concurrent.forkjoin.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1339)
at scala.concurrent.forkjoin.ForkJoinPool.runWorker(ForkJoinPool.java:1979)
at scala.concurrent.forkjoin.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:107)
Testing failed futures
Failed futures can be tested in two ways: using recoverToSucceededIf or recoverToExceptionIf:
recoverToSucceededIfis the async sister ofassertThrowswhich is used for asserting the type of the exception the future ends in. It yields aFuture[Assertion]:
"return UserNotFoundException" when {
"the user does not exist" in {
val id = "1"
recoverToSucceededIf[UserNotFoundException](userService.findUser(id))
}
}
recoverToExceptionIfyields aFuture[T], whereTis the type of the expected exception. This is useful when you want to test some of the exception's fields (e.g.ex.message):
"return UserAlreadyExistsException" when {
"adding a user with existing username" in {
val username = "johndoe"
val user = User("12345", username, 25)
recoverToExceptionIf[UserAlreadyExistsException] {
userService.addUser(user)
}.map { ex =>
ex.message shouldBe s"User with username: $username already exists!"
}
}
}
recoverToExceptionIf is the async equivalent of intercept.
Worth to mention:
- You can still opt for using
ScalaFuturesas it wasn't removed from the library. - It is possible to have synchronous tests in async Spec/Suite. However, the result type of the test body must be
Assertion.
New matchers
As previously mentioned, a series of matcher words for containers were added. Here you can find some examples:
val collection = Seq(1, 2, 3, 4)
collection should contain oneElementOf List(8, 7, 6, 3)
collection should contain oneElementOf Vector(2)
collection should contain noElementsOf Seq(8, 7, 6, 5)
collection should contain noElementsOf List()
collection should contain atLeastOneElementOf List(8, 7, 6, 3)
collection should contain atLeastOneElementOf Set(1, 2, 6, 3)
collection should contain atMostOneElementOf List(8, 7, 6, 3)
collection should contain atMostOneElementOf Seq(5)
collection should contain atMostOneElementOf Vector()
collection should contain allElementsOf List(1, 3, 3, 3, 4, 3, 2)
collection should contain inOrderElementsOf List(1, 2, 3, 4)
collection should contain inOrderElementsOf Vector(1, 2, 3, 4, 1)
collection should contain inOrderElementsOf List(1, 2, 3, 4, 2)
Wrap-up
The full release notes for Scalactic/ScalaTest 3 are available here, with details about breaking changes, new deprecations and the expired deprecations. Scaladocs for ScalaTest 3 can be found here.
If you wish to play around with the code, feel free to fork my Scalatest 3 playground on github.
Enjoy the testing!