In a specification some examples are very straightforward. They just check that a function is returning expected values when given some inputs. However other examples can be more complex and require to execute in a specific context:
For all those situations, there is a
The org.specs2.specification.BeforeEach
trait defines an action that will be executed before each example:
class BeforeSpecification extends org.specs2.mutable.Specification with BeforeEach:
// you need to define the "before" action
def before = step(println("before"))
"example 1" >> {
println("example1"); ok
}
"example 2" >> {
println("example2"); ok
}
If you execute this specification you may see something like (note that examples and before actions are executed concurrently):
[info] before
[info] before
[info] example2
[info] example1
As you can guess, defining a behaviour “after” is very similar:
class AfterSpecification extends org.specs2.mutable.Specification with AfterEach:
// you need to define the "after" action
def after = step(println("after"))
"example 1" >> {
println("example1"); ok
}
"example 2" >> {
println("example2"); ok
}
You might also want to mix the two:
class BeforeAfterSpecification extends org.specs2.mutable.Specification with BeforeAfterEach:
def before = step(println("before"))
def after = step(println("after"))
"example 1" >> {
println("example1"); ok
}
"example 2" >> {
println("example2"); ok
}
IMPORTANT: Mixing traits like BeforeEach
and BeforeAfterEach
can lead to surprising behaviour where the before
action is executed twice. You should rather have both traits extends BeforeAfterEach
to avoid that:
import org.specs2.specification.dsl.ActionDsl
trait B1 extends BeforeAfterEach with ActionDsl:
def before = step(println("before 1"))
def after = step(()) // do nothing
trait B2 extends BeforeAfterEach with ActionDsl:
def before = step(println("before 2"))
def after = step(println("after 2"))
Another very common situation is when you need to execute in the context of a database transaction or a web request. In this case you can use the AroundEach
trait to execute each example in the proper context:
trait DatabaseContext extends AroundEach:
// you need to define the "around" method
def around[R: AsResult](r: =>R): Result =
openDatabaseTransaction
try AsResult(r)
finally closeDatabaseTransaction
// do what you need to do with the database
def openDatabaseTransaction = ???
def closeDatabaseTransaction = ???
class AroundSpecification extends org.specs2.mutable.Specification with DatabaseContext:
"example 1" >> {
println("using the database"); ok
}
"example 2" >> {
println("using the database too"); ok
}
The specification above shows a trait DatabaseContext
extending AroundEach
(so that trait can be reused for other specifications). It defines a method named around
taking the body of the example, anything with an AsResult typeclass, and returns a result. Because r
is a byname parameter, you are free to do whatever you want before or after evaluating it, like opening and closing a database transaction.
The AroundEach
trait can be used for lots of different purposes:
There is however one thing you cannot do with AroundExample
. You can’t pass a specific context to the example. The ForEach
trait solves this problem.
Sometimes you need to manage a specific context for each example but you also want to make it accessible to the examples themselves. Here is a specification having examples using an active database transaction:
// a transaction with the database
trait Transaction
trait DatabaseContext extends ForEach[Transaction]:
// you need to define the "foreach" method
def foreach[R: AsExecution](f: Transaction => R): R =
val transaction = openDatabaseTransaction
try f(transaction)
finally closeDatabaseTransaction(transaction)
// create and close a transaction
def openDatabaseTransaction: Transaction = ???
def closeDatabaseTransaction(t: Transaction) = ???
class FixtureSpecification extends org.specs2.mutable.Specification with DatabaseContext:
"example 1" >> { (t: Transaction) =>
println("use the transaction")
ok
}
"example 2" >> { (t: Transaction) =>
println("use it here as well")
ok
}
Some setups are very expensive and can be shared across all examples. For example you might want to start an application server just at the beginning of the specification and then close it at the end.
You can use 3 traits to do this:
BeforeSpec
inserts any Fragments
, for example a Step
, before all the examplesAfterSpec
inserts any Fragments
, for example a Step
,` after all the examplesBeforeAfterSpec
inserts Fragments
before all the examples and after all of them Fragments are the pieces making a `Specification: examples, text, steps, etc…. You can learn more about the Fragments API in Fragments API.
A very common use case for a BeforeSpec/AfterSpec
behaviour is to acquire an expensive resource and release it at the end of the specification.
You can do this with the Resource
trait:
import org.specs2.Specification
import org.specs2.specification.Resource
import org.specs2.specification.core.Execution
import org.specs2.control.Ref
import scala.collection.mutable.ArrayBuffer
import scala.concurrent.*
trait LocalRef(using ec: ExecutionContext) extends Resource[Ref[Int]]:
def acquire: Future[Ref[Int]] =
Future.successful(Ref(0))
def release(ref: Ref[Int]): Execution =
Future { true }
class ResourceExample(using ec: ExecutionContext) extends Specification, LocalRef:
val messages: ArrayBuffer[String] = ArrayBuffer()
def is = sequential ^ s2"""
e1 $e1
e2 $e2
"""
def e1 = { (ref: Ref[Int]) =>
messages.append("e1 "+ref.get)
ref.update(v => v + 1)
// the resource will be released even if there is an exception here
throw Exception("boom")
ok
}
def e2 = { (ref: Ref[Int]) =>
messages.append("e2 "+ref.get)
ref.update(v => v + 1)
ok
}
In this example we use a mutable reference as our “expensive” resource. It is created with the acquire
method which returns a Future
so that we don’t need to block on the acquisition. Then the resource is made available to any example in the specification by using it as a parameter as in e1
for example. And finally the resource is released (the database is shut down, some files are closed, …) with the release
method. You will need to return anything that can be converted to a specs2 Execution
: a Result
, a Future[Result]
or even a simple Boolean
.
Sometimes it is necessary to keep a resource open across several specifications invocations. In order to do this you need to override the resourceKey
function to provide a unique key for the resource:
trait GlobalRef(using ec: ExecutionContext) extends Resource[Ref[Int]]:
override def resourceKey: Option[String] =
Some("global reference")
def acquire: Future[Ref[Int]] =
Future.successful(Ref(0))
def release(ref: Ref[Int]): Execution =
Future { true }
Then specs2
will release all global resources at the end of a run.
Examples
and Steps
are being executed