Hi, I’m Phuc, software engineer at AI Tech Studio, Dynalyst Team.
Automated testing is an important part of Software development. A well implemented automated test system improves development productivity, but on the other hand is difficult to write, understand, and tests that are difficult to maintain will increase the software development effort. Gerard Meszaros pointed out in his famous xUnit Test Patterns: Refactoring Test Code book that test maintenance should not increase the cost of software development. Today, I would like to introduce some simple ways in which ScalaTest – a famous testing library in Scala, helps write better tests.
1. Sharing fixture setups
When multiple tests need to work with the same fixtures, we should try to avoid duplicated fixture setup code. A good way of structuring tests is separating the fixture setup and behavior assertions. ScalaTest provides withFixture
to share fixtures across tests.
class AdServingServiceSpec extends FixtureAnyFlatSpec with Matchers {
case class FixtureParam(
userService: UserService,
adCandidateService: AdCandidateService
)
override protected def withFixture(test: OneArgTest): Outcome = {
val fixture = FixtureParam(
new UserService(),
new AdCandidateService()
)
withFixture(test.toNoArgTest(fixture))
}
it should "find right ad for right user" in { fixture =>
val user = fixture.userService.getUser()
val ad = fixture.adCandidateService.getBestCandidateFor(Some(user))
ad shouldBe bestAdForUser
}
it should "return nothing when no user" in { fixture =>
val ad = fixture.adCandidateService.getBestCandidateFor(None)
ad shouldBe empty
}
}
If your fixture setup is complicated, another approach is using fixture-context objects.
2. Type-safe assertions
ScalaTest users often use shouldBe
(or should equal
) for equality assertions.
For example:
"Hi" should equal "Hi"
How about this case?
Some("Hi") should equal "Hi"
Actually, it failed. But wait, can we do it better? Like, enforce type constraints, and raise an error at compile-time.
ScalaTest has triple equals syntax ===
and TypeCheckedTripleEquals
trait to do this.
The following code raises compilation error with types Some[String] and String do not adhere to the type constraint
message.
class SampleSpec extends AnyWordSpec with Matchers with TypeCheckedTripleEquals {
"sample spec" should {
"return true" in {
Some("1") should ===("1")
}
}
}
For negative equality assertion and type constraints, we can use !==
syntax.
3. Property-based testing
Property-based testing is a test writing strategy for checking whether the application behavior abides by the property or composed property rules. Instead of focusing on individual examples, property-based testing takes into consideration the higher-level behavior covered by multiple inputs at the same time. ScalaTest supports property-based testing as follows:
class TestAddMethodSpec
extends AnyFlatSpec
with Matchers
with TableDrivenPropertyChecks {
"add method" should "return addition of 2 numbers" in {
val combos = Table(
("a", "b"),
(-10, -11),
(0, 1),
(10, 11),
(20, 21)
)
forAll(combos) { (a: Int, b: Int) =>
add(a, b) should ===(a + b)
}
}
}
You can also use ScalaCheck for generating the test data automatically.
4. Informative fail message
Informative fail messages save us a lot of time when debugging. For example, we have the following multiple assertions test:
class ExampleSpec extends AnyFlatSpec with Matchers {
case class Obj(attr1: Int, attr2: Int, attr3: Int)
"constructor" should "create new object" in {
val data = Obj(2, 1, 2)
data.attr1 shouldBe (2)
data.attr2 shouldBe (2)
data.attr3 shouldBe (2)
}
}
The test will fail with:
[info] - should sample *** FAILED ***
[info] 1 was not equal to 2
Looking at the message we do not get an idea of where the failure occurs. Imagine the test failed on CI, to fix the test, we open the IDE, run tests locally, start debugging, add some println
…
We can reduce all the above steps by adding a clue to the failure message.
withClue("attr1 =") { data.attr1 shouldBe (2) }
withClue("attr2 =") { data.attr2 shouldBe (2) }
withClue("attr3 =") { data.attr3 shouldBe (2) }
Then, the failure message is [info] attr2 = 1 was not equal to 2
.
Summary
In this blog, I have introduced four techniques in ScalaTest for writing better tests.
1. Share fixture setups
2. Use type-safe assertions
3. Use Property-based testing
4. Provide informative fail messages
I hope it will help you.
Happy codding!