It has been now almost 3 years since the last major version of specs2. But why would a new major version of a testing library be even needed? Because the world doesn’t stop revolving!
In particular Scala can now be compiled to JavaScript and even natively. It is then only natural that
How hard can this be? First of all
This is where an issue becomes an opportunity! If I need to find an alternative to scalaz-stream
why not remove scalaz
altogether? Indeed it is very inconvenient for a test library to rely on third-party libraries which might conflict with users libraries. Because of this conflict I have to publish several versions of scalaz
(7.0.x
, 7.1.x
, 7.2.x
and so on). This is pretty time-consuming, not only because of having to wait for more than 30 minutes for all the jars to be published but also because I have to adapt to some small differences between each scalaz
version.
Removing scalaz
and scalaz-stream
is easier said than done:
specs2 relies heavily on functional programming so I need the usual FP suspects:Functor
,Applicative
,Monad
and so onspecs2 interacts with the file system, writes out to the console, executes specifications concurrently so it needs a way to control effectsspecs2 is structured around the idea of a “stream of specification fragments” so it needs a streaming abstraction
All of this before I can even consider moving to a Scala.js build!
specs2 own FP / streaming infrastructure
Fortunately I have developed on the side many of the pieces needed above:
eff
is a library to handle effects, including concurrency and resources managementproducer
is a simple streaming library designed to work with anyMonad
, including theEff
monadorigami
implements a notion of composable “folds” which can be applied to any stream, which is a pattern I (re)-discovered on earlier versions ofspecs2 : reporting the results of an executed specification is just “folding” the specification, a bit like usingfoldLeft
on aList
A copy of those libraries is now included in the specs2-common
module.
The only thing missing was the FP abstractions. It turns out that it is not too hard to implement the whole menagerie:
Functor
,Applicative
,Monad
,Traverse
,Foldable
,Monoid
. Those typeclasses are very tied togetherTree
andTreeLoc
for manipulating trees
I am greatly indebted to the Scalaz and cats projects for this code, which I reduced to the only parts I needed for specs2-fp
module (not intended for external use).
Execution model
At the heart of Fragment
, which is a Description
and an Execution
yielding a Result
. The Description
holds an informal description of the specified system and the Execution
runs some Scala code to verify that behaviour.
Using Scala.js imposes a big change is await
to the get the result of executing a Fragment
. Also since scalaz.concurrent.Task
is out of the equation, in Execution
is implemented using a scala.concurrent.Future
, like this:
case class Execution(
run: Option[Env => Future[() => Result]],
executing: Option[Throwable Either Future[Result]])
This is a lot more complicated than just Future[Result]
. Why is that?
run
takes an action to execute () => Result
, possibly concurrently Future[() => Result]
and which might depend on the environment Env => Future[() => Result]
. You might wonder why we don’t simply use Env => Future[Result]
. This is to allow the user to add some behaviour around the result, for example with the AroundEach
trait and the def around[R : AsResult](r: =>R): Result
method.
We also want to “stream” results, which are executed concurrently and display them as soon as they are available. This is done by having executing
holding either a Throwable
if we could not even start the execution or the Result
being currently computed.
Note that we can never call await
in a Scala.js program so when we are executing a Fragment
the best we can get is the equivalent of Future[Result]
(in reality an Action[Result]
which is a type possibly having more effects than just concurrency).
This triggers major changes in the implementation of Runners
used to run a specification. However most of the users of await
to get the result of a fragment execution you can do so by using one of the methods in the org.specs2.control.ExecuteActions
object. For example result.runAction(executionEnv)
will return an Error Either Result
. Running the same method on Scala.js will throw an exception.
Scala.js modules
“specs2-core
can be used on any Scala.js project, there are some
the
isolate
argument to run each example in its own copy of the specification cannot be used (because this uses reflection to instantiate classes)no custom
Selector
/Executor
/Printer
/Notifier
can be passed from the command line (for the same reason)the
RandomSequentialExecution
trait cannot be used because the implementation uses aTrieMap
not available on Scala.js. This could be fixed in the future.running only examples which previously failed with
was x
on the command-line is not possible because this accesses file systemall the modules where
scala-xml
is used don’t have a Scala.js version because there’s noscala-xml
for Scala.js:specs2-form
,specs2-html
modules accessing the file system cannot be used:
specs2-html
,specs2-markdown
the
specs2-gwt
module defines fragments based on the result of previous fragments, so it currently needs toawait
. This could be fixed potentially with a smart enough implementation in the future.
Breaking changes
A major version number is also the opportunity to fix some of the API flaws. I tried to limit the changes in the low-level API to the strict necessary but I also wanted to clean-up one important aspect of the high-level API.
When ExecutionContext
(to execute futures). With the addition of some traits like CommandLineArguments
you could declare:
class MySpec extends Specification with CommandLineArguments {
def is(args: CommandLine) = s2"""
try this $ok
"""
}
or
class MySpec extends Specification { def is = s2"""
try this $ex1
"""
def ex1 = { implicit ec: ExecutionContext => Future(1) must be_==(1).await }
}
I later realized that it would be a lot more convenient to directly “inject” the command line arguments or the execution context in the specification like so:
class MySpec(args: CommandLine) extends Specification { def is = s2"""
try this $ex1
"""
def ex1 = { (1 + 1) must be_==(2).when(args.contains("universe-is-sane"))}
}
class MySpec(implicit ec: ExecutionContext) extends Specification { def is = s2"""
try this $ex1
"""
def ex1 = { Future(1) must be_==(1).await }
}
Now there were 2 ways to do the same thing, the second one being a lot easier than the first one. In org.specs2.specification.Environment
, org.specs2.specification.CommandLineArguments
, org.specs2.specification.ExecutionEnvironment
have been removed. Same thing for the implicit conversions for functions like implicit ee: ExecutionEnv => Result
to create examples.
This will demand some migration efforts for some users but the result will be more concise specifications.
Another word on injected execution environments. From specs2 3.8.9
, 2 distinct execution environments are being used in
With
import org.specs2.specification.core.{Env, OwnExecutionEnv}
class MySpec extends(val env: Env) extends Specification with OwnExecutionEnv { def is = s2"""
...
"""
}
The injected env
is used to pass command-line argument to a copy of the user execution env, implicitly available as a member of the OwnExecutionEnv
trait. That trait also takes care of shutting down the execution environment once the specification has finished executing so as not to lose resources.
Feedback is essential
In conclusion,
no more dependencies for
specs2-core
a Scala.js version for
specs2-core
better control on execution environments
The release notes can be found here.
Please, please, please ask on gitter, on the mailing-list, on twitter if you have any issue. And a big thank you for reporting any mistake, big or small, that I may have made on this release!
No comments:
Post a Comment