I would say that mixing in BeforeAndAfter or BeforeAndAfterAll is one of the most intuitive ways to reduce duplication in a scenario where you want: "Settings" β "run test1" β "Settings" β "run test2", "Setup" will be (basically ) same.
Suppose we have a nasty, difficult Database test:
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() }
This is a singleton (therefore, we cannot just create a new Database for each test) and its mutable (therefore, if the first test changes something, this will affect the second test).
Obviously, it would be more desirable to have such a structure (for example, it would be much better to work with the List that implements Database in this example), but suppose we cannot just change this structure,
Change Please note that in this case it is impossible (at least I cannot think of a way) to run tests that mutate the same singleton instance in parallel.
To still be able to test it, we must 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 testuite base class extend BeforeAndAfter . Note There are two features: BeforeAndAfter , which defines before and after , which are executed before and after each test case , and BeforeAndAfterAll , which differs in that it defines methods that are executed before and after each set of tests .
class RestAPITest extends FlatSpec with ShouldMatchers with BeforeAndAfter { before { Database.delete() Database.add(4) Database.add(2) } }
Now we can have the ATest test suite expand 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 two values, 4 and 2 . Now we can use other tests for 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 share the common functionality in ordinary methods, as is done in getAll , which is used by both test cases.
Addendum :
Quote from the question:
How do you keep your test packages short?
The test code in my opinion is not much different from the production code. Perform common functions with the methods and put them in separate traits if they do not belong to a specific class that you already have.
However, if your production code requires that the tests always run the same piece of code, there may be 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 check this function if you did not populate the database with the two values ββyou want to add. The best way to keep your tests short and readable in this case is to reorganize your production code.
"plus 3 2" should "be 5" in { Database.add(3) Database.add(2) plus shouldBe 5 }
can be
"plus 3 2" should "be 5" in { plus(3,2) shouldBe 5 }
In some cases, it is not easy to get rid of dependencies. But you may want your objects in a test script to depend on a special test environment. A database is a great example for this, as is a file system, or logging. These things are usually more expensive to execute (I / O access) and may have additional dependencies that you must first install.
In these cases, your tests are likely to profit from using mock objects . For example, you may need to implement a database in memory that implements the interface of your database.