I'd say mixing in BeforeAndAfter
or BeforeAndAfterAll
are among the most intuitive ways to reduce duplication in a scenario where you want to do: "Setup" -> "run test1" -> "Setup" -> "run test2", "Setup" being (mostly) the same.
Suppose we have a nasty, hard to test Database
:
object Database {
private var content: List[Int] = Nil
def add(value: Int) = content = value :: content
def remove(): Unit = content = if (content.nonEmpty) content.tail else Nil
def delete(): Unit = content = Nil
def get: Option[Int] = content.headOption
override def toString = content.toString()
}
It's a singleton (so we can't just instantiate a new Database
for each test) and its mutable (so if the first test changes something, it would affect the second test).
Obviously, it would be more desireable to not have such a structure (e.g., it would be much nicer to work with the List
that implements the Database
in this example), but suppose we cannot simply change this structure.
Edit: Note that, in such a case, it is impossible (at least I can't think of a way) to run tests mutating the same singleton instance in parallel.
To still be able to test it, we need to have a clean state before running each test. Assuming we want to populate the database with the same values for each test, we could let our base testsuite class extend BeforeAndAfter
. Note: There exists two traits: BeforeAndAfter
, which defines before
and after
that are run before and after the execution of each test case, and BeforeAndAfterAll
, which is different in that it defines methods that are run before and after each test suite.
class RestAPITest extends FlatSpec with ShouldMatchers with BeforeAndAfter {
before {
Database.delete()
Database.add(4)
Database.add(2)
}
}
Now we can have a test suite ATest
extend this base class:
class ATest extends RestAPITest {
"The database" should "not be empty" in {
Database.get shouldBe defined
}
it should "contain at least two entries" in {
Database.remove()
Database.get shouldBe defined
}
it should "contain at most two entries" in {
Database.remove()
Database.remove()
Database.get should not be defined
}
}
At the beginning of each test, the database contains the two values 4
and 2
. We can now have other testsuits extend this base class:
class BTest extends RestAPITest {
"The contents of the database" should "add up to 6" in {
getAll.sum shouldBe 6
}
"After adding seven, the contents of the database" should "add up to 13" in {
Database.add(7)
getAll.sum shouldBe 13
}
def getAll: List[Int] = {
var result: List[Int] = Nil
var next = Database.get
while(next.isDefined){
result = next.get :: result
Database.remove()
next = Database.get
}
result
}
}
Of course, we can also factor out common functionality in regular methods, as done in getAll
which is used by both test cases.
Addendum:
Quote from the question:
How do you keep your test suites short?
Test code isn't much different from production code in my opinion. Factor out common functionality using methods and put them into separate traits if they don't belong to a specific class you already have.
However, if your production code requires the tests to execute always the same piece of code, then maybe there are too many dependencies in your production code. Say you have a function (in your production code)
def plus: Int = {
val x = Database.get.get
Database.remove()
x + Database.get.get
}
then you cannot test this function unless you populate your database with the two values that you want to add. The best way to make your tests shorter and more readable in such a case would be to refactor your production code.
"plus 3 2" should "be 5" in {
Database.add(3)
Database.add(2)
plus shouldBe 5
}
may become
"plus 3 2" should "be 5" in {
plus(3,2) shouldBe 5
}
In some cases it's not easy to get rid of dependencies. But you may want your objects in a test scenario to depend on a special test environment. The database is a great example for that, as is the file system, or logging. Those things tend to be more costly in execution (I/O access) and may have themselves further dependencies that you must first establish.
In these cases, your tests will most likely profit from using mock objects. For example, you may like to implement an in-memory database that implements your database's interface.
BeforeAndAfterAll
is probably the way to go. What's not working for you regarding that? – Fromenty