Rewrite is rarely the right option
Well, except when that's the only one :-).
Before starting specs2, I did try to refactor specs. It turned out that my first design was clearly not adapted to my goals. So I eventually decided to start from a blank page, as explained here. While I wanted to keep most features I didn't try to go for a 100% backward compatibility. I tried to think to features as part of the design space and wanted to be sure I had enough design freedom (a complementary view is that implementation is part of the features space).
That being said, I know that migrating to a new API is not something that people just do for the sheer fun of it. They have to have compelling reasons for doing so.
What are the good reasons why one would like to use specs2 instead of specs?
concurrent execution of examples: that's one major thing that is enabled by specs2 new design. Easy and reliable
acceptance specifications: something which was really experimental in specs and which is now completely integrated. You can use it to create an executable User Guide for example
nifty features such as Auto-Examples (an example here), implicit Matchers creation or Json matchers
lots of small fixes and consistency changes so that writing tests/specifications is just a pleasure!
Now that you've decided to take the ride with specs2, what are the steps for a successful migration?
Just replaceorg.specs._
with org.specs2.mutable._
The first thing to do is to switch the base import from org.specs._
to org.specs2.mutable._
. For simple specifications, with no context setup and simple equality matchers, nothing else is required.
However, depending on the specs features you've been using you'll have to change a few more things:
- matchers
- context setup
- ScalaCheck
- miscellaneous: arguments, specification title, specification inclusions, tags, ...
Most of those changes have specific motivations which I leave out of this post to keep it short. If you have questions on any given change please ask on the mailing-list.
You may also want to watch this presentation by @prasinous to learn how she did her own migration of the Salat project.
Matchers
Most of the matchers in specs2 have simply been copied over from the same matchers in specs. There are however some differences:
mustBe
,mustNotBe
and othermustXXX
variants don't exist anymore. Onlymust_==
,must_!=
,mustEqual
,mustNotEqual
are left. Otherwise you have to writemust not be(x)
instead ofmustNotBe
the matchers having
not
as a prefix have been removed too. You're encouraged to writemust not beEmpty
instead ofmust notBeEmpty
the
verify
matcher has been removed, so you should writef(a) must beTrue
instead ofa must verify(f)
(or better, use ScalaCheck properties!)beLike
used to take aPartialFunction[T, Boolean]
as an argument. It is nowPartialFunction[T, MatchResult[_]]
which allows better failure messages:a must beLike { case ThisThing(b) => b must be_>(0) }
. If you have nothing special to assert, just returnok
:a must beLike { case ThisThing(_) => ok }
the
fail()
method has been removed in favor of the simple return of aFailure
object:aFailure
orfailure(message)
String matchers:
ignoreCase
andignoreSpace
matchers for string equality are now constraints which can be added to thebeEqualTo
matcher:eEqualTo(b).ignoreSpace.ignoreCase
Iterable matchers:
only
andinOrder
are now constraints with can be applied to thecontain
matcher instead of having dedicated matchers likecontainInOrder
Option matchers: "beSomething" is now just
beSome
xUnit assertions have been removed
There are certainly other differences, I'll keep the list updated as I'll see them.
Contexts
That's the tough part! There are 3 ways to manage contexts in specs:
- "automagic" variables
- before / after methods
- system / specification contexts
All of this has actually been reduced to the direct use of Scala natural features: the easy creation of traits and case classes. And a few support traits to avoid duplication of code. You definitely should read the User Guide section on Contexts before starting your migration.
Automagic variables
This was a very cool functionality of specs but also the greatest source of bugs! In specs you can nest examples, declare variables in each scope and have those variables being automatically reset when executing each example:
"this system" should {
val var1 = ...
"example 1" in {
val var2 = ...
"subexample 1" in { ... }
"subexample 2" in { ... }
}
"example 2" in { ... }
}
Handling those variables is a lot less magic in specs2. What's the simplest way to get new variables in Scala? Simply open a new scope by placing some code into a new object! That's it, nothing more to say:
"this system" should {
"example 1" in new c1 {
// do something with var1
}
"example 2" in new c1 {
// do something else with a new var1
}
}
trait c1 {
val var1 = ...
}
Well, almost :-). It turns out that the body of an Example
has to be something akin to a Result
. In order to allow our context, and everything inside, to be a Result
, we need to have our c1
trait extend the Scope
trait and benefit from an implicit conversion from Scope
to Result
:
import org.specs2.specification._
trait c1 extends Scope {
val var1 = ...
}
From there, having nested contexts, like the ones in the first specs example is easy. We use inheritance to create them:
"this system" should {
"example 1" in {
"subexample 1" in new c2 { ... }
"subexample 2" in new c2 { ... }
}
"example 2" in new c1 { ... }
}
trait c1 extends Scope { val var1 = ... }
trait c2 extends c1 { val var2 = ... }
Before / After
In specs, you can run setup code as you would do with any kind of JUnit code. This is done by declaring a doBefore block inside a sus:
"this system" should {
doBefore(cleanAll)
"example 1" { ... }
"example 2" { ... }
}
In specs2, there are several ways to do that. The first one is as simple as running that code into the Scope trait:
"this system" should {
"example 1" in new c1 { ... }
"example 2" in new c1 { ... }
}
trait c1 extends Scope {
val var1 = ...
cleanAll
}
This works well for "before" setup but we can't easily setup any "after" behavior because we need additional machinery to make sure that the teardown code is executed even if there is a failure. This is where you can use the After
trait and define the after
method:
"this system" should {
"example 1" in new c1 { ... }
"example 2" in new c1 { ... }
}
trait c1 extends Scope with After {
def after = // teardown code goes here
}
For good measure, even if it's not necessary, there is a corresponding Before
trait and before
method for the setup code.
Remove duplication
If you don't need any "local" variable in your contexts, but only before/after behavior, you can reduce the amount of code in the example above with the AfterExample
trait (or BeforeExample
for before behavior):
class MySpec extends Specification with AfterExample {
def after = // teardown code goes here
"this system" should {
"example 1" in { ... }
"example 2" in { ... }
}
}
Yet another alternative is to use an implicit context:
implicit val context = new Scope with After {
def after = // teardown code goes here
}
"this system" should {
"example 1" in { ... }
"example 2" in { ... }
}
BeforeSus / AfterSus / BeforeSpec / AfterSpec
Some other declarations in specs, like beforeSpec
, allow to specify some setup code to be executed before all the specification examples. In specs2, the Specification is seen as a sequence of Fragments and you have to insert a Step
Fragment at the appropriate place:
step(cleanDB)
"first example" in { ... }
"second example" in { ... }
step(println("finished!"))
Specification / sus contexts
The purpose of Contexts in specs is to be able to define and reuse a given setup/teardown procedure. As seen above, in specs2, traits extending Before
or After
play exactly the same role.
ScalaCheck
With specs there is a special matcher to check ScalaCheck properties. It comes in 4 forms:
property must pass
generator must pass(function)
function must pass(generator)
generator must validate(partialFunction)
In specs2 we're using the fact that the body of an Example expects anything that can be converted to a Result
, so there are implicit conversions transforming ScalaCheck properties and Scala functions to Result
s and the examples above become:
(we suppose that the function to test has 2 parameters of type T1
and T2
, and 2 implicit Arbitrary[T1]
and Arbitrary[T2]
instances in scope)
"ex" in check { property }
"ex" in check { function }
or"ex" in check (arbitrary1, arbitrary2) { function }
to be explicit about theArbitrary
instances to use- no equivalent
- no equivalent
You can also notice that specs2 uses implicit Arbitrary
instances instead of Gen
instances directly but creating an Arbitrary
from a Gen
is easy:
import org.scalacheck._
val arbitrary: Arbitrary[T] = Arbitrary(generator)
Miscellaneous
ArgumentsThis is also a part of your specifications which is likely to necessitate a change. In specs there were several ways to modify the behavior of the execution or the reporting:
detailedDiffs()
shareVariables()
setSequential()
- Command-line options:
-sus
,-ex
,--color
,-finalstats
,... - Configuration objects
All of this has been completely redesigned in specs2:
- all the arguments can be specified from inside the Specification
- most of them can be passed on the command-line as Strings
And just to be specific about what's not going to compile during your specs2 migration:
detailedDiffs()
needs to be replaced by adiffs(...)
or your ownDiffs
objectshareVariables()
makes no sense because all variables are shared in specs2 unless you isolate them in ContextssetSequential()
is replaced with the addition of asequential
argument
DataTables have not really changed, except for their package being org.specs2.matcher
instead of org.specs.util
. You may however get a few compilation errors because of the !
operator as explained here. Just replace it with !!
in that case.
In specs the specification title is a member of the Specification
class whereas Specifications
in specs2 are traits. If you want to specify a Specification title in specs2 you can insert it as a Fragment:
class MySpec extends Specification { def is =
"Specification title".title ^
...
^ end
}
class MySpec extends mutable.Specification {
"Specification title".title
...
}
Include a specification in another oneThis used to be done with include
or isSpecifiedBy
/areSpecifiedBy
. In specs2 there are 3 ways to "include" other specifications with different behaviours which mostly make sense with the HmtlRunner
:
If you have a "parent" specification spec1
include(spec2)
will include all the Fragments of spec2 into spec1 as if they were part of itlink(spec2)
will include all the Fragments of spec2 into spec1. When executing spec1, spec2 will be executed and the html runner will create a link to a separate page for spec2see(spec2)
will include all the Fragments of spec2 into spec1. When executing spec1, spec2 will not be executed but the html runner will create a link to a separate page for spec2.
The tagging system in specs2 has been completely changed but not in way drastic way for users of the API. The major difference is that tags are positional which opens new possibilities for tagging like creating Section
s.
There are certainly a million other small differences between your specification written with specs and what it will look like with specs2. I can only apologize in advance for the additional work, offer my best support, and hope that you'll be able to rip out more benefits and more fun of writing specifications with specs2!
7 comments:
I have no complaints with a rewrite.
But why the "2" ?
How would someone differ between (some of them may be purely fictional)
- specs 1.4
- specs 2.0
- specs 3.0
- specs2 1.4
- specs2 2.0
Which one is newer: specs 3.0 or specs2 1.4 ? Is specs 2.1.4 and specs2 1.4 the same thing?
I think just incrementing the *major* version number is enough to signify backwards incompatibility, even skip versions if you want (example: Thunderbird 3.0 then Thunderbird 5.0).
Please don't repeat Java2's mistake (Java EE, to J2EE, then back to Java EE, should've never been Java2 anyway)...
...to email followup comments...
I did this to allow some projects to have a progressive migration, where part of their specifications would use specs and the other part specs2.
If I had left the same artefacts and package names that wouldn't have been possible.
Eric,
I agree that the use case you described is perfectly valid.
However, when you look at popular shared libraries like Qt, GTK, etc. there is a good pattern to follow.
You don't find libqt4 1.0 or libgtk3 1.0.
It's okay to skip versions, libqt4 4.0 as the first ever release just made sense.
My suggestion is, assuming the current specs2 version is 1.4, release the next specs2 as 2.5 (let users know of the "version jump"), and stick with the 2.x.x version numbering for the life of specs2. If you someday decide to create another major version specs3, start it from version 3.0.0.
This would make sense for everybody without you needing to write any readme on versioning.
Hi Hendy,
I've answered your comment by giving my opinion and moved the discussion to the mailing-list so that we can involve other users:
http://bit.ly/lHWhaq
Thanks,
Eric.
Thank you. Seems like "nobody cares" ;-)
I assume the reason is because they're already familiar with specs anyway, and is able to distinguish specs vs specs2 without even thinking about it.
Someone that is about to try specs for the first time (and most probably not joined in the mailing list) I think will have a different opinion. (like me) :-)
Yes, given the answers of existing users I'm going to leave things as they are. Thank you anyway for alerting me on the matter, I'll think twice about it next time I encounter that situation.
Post a Comment