Specs2 is a testing framework with a different focus and perspective that’s different from ScalaTest’s. Much like ScalaTest’s, Specs2 works with SBT, but it has some unique differences that a developer may choose based on the functionality needed. Specs2 has a different set of matchers, a different way of structuring tests, as well as DataTable specifications, AutoExamples, and Fitnesse style specifications used for collaboration purposes with the stakeholders of your project. Specs2 tests are also unique in that they are concurrently executed in each thread.
Since the book is focused on testing frameworks used with SBT, the following setup in build.sbt will bring in some dependencies and resolvers that are required for Specs2 to run.
name := "Testing Scala" version := "1.0" scalaVersion := "2.9.2" resolvers ++= Seq( "snapshots" at "http://scala-tools.org/repo-snapshots", "releases" at "http://scala-tools.org/repo-releases") resolvers ++= Seq( "sonatype-snapshots" at "http://oss.sonatype.org/content /repositories/snapshots", "sonatype-releases" at "http://oss.sonatype.org/content /repositories/releases") libraryDependencies ++= Seq( "org.scalatest" % "scalatest_2.9.2" % "1.8" % "test" withSources() withJavadoc(), "joda-time" % "joda-time" % "1.6.2" withSources() withJavadoc(), "junit" % "junit" % "4.10" withSources() withJavadoc(), "org.testng" % "testng" % "6.1.1" % "test" withSources() withJavadoc(), "org.specs2" %% "specs2" % "1.12.3" withSources() withJavadoc(),
The build.sbt file must be edited to include a new element: a resolvers
setting. resolvers
is a list or Seq
of Maven repositories for SBT to look for any required dependencies. The ++=
operator is an SBT-0 overloaded operator that tells SBT to add the resolvers that follow the list that comes with SBT out of the box. The Specs2 library is housed in the oss sonatype
repository, therefore the repository URLs must be declared so that sbt
will know where to look. For Specs2, the oss.sonatype.org
snapshot and release repository are required for the build so those URLs are added.
The first few elements in the libraryDependencies
setting have already been described and used in the previous chapters. The last dependency is needed to use Specs2. The specs2
dependency is the core library and as mentioned in the first chapter, withSources()
and withJavadoc()
will also download jar files containing the source code and the java/scaladoc respectively in the ivy local repository.
After making these amendments to build.sbt
, run sbt
and enter reload
in the interactive shell, or run sbt reload
at the shell command prompt.
The first flavor of test in Specs2 is the unit specification. It has a similar intent to ScalaTest, but is distinguished by its structure and design.
package com.oreilly.testingscala import org.specs2.mutable._ import org.joda.time.Period class JukeboxUnitSpec extends Specification { "A Jukebox" should { """have a play method that returns a copy of the jukebox that selects the first song on the first album as the current track""" in { val groupTherapy = new Album("Group Therapy", 2011, Some(List(new Track("Filmic", "3:49"), new Track("Alchemy", "5:17"), new Track("Sun & Moon", "5:25"), new Track("You Got to Go", "5:34"))), new Band("Above and Beyond")) val jukebox = new JukeBox(Some(List(groupTherapy))) val jukeboxNext = jukebox.play jukeboxNext.currentTrack.get.name must be_==("Filmic") jukeboxNext.currentTrack.get.period must be_==(new Period(0, 3, 49, 0)) //Must be 3 minutes and 49 seconds } } }
The unit specification in Spec2 starts with a string that describes the class undergoing the test. The description ends with a should
method, and starts a block that should be familiar ground. Within the should
block are one or more String
test descriptions. The should
block then ends with an in
block containing the actual test code. In the previous example, “A Jukebox” specifies what is to be tested, and within the should
block is one test, which describes a play
method and the behavior that is expected from the test.
Note that this unit specification imports import org.specs2.mutable._
. This is a different package from the acceptance specification covered later in this chapter.
The code within the in
block contains a declaration of an album by the group Above and Beyond. The code also instantiates a jukebox with one album, runs the method play
(which has not been implemented yet since we are TDDers). A jukebox is immutable, so invoking the method can’t change the current state of the jukebox. Instead, it instantiates a new object and returns it with a different state. That new jukebox is assigned to the variable jukeboxNext
. The last two lines of the test are the expectations. The test asserts that the current track name after play
has been invoked is "Filmic"
and that the Period
of the track is 3 minutes and 49 seconds.
When comparing and contrasting Specs2 with ScalaTest, it should strike you that the matchers are different. In JukeboxUnitSpec
, the test for equality uses must be_==
instead of ScalaTest’s must be (...)
. Each testing framework has its own set of matchers and its own strengths. In Specs2, each block of testing expectations returns a Result
object.
Another interesting point is that all Specs2 tests are asynchronous and each runs in its thread using a Promise
. Promises are processes that run on separate threads asynchronously using Actors
and send objects, in this case an ExecutedResult
to one another. Every Specs2 test sends each test as a message to an actor, and the result of the test is sent back as a ExecutedResult
message.
In the previous test, the two expectations will generate a Result
type of success
. If any of the test expectations were to fail, a FailureException
would be thrown, which encapsulates a Failure
object. Contrast this with ScalaTest, which throws a TestFailed
Exception
if a test has failed. Specs2 offers a few more states to return for a test, including anError
to indicate that some unknown exception has occured, skipped
if the tester wishes for the test to be skipped at this time, and pending
if the test is still under construction. The decision whether a test is skipped
or pending
follows the same logic as it does in ScalaTest.
The resulting production code is driven by the test.
package com.oreilly.testingscala class JukeBox private (val albums:Option[List[Album]], val currentTrack:Option[Track]) { def this(albums:Option[List[Album]]) = this(albums, None) def readyToPlay = albums.isDefined def play = new JukeBox(albums, Some(albums.get(0).tracks.get(0))) }
Specs2 offers two major constructs for authoring tests: at this time we are only covering unit specification. Specs2 offers varying ways to organize your tests. But first, a quick introduction of Specs2 matchers is in order to get a better feel for the framework.
Specs2 offers an abundance of matchers, sometimes offering aliases for the same matchers just to offer the test developer a choice.
The following example tests for equality using Specs2 Matchers, showing how it differs from ScalaTest. The example uses Fleetwood Mac’s Rumours album and merely tests the title. The second half of the example asserts that the title of the Rumours album has nothing to do with Aerosmith’s Sweet Emotion.
val rumours= new Album("Rumours", 1977, Some(List(new Track("Second Hand News", "2:43"), new Track("Dreams", "4:14"), new Track("Never Going Back Again", "2:02"), new Track("Don't Stop", "3:11"))), new Band("Fleetwood Mac")) rumours.title must be_==("Rumours") rumours.title must beEqualTo("Rumours") rumours.title must_== ("Rumours") rumours.title mustEqual "Rumours" rumours.title should_== "Rumours" rumours.title === "Rumours" rumours.title must be equalTo ("Rumours") rumours.title must not be equalTo("Sweet Emotion") rumours.title must_!= "Sweet Emotion" rumours.title mustNotEqual "Sweet Emotion" rumours.title must be_!=("Sweet Emotion") rumours.title !== "Sweet Emotion"
Specs2 also offers an extensive list of matchers meant specifically for strings, including some powerful regular expresssion matchers.
val boston = new Album("Boston", 1976, Some(List(new Track("More Than a Feeling", "4:44"), new Track("Peace of Mind", "5:02"), new Track("Foreplay/Long Time", "7:47"), new Track("Rock and Roll Band", "2:59"))), new Band("Boston")) boston.title must beEqualTo("BoSTon").ignoreCase boston.title must beEqualTo(" Boston").ignoreSpace boston.title must beEqualTo(" BoStOn ").ignoreSpace.ignoreCase boston.title must contain ("os") boston.title must startWith ("Bos") boston.title must endWith ("ton") boston.title must not startWith ("Char") boston.title must have size(6) boston.title must beMatching ("B\\w{4}n") boston.title must beMatching ("""B\w{4}n""") boston.title must =~("""B\w{4}n""") boston.title must find("""(os.)""").withGroups("ost")
Most lines are self-explanatory. String can be matched with a must beMatching(...)
method. The examples given use both the regular strings and raw strings, so there is no need to escape the backslash. beMatching
can be replaced with =~
. Finally, Specs2 string matching can find a substring with in a string and assert that the regular expression groups found are equal to the expected results. The regular expression B\w{4}n
refers to a B
followed by any four characters found in a word, and finishing with n
.
The following example reproduces the ScalaTest answer-of-life example from RelationalOperatorMatchers
in ScalaTest to illustrate its relational operators. These operators can use either a DSL-like syntax or symbolic operators to set expectations.
val answerToLife = 42 answerToLife should be_<(50) answerToLife should not be_>(50) answerToLife must beLessThan(50) answerToLife should be_>(3) answerToLife must beGreaterThan(3) answerToLife should be_<=(100) answerToLife must beLessThanOrEqualTo(100) answerToLife should be_>=(0) answerToLife must beGreaterThanOrEqualTo(0) answerToLife === (42)
Specs2 also offers inexact measurements of floating-point calculations much like ScalaTest, but with a different DSL structure.
(4.0 + 1.2) must be_==(5.2) (0.9 - 0.8) must beCloseTo (0.1, .01) (0.4 + 0.1) must not beCloseTo (40.00, .30) (0.4 + 0.1) must not be closeTo (40.00, .30)
Garth Brooks time again—this time analyzing reference matchers in Specs2.
val garthBrooks = new Artist("Garth", "Brooks") val chrisGaines = garthBrooks garthBrooks must beTheSameAs(chrisGaines) val debbieHarry = new Artist("Debbie", "Harry") garthBrooks must not beTheSameAs (debbieHarry)
These use the same iterator tests from ScalaTest, with a few interesting new versions.
(Nil must be).empty List(1, 2, 3) must not be empty List(1, 2, 3) must contain(3) List(1, 2, 3) must not contain (5) List(4, 5, 6) must not contain(7, 8, 9) List(1, 2, 3, 4, 5, 6) must contain(3, 4, 5).inOrder List(4, 5, 6) must contain(4, 5, 6).only.inOrder List(1, 2) must have size (2) List(1, 2) must have length (2)
Specs2 contains a some really neat tricks for asserting conditions within any Seq
or Traversable
.
List("Hello", "World") must containMatch("ll") // matches with .*ll.* List("Hello", "World") must containMatch("Hello") // matches with .*ll.* List("Hello", "World") must containPattern(".*llo") // matches with .*llo List("Hello", "World") must containPattern("\\w{5}") List("Hello", "World") must containMatch("ll").onlyOnce List("Hello", "World") must have(_.size >= 5) List("Hello", "World") must haveTheSameElementsAs(List("World", "Hello"))
The first and second lines determine whether any of the elements contain the string. The third and fourth lines determine whether any of the line items contain a particular pattern (regular expression). The fifth line calls a modifier method onlyOne
, which asserts that ll
is in a string of lists somewhere and that it occurs in that particular list only one time. The sixth matcher accepts a Boolean function and asserts that every element in the Traversable
abides by it. In this case, each element must have a size greater than 5. The last line item matches the Seq
on the left side with the Seq
on the right side.
Using the map of singers to bands shown in MapMatchers
of ScalaTest, here are the analogous matchers for Specs2.
val map = Map("Jimmy Page" -> "Led Zeppelin", "Sting" -> "The Police", "Aimee Mann" -> "Til\' Tuesday") map must haveKey("Sting") map must haveValue("Led Zeppelin") map must not haveKey ("Brian May") map must havePair("Aimee Mann" -> "Til\' Tuesday")
All these methods are fairly straightforward. All matchers can determine whether the map has a particular key, a particular value, or pair. And each matcher can check the opposite expectations with a not
modifier.
Specs2 has some special sugar to determine whether two XML Elem
elements are equal without regard to white space. For those still unfamiliar with Scala, Scala has built-in support for XML. Each XML element, is of type Elem
; therefore, Specs2 can compare these objects and their spacing either strictly or leniently. Consider the sample ColdPlay album list that follows, where the <albums>
parent tag nest five separate albums.
val coldPlayAlbums = <albums> <album name="Parachutes"/> <album name="A Rush of Blood to the Head"/> <album name="X&Y"/> <album name="Viva la Vida or Death and All His Friends"/> <album name="Mylo Xyloto"/> </albums>
We might naively try to match it as follows, but the match will fail.
coldPlayAlbums must beEqualTo(<albums> <album name="Parachutes"/> <album name="A Rush of Blood to the Head"/> <album name="X&Y"/> <album name="Viva la Vida or Death and All His Friends"/> <album name="Mylo Xyloto"/> </albums>)
The stack trace of the failed test is too hideous to paste in the book, but it shows that the match does not work even though both XML elements are equal, because beEqualTo
is tripped up by the different spacing. To test for XML equality, replace beEqualTo
with beEqualToIgnoringSpace
, or change be_==
to be_==\
.
coldPlayAlbums must beEqualToIgnoringSpace(<albums> <album name="Parachutes"/> <album name="A Rush of Blood to the Head"/> <album name="X&Y"/> <album name="Viva la Vida or Death and All His Friends"/> <album name="Mylo Xyloto"/> </albums>)
Partial functions determine whether a predicate applies to their input and, if so, run the code you specify. The following example uses PartialFunctions
to determine whether a given record is a gold album, a platinum album, or, as a joke, an alternative album.
val goldPartialFunction: PartialFunction[Int, String] = new PartialFunction[Int, String] { //States that this partial function will take on the task def isDefinedAt(x: Int) = x >= 500000 //What we do if this does partial function matches def apply(v1: Int) = "Gold" } val platinumPartialFunction: PartialFunction[Int, String] = {case x: Int if (x >= 1000000) => "Platinum"} val junkPartialFunction: PartialFunction[Int, String] = {case x: Int if (x < 500000) => "Alternative"} val riaaCertification = goldPartialFunction orElse platinumPartialFunction orElse junkPartialFunction riaaCertification must beDefinedAt (100) riaaCertification must beDefinedBy (100 -> "Alternative")
GoldPartialFunction
determines whether the number given is greater than 500,000 and, if so, returns Gold
. platinumPartialFunction
and junkPartialFunction
are also partial functions, but are created through case statements. The variable riaaCertification
combines the three partial functions into one. riaaCertification
accepts an Int
input to represent the number of albums sold and outputs the resulting record status.
The line riaaCertification must beDefinedAt (100)
asserts that the given value is supported in the riaaCertification
partial function chain. The last line asserts that the given value to a partial function will indeed return the ideal result. This example asserts that, given album sales of 100, the result will be labeled as Alternative
.
A few more matchers come with the Specs2 matchers, and it’s amazing that both Specs2 and ScalaTest push the envelope of matchers.
On a side note, Specs2 is very flexible when it comes to matchers, and you can make custom matchers if desired. In the following snippet of code, two matchers are created and can be used with in Specs2. beEven
can be in an expectation that states 4 must beEven
. "Flash" must beCapitalizedAs ("FLASH")
The ^^
in the following code represents a function that returns what the expected value should be if an exception is returned. What is interesting about the last matcher is that it is built upon another Matcher
, capitalized
.
def beEven: Matcher[Int] = (i: Int) => (i % 2 == 0, i+" is even", i+" is odd") def beCapitalizedAs(capitalized: String) = be_==(capitalized) ^^ ((_:String).toUpperCase)
An acceptance specification separates what the test is expected to do from what actually happens during the test. An oversimplified example of using a Specs2 acceptance specification follows.
package com.oreilly.testingscala import org.specs2.Specification class SimpleAcceptanceSpec extends Specification { def is = "This is a simple specification" ^ "and this should run f1" ! f1 ^ "and this example should run f2" ! f2 def f1 = success def f2 = pending }
A very important item to note is the Specification
that is imported into the package. This is org.specs2.Specification
and not import org.specs2.mutable._
, which is used in the unit specification that is covered in the first section of this chapter.
In SimpleAcceptanceSpec
, the method that bootstraps the entire test for the class is the method is
. The method returns a Fragments
object containing all the examples. The SimpleAcceptanceSpec
contains two examples. One will run the f1
method, as dictated after the intro string, and the next should run f2
. The !
notation is used to divide the test and does so in its own separate thread.
Carets divide the specifications. Any string that does not call a method using the !
operator is considered a header for the following tests. For SimpleAcceptanceSpec
, This is a simple specification
is a string followed by a ^
but not a !
, so it will not be considered a test and will merely echo the results of sbt
or the Specs2 runner. On the other lines, the carets divide the specifications. The last line requires no final caret since it needs no division from a following specification.
The result from each of the actors are returned and the results are reported in SBT.
[info] Compiling 1 Scala source to /home/danno/testing_scala_book.git /testingscala/target/scala-2.9.2/test-classes... [info] This is a simple specification [info] + and this should run f1 [info] * and this example should run f2 PENDING [info] [info] Total for specification SimpleAcceptanceSpec [info] Finished in 87 ms [info] 2 examples, 0 failure, 0 error, 1 pending (+1) [info] Passed: : Total 2, Failed 0, Errors 0, Passed 1, Skipped 1 [success] Total time: 5 s, completed Dec 28, 2011 9:22:26 PM
If you do not wish to run each method in its own thread, it can be annotated with an argument to make it sequential. To do this, merely add args(sequential=true)
to the test as follows:
/src/test/scala/com/oreilly/testingscala/SimpleSequentialAcceptanceSpec.scala.
class SimpleSequentialAcceptanceSpec extends Specification { def is = args(sequential = true) ^ "This is a simple specification" ^ "and this should run f1" ! f1 ^ "and this example should run f2" ! f2 def f1 = success def f2 = pending }
In the specification results above, the +
indicates that and this should run f1
ran successfully. The last test result shown next to PENDING
and bears a *
symbol to state that the test is pending. Notice that This is a simple specification
has no preceding symbol, because the line never invoked an actor with !
. It is just considered informational, much like the way informers are used in ScalaTest.
The previous example was boring, so it’s time to get back to the music. We’ll create a simple Artist
test that adds a middle name to an artist and expects a fullName
method to get the full name of the artist. The overall goal is to make sure that an Artist
object can optionally include a middle name.
package com.oreilly.testingscala import org.specs2.Specification class ArtistAcceptanceSpec extends Specification { def is = "An artist should have a middle name at construction" ^ """An artist should be able to be constructed with a middle name and get it back calling 'middleName'""" ! makeAnArtistWithMiddleName ^ """An artist should be able to have a full name made of the first and last name given a first and last name at construction time""" ! testFullNameWithFirstAndLast ^ """An artist should be able to have a full name made of the first, middle and last name given a first, middle, and last name at construction time""" ! testFullNameWithFirstMiddleAndLast def makeAnArtistWithMiddleName = pending def testFullNameWithFirstAndLast = pending def testFullNameWithFirstMiddleAndLast = pending }
This is a beefier example of the test created initially in this section. Three test specifications support this topic. Each calls one of the three methods implemented in the following code and tests the results. All three testing results at the moment will return a Result
of pending
, because we’re still thinking over how to implement the production code.
[info] Compiling 1 Scala source to /home/danno/testing_scala_book.git/testingscala /target/scala-2.9.2/test-classes... [info] An artist should have a middle name at construction [info] * An artist should be able to be constructed with a middle name and [info] get it back calling 'middleName' PENDING [info] * An artist should be able to have a full name made of the first and last name [info] given a first and last name at construction time PENDING [info] * An artist should be able to have a full name made of the first, middle and last name [info] given a first, middle, and last name at construction time PENDING [info] [info] Total for specification ArtistAcceptanceSpec [info] Finished in 124 ms [info] 3 examples, 0 failure, 0 error, 3 pendings [info] Passed: : Total 3, Failed 0, Errors 0, Passed 0, Skipped 3 [success] Total time: 6 s, completed Dec 28, 2011 10:04:01 PM
Next it’s time to fill in the pending specifications, and give them some concrete tests.
package com.oreilly.testingscala import org.specs2.Specification class ArtistAcceptanceSpec extends Specification { def is = "An artist should have a middle name at construction" ^ """An artist should be able to be constructed with a Option[String] middle name and get it back calling 'middleName'""" ! makeAnArtistWithMiddleName ^ """An artist should be able to have a full name made of the first and last name given a first and last name at construction time""" ! testFullNameWithFirstAndLast ^ """An artist should be able to have a full name made of the first, middle and last name given a first, middle, and last name at construction time""" ! testFullNameWithFirstMiddleAndLast def makeAnArtistWithMiddleName = { val vaughn = new Artist("Stevie", "Ray", "Vaughn") vaughn.middleName must be_==(Some("Ray")) } def testFullNameWithFirstAndLast = { val luther = new Artist("Luther", "Vandross") luther.fullName must be_==("Luther Vandross") } def testFullNameWithFirstMiddleAndLast = { val bonJovi = new Artist("Jon", "Bon", "Jovi") bonJovi.fullName must be_==("Jon Bon Jovi") } }
The example fills in some expectations regarding the middle names for an Artist
using the artists Stevie Ray Vaughn, guitarist extraordinare; Luther Vandross, voice extraordinaire; and Jon Bon Jovi, steel horse rider extraordinaire.
Adding compile-time errors and run-time exceptions to meet the specification requirements, including breaking some of the previous tests, makes the production code more robust with some extra functionality.
package com.oreilly.testingscala case class Artist(firstName: String, middleName: Option[String], lastName: String, albums: List[Album]) extends Act { def this(firstName: String, lastName: String) = this (firstName, None, lastName, Nil) def this(firstName: String, middleName: String, lastName: String) = this (firstName, Some(middleName), lastName, Nil) def getAlbums = albums def addAlbum(album: Album) = new Artist(firstName, middleName, lastName, album :: albums) def fullName = middleName match { case Some(x) => firstName + " " + x + " " + lastName case _ => firstName + " " + lastName } }
The changes in Artist
include an extra parameter in the default constructor, and an additional constructor to support some of the older tests that still need to create an artist with first and last name only. The last method is a fullName
method that uses pattern matching to determine whether the artist has a middle name; if so, it returns the first, middle, and last names divided by spaces; if not, it returns the first and last names. The results in SBT or the Specs2 runner show the progress of TDD.
[info] Compiling 1 Scala source to /home/danno/testing_scala_book.git/testingscala/target/scala-2.9.2/test-classes... [info] An artist should have a middle name at construction [info] + An artist should be able to be constructed with a Option[String] middle name and [info] get it back calling 'middleName' [info] + An artist should be able to have a full name made of the first and last name [info] given a first and last name at construction time [info] + An artist should be able to have a full name made of the first, middle and last name [info] given a first, middle, and last name at construction time [info] [info] Total for specification ArtistAcceptanceSpec [info] Finished in 236 ms [info] 3 examples, 0 failure, 0 error [info] Passed: : Total 3, Failed 0, Errors 0, Passed 3, Skipped 0 [success] Total time: 5 s, completed Dec 28, 2011 10:45:29 PM
Specs2 offers formatting tags to prettify the end results of the tests. Some formatting is implicit. Any text that directly follows another is indented under the preceding text. Thus, in the previous example, An artist should have a middle name at construction
is followed by a ^
to delineate the end of the line. Since the next element following the ^
is also a String
, it is indented and labeled with a +
mark.
Adjacent specification examples (examples defined by both the string description and the call to the test) will have the same indentation level. Thus, in the ArtistAcceptanceSpec
, the two specification examples will have the same indentation level.
"""An artist should be able to be constructed with a Option[String] middle name and get it back calling 'middleName'""" ! makeAnArtistWithMiddleName ^ """An artist should be able to have a full name made of the first and last name given a first and last name at construction time""" ! testFullNameWithFirstAndLast
If either the next string after the specification or the specification example is not to be indented, you can add a ^p
tag after the previous caret. The ^p
tag terminates the line with a carriage return and decrements the indentation by 1 for the next specification example or string. This is nearly analogous to the <p>
tag in HTML/XHTML. In the next example, a ^p
is added to separate the test, since the next test will focus on creating an alias, and it is a perfect place to add a paragraph delimiter.
package com.oreilly.testingscala import org.specs2.Specification class ArtistAcceptanceSpec extends Specification { def is = "An artist should have a middle name at construction" ^ """An artist should be able to be constructed with a Option[String] middle name and get it back calling 'middleName'""" ! makeAnArtistWithMiddleName ^ """An artist should be able to have a full name made of the first and last name given a first and last name at construction time""" ! testFullNameWithFirstAndLast ^ """An artist should be able to have a full name made of the first, middle and last name given a first, middle, and last name at construction time""" ! testFullNameWithFirstMiddleAndLast ^ p^ "An artist should have an alias" ^ """method called withAlias(String) that returns a copy of Artist with an alias""" ! testAlias //Code removed for brevity def testAlias = {pending} }
Here, ^p
is used to visually separate one “paragraph” from another, displaying the testing categories with a clear break. Separating testing categories using ^p
is not optimal, as we’ll see later, but for now fits the purpose. The end result in the output will also show the separation.
48. Waiting for source changes... (press enter to interrupt) [info] Compiling 1 Scala source to /home/danno/testing_scala_book.git/testingscala/target/scala-2.9.2/test-classes... [info] An artist should have a middle name at construction [info] + An artist should be able to be constructed with a Option[String] middle name and [info] get it back calling 'middleName' [info] + An artist should be able to have a full name made of the first and last name [info] given a first and last name at construction time [info] + An artist should be able to have a full name made of the first, middle and last name [info] given a first, middle, and last name at construction time [info] [info] An artist should have an alias [info] * method called withAlias(String) that returns a copy of Artist with an alias PENDING [info] [info] Total for specification ArtistAcceptanceSpec [info] Finished in 266 ms [info] 4 examples, 0 failure, 0 error, 1 pending (+1) [info] Passed: : Total 4, Failed 0, Errors 0, Passed 3, Skipped 1 [success] Total time: 5 s, completed Dec 30, 2011 3:36:28 PM
Again, remember that ^p
decrements the next indentation by 1. If the line is indented 5 levels and is followed by ^p
, the next line will be at indentation level 4. To go back to 0, use the end^
tag instead.
class ArtistAcceptanceSpec extends Specification { def is = "An artist should have a middle name at construction" ^ """An artist should be able to be constructed with a Option[String] middle name and get it back calling 'middleName'""" ! makeAnArtistWithMiddleName ^ """An artist should be able to have a full name made of the first and last name given a first and last name at construction time""" ! testFullNameWithFirstAndLast ^ """An artist should be able to have a full name made of the first, middle and last name given a first, middle, and last name at construction time""" ! testFullNameWithFirstMiddleAndLast ^ end^ "An artist should have an alias" ^ """method called withAlias(String) that returns a copy of Artist with an alias""" ! testAlias def makeAnArtistWithMiddleName = {...} def testFullNameWithFirstAndLast = {...} def testFullNameWithFirstMiddleAndLast = {...} def testAlias = {pending}
Although end^
will end the paragraph, it will not add another line. You can get both by using both an end^
and a ^p
, but a combination endp^
marker also creates the desired effect.
You can get even more control over indention through the bt^
or t^
tags. For the sake of example, if the first part of the ArtistAcceptanceSpec
was written with a three ^t
tags after the end of the initial string, the end result would indent the next line three times.
"An artist should have a middle name at construction" ^ t ^ t ^ t ^ """An artist should be able to be constructed with a Option[String] middle name and get it back calling 'middleName'""" ! makeAnArtistWithMiddleName ^ //code omitted for brevity
The t
method that does the work of indenting can also accept an Int
(a Scala Int
) that indents the next line by the number indicated. Rewriting the short example above with the Int
parameter produces:
"An artist should have a middle name at construction" ^ t(3) ^ """An artist should be able to be constructed with a Option[String] middle name and get it back calling 'middleName'""" ! makeAnArtistWithMiddleName ^ //code omitted for brevity
In contrast, ^bt^
backtabs. To manipulate tabs for the subsequent line, the same rules apply, only in reverse.
We’ll add what we just covered and create an implementation for aliasTest
in Artist
AcceptanceSpec
.
package com.oreilly.testingscala import org.specs2.Specification class ArtistAcceptanceSpec extends Specification { def is = "An artist should have a middle name at construction" ^ t(3) ^ """An artist should be able to be constructed with a Option[String] middle name and get it back calling 'middleName'""" ! makeAnArtistWithMiddleName ^ p^ """An artist should be able to have a full name made of the first and last name given a first and last name at construction time""" ! testFullNameWithFirstAndLast ^ """An artist should be able to have a full name made of the first, middle and last name given a first, middle, and last name at construction time""" ! testFullNameWithFirstMiddleAndLast ^ endp^ "An artist should have an alias" ^ """method called withAlias(String) that returns a copy of Artist with an alias""" ! testAlias def makeAnArtistWithMiddleName = { val vaughn = new Artist("Stevie", "Ray", "Vaughn") vaughn.middleName must be_==(Some("Ray")) } def testFullNameWithFirstAndLast = { val luther = new Artist("Luther", "Vandross") luther.fullName must be_==("Luther Vandross") } def testFullNameWithFirstMiddleAndLast = { val bonJovi = new Artist("Jon", "Bon", "Jovi") bonJovi.fullName must be_==("Jon Bon Jovi") } def testAlias = { val theEdge = new Artist("David", "Howell", "Evans").withAlias("The Edge") theEdge.alias must be_==(Some("The Edge")) } }
The result of the test-driven development will in turn cause changes in the Artist
production code—notably, the new withAlias
method and a change in the main constructor, as well as the call to that constructor from the auxiliary constructors.
package com.oreilly.testingscala case class Artist(firstName: String, middleName: Option[String], lastName: String, albums: List[Album], alias:Option[String]) extends Act { def this(firstName: String, lastName: String) = this (firstName, None, lastName, Nil, None) def this(firstName: String, middleName: String, lastName: String) = this (firstName, Some(middleName), lastName, Nil, None) def getAlbums = albums def addAlbum(album: Album) = new Artist(firstName, middleName, lastName, album :: albums, alias) def fullName = middleName match { case Some(x) => firstName + " " + x + " " + lastName case _ => firstName + " " + lastName } def withAlias(alias:String) = new Artist(firstName, middleName, lastName, albums, Some(alias))
Each test in specs can be chained to hand off calls and functions to another. This should be no surprise, since the acceptance specification is merely a collection of objects of the Result
type, or Strings
and methods that return a Result
type. There is nothing particularly magical about acceptance specifications.
In ScalaTest, GivenWhenThen
structures were in the form of an Informer
, an object whose only job is to output any extra information to the end report of the test suite. In Specs2, GivenWhenThen
takes on a totally different role. Within the AcceptanceSpec
, GivenWhenThen
is a Result
fragment object that holds states—one possible state being Given
, another When
, and the final one Then
—and it passes that state on through to the test. GWTs are built by inserting GWT “steps” between the textual descriptions and that those steps keep the state of the current execution and are eventually translated to regular Text, Step, and Example fragments. Each object is in charge of taking in some data—either from the previous state or from a String
specification—and creating another object, then passing that object on like a baton in a relay race.
GivenWhenThen
in Specs2, as in ScalaTest, is used to mentally reinforce the definition of the test into distinct ideas. The examples in this section will be done in parts, due to a somewhat steep learning curve. The first example shows the basic parts of the test without the supporting objects it requires.
package com.oreilly.testingscala import org.specs2.Specification class GivenWhenThenAcceptanceSpec extends Specification { def is = { "Demonstrating a Given When Then block for a Specs2 Specification".title ^ "Given the first name ${David} and the last ${Bowie} create an Artist" ^ setUpBowie ^ "When we add the artist to an album called ${Hunky Dory} with the year ${1971}" ^ setUpHunkyDory ^ "And when an the album is added to a jukebox" ^ addTheAlbumToAJukebox ^ "Then the jukebox should have one album whose name is ${Hunky Dory}" ^ result object setUpBowie object setUpAlbum object addTheAlbumToAJukebox object result }
Let’s take the example a little bit at a time. The class declaration and the is
method have been covered already in the previous sections. The first string is the title of the test, and is marked as such by the title
method.
The second statement is the Given
statement. The words David and Bowie, which are encased in ${}
, will be used in the setUpBowie
object to create an Artist
that will passed down the test.
The next statement, setUpHunkyDory
, will take the words Hunky Dory and 1971, which are also encased in ${}
and use them to create an Album
that will be passed down the test. The following statement will add an album to a JukeBox
. A jukebox instance will be created and passed down to the last link of the specification. This link will do the final expectations and return a proper Result
.
The example ends by defining objects that will parse the contents of ${}
and spit out the appropriate objects for the other specification links to take and return the results needed by the next test in sequence.
The next example extends the Given
, When
, and Then
parent classes with appropriate type parameters. This should show how the analogy “passing the baton in a relay race” is appropriate.
//Code removed for brevity object setUpBowie extends Given[Artist] object setUpHunkyDory extends When[Artist, Album] object addTheAlbumToAJukebox extends When[Album, JukeBox] object result extends Then[JukeBox] //Code removed for brevity
Now, the Given
, When
, and Then
rules are in place with their type parameters. The type parameters are the key to understanding how to use GivenWhenThen
constructs. First, setUpBowie
is used as the Given
object. The type parameter is the return parameter, and states that it should return an object of type Artist
. Since the Given
object returns Artist
, there must be either a When
object or a Then
object that accepts an Artist
as its first type parameter, and setUpHunkyDory
will answer that call.
setUpHunkyDory
is a When
object that has two type parameters (as all When
objects must). In this case, the first is the type created by the previous link Given
object, Artist
. The second is the type returned by this object, in this case an Album
. In short, setUpHunkyDory
will take in an Artist
and return an Album
.
Next in the chain is a When[Album, Jukebox]
object that will take in an Album
, the one being returned by setUpHunkyDory
, and return an instance of a JukeBox
.
The final link in the chain is result
, which is an object that will take the last object created in this relay race, the Jukebox
created by addTheAlbumToAJukebox
.
For the beginner, it might be a good idea to start out with the objects to see how they work and use what is learned to sculpt the specifications accordingly.
object setUpBowie extends Given[Artist] { def extract(text: String) = { val tokens = extract2(text) new Artist(tokens._1, tokens._2) } }
In setUpHunkyDory
, the object
extends When
with parameter types Artist
and Album
. This indicates that the previous step must return an Artist
and that the setUpHunkyDory
object must return an Album
. The extract
method in the object is slightly different because it takes two parameters. The first is the Artist
that was returned from the Given
case, and the second is the text of the specification. Here extract2
is used to parse out the values into a tuple with the values HunkyDory
and 1971
. Since an Album
is the return type (again, because it’s listed as the second type parameter of When
), the last line of the extract
method will return a new Album
using the information parsed and the Artist
object that was passed down.
object setUpHunkyDory extends When[Artist, Album] { def extract(p: Artist, text: String) = { val tokens = extract2(text) new Album(tokens._1, tokens._2.toInt, p) } }
For addTheAlbumToAJukebox
the object
also extends a When
with the type parameters Album
and Jukebox
. In this implementation, extracting the text
isn’t required since the specification string doesn’t contain any required data. The only requirement is the Album
that was returned by the previous object, setUpHunkyDory
. With that album, a new JukeBox
is instantiated and the album is added and returned for the next object to use.
object addTheAlbumToAJukebox extends When[Album, JukeBox] { def extract(p: Album, text: String) = new JukeBox(Some(List(p))) }
The final link is the result
object, which extends the Then
abstract class. The type parameter is the type that is required from the previous object, addTheAlbumToAJukebox
, which of course is an Album
type. The extract
method is the same as it was from the When
class. The first parameter is the object passed down, and the second text parameter is the String
from its accompanying specification string. The difference between extending When
and extending then
is that the return type in the Then
class has to be a Result
type since that is the last element of the chain. In the following example, the Result
returned is the expectation that the albums in the jukebox total to 1.
object result extends Then[JukeBox] { def extract(t: JukeBox, text: String) = t.albums.get must have size (1) }
The GivenWhenThen
specification takes a little work to understand, but once the test developer gets a thorough understanding of how the return types pass results down the chain, the structure becomes self-explanatory, useful, and at times reuseable.
The end result for the previous GivenWhenThen
example should return the following:
[info] Given the first name David and the last Bowie create an Artist [info] When we add the artist to an album called Hunky Dory with the year 1971 [info] And when an the album is added to a jukebox [info] + Then the jukebox should have one album whose name is Hunky Dory [info] [info] Total for specification Demonstrating a Given When Then block for a Specs2 Specification [info] Finished in 26 ms [info] 1 example, 0 failure, 0 error [info] Passed: : Total 1, Failed 0, Errors 0, Passed 1, Skipped 0 [success] Total time: 2 s, completed Jan 3, 2012 2:54:47 PM
Data tables are ASCII tables that contain sample values and the end result. Once the table is established, a function test case is attached to the end to verify that all the values provided match the criteria. In the following example, we’ll show a test that makes sure the date of an album’s release matches the age correctly.
package com.oreilly.testingscala import org.specs2.Specification import org.specs2.matcher.DataTables class AlbumAgeDataTableSpecification extends Specification with DataTables {def is = "Trying out a table of values for testing purposes to determine the age of albums".title ^ """The first column is the album name, the second is a band name, and third is a year, and the fourth is the age from the year 2070""" ! ageTable def ageTable = "Album Name" | "Band Name" | "Year" | "Age" | "Under the Iron Sea" !! "Keane" ! 2006 ! 64 | "Rio" !! "Duran Duran" ! 1982 ! 88 | "Soul Revolution" !! "Bob Marley & the Wailers" ! 1971 ! 99 |> { (a:String, b:String, c:Int, d:Int) ⇒ new Album(a, c, new Band(b)).ageFrom(2070) must_== d } }
The class definition is an acceptance specification with the inclusion of a DataTables
trait. The DataTable
trait contains case classes and methods that makes the data table magic happen. The example just shown has delineators,the same overall setup as we’ve seen in the past: a def declaration
, a title
, a string specification
, and a call to some Result
object or method that returns a Result
. What is different, of course, is the data table.
After the def ageTable =
method declaration, the first line contains header information for the test, delineated by a |
pipe character. Each subsequent row of data takes different delineators, either a !!
or a !
to delineate each column. The exception is the last column of each row, because the end of the row is marked with another |
pipe character. A data table can go indefinitely until the a row is terminated with |>
, at which point the table is going to be executed and returned with a Result
.
Each column is sent into the function in the form of a Tuple
. Since there are four columns in our table, it will require a Tuple4
parameter that receives each row of data. The function receives the album name as a string, the band name as a string, the year as an Int
, and the expected Age
as an Int
. For each row of data, the function will create an Album
, assign the name and year, and create a Band
object from the second column. Then it calls a method called ageFrom
, which does not yet exist, that takes the current year and returns the age of the album as an Int
. Finally, an expectation checks whether the age returned equals the fourth column of the data table, Age
.
The use of the current year in the method is intentional. It’s a good idea not to use the current year in actual production code because the year constantly changes. That means that any test is likely to fail over time. The next year will likely kill all your unit tests. Having consistent unit tests is not the only reason why the current year, or any temporal information, should not be calculated in actual production code. The other good reason is that it makes the code less reusable. If, for instance, there is a requirement to calculate future statistics on code, it seems a lot of work to redo the guts of a class or object and extract the current date just to do some forecasting. It’s nice to leave such hard dependencies out and plug in what is needed when it is needed.
The results of the test after changing production code are as follows.
[info] Compiling 1 Scala source to /home/danno/testing_scala_book.git/testingscala/target/scala-2.9.2/test-classes... [info] + The first column is the album name, the second is a band name, [info] and third is a year, and the fourth is the age from the year 2070 [info] [info] Total for specification Trying out a table of values for testing purposes to determine the age of albums [info] Finished in 108 ms [info] 1 example, 0 failure, 0 error [info] Passed: : Total 1, Failed 0, Errors 0, Passed 1, Skipped 0 [success] Total time: 2 s, completed Jan 3, 2012 2:51:46 PM
The changes to the production code include the addition of ageFrom
.
package com.oreilly.testingscala class Album(val title: String, val year: Int, val tracks: Option[List[Track]], val acts: Act*) { require(acts.size > 0) def this(title: String, year: Int, acts: Act*) = this (title, year, None, acts: _*) def ageFrom(now: Int) = now - year }
Specs2 also has a tagging feature that allows the developer to categorize tests. Categorization of tests can be used in both in a unit specification and an acceptance specification. Tests can be filtered in SBT or in the specification itself in case you wish to create one specification that would run other specifications. The example below is an acceptance specification that uses tags to denote a category for the test. The first specification in the test is categorized with the strings "timeLength"
and "threeTracks"
. The second test is categorized also with the String "timeLength"
and with "zeroTracks"
. In order to make these work, you must import the trait org.specs2.specification.Tags
and add that trait to the specification. In the example below, with Tags
is added to the specification.
import org.specs2.Specification import org.specs2.specification.Tags import org.joda.time.Period class Specs2TagAcceptanceSpecification extends Specification with Tags { def is = "The total time of an album is based on the sum of the tracks".title ^ "When an album is given three tracks" ! testThreeTracks ^ tag("timeLength", "threeTracks") ^ "When an album is given zero tracks" ! testZeroTracks ^ tag("timeLength", "zeroTracks") def testThreeTracks = { val beyonceFirstAlbum = new Album("Dangerously in Love", 2003, Some(List( new Track("Crazy In Love", "3:56"), new Track("Naughty Girl", "3:29"), new Track("Baby Paul", "4:05") )), new Artist("Beyonce", "Knowles")) beyonceFirstAlbum.period must be_== (new Period(0, 10, 90, 0)) } def testZeroTracks = { val frankZappaAlbum = new Album("We're only in it for the Money", 1968, None, new Band("The Mothers of Invention")) frankZappaAlbum.period must be_== (Period.ZERO) } }
To run the test using tags in SBT use test-only
or ~test-only
with the name of the test followed by -- include
, with the name of the tags that you wish to run delimited with a comma. For example, to run only zeroTracks
tests from the Specs2TagAcceptance
Specification
, the following command line would work:
> test-only com.oreilly.testingscala.Specs2TagAcceptanceSpecification -- include zeroTracks [info] + When an album is given zero tracks [info] [info] Total for specification The total time of an album is based on the sum of the tracks [info] Finished in 145 ms [info] 1 example (+1), 0 failure, 0 error [info] Passed: : Total 1, Failed 0, Errors 0, Passed 1, Skipped 0 [success] Total time: 1 s, completed Jan 4, 2012 2:57:31 PM
Again it is worth noting that --include
can accept any number of tag keywords, and every test that contains the tags specified will run. In the next example, we show that we are including the tags zeroTracks
, completeAlbums
, classical
, and short-tests
to the list of tests that we wish to include.
> test-only com.oreilly.testingscala.Specs2TagAcceptanceSpecification -- include zeroTracks completeAlbums classical short-tests [info] + When an album is given zero tracks [info] [info] Total for specification The total time of an album is based on the sum of the tracks [info] Finished in 94 ms [info] 1 example, 0 failure, 0 error [info] Passed: : Total 1, Failed 0, Errors 0, Passed 1, Skipped 0 [success] Total time: 1 s, completed Jan 4, 2012 3:04:24 PM
Fixtures, the ability to call a start method and a close method for a test class mostly standard Scala programming features. Depending on what kind of test you are using, there are different ways to create fixtures. Specs2 tends to take the idealistic stance that most of what you need to do can be achieved using Scala. Let’s take a first example of using a mutable list that is shared by two tests in a Specification
.
class Specs2WithoutFixtures extends Specification { def is = "Add an album to a shared list" ! test1 ^ "Remove an album to a shared list" ! test2 lazy val lst = scala.collection.mutable.Buffer( new Album("Fly By Night", 1974, new Band("Rush")), new Album("19", 2008, new Artist("Adele", "Laurie", "Adkins").withAlias("Adele"))) def test1 = { lst.append(new Album("Prokofiev and Rachmaninoff: Cello Sonatas", 1991, new Artist("Yo", "Yo", "Ma"))) lst must have size(3) } def test2 = lst.drop(1) must have size(1) }
In the above example, test1
uses the shared list mutable
lst
and appends one album by Yo Yo Ma to that list. After test1
is run since according to our spec “Add an album to a shared list” starts first. “Remove an album to a shared list” starts next. Each test, as you can tell, was written with the assumption that either the lst
provided would be unique to the test and not shared. If this test is run, then failure will occur because the lst
is shared across test1
and test2
, and test2
fails because we were assuming that the lst
originally had two items.
> test-only com.oreilly.testingscala.Specs2WithoutFixtures [info] Compiling 1 Scala source to /home/danno/testing_scala_book.svn/testingscala/target/scala-2.9.2/test-classes... [info] + Add an album to a shared list [error] x Remove an album to a shared list [error] 'Album[19], Album[Prokofiev and Rachmaninoff: Cello Sonatas]' doesn't have size 1 but size 2 (Specs2CaseClassContext.scala:13) [info] [info] Total for specification Specs2WithoutFixtures [info] Finished in 143 ms [info] 2 examples, 1 failure, 0 error
This is because in this specification we have a shared mutable state. How do we go about creating a unique list for each test? Perhaps the easiest and most functional way is to make the lst
an immutable data structure which is the default.
class Specs2WithoutFixturesButImmutable extends Specification { def is = "Add an album to a shared list" ! test1 ^ "Remove an album to a shared list" ! test2 lazy val lst = List( new Album("Fly By Night", 1974, new Band("Rush")), new Album("19", 2008, new Artist("Adele", "Laurie", "Adkins").withAlias("Adele"))) def test1 = { val result = lst :+ new Album("Prokofiev and Rachmaninoff: Cello Sonatas", 1991, new Artist("Yo", "Yo", "Ma")) result must have size(3) } def test2 = lst.drop(1) must have size(1) }
Another score for immutability. So shared state is often the least of your worries with immutability. Let’s say that although there may be teams where you do have to manage shared states across tests, or you require a method to initialize a database or a service, having a setup and teardown method (using JUnit parlance) is needed. Given the shared state example Specs2WithoutFixtures
above, a set up method can be established using a Scope
trait in either a unit specification or an acceptance specification. In an acceptance specification all that is required is a trait that extends Scope
and extends that trait in a case class that envelops all tests that require the scoped setup.
class Specs2WithScope extends Specification { def is = "Add an album to a shared list" ! AddItemTest().test ^ "Remove an album to a shared list" ! RemoveItemTest().test trait ListMaker { lazy val lst = scala.collection.mutable.Buffer( new Album("Fly By Night", 1974, new Band("Rush")), new Album("19", 2008, new Artist("Adele", "Laurie", "Adkins").withAlias("Adele"))) } case class AddItemTest() extends ListMaker { def test = { lst.append(new Album("Prokofiev and Rachmaninoff: Cello Sonatas", 1991, new Artist("Yo", "Yo", "Ma"))) lst must have size(3) } } case class RemoveItemTest() extends ListMaker { def test = lst.drop(1) must have size(1) } }
Each case class will have one or more test methods in it. Each case class AddItem
Test()
and RemoveItemTest()
extends from ListMaker
, which is a trait. The reason a trait works is that its state is unique to each class that extends it. Therefore AddItem
Test()
and RemoveItemTest
will each have its own list to test.
How do we achieve the same thing for a unit specification? Remember that a unit specification is a specification like its sibling AcceptanceSpecification
but with a different form. Instead of calling methods from the specification, the tests are run within an in
clause. Below is the same test as the acceptence specifications that we have been using, but restructured as a unit specification.
import org.specs2.mutable.Specification import org.specs2.specification.Scope class Specs2UnitSpecificationFixtures extends Specification { "Add an album to a shared list" in new ListMaker { lst.append(new Album("Prokofiev and Rachmaninoff: Cello Sonatas", 1991, new Artist("Yo", "Yo", "Ma"))) lst must have size (3) } "Remove an album to a shared list" in new ListMaker { lst.drop(1) must have size (1) } trait ListMaker extends Scope { lazy val lst = scala.collection.mutable.Buffer( new Album("Fly By Night", 1974, new Band("Rush")), new Album("19", 2008, new Artist("Adele", "Laurie", "Adkins").withAlias("Adele"))) } }
By now all the players should be familiar, except now each of the tests are inline with the specification, and after the in
clause we instantiate an anonymous trait that will make available a unique lst
of albums for each test. This is what we want. But to actually make this work, each trait that must extend ‘org.specs2.specification.Scope` in order for Specs2 to understand that the trait will return Result
type, which is required by the framework. Without extending the Scope
trait, Specs2 will complain that it cannot implicitly convert a ListMaker
into a org.specs2.execute.Result
.
For teardown or cleanup methods, each specification has its own way of doing things. In the unit specification, you continue to use the trait strategy but instead of using Scope
you use the org.specs2.mutable.After
trait, which will give a method for you to override—-appropriately called after
. The after
method will be called by Specs2 when the test is completed, whether the test fails or succeeds. This next example uses the same data as the previous example but uses the After
trait instead of the Scope
trait.
class Specs2UnitSpecificationWithAfter extends Specification { "Add an album to a shared list" in new ListMaker { lst.append(new Album("Prokofiev and Rachmaninoff: Cello Sonatas", 1991, new Artist("Yo", "Yo", "Ma"))) lst must have size (3) def after {printf("Final tally: %d\n", lst.size)} } "Remove an album to a shared list" in new ListMaker { lst.drop(1) must have size (1) def after {printf("Final tally: %d\n", lst.size)} } trait ListMaker extends After { lazy val lst = scala.collection.mutable.Buffer( new Album("Fly By Night", 1974, new Band("Rush")), new Album("19", 2008, new Artist("Adele", "Laurie", "Adkins").withAlias("Adele"))) } }
Since After
requires that we implement an after
method that returns Any
object, we can define an after
method in each anonymous instantiation of ListMaker
for every test that we run. In each of tests, the after
method returns Unit
, which is a type that represents what void
is in Java, C, and C+. This can be refactored though: since the implementation of +after
is the same across multiple tests we can move that down to the ListMaker
trait, where it will look cleaner and still run successfully.
class Specs2UnitSpecificationWithAfter extends Specification { "Add an album to a shared list" in new ListMaker { lst.append(new Album("Prokofiev and Rachmaninoff: Cello Sonatas", 1991, new Artist("Yo", "Yo", "Ma"))) lst must have size (3) } "Remove an album to a shared list" in new ListMaker { lst.drop(1) must have size (1) } trait ListMaker extends After { lazy val lst = scala.collection.mutable.Buffer( new Album("Fly By Night", 1974, new Band("Rush")), new Album("19", 2008, new Artist("Adele", "Laurie", "Adkins").withAlias("Adele"))) def after {printf("Final tally: %d\n", lst.size)} } }
Below are the results of the run. The final tallies are printed after the tallies are run. The test reporting occurs after the tests have run.
> test-only com.oreilly.testingscala.Specs2UnitSpecificationWithAfter [info] Compiling 1 Scala source to /home/danno/testing_scala_book.svn/testingscala/target/scala-2.9.2/test-classes... Final tally: 3 Final tally: 2 [info] + Add an album to a shared list [info] + Remove an album to a shared list [info] [info] Total for specification Specs2UnitSpecificationWithAfter [info] Finished in 147 ms [info] 2 examples, 0 failure, 0 error [info] Passed: : Total 2, Failed 0, Errors 0, Passed 2, Skipped 0 [success] Total time: 2 s, completed Jan 5, 2012 10:49:04 AM
There are multiple solutions for creating fixtures in Specs2. Specs2 has an Around
trait that can do the same as previous examples. What is different with the Around
trait is that there is one place where a programmer can create logic to be wrapped around the test. You may find the trait is similar to either an JavaEE Interceptor
or a servlet specification Filter
. The recipe for the Around
trait is to first do setup, then call the test, which is given as a function parameter, and when the function parameter returns, perform any cleanup that is required.
The object below logs the start and stop of the test. It’s a simple fixture. The example uses a simple println
to output before and after messages when the test is run. Between each of the outputs, the test is run by calling t
, and the result is captured in a variable, result
. That reference is held until the end of the test, when it is returned.
object log extends org.specs2.specification.Around { def around[T <% Result](t: =>T):Result = { println("Start process") val result:T = t println("End process") result } }
What can be very confusing is the [T <% Result]
. This is Scala’s type bounds, if there is a converter in the scope of this object that can convert any type T into a Result. That means whatever the type T
is, it either has to be a type that is of type Result
or it is of type of something that can be converted into a type Result
. The log
extends the org.specs2.specification.Around
trait which mandates that the method def around
be declared. The def around
method accepts a function parameter of type Unit=>T
, which of course can be shortened to =>T
. As a reminder, Unit
is the equivalent of a void
in Java.
Below we run the test using the log Around
trait to do our println
log of the test in an acceptance specification. In the specification, we run not just e1
but log(e1)
. This will wrap the log object around the test method so that when the test runs the around
method in the log
will run.
class UsingTheAroundProcess extends Specification { def is = "this will log something before running" ! log(e1) lazy val lst = List( new Album("Storms of Life", 1986, new Artist("Randy", "Travis")), new Album("The Bad Touch", 1999, new Band("Bloodhound Gang")), new Album("Billie Holiday Sings", 1952, new Artist("Billie", "Holiday"))) def e1 = { println("Running test") lst.drop(1) must have size (2) } }
Running the above test, we find that everything falls into place. Start Process
is invoked first, Running Test
is next, and finally End Process
is displayed. The advantage of using an Around
trait is that this is now extremely reusable. The object can be used in other test methods in other tests.
One of the disadvantages of using an Around
trait is that you cannot get access to the state of objects that have been declared inside of the trait. If you establish anything like a service and database, or any object state, you cannot get access to it. If you need this kind of functionality, the Outside
trait is useful for declaring a stateful object that needs to instantiated and set up before the test runs. Once the object is set, it can then be delivered to the tester in a function parameter.
Note
Please don’t use external services or databases in unit tests or test-driven development. That is reserved for integration testing or functional testing.
The next example uses the Outside
trait to set up a Joda-Time DateTime
object that provides the current date and time to the test. The withCurrentDate
object extends the Outside
trait with the parameterized type DateTime
—the type that is the focus the trait. Extending the trait requires the outside
method to be declared, which should return the object to be used inside the test. In our example, that is the current DateTime
.
object withCurrentDate extends org.specs2.specification.Outside[DateTime] { def outside = new DateTime }
For good measure we will also include a withFakeDate
object that is also used inside a test, although this Outside
trait will return a fixed date of January 2, 1980.
object withFakeDate extends org.specs2.specification.Outside[DateTime] { def outside = new DateMidnight(1980, 1, 2).toDateTime }
Now we can use these objects inside a test, and much like the Around
trait, we can use the Outside
trait nearly the same way, except that it will provide information before running the test. In the next example, UsingDates
is also an acceptance specification. Instead of calling the test method testDate
straight away, it is wrapped with the Outside
trait that provides the needed date. Each specification is given a different date but calls one test method with each of those different dates.
class UsingDates extends Specification {def is = "this will use the real date" ! (withCurrentDate(x => testDate(x))) ^ "this will use a fake date" ! (withFakeDate(x => testDate(x))) def testDate(x: DateTime) = (x.plusDays(20).isAfterNow) }
This test will run for the top specification but not the bottom one, since the arithmetic doesn’t add up.
[info] Compiling 1 Scala source to /home/danno/testing_scala_book.svn/testingscala/target/scala-2.9.2/test-classes... [info] + this will use the real date [error] x this will use a fake date [error] the value is false (Specs2AcceptanceSpecificationFixtures.scala:145) [info] [info] Total for specification UsingDates [info] Finished in 539 ms [info] 2 examples, 1 failure, 0 error [error] Failed: : Total 2, Failed 1, Errors 0, Passed 1, Skipped 0 [error] Failed tests: [error] com.oreilly.testingscala.UsingDates [error] {file:/home/danno/testing_scala_book.svn/testingscala/}Testing Scala/test:test-only: Tests unsuccessful [error] Total time: 3 s, completed Jan 6, 2012 2:49:55 PM
Finally, what if you wish to have the best of both Around
and Outside
? Of course there is an AroundOutside
that provides that specific solution. The following code is a logWithFakeDateTime
object that extends the AroundOutside[DateTime]
trait. The trait requires that the tester use both the outside
and around
methods. Based on the previous examples, we can infer that outside
will set up the object that will be used inside the test, and the around
method will be run around the test method using the same object.
object logWithFakeDateTime extends org.specs2.specification.AroundOutside[DateTime] { def outside = new DateMidnight(1980, 1, 2).toDateTime def around[T <% Result](t: ⇒ T) = { println(outside + ": Start process") val result = t println(outside + ": End process") result } }
Now we can make use of this trait inside the test both as an Around
and an Outside
.
class UsingTheAroundOutsideProcess extends Specification { def is = "this will log something before running" ! logWithFakeDateTime(dateTime ⇒ e1(dateTime)) lazy val lst = List( new Album("Storms of Life", 1986, new Artist("Randy", "Travis")), new Album("The Bad Touch", 1999, new Band("Bloodhound Gang")), new Album("Billie Holiday Sings", 1952, new Artist("Billie", "Holiday"))) def e1(dt: DateTime) = { println("Running test at " + dt) lst.drop(1) must have size (2) } }
In the above example, logWithFakeDateTime
is given the function that accepts the DateTime
object as a parameter that is created within the OutsideAround
object. We use that DateTime
object inside the test method since we need it for our test. Remember that this is also the Around
trait, so whatever logic that we stated in the around
method will be run. The end result will show the full combination.
> test-only com.oreilly.testingscala.UsingTheAroundOutsideProcess [info] Compiling 1 Scala source to /home/danno/testing_scala_book.svn/testingscala/target/scala-2.9.2/test-classes... 2012-01-06T15:08:42.147-06:00: Start process Running test at 2012-01-06T15:08:42.265-06:00 2012-01-06T15:08:42.280-06:00: End process [info] + this will log something before running [info] [info] Total for specification UsingTheAroundOutsideProcess [info] Finished in 188 ms [info] 1 example, 0 failure, 0 error [info] Passed: : Total 1, Failed 0, Errors 0, Passed 1, Skipped 0 [success] Total time: 3 s, completed Jan 6, 2012 3:08:42 PM
Analyzing the end result, we find that the first item, 2012-01-06T15:08:42.147-06:00: Start process
came from the around
method. The around
method then ran the test producing , followed by printing 2012-01-06T15:08:42.280-06:00: End process
. The test passed and we successfully established a fixture in Specs2.
Specs2 flexes its muscle with the Scala language. Eric Torreborre, the testing framework’s author, likes to be pushing the envelope and trying different things to enhance the framework and create more and more functionality for the test-driven developer. This book covers a lot of what Specs2 covers, but it doesn’t cover everything, especially since it is constantly being developed.
Which testing to framework to use? This is up to you. But the real answer is why not both? ScalaTest and Specs2 cover different things for different reasons. ScalaTest offers various specs that are clear and easy to use, and that clarity comes from a well-engineered and well-documented framework. You may find that you need to gradually get used to Scala—especially testing in Scala—and you still enjoy JUnit- and TestNG-style tests. You may also find that data tables in Specs2 come in very handy. If you wish to use ScalaMock (covered later in this book), you will really love its integration with ScalaTest. Both frameworks can run ScalaCheck (also covered later) very well too, and it’s recipes will help you decide which framework is best for you. Competition always makes its participants better, and I expect that both these frameworks will have a lot to show in the future.
Get Testing in Scala now with the O’Reilly learning platform.
O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.