Pages

21 May 2011

specs2 migration guide

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 replace org.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:

  1. matchers
  2. context setup
  3. ScalaCheck
  4. 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 other mustXXX variants don't exist anymore. Only must_==, must_!=, mustEqual, mustNotEqual are left. Otherwise you have to write must not be(x) instead of mustNotBe

  • the matchers having not as a prefix have been removed too. You're encouraged to write must not beEmpty instead of must notBeEmpty

  • the verify matcher has been removed, so you should write f(a) must beTrue instead of a must verify(f) (or better, use ScalaCheck properties!)

  • beLike used to take a PartialFunction[T, Boolean] as an argument. It is now PartialFunction[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 return ok: a must beLike { case ThisThing(_) => ok }

  • the fail() method has been removed in favor of the simple return of a Failure object: aFailure or failure(message)

  • String matchers: ignoreCase and ignoreSpace matchers for string equality are now constraints which can be added to the beEqualTo matcher: eEqualTo(b).ignoreSpace.ignoreCase

  • Iterable matchers: only and inOrder are now constraints with can be applied to the contain matcher instead of having dedicated matchers like containInOrder

  • 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:

  1. property must pass
  2. generator must pass(function)
  3. function must pass(generator)
  4. 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 Results 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)

  1. "ex" in check { property }
  2. "ex" in check { function }
    or "ex" in check (arbitrary1, arbitrary2) { function } to be explicit about the Arbitrary instances to use
  3. no equivalent
  4. 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

Arguments

This 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:

All of this has been completely redesigned in specs2:

And just to be specific about what's not going to compile during your specs2 migration:

  • detailedDiffs() needs to be replaced by a diffs(...) or your own Diffs object
  • shareVariables() makes no sense because all variables are shared in specs2 unless you isolate them in Contexts
  • setSequential() is replaced with the addition of a sequential argument
DataTables

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.

Specification title

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 one

This 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

  1. include(spec2) will include all the Fragments of spec2 into spec1 as if they were part of it

  2. link(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 spec2

  3. see(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.
Tags

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 Sections.

Conclusion

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:

Hendy said...

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)...

Hendy said...

...to email followup comments...

Eric said...

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.

Hendy said...

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.

Eric said...

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.

Hendy said...

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) :-)

Eric said...

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.