1. Introduction
Exception handling is one of the most crucial aspects of writing a robust application. Exceptions can arise in an application for various reasons, either unexpectedly or as a result of deliberately managing undesired situations.
In the Scala ecosystem, ScalaTest is one of the most popular testing frameworks. In addition to the numerous methods for verifying expected data or states, ScalaTest offers a variety of approaches to testing exceptions. We can use ScalaTest to test scenarios that throw exceptions, as well as to handle failures in Try or Future constructs.
In this tutorial, we’ll explore various ways of handling exceptions using ScalaTest.
2. Testing Methods That Throw Exception
Typically, in Scala, we avoid throwing exceptions as it may not be evident from the method signature. Nonetheless, in certain scenarios, such as when utilizing the require() method or calling Java methods, exceptions might be raised. We can test such methods in ScalaTest using two different approaches.
Let’s create a method to showcase the utilization of exception handling in ScalaTest:
def explodingMethod(): Unit = {
throw new RuntimeException("Boom boom!")
}
Notably we’re going to use the AsyncWordSpec flavor of ScalaTest for this tutorial.
2.1. Using intercept()
We can use the intercept() method in ScalaTest to capture and verify expected exceptions.
Let’s write the test case for testing the above-created method:
val exception = intercept[RuntimeException](explodingMethod())
exception shouldBe a[RuntimeException]
exception.getMessage shouldBe "Boom boom!"
In this context, the intercept() method requires the type of the exception as its type parameter. Additionally, it takes a method or a code block to be tested as the parameter. The intercept() method extracts the exception and returns it. However, if the code block doesn’t throw the expected exception, the test becomes a failure.
Since we extracted the exception, we can perform additional checks on the resultant object.
Moreover, we can also use any parent class of the expected exception:
val exception = intercept[Exception](explodingMethod())
exception shouldBe a[Exception]
exception shouldBe a[RuntimeException]
In the code above, we’re using Exception in the intercept block instead of RuntimeException.
2.2. Using assertThrows()
Another way to test for an exception is by using the assertThrows() method. Let’s test the previously created method using assertThrows():
assertThrows[RuntimeException](explodingMethod())
The test fails if there is no RuntimeException thrown in the explodingMethod(). Similar to the intercept() method, we can also utilize any parent type of RuntimeException.
*The intercept() and assertThrows() operate similarly, except that in intercept(), we can extract the exception object, which isn’t possible with assertThrows().*
3. Testing for Exceptions in Try
In Scala, we commonly utilize Try when dealing with code that might fail with an exception. Let’s explore how we can test for exceptions in methods that return a Try instance.
Let’s create a sample method that returns a Try:
def getLastDigit(num: Int): Try[Int] = {
Try {
require(num > 0, "Only positive numbers supported!")
num % 10
}
}
Now, we can write the test case to test this method:
val result = getLastDigit(-100)
result.failed.get shouldBe a[IllegalArgumentException]
result.failed.get.getMessage should include(
"Only positive numbers supported!"
)
In this case, we’re extracting the exception object from the Try instance using the failed() method. We can then compare the type and the error message easily.
4. Testing for Exceptions in Future
In Scala, asynchronous operations are generally handled using the Future type. In this section, we’ll test for exceptions on Future values.
Firstly, let’s write a method to simulate asynchronous operation using Future:
def getDBResult(): Future[Int] = {
Future.failed(new RuntimeException("Unexpected error occurred!"))
}
We can use multiple ways in ScalaTest to test for the failures in Future type. We’re using the AsyncWordSpec that expects Future[Assertion] as the response from the test case.
4.1. Using Future.failed()
We can use the failed() method on the Future instance to extract the exception object and compare it against the expected exception:
val futureResult = getDBResult()
futureResult.failed.map { ex =>
ex shouldBe a[RuntimeException]
ex.getMessage shouldBe "Unexpected error occurred!"
}
Here, we extract the exception object and compare it against the expected exception message.
4.2. Using recoverToSucceededIf()
Another way to test a failed Future is by using the recoverToSucceededIf() method:
val futureResult = getDBResult()
recoverToSucceededIf[RuntimeException](futureResult)
This is the async version of the assertThrows() method we discussed earlier. In this case, we can only compare the exception type, and not the error message.
4.3. Using recoverToExceptionIf()
ScalaTest also offers the recoverToExceptionIf() method, allowing us to retrieve the exception object and perform more detailed comparisons. This is similar to the intercept() method we discussed earlier:
val futureResult = getDBResult()
recoverToExceptionIf[RuntimeException](futureResult).map { rt =>
rt.getMessage shouldBe "Unexpected error occurred!"
}
This approach provides more flexibility in terms of inspecting and asserting specific details of the exception.
5. Conclusion
In this article, we explored various approaches for exception testing with ScalaTest. We also discussed techniques to assess failures in Try and Future data types. Furthermore, we explored ways to perform detailed tests and closely examine specific details in the context of failures.
As always, the sample code used in this article is available over on GitHub.