Forms

Forms are a way to represent domain objects or services, and declare expected values in a tabular format. Forms can be designed as reusable pieces of specification where complex forms can be built out of simple ones.

Forms are built by creating Fields or Props and placing them on rows. The following examples show, by order of complexity, the creation of:

  1. fields
  2. effects
  3. properties
  4. a simple Form using properties
  5. a simple Address entity encapsulating the above form
  6. a composite Customer entity using the Address instance
  7. a decision table having some related columns
  8. a composite Order - OrderLine entity (1-n) relationship

For all the code samples below you need to extend the org.specs2.specification.Forms trait.

Fields

A Field is simply a label and a value. It is used in forms to display regular information. You can create a Field with these methods:

When the form is displayed, here is how the fields are displayed:

Fields
codeis displayed as
field("value")value
field("label", "value")labelvalue
field("label", field("value1"), field("value2"))labelvalue1/value2

In terms of execution, the value is only evaluated when the Field is executed (when executing the parent Form for example). If an exception is thrown during that evaluation, the exception message will be displayed in place of the value.

Effects

An Effect is almost like a Field but it never shows its value. The value of an Effect is supposed to have some kind of side-effect, like clicking on a webpage, and only the effect label will be displayed (except when there is an exception, in that case the exception message is added). You can create an Effect with these methods:

Properties

A Prop is like a Field, it has a label. But you can give it 2 values, an “actual” one and an “expected” one. When executing the property, both values are compared to get a result. You can create a Prop with the following functions:

Expression Description
prop(value) a property with no label
prop(label, actual) a property with a label and an actual value
prop(label, actual, expected) a property with a label, an actual value and an expected one
prop(label, actual, constraint) a property with a label, an actual value and a function taking the actual value, an expected one and returning a Result
prop("label", "actual", (a: String, b: String) => (a === b).toResult) a property with a label, an actual value and a function taking the expected value, returning a Matcher that will be applied to the actual one
prop("label", "expected", (expected: String) => beEqualTo(expected)) a property with a label, an actual value and function applying a matcher to that value
prop(label, actual, matcher) a property with a label, an actual value and a matcher to apply to that value
If the matcher is `mute`d then no message will be displayed in case of a failure.

If the expected value is not provided when building the property, it can be given with the apply method:

// apply "sets" the expected value
prop1.apply("expected")
// or
prop1("expected")

Let’s look at a few examples:

Properties
codeis displayed as
prop("expected")("expected")expected
prop("label", "expected", "expected")labelexpected
prop("label", "expected")("expected")labelexpected
prop("label", "actual")("expected")labelactual'actual' != 'expected'
prop("label", { error("but got an error"); "actual" })("expected")labeljava.lang.RuntimeException: but got an error
prop("label", "actual", (a: String, b: String) => (a === b).toResult)("expected")labelexpected
prop("label", "actual", (s: String) => beEqualTo(s))("expected")labelexpected
prop("label", "actual", beEqualTo("expected"))labelactual'actual' != 'expected'
prop("label", "actual", beEqualTo("expected").mute)labelactual
[click on failed cells to see the stacktraces]
'actual' != 'expected' (file:1)
java.lang.RuntimeException: but got an error (file:1)
scala.sys.package$.error(package.scala:27)
org.specs2.guide.UseForms$.$anonfun$is$32(UseForms.scala:85)
org.specs2.control.Property$.$anonfun$apply$1(Property.scala:67)
org.specs2.control.Property.execute(Property.scala:51)
org.specs2.control.Property.optionalValue(Property.scala:17)
org.specs2.execute.ResultExecution.$anonfun$executeProperty$1(ResultExecution.scala:112)
org.specs2.control.Exceptions.trye(Exceptions.scala:85)
org.specs2.control.Exceptions.trye$(Exceptions.scala:84)
org.specs2.control.Exceptions$.trye(Exceptions.scala:131)
org.specs2.execute.ResultExecution.executeEither(ResultExecution.scala:83)
org.specs2.execute.ResultExecution.executeEither$(ResultExecution.scala:82)
org.specs2.execute.ResultExecution$.executeEither(ResultExecution.scala:124)
org.specs2.execute.ResultExecution.executeProperty(ResultExecution.scala:112)
org.specs2.execute.ResultExecution.executeProperty$(ResultExecution.scala:112)
org.specs2.execute.ResultExecution$.executeProperty(ResultExecution.scala:124)
org.specs2.form.Prop.actualValue$lzycompute(Prop.scala:49)
org.specs2.form.Prop.actualValue(Prop.scala:49)
org.specs2.form.Prop.execute(Prop.scala:57)
org.specs2.form.PropCell.$anonfun$executeCell$3(Cells.scala:166)
scala.Option.orElse(Option.scala:477)
org.specs2.form.PropCell.executeCell(Cells.scala:166)
org.specs2.form.PropCell.executeCell(Cells.scala:162)
org.specs2.form.Row.$anonfun$executeRow$1(Row.scala:37)
scala.collection.immutable.List.map(List.scala:250)
org.specs2.form.Row.executeRow(Row.scala:37)
org.specs2.form.Form.$anonfun$executeRows$1(Form.scala:72)
scala.collection.immutable.Vector1.map(Vector.scala:1886)
scala.collection.immutable.Vector1.map(Vector.scala:375)
org.specs2.form.Form.executeRows(Form.scala:72)
org.specs2.form.Form.executeForm(Form.scala:80)
org.specs2.guide.UseForms$.$anonfun$is$16(UseForms.scala:89)
org.specs2.specification.create.S2StringContext$$anon$5.append(S2StringContext.scala:71)
org.specs2.specification.create.S2StringContextCreation.$anonfun$s2$1(S2StringContext.scala:190)
scala.collection.LinearSeqOps.foldLeft(LinearSeq.scala:169)
scala.collection.LinearSeqOps.foldLeft$(LinearSeq.scala:165)
scala.collection.immutable.List.foldLeft(List.scala:79)
org.specs2.specification.create.S2StringContextCreation.s2(S2StringContext.scala:188)
org.specs2.specification.create.S2StringContextCreation.s2$(S2StringContext.scala:175)
org.specs2.Specification.s2(Specification.scala:19)
org.specs2.guide.UseForms$.$anonfun$is$1(UseForms.scala:9)
org.specs2.specification.core.SpecStructure.fragments$lzycompute(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure.fragments(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure.$anonfun$map$1(SpecStructure.scala:29)
org.specs2.specification.core.SpecStructure.fragments$lzycompute(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure.fragments(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure.$anonfun$map$1(SpecStructure.scala:29)
org.specs2.specification.core.SpecStructure.fragments$lzycompute(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure.fragments(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure$.selected(SpecStructure.scala:170)
org.specs2.specification.core.SpecStructure$.linkedSpecStructuresRefs(SpecStructure.scala:159)
org.specs2.specification.core.SpecStructure$.$anonfun$linkedSpecifications$1(SpecStructure.scala:117)
org.specs2.specification.core.SpecStructure$.getRefs$1(SpecStructure.scala:134)
org.specs2.specification.core.SpecStructure$.$anonfun$specStructuresRefs$11(SpecStructure.scala:143)
scala.collection.StrictOptimizedIterableOps.flatMap(StrictOptimizedIterableOps.scala:117)
scala.collection.StrictOptimizedIterableOps.flatMap$(StrictOptimizedIterableOps.scala:104)
scala.collection.immutable.Vector.flatMap(Vector.scala:113)
org.specs2.specification.core.SpecStructure$.getAll$1(SpecStructure.scala:143)
org.specs2.specification.core.SpecStructure$.$anonfun$specStructuresRefs$9(SpecStructure.scala:149)
org.specs2.fp.Need$$anon$3.value0$lzycompute(Name.scala:30)
org.specs2.fp.Need$$anon$3.value0(Name.scala:30)
org.specs2.fp.Need$$anon$3.value(Name.scala:31)
org.specs2.control.eff.ErrorInterpretation$$anon$1.$anonfun$applicative$3(ErrorEffect.scala:83)
scala.collection.immutable.List.map(List.scala:246)
org.specs2.fp.Traverse$$anon$1.map(Traverse.scala:43)
org.specs2.fp.Traverse$$anon$1.map(Traverse.scala:33)
org.specs2.fp.FunctorSyntax$FunctorOps.map(Functor.scala:25)
org.specs2.control.eff.ErrorInterpretation$$anon$1.$anonfun$applicative$2(ErrorEffect.scala:83)
org.specs2.fp.Need$$anon$3.value0$lzycompute(Name.scala:30)
org.specs2.fp.Need$$anon$3.value0(Name.scala:30)
org.specs2.fp.Need$$anon$3.value(Name.scala:31)
org.specs2.control.eff.ErrorInterpretation$$anon$1.apply(ErrorEffect.scala:76)
org.specs2.control.eff.ErrorInterpretation$$anon$1.apply(ErrorEffect.scala:69)
org.specs2.control.eff.Interpret$$anon$1.onEffect(Interpret.scala:53)
org.specs2.control.eff.Interpret$$anon$1.onApplicativeEffect(Interpret.scala:61)
org.specs2.control.eff.Interpret$$anon$1.onApplicativeEffect(Interpret.scala:45)
org.specs2.control.eff.Interpret.go$1(Interpret.scala:200)
org.specs2.control.eff.Interpret.$anonfun$interpretLoop$11(Interpret.scala:198)
org.specs2.control.eff.Arrs.go$1(Eff.scala:380)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.CollectedUnions.$anonfun$othersEff$1(Unions.scala:97)
org.specs2.control.eff.Arrs.go$1(Eff.scala:380)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.Interpret.$anonfun$interpretLoop$11(Interpret.scala:198)
org.specs2.control.eff.Arrs.go$1(Eff.scala:380)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.CollectedUnions.$anonfun$othersEff$1(Unions.scala:97)
org.specs2.control.eff.Arrs.go$1(Eff.scala:383)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.Interpret.$anonfun$interpretLoop$11(Interpret.scala:198)
org.specs2.control.eff.Arrs.go$1(Eff.scala:380)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.CollectedUnions.$anonfun$othersEff$1(Unions.scala:97)
org.specs2.control.eff.Arrs.go$1(Eff.scala:383)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.Arrs.apply(Eff.scala:348)
org.specs2.control.eff.CollectedUnions.$anonfun$continuation$1(Unions.scala:84)
org.specs2.control.eff.Arrs.go$1(Eff.scala:380)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.SafeInterpretation$$anon$1.onApplicativeEffect(SafeEffect.scala:140)
org.specs2.control.eff.SafeInterpretation$$anon$1.onApplicativeEffect(SafeEffect.scala:65)
org.specs2.control.eff.Interpret.go$1(Interpret.scala:200)
org.specs2.control.eff.Interpret.interpretLoop(Interpret.scala:207)
org.specs2.control.eff.Interpret.interpretLoop$(Interpret.scala:142)
org.specs2.control.eff.package$interpret$.interpretLoop(package.scala:31)
org.specs2.control.eff.Interpret.interpretLoop1(Interpret.scala:211)
org.specs2.control.eff.Interpret.interpretLoop1$(Interpret.scala:210)
org.specs2.control.eff.package$interpret$.interpretLoop1(package.scala:31)
org.specs2.control.eff.SafeInterpretation.runSafe(SafeEffect.scala:47)
org.specs2.control.eff.SafeInterpretation.runSafe$(SafeEffect.scala:45)
org.specs2.control.eff.SafeEffect$.runSafe(SafeEffect.scala:15)
org.specs2.control.eff.SafeInterpretation.execSafe(SafeEffect.scala:52)
org.specs2.control.eff.SafeInterpretation.execSafe$(SafeEffect.scala:51)
org.specs2.control.eff.SafeEffect$.execSafe(SafeEffect.scala:15)
org.specs2.control.eff.syntax.safe$SafeEffectOps.execSafe(safe.scala:17)
org.specs2.control.ExecuteActions.attemptExecuteAction(ExecuteActions.scala:59)
org.specs2.control.ExecuteActions.attemptExecuteAction$(ExecuteActions.scala:56)
org.specs2.control.ExecuteActions$.attemptExecuteAction(ExecuteActions.scala:93)
org.specs2.control.ExecuteActions.runAction(ExecuteActions.scala:52)
org.specs2.control.ExecuteActions.runAction$(ExecuteActions.scala:51)
org.specs2.control.ExecuteActions$.runAction(ExecuteActions.scala:93)
org.specs2.control.ExecuteActions$$anon$1.asResult(ExecuteActions.scala:70)
org.specs2.execute.AsResult$.apply(AsResult.scala:32)
org.specs2.specification.core.AsExecution$$anon$1.$anonfun$execute$1(AsExecution.scala:17)
org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:22)
org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:124)
org.specs2.execute.Result$$anon$4.asResult(Result.scala:246)
org.specs2.execute.AsResult$.apply(AsResult.scala:32)
org.specs2.execute.AsResult$.$anonfun$safely$1(AsResult.scala:40)
org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:22)
org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:124)
org.specs2.execute.AsResult$.safely(AsResult.scala:40)
org.specs2.specification.core.Execution$.$anonfun$result$1(Execution.scala:345)
org.specs2.specification.core.Execution$.$anonfun$withEnvSync$3(Execution.scala:363)
org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:22)
org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:124)
org.specs2.execute.Result$$anon$4.asResult(Result.scala:246)
org.specs2.execute.AsResult$.apply(AsResult.scala:32)
org.specs2.execute.AsResult$.$anonfun$safely$1(AsResult.scala:40)
org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:22)
org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:124)
org.specs2.execute.AsResult$.safely(AsResult.scala:40)
org.specs2.specification.core.Execution$.$anonfun$withEnvSync$2(Execution.scala:363)
org.specs2.specification.core.Execution.$anonfun$startExecution$3(Execution.scala:143)
scala.concurrent.impl.Promise$Transformation.run(Promise.scala:434)
java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
java.base/java.lang.Thread.run(Thread.java:832)
'actual' != 'expected' (file:1)
(file:1)

Styles

Most of the time, the display of Fields and Properties can be left as it is but sometimes you want to style the output of labels and values. You can do this by using decorateWith and styleWith methods, or some equivalent shortcuts:

Style
codeis displayed as
decorate with
field("label", "value").decorateWith(f: Any => <em>{f}</em>)labelvalue
field("label", "value").boldlabelvalue
field("label", "value").boldLabellabelvalue
field("label", "value").boldValuelabelvalue
field("label", "value").italicslabelvalue
field("label", "value").italics.boldlabelvalue
field("1 must_== 1").code1 must_== 1
style with
field("label", "value").styleWith("color"->"#FF1493")labelvalue
field("label", "value").color("#FF1493")labelvalue
field("label", "value").bkColor("#FF1493")labelvalue
field("label", "value").greenlabelvalue
field("label", "value").bkGreenlabelvalue

All the methods above, when named xxx are available as xxxLabel and xxxValue to do the formatting for the label or the value only. The available colors are:

Colors
namecodecolor
white#FFFFFF
blue#1E90FF
red#FF9999
green#CCFFCC
yellow#FFFF99
grey#EEEEEE

Simple form

Now that we know how to create Fields and Properties, creating a Form is as easy as putting them on separate lines:

Form("Address").
    tr(prop("street", actualStreet(123), "Oxford St")).
    tr(prop("number", actualNumber(123), 20))

The form has a title "Address" and 2 properties, each one on a distinct row. The actualStreet() and actualNumber() methods are supposed to retrieve the relevant values from a database.

In some cases (see the Calculator example below) you can create a header row using the th method:

Inserting the form in a Specification is also very simple:

class SpecificationWithForms extends Specification with Forms { def is = s2"""

The address must be retrieved from the database with the proper street and number
  ${Form("Address").
  tr(prop("street", actualStreet(123), "Oxford St")).
  tr(prop("number", actualNumber(123), 20))}
"""
}

One way to encapsulate and reuse this Form across specifications is to define a case class:

case class Address(street: String, number: Int) {
  def retrieve(addressId: Int) = {
    val address = actualAddress(addressId)
    Form("Address").
      tr(prop("street", address.street, street)).
      tr(prop("number", address.number, number))
  }
}

And then you can use it like this:

class AddressSpecification extends Specification with Forms { def is = s2"""
The address must be retrieved from the database with the proper street and number
  ${Address("Oxford St", 20).     /** expected values */
      retrieve(123)               /** actual address id */}
"""
}
Adding several rows at once

A very practical way to add rows programatically is to start from a list of values and have a function creating a Row object for each value:

Form("a new Form").trs(addresses) { a: Address => Row.tr(field(a.number), field(a.street)) }
a new Form
3Rose Crescent
4Oxfort St

Nesting into another Form

Forms can be composed of other Forms to display composite information:

val address = Form("Address").
  tr(field("street", "Rose Crescent")).
  tr(field("number", 3))

val person = Form("Person").
  tr(field("name", "Eric")).
  tr(address)
Person
nameEric

This will be displayed with the address as a nested table inside the main one on the last row. However in some case, it’s preferable to have the rows of that Form to be included directly in the outer table. This can be done by inlining the nesting Form:

val person = Form("Person").
    tr(field("name", "Eric")).
    tr(address.inline)            // address is inlined

And the result is:

Person
nameEric
Address
streetRose Crescent
number3

Nesting into an Effect or a Prop

When using Forms in specifications we can describe different levels of abstraction. If we consider the specification of a website for example, we want to be able to use a Form having 2 rows and describing the exact actions to do on the Login page:

val loginForm = Form("login").
  tr(effect("click on login", clickOn("login"))).
  tr(effect("enter name",     enter("name", "me"))).
  tr(effect("enter password", enter("password", "pw"))).
  tr(effect("submit",         submit()))
login
click on login
enter name
enter password
submit

However in a “purchase” scenario we want all the steps above to represent the login actions as just one step. One way to do this is to transform the login Form to an Effect or a Prop:

Form("purchase").
  tr(loginForm.toEffect("login")).
  tr(selectForm.toEffect("select goods")).
  tr(checkTotalForm.toProp("the total must be computed ok").bkWhiteLabel)

If everything goes fine, the detailed nested form is not shown:

purchase
login
select goods
the total must be computed okorg.specs2.form.Form@193de710

Otherwise:

purchase
login
select goods
the total must be computed okorg.specs2.form.Form@193de710failed
[click on failed cells to see the stacktraces]
failed (file:1)
org.specs2.execute.Failure$.apply$default$3(Result.scala:332)
org.specs2.form.Form.executedResult$lzycompute$1(Form.scala:130)
org.specs2.form.Form.executedResult$1(Form.scala:128)
org.specs2.form.Form.$anonfun$toProp$3(Form.scala:135)
org.specs2.control.Property$.$anonfun$apply$1(Property.scala:67)
org.specs2.control.Property.execute(Property.scala:51)
org.specs2.control.Property.optionalValue(Property.scala:17)
org.specs2.execute.ResultExecution.$anonfun$executeProperty$1(ResultExecution.scala:112)
org.specs2.control.Exceptions.trye(Exceptions.scala:85)
org.specs2.control.Exceptions.trye$(Exceptions.scala:84)
org.specs2.control.Exceptions$.trye(Exceptions.scala:131)
org.specs2.execute.ResultExecution.executeEither(ResultExecution.scala:83)
org.specs2.execute.ResultExecution.executeEither$(ResultExecution.scala:82)
org.specs2.execute.ResultExecution$.executeEither(ResultExecution.scala:124)
org.specs2.execute.ResultExecution.executeProperty(ResultExecution.scala:112)
org.specs2.execute.ResultExecution.executeProperty$(ResultExecution.scala:112)
org.specs2.execute.ResultExecution$.executeProperty(ResultExecution.scala:124)
org.specs2.form.Prop.expectedValue$lzycompute(Prop.scala:52)
org.specs2.form.Prop.expectedValue(Prop.scala:52)
org.specs2.form.Prop.$anonfun$execute$1(Prop.scala:58)
scala.Option.flatMap(Option.scala:283)
org.specs2.form.Prop.execute(Prop.scala:57)
org.specs2.form.PropCell.$anonfun$executeCell$3(Cells.scala:166)
scala.Option.orElse(Option.scala:477)
org.specs2.form.PropCell.executeCell(Cells.scala:166)
org.specs2.form.PropCell.executeCell(Cells.scala:162)
org.specs2.form.Row.$anonfun$executeRow$1(Row.scala:37)
scala.collection.immutable.List.map(List.scala:246)
org.specs2.form.Row.executeRow(Row.scala:37)
org.specs2.form.Form.$anonfun$executeRows$1(Form.scala:72)
scala.collection.immutable.Vector1.map(Vector.scala:1886)
scala.collection.immutable.Vector1.map(Vector.scala:375)
org.specs2.form.Form.executeRows(Form.scala:72)
org.specs2.form.Form.executeForm(Form.scala:80)
org.specs2.guide.UseForms$.$anonfun$is$147(UseForms.scala:244)
org.specs2.specification.create.S2StringContext$$anon$5.append(S2StringContext.scala:71)
org.specs2.specification.create.S2StringContextCreation.$anonfun$s2$1(S2StringContext.scala:190)
scala.collection.LinearSeqOps.foldLeft(LinearSeq.scala:169)
scala.collection.LinearSeqOps.foldLeft$(LinearSeq.scala:165)
scala.collection.immutable.List.foldLeft(List.scala:79)
org.specs2.specification.create.S2StringContextCreation.s2(S2StringContext.scala:188)
org.specs2.specification.create.S2StringContextCreation.s2$(S2StringContext.scala:175)
org.specs2.Specification.s2(Specification.scala:19)
org.specs2.guide.UseForms$.$anonfun$is$1(UseForms.scala:9)
org.specs2.specification.core.SpecStructure.fragments$lzycompute(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure.fragments(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure.$anonfun$map$1(SpecStructure.scala:29)
org.specs2.specification.core.SpecStructure.fragments$lzycompute(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure.fragments(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure.$anonfun$map$1(SpecStructure.scala:29)
org.specs2.specification.core.SpecStructure.fragments$lzycompute(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure.fragments(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure$.selected(SpecStructure.scala:170)
org.specs2.specification.core.SpecStructure$.linkedSpecStructuresRefs(SpecStructure.scala:159)
org.specs2.specification.core.SpecStructure$.$anonfun$linkedSpecifications$1(SpecStructure.scala:117)
org.specs2.specification.core.SpecStructure$.getRefs$1(SpecStructure.scala:134)
org.specs2.specification.core.SpecStructure$.$anonfun$specStructuresRefs$11(SpecStructure.scala:143)
scala.collection.StrictOptimizedIterableOps.flatMap(StrictOptimizedIterableOps.scala:117)
scala.collection.StrictOptimizedIterableOps.flatMap$(StrictOptimizedIterableOps.scala:104)
scala.collection.immutable.Vector.flatMap(Vector.scala:113)
org.specs2.specification.core.SpecStructure$.getAll$1(SpecStructure.scala:143)
org.specs2.specification.core.SpecStructure$.$anonfun$specStructuresRefs$9(SpecStructure.scala:149)
org.specs2.fp.Need$$anon$3.value0$lzycompute(Name.scala:30)
org.specs2.fp.Need$$anon$3.value0(Name.scala:30)
org.specs2.fp.Need$$anon$3.value(Name.scala:31)
org.specs2.control.eff.ErrorInterpretation$$anon$1.$anonfun$applicative$3(ErrorEffect.scala:83)
scala.collection.immutable.List.map(List.scala:246)
org.specs2.fp.Traverse$$anon$1.map(Traverse.scala:43)
org.specs2.fp.Traverse$$anon$1.map(Traverse.scala:33)
org.specs2.fp.FunctorSyntax$FunctorOps.map(Functor.scala:25)
org.specs2.control.eff.ErrorInterpretation$$anon$1.$anonfun$applicative$2(ErrorEffect.scala:83)
org.specs2.fp.Need$$anon$3.value0$lzycompute(Name.scala:30)
org.specs2.fp.Need$$anon$3.value0(Name.scala:30)
org.specs2.fp.Need$$anon$3.value(Name.scala:31)
org.specs2.control.eff.ErrorInterpretation$$anon$1.apply(ErrorEffect.scala:76)
org.specs2.control.eff.ErrorInterpretation$$anon$1.apply(ErrorEffect.scala:69)
org.specs2.control.eff.Interpret$$anon$1.onEffect(Interpret.scala:53)
org.specs2.control.eff.Interpret$$anon$1.onApplicativeEffect(Interpret.scala:61)
org.specs2.control.eff.Interpret$$anon$1.onApplicativeEffect(Interpret.scala:45)
org.specs2.control.eff.Interpret.go$1(Interpret.scala:200)
org.specs2.control.eff.Interpret.$anonfun$interpretLoop$11(Interpret.scala:198)
org.specs2.control.eff.Arrs.go$1(Eff.scala:380)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.CollectedUnions.$anonfun$othersEff$1(Unions.scala:97)
org.specs2.control.eff.Arrs.go$1(Eff.scala:380)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.Interpret.$anonfun$interpretLoop$11(Interpret.scala:198)
org.specs2.control.eff.Arrs.go$1(Eff.scala:380)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.CollectedUnions.$anonfun$othersEff$1(Unions.scala:97)
org.specs2.control.eff.Arrs.go$1(Eff.scala:383)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.Interpret.$anonfun$interpretLoop$11(Interpret.scala:198)
org.specs2.control.eff.Arrs.go$1(Eff.scala:380)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.CollectedUnions.$anonfun$othersEff$1(Unions.scala:97)
org.specs2.control.eff.Arrs.go$1(Eff.scala:383)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.Arrs.apply(Eff.scala:348)
org.specs2.control.eff.CollectedUnions.$anonfun$continuation$1(Unions.scala:84)
org.specs2.control.eff.Arrs.go$1(Eff.scala:380)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.SafeInterpretation$$anon$1.onApplicativeEffect(SafeEffect.scala:140)
org.specs2.control.eff.SafeInterpretation$$anon$1.onApplicativeEffect(SafeEffect.scala:65)
org.specs2.control.eff.Interpret.go$1(Interpret.scala:200)
org.specs2.control.eff.Interpret.interpretLoop(Interpret.scala:207)
org.specs2.control.eff.Interpret.interpretLoop$(Interpret.scala:142)
org.specs2.control.eff.package$interpret$.interpretLoop(package.scala:31)
org.specs2.control.eff.Interpret.interpretLoop1(Interpret.scala:211)
org.specs2.control.eff.Interpret.interpretLoop1$(Interpret.scala:210)
org.specs2.control.eff.package$interpret$.interpretLoop1(package.scala:31)
org.specs2.control.eff.SafeInterpretation.runSafe(SafeEffect.scala:47)
org.specs2.control.eff.SafeInterpretation.runSafe$(SafeEffect.scala:45)
org.specs2.control.eff.SafeEffect$.runSafe(SafeEffect.scala:15)
org.specs2.control.eff.SafeInterpretation.execSafe(SafeEffect.scala:52)
org.specs2.control.eff.SafeInterpretation.execSafe$(SafeEffect.scala:51)
org.specs2.control.eff.SafeEffect$.execSafe(SafeEffect.scala:15)
org.specs2.control.eff.syntax.safe$SafeEffectOps.execSafe(safe.scala:17)
org.specs2.control.ExecuteActions.attemptExecuteAction(ExecuteActions.scala:59)
org.specs2.control.ExecuteActions.attemptExecuteAction$(ExecuteActions.scala:56)
org.specs2.control.ExecuteActions$.attemptExecuteAction(ExecuteActions.scala:93)
org.specs2.control.ExecuteActions.runAction(ExecuteActions.scala:52)
org.specs2.control.ExecuteActions.runAction$(ExecuteActions.scala:51)
org.specs2.control.ExecuteActions$.runAction(ExecuteActions.scala:93)
org.specs2.control.ExecuteActions$$anon$1.asResult(ExecuteActions.scala:70)
org.specs2.execute.AsResult$.apply(AsResult.scala:32)
org.specs2.specification.core.AsExecution$$anon$1.$anonfun$execute$1(AsExecution.scala:17)
org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:22)
org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:124)
org.specs2.execute.Result$$anon$4.asResult(Result.scala:246)
org.specs2.execute.AsResult$.apply(AsResult.scala:32)
org.specs2.execute.AsResult$.$anonfun$safely$1(AsResult.scala:40)
org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:22)
org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:124)
org.specs2.execute.AsResult$.safely(AsResult.scala:40)
org.specs2.specification.core.Execution$.$anonfun$result$1(Execution.scala:345)
org.specs2.specification.core.Execution$.$anonfun$withEnvSync$3(Execution.scala:363)
org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:22)
org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:124)
org.specs2.execute.Result$$anon$4.asResult(Result.scala:246)
org.specs2.execute.AsResult$.apply(AsResult.scala:32)
org.specs2.execute.AsResult$.$anonfun$safely$1(AsResult.scala:40)
org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:22)
org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:124)
org.specs2.execute.AsResult$.safely(AsResult.scala:40)
org.specs2.specification.core.Execution$.$anonfun$withEnvSync$2(Execution.scala:363)
org.specs2.specification.core.Execution.$anonfun$startExecution$3(Execution.scala:143)
scala.concurrent.impl.Promise$Transformation.run(Promise.scala:434)
java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
java.base/java.lang.Thread.run(Thread.java:832)

Using tabs

If there are too many fields to be displayed on a Form you can use tabs:

s2"""
A person can have 2 addresses ${
  Form("Addresses").tr {
    tab("home",
      Address("Oxford St", 12).
        fill("Oxford St", 12)).
      tab("work",
        Address("Rose Cr.", 3).
          fill("Rose Cr.", 3))
  }
}
"""

The first tab call will create a Tabs object containing the a first tab with “home” as the title and an Address form as its content. Then every subsequent tab calls on the Tabs object will create new tabs:

Addresses
Address
streetOxford St
number12
Address
streetRose Cr.
number2

Tabs can also be created from a seq of values. Let’s pretend we have a list of Address objects with a name and a Form displaying the Address values. You can write:

Form("Addresses").tabs(addresses) { address: Address => tab(address.street, address.form) }

Aggregating forms

Now that we’ve defined a form for a simple entity, let’s see how we can reuse it with a larger entity:

  • the Customer form defines a name attribute and embeds an instance of the Address form
  • it is defined by setting the name on one row and the Address form on the second row

[and for this example, we define a slightly different Address form]

case class Address(street: String, number: Int) {
  def actualIs(address: Address) = {
    Form("Address").
      tr(prop("street", address.street, street)).
      tr(prop("number", address.number, number))
  }
}

case class Customer(name: String, address: Address) {
  def retrieve(customerId: Int) = {
    val customer = actualCustomer(customerId)
    Form("Customer").
      tr(prop("name", customer.name)(name)).
      tr(address.actualIs(customer.address))
  }
  def actualCustomer(customerId: Int): Customer = this // fetch from the database
}

class CustomerSpecification extends Specification with Forms { def is = s2"""
The customer must be retrieved from the database with a proper name and address ${
  Customer(name = "Eric",
    address = Address(street = "Rose Crescent", number = 2)).
    retrieve(123)
  }"""
}

As you also see above, named arguments can bring more readability to the expected values.

Lazy cells

Fields, Props and Forms are added right away to a row when building a Form with the tr method. If it is necessary to add them with a “call-by-name” behavior, the lazify method can be used:

def address = Address() // build an Address
def customer = Customer()

Form("Customer").
  tr(prop("name", customer.name)("name")).
  // the address Form will be built only when the Customer Form is rendered
  tr(lazify(address.actualIs(customer.address)))

Xml cells

Any xml can be “injected” on a row by using an XmlCell:

Form("Customer").
  tr(prop("name", Customer().name)("name")).
  tr(XmlCell(<div><b>this is a bold statement</b></div>))

1-n relationships

When there are 1 - n relationships between entities the situation gets bit more complex.

For example you can have an “Order” entity, which has several “OrderLines”. In that case there are several things that we might want to specify:

  • the expected rows are included in the actual rows, with no specific order (this is the usual case)
  • the expected rows are included in the actual rows, in the same order
  • the expected rows are exactly the actual rows, with no specific order
  • the expected rows are exactly the actual rows, in the same order

Let’s see how to declare this. The 2 classes we’re going to use are:

import Form._

case class Order(orderId: Int) {
  lazy val actualLines = // those should be extracted from the actual order entity retrieved by id
    OrderLine("PIS", 1) ::
      OrderLine("PS", 2) ::
      OrderLine("BS", 3) ::
      OrderLine("SIS", 4) ::
      Nil

  def base = Forms.form("Order").th("name", "qty")
  def hasSubset(ls: OrderLine*)      = base.subset(actualLines, ls)
  def hasSubsequence(ls: OrderLine*) = base.subsequence(actualLines, ls)
  def hasSet(ls: OrderLine*)         = base.set(actualLines, ls)
  def hasSequence(ls: OrderLine*)    = base.sequence(actualLines, ls)
}

case class OrderLine(name: String, quantity: Int) {
  def form = tr(field(name), field(quantity))
}

The OrderLine class simply creates a form with 2 fields: name and quantity. The Order class is able to retrieve the actual order entity (say, from a database) and to extract OrderLine instances. It also has several methods to build Forms depending on the kind of comparison that we want to do.

Subset

Form.subset uses the FormDiffs.subset(a, b) method to calculate the differences between the lines of a and b:

  • lines existing in a but not b are left untouched
  • lines existing in a and b are marked as success
  • lines existing in b and not a are marked as failures
Order(123).hasSubset(
  OrderLine("BS", 3),
  OrderLine("PIS", 1),
  OrderLine("TDGL", 5))

This form returns:

Order
nameqty
PIS1
PS2
BS3
SIS4
TDGL5

Subsequence

Form.subsequence uses the FormDiffs.subsequence(a, b) method to calculate the differences and add them to the Form:

  • lines existing in a but not b are left untouched
  • lines existing in a and b in the same order are marked as success
  • lines existing in b and not a are marked as failures
  • lines existing in b and a but out of order are marked as failures
Order(123).hasSubsequence(
  OrderLine("PS", 2),
  OrderLine("BS", 3),
  OrderLine("PIS", 1),
  OrderLine("TDGL", 5))

This form returns:

Order
nameqty
PIS1
PS2
BS3
SIS4
TDGL5

Set

Form.set uses the FormDiffs.set(a, b) method to calculate the differences between the lines of a and b:

  • lines existing in a but not b are marked as failures
  • lines existing in a and b are marked as success
  • lines existing in b and not a are marked as failures
Order(123).hasSet(
  OrderLine("BS", 3),
  OrderLine("PIS", 1),
  OrderLine("TDGL", 5))

This form returns:

Order
nameqty
PIS1
PS2
BS3
SIS4
TDGL5

Sequence

Form.sequence uses the FormDiffs.sequence(a, b) method to calculate the differences between the lines of a and b:

  • lines existing in a but not b are marked as failures
  • lines existing in a and b in the right order are marked as success
  • lines existing in b and not a are marked as failures
Order(123).hasSequence(
    OrderLine("PS", 2),
    OrderLine("BS", 3),
    OrderLine("PIS", 1),
    OrderLine("TDGL", 5))

This form returns:

Order
nameqty
PIS1
PS2
BS3
SIS4
TDGL5

Decision tables

One very popular type of Forms are decision tables. A decision table is a Form where, on each row, several values are used for a computation and the result must be equal to other values on the same row. A very simple example of this is a calculator:

case class Calculator(form: Form = Form()) {
  def tr(a: Int, b: Int, a_plus_b: Int, a_minus_b: Int) = Calculator {
    def plus = prop(a + b)(a_plus_b)
    def minus = prop(a - b)(a_minus_b)
    form.tr(a, b, plus, minus)
  }
}

def th(title1: String, titles: String*) = Calculator(Form.th(title1, titles:_*))

The Calculator object defines a th method to create the first Calculator Form, with the proper title. The th method:

  • takes the column titles (there must be at least one title)
  • creates a header row on the form
  • returns a new Calculator containing this form (note that everything is immutable here)

The Calculator case class embeds a Form and defines a tr method which

  • takes actual and expected values
  • creates properties for the computations
  • creates a form with a new row containing those fields and properties
  • returns a new Calculator containing this form

And you use the Calculator Form like this:

class CalculatorSpecification extends Specification with Forms { def is  = s2"""
 A calculator must add and subtract Ints ${
   Calculator.
     th("a", "b", "a + b", "a - b").
     tr(1,   2,   3,       -1     ).
     tr(2,   2,   4,       0      )
 }"""
}

Here is the output:

aba + ba - b
123-1
2240

And if something goes wrong:

aba + ba - b
123-1
22400 != 2
[click on failed cells to see the stacktraces]
0 != 2 (file:1)

And when it goes very wrong (like throwing an error("very wrong")), there will be red cells and stacktraces:

aba + ba - b
123-1
224java.lang.RuntimeException: very wrong
[click on failed cells to see the stacktraces]
java.lang.RuntimeException: very wrong (file:1)
scala.sys.package$.error(package.scala:27)
org.specs2.guide.UseForms$WrongCalculator.$anonfun$tr$17(UseForms.scala:574)
scala.runtime.java8.JFunction0$mcI$sp.apply(JFunction0$mcI$sp.scala:17)
org.specs2.control.Property$.$anonfun$apply$1(Property.scala:67)
org.specs2.control.Property.execute(Property.scala:51)
org.specs2.control.Property.optionalValue(Property.scala:17)
org.specs2.execute.ResultExecution.$anonfun$executeProperty$1(ResultExecution.scala:112)
org.specs2.control.Exceptions.trye(Exceptions.scala:85)
org.specs2.control.Exceptions.trye$(Exceptions.scala:84)
org.specs2.control.Exceptions$.trye(Exceptions.scala:131)
org.specs2.execute.ResultExecution.executeEither(ResultExecution.scala:83)
org.specs2.execute.ResultExecution.executeEither$(ResultExecution.scala:82)
org.specs2.execute.ResultExecution$.executeEither(ResultExecution.scala:124)
org.specs2.execute.ResultExecution.executeProperty(ResultExecution.scala:112)
org.specs2.execute.ResultExecution.executeProperty$(ResultExecution.scala:112)
org.specs2.execute.ResultExecution$.executeProperty(ResultExecution.scala:124)
org.specs2.form.Prop.actualValue$lzycompute(Prop.scala:49)
org.specs2.form.Prop.actualValue(Prop.scala:49)
org.specs2.form.Prop.execute(Prop.scala:57)
org.specs2.form.PropCell.$anonfun$executeCell$3(Cells.scala:166)
scala.Option.orElse(Option.scala:477)
org.specs2.form.PropCell.executeCell(Cells.scala:166)
org.specs2.form.PropCell.executeCell(Cells.scala:162)
org.specs2.form.Row.$anonfun$executeRow$1(Row.scala:37)
scala.collection.immutable.List.map(List.scala:250)
org.specs2.form.Row.executeRow(Row.scala:37)
org.specs2.form.Form.$anonfun$executeRows$1(Form.scala:72)
scala.collection.immutable.Vector1.map(Vector.scala:1886)
scala.collection.immutable.Vector1.map(Vector.scala:375)
org.specs2.form.Form.executeRows(Form.scala:72)
org.specs2.form.Form.executeForm(Form.scala:80)
org.specs2.guide.UseForms$.$anonfun$is$186(UseForms.scala:527)
org.specs2.specification.create.S2StringContext$$anon$5.append(S2StringContext.scala:71)
org.specs2.specification.create.S2StringContextCreation.$anonfun$s2$1(S2StringContext.scala:190)
scala.collection.LinearSeqOps.foldLeft(LinearSeq.scala:169)
scala.collection.LinearSeqOps.foldLeft$(LinearSeq.scala:165)
scala.collection.immutable.List.foldLeft(List.scala:79)
org.specs2.specification.create.S2StringContextCreation.s2(S2StringContext.scala:188)
org.specs2.specification.create.S2StringContextCreation.s2$(S2StringContext.scala:175)
org.specs2.Specification.s2(Specification.scala:19)
org.specs2.guide.UseForms$.$anonfun$is$1(UseForms.scala:9)
org.specs2.specification.core.SpecStructure.fragments$lzycompute(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure.fragments(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure.$anonfun$map$1(SpecStructure.scala:29)
org.specs2.specification.core.SpecStructure.fragments$lzycompute(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure.fragments(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure.$anonfun$map$1(SpecStructure.scala:29)
org.specs2.specification.core.SpecStructure.fragments$lzycompute(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure.fragments(SpecStructure.scala:26)
org.specs2.specification.core.SpecStructure$.selected(SpecStructure.scala:170)
org.specs2.specification.core.SpecStructure$.linkedSpecStructuresRefs(SpecStructure.scala:159)
org.specs2.specification.core.SpecStructure$.$anonfun$linkedSpecifications$1(SpecStructure.scala:117)
org.specs2.specification.core.SpecStructure$.getRefs$1(SpecStructure.scala:134)
org.specs2.specification.core.SpecStructure$.$anonfun$specStructuresRefs$11(SpecStructure.scala:143)
scala.collection.StrictOptimizedIterableOps.flatMap(StrictOptimizedIterableOps.scala:117)
scala.collection.StrictOptimizedIterableOps.flatMap$(StrictOptimizedIterableOps.scala:104)
scala.collection.immutable.Vector.flatMap(Vector.scala:113)
org.specs2.specification.core.SpecStructure$.getAll$1(SpecStructure.scala:143)
org.specs2.specification.core.SpecStructure$.$anonfun$specStructuresRefs$9(SpecStructure.scala:149)
org.specs2.fp.Need$$anon$3.value0$lzycompute(Name.scala:30)
org.specs2.fp.Need$$anon$3.value0(Name.scala:30)
org.specs2.fp.Need$$anon$3.value(Name.scala:31)
org.specs2.control.eff.ErrorInterpretation$$anon$1.$anonfun$applicative$3(ErrorEffect.scala:83)
scala.collection.immutable.List.map(List.scala:246)
org.specs2.fp.Traverse$$anon$1.map(Traverse.scala:43)
org.specs2.fp.Traverse$$anon$1.map(Traverse.scala:33)
org.specs2.fp.FunctorSyntax$FunctorOps.map(Functor.scala:25)
org.specs2.control.eff.ErrorInterpretation$$anon$1.$anonfun$applicative$2(ErrorEffect.scala:83)
org.specs2.fp.Need$$anon$3.value0$lzycompute(Name.scala:30)
org.specs2.fp.Need$$anon$3.value0(Name.scala:30)
org.specs2.fp.Need$$anon$3.value(Name.scala:31)
org.specs2.control.eff.ErrorInterpretation$$anon$1.apply(ErrorEffect.scala:76)
org.specs2.control.eff.ErrorInterpretation$$anon$1.apply(ErrorEffect.scala:69)
org.specs2.control.eff.Interpret$$anon$1.onEffect(Interpret.scala:53)
org.specs2.control.eff.Interpret$$anon$1.onApplicativeEffect(Interpret.scala:61)
org.specs2.control.eff.Interpret$$anon$1.onApplicativeEffect(Interpret.scala:45)
org.specs2.control.eff.Interpret.go$1(Interpret.scala:200)
org.specs2.control.eff.Interpret.$anonfun$interpretLoop$11(Interpret.scala:198)
org.specs2.control.eff.Arrs.go$1(Eff.scala:380)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.CollectedUnions.$anonfun$othersEff$1(Unions.scala:97)
org.specs2.control.eff.Arrs.go$1(Eff.scala:380)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.Interpret.$anonfun$interpretLoop$11(Interpret.scala:198)
org.specs2.control.eff.Arrs.go$1(Eff.scala:380)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.CollectedUnions.$anonfun$othersEff$1(Unions.scala:97)
org.specs2.control.eff.Arrs.go$1(Eff.scala:383)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.Interpret.$anonfun$interpretLoop$11(Interpret.scala:198)
org.specs2.control.eff.Arrs.go$1(Eff.scala:380)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.CollectedUnions.$anonfun$othersEff$1(Unions.scala:97)
org.specs2.control.eff.Arrs.go$1(Eff.scala:383)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.Arrs.apply(Eff.scala:348)
org.specs2.control.eff.CollectedUnions.$anonfun$continuation$1(Unions.scala:84)
org.specs2.control.eff.Arrs.go$1(Eff.scala:380)
org.specs2.control.eff.Arrs.apply(Eff.scala:399)
org.specs2.control.eff.SafeInterpretation$$anon$1.onApplicativeEffect(SafeEffect.scala:140)
org.specs2.control.eff.SafeInterpretation$$anon$1.onApplicativeEffect(SafeEffect.scala:65)
org.specs2.control.eff.Interpret.go$1(Interpret.scala:200)
org.specs2.control.eff.Interpret.interpretLoop(Interpret.scala:207)
org.specs2.control.eff.Interpret.interpretLoop$(Interpret.scala:142)
org.specs2.control.eff.package$interpret$.interpretLoop(package.scala:31)
org.specs2.control.eff.Interpret.interpretLoop1(Interpret.scala:211)
org.specs2.control.eff.Interpret.interpretLoop1$(Interpret.scala:210)
org.specs2.control.eff.package$interpret$.interpretLoop1(package.scala:31)
org.specs2.control.eff.SafeInterpretation.runSafe(SafeEffect.scala:47)
org.specs2.control.eff.SafeInterpretation.runSafe$(SafeEffect.scala:45)
org.specs2.control.eff.SafeEffect$.runSafe(SafeEffect.scala:15)
org.specs2.control.eff.SafeInterpretation.execSafe(SafeEffect.scala:52)
org.specs2.control.eff.SafeInterpretation.execSafe$(SafeEffect.scala:51)
org.specs2.control.eff.SafeEffect$.execSafe(SafeEffect.scala:15)
org.specs2.control.eff.syntax.safe$SafeEffectOps.execSafe(safe.scala:17)
org.specs2.control.ExecuteActions.attemptExecuteAction(ExecuteActions.scala:59)
org.specs2.control.ExecuteActions.attemptExecuteAction$(ExecuteActions.scala:56)
org.specs2.control.ExecuteActions$.attemptExecuteAction(ExecuteActions.scala:93)
org.specs2.control.ExecuteActions.runAction(ExecuteActions.scala:52)
org.specs2.control.ExecuteActions.runAction$(ExecuteActions.scala:51)
org.specs2.control.ExecuteActions$.runAction(ExecuteActions.scala:93)
org.specs2.control.ExecuteActions$$anon$1.asResult(ExecuteActions.scala:70)
org.specs2.execute.AsResult$.apply(AsResult.scala:32)
org.specs2.specification.core.AsExecution$$anon$1.$anonfun$execute$1(AsExecution.scala:17)
org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:22)
org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:124)
org.specs2.execute.Result$$anon$4.asResult(Result.scala:246)
org.specs2.execute.AsResult$.apply(AsResult.scala:32)
org.specs2.execute.AsResult$.$anonfun$safely$1(AsResult.scala:40)
org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:22)
org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:124)
org.specs2.execute.AsResult$.safely(AsResult.scala:40)
org.specs2.specification.core.Execution$.$anonfun$result$1(Execution.scala:345)
org.specs2.specification.core.Execution$.$anonfun$withEnvSync$3(Execution.scala:363)
org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:22)
org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:124)
org.specs2.execute.Result$$anon$4.asResult(Result.scala:246)
org.specs2.execute.AsResult$.apply(AsResult.scala:32)
org.specs2.execute.AsResult$.$anonfun$safely$1(AsResult.scala:40)
org.specs2.execute.ResultExecution.execute(ResultExecution.scala:23)
org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:22)
org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:124)
org.specs2.execute.AsResult$.safely(AsResult.scala:40)
org.specs2.specification.core.Execution$.$anonfun$withEnvSync$2(Execution.scala:363)
org.specs2.specification.core.Execution.$anonfun$startExecution$3(Execution.scala:143)
scala.concurrent.impl.Promise$Transformation.run(Promise.scala:434)
java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1130)
java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:630)
java.base/java.lang.Thread.run(Thread.java:832)

Note that the Calculator class is not, in itself an Example. But there is an implicit definition automatically transforming Any { def form: Form } to Example so that an explicit call to .form is not necessary in order to include the Form in the specification.