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:
For all the code samples below you need to extend the org.specs2.specification.Forms
trait.
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:
field(value)
creates a field for a value, where the label is emptyfield(label, value)
creates a field with a label and a valuefield(label, field1, field2, ...)
creates a field with a label and values coming from other fields, concatenated as strings When the form is displayed, here is how the fields are displayed:
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.
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:
effect(value)
creates an effect with no labeleffect(label, value)
creates an effect with a label and a value that will be evaluated when the Effect
is executedeffect(effect1, effect2, ...)
creates an effect with all the effects labels and a side-effect sequencing all side-effects 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)) |
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:
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:
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:
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:
th(field("a"), field("b"))
th("a", "b")
using an implicit conversion of Any => Field[Any] 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
*/}
"""
A very practical way to add rows programmatically 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: ComponentsDefinitions.Address) => Row.tr(field(a.number), field(a.street)) }
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)
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:
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()))
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:
Otherwise:
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:
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) }
Now that we’ve defined a form for a simple entity, let’s see how we can reuse it with a larger entity:
[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.
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)))
Any xml can be “injected” on a row by using an XmlCell
:
def actualAddress(i: Int) = addresses(0)
Form("Customer").tr(prop("name", Customer().name)("name")).tr(XmlCell(<div><b>this is a bold statement</b></div>))
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:
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: 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.
Form.subset
uses the FormDiffs.subset(a, b)
method to calculate the differences between the lines of a
and b
:
a
but not b
are left untoucheda
and b
are marked as successb
and not a
are marked as failures Order(123).hasSubset(OrderLine("BS", 3), OrderLine("PIS", 1), OrderLine("TDGL", 5))
This form returns:
Order | ||||||
---|---|---|---|---|---|---|
name | qty | |||||
PIS | 1 | |||||
PS | 2 | |||||
BS | 3 | |||||
SIS | 4 | |||||
TDGL | 5 |
Form.subsequence
uses the FormDiffs.subsequence(a, b)
method to calculate the differences and add them to the Form:
a
but not b
are left untoucheda
and b
in the same order are marked as successb
and not a
are marked as failuresb
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 | ||||||
---|---|---|---|---|---|---|
name | qty | |||||
PIS | 1 | |||||
PS | 2 | |||||
BS | 3 | |||||
SIS | 4 | |||||
TDGL | 5 |
Form.set
uses the FormDiffs.set(a, b)
method to calculate the differences between the lines of a
and b
:
a
but not b
are marked as failuresa
and b
are marked as successb
and not a
are marked as failures Order(123).hasSet(OrderLine("BS", 3), OrderLine("PIS", 1), OrderLine("TDGL", 5))
This form returns:
Order | ||||||
---|---|---|---|---|---|---|
name | qty | |||||
PIS | 1 | |||||
PS | 2 | |||||
BS | 3 | |||||
SIS | 4 | |||||
TDGL | 5 |
Form.sequence
uses the FormDiffs.sequence(a, b)
method to calculate the differences between the lines of a
and b
:
a
but not b
are marked as failuresa
and b
in the right order are marked as successb
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 | ||||||
---|---|---|---|---|---|---|
name | qty | |||||
PIS | 1 | |||||
PS | 2 | |||||
BS | 3 | |||||
SIS | 4 | |||||
TDGL | 5 |
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 =
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:
The Calculator
case class embeds a Form and defines a tr
method which
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:
a | b | a + b | a - b | ||||||||
1 | 2 | 3 | -1 | ||||||||
2 | 2 | 4 | 0 |
And if something goes wrong:
a | b | a + b | a - b | |||||||||
1 | 2 | 3 | -1 | |||||||||
2 | 2 | 4 | 0 | 0 != 2 |
[click on failed cells to see the stacktraces]0 != 2 (file:1)org.specs2.execute.Failure$.$lessinit$greater$default$3(Result.scala:365)org.specs2.execute.Result$.result(Result.scala:166)org.specs2.execute.Result$.result(Result.scala:170)org.specs2.execute.Result$.result(Result.scala:174)org.specs2.matcher.EqualityMatcher.apply(EqualityMatcher.scala:32)org.specs2.form.Prop$.checkProp$$anonfun$1(Prop.scala:139)org.specs2.form.Prop.$anonfun$1$$anonfun$1$$anonfun$1(Prop.scala:59)org.specs2.execute.ResultExecution.execute(ResultExecution.scala:27)org.specs2.execute.ResultExecution.execute$(ResultExecution.scala:15)org.specs2.execute.ResultExecution$.execute(ResultExecution.scala:136)org.specs2.form.Prop.$anonfun$1$$anonfun$1(Prop.scala:59)scala.Option.map(Option.scala:242)org.specs2.form.Prop.$anonfun$1(Prop.scala:59)scala.Option.flatMap(Option.scala:283)org.specs2.form.Prop.execute(Prop.scala:59)org.specs2.form.PropCell.executeCell$$anonfun$3(Cells.scala:155)scala.Option.orElse(Option.scala:477)org.specs2.form.PropCell.executeCell(Cells.scala:155)org.specs2.form.Row.executeRow$$anonfun$1(Row.scala:43)scala.collection.immutable.List.map(List.scala:250)org.specs2.form.Row.executeRow(Row.scala:43)org.specs2.form.Form.executeRows$$anonfun$1(Form.scala:87)scala.collection.immutable.Vector1.map(Vector.scala:1886)scala.collection.immutable.Vector1.map(Vector.scala:375)org.specs2.form.Form.executeRows(Form.scala:87)org.specs2.form.Form.executeForm(Form.scala:96)org.specs2.guide.UseForms$.is$$anonfun$1$$anonfun$78(UseForms.scala:542)org.specs2.specification.create.S2StringContext$$anon$4.prepend(S2StringContext.scala:58)org.specs2.specification.create.S2StringContext$.$anonfun$1(S2StringContext.scala:132)scala.collection.immutable.ArraySeq.foldLeft(ArraySeq.scala:222)org.specs2.specification.core.Fragments$.reduce(Fragments.scala:139)org.specs2.specification.create.S2StringContext$.s2(S2StringContext.scala:133)org.specs2.guide.UseForms$.is$$anonfun$1(UseForms.scala:11)org.specs2.specification.core.SpecStructure.fragments(SpecStructure.scala:23)org.specs2.specification.core.SpecStructure.map$$anonfun$1(SpecStructure.scala:26)org.specs2.specification.core.SpecStructure.fragments(SpecStructure.scala:23)org.specs2.specification.core.SpecStructure.map$$anonfun$1(SpecStructure.scala:26)org.specs2.specification.core.SpecStructure.fragments(SpecStructure.scala:23)org.specs2.specification.core.SpecStructure$.selected(SpecStructure.scala:168)org.specs2.specification.core.SpecStructure$.linkedSpecStructuresRefs(SpecStructure.scala:157)org.specs2.specification.core.SpecStructure$.linkedSpecifications$$anonfun$1(SpecStructure.scala:110)org.specs2.specification.core.SpecStructure$.getRefs$1(SpecStructure.scala:129)org.specs2.specification.core.SpecStructure$.$anonfun$3(SpecStructure.scala:144)scala.collection.StrictOptimizedIterableOps.flatMap(StrictOptimizedIterableOps.scala:118)scala.collection.StrictOptimizedIterableOps.flatMap$(StrictOptimizedIterableOps.scala:105)scala.collection.immutable.Vector.flatMap(Vector.scala:113)org.specs2.specification.core.SpecStructure$.getAll$1(SpecStructure.scala:144)org.specs2.specification.core.SpecStructure$.specStructuresRefs$$anonfun$1(SpecStructure.scala:148)org.specs2.control.Operation$OperationMonad$.point$$anonfun$1(Operation.scala:128)org.specs2.control.Operation.org$specs2$control$Operation$$run(Operation.scala:18)org.specs2.control.Operation.attempt$$anonfun$1(Operation.scala:81)org.specs2.control.Operation.org$specs2$control$Operation$$run(Operation.scala:18)org.specs2.control.Operation.$anonfun$1(Operation.scala:89)org.specs2.control.Action$.either(Action.scala:107)org.specs2.control.Operation.toAction(Operation.scala:89)org.specs2.runner.DefaultClassRunner.run(ClassRunner.scala:43)org.specs2.runner.DefaultClassRunner.run(ClassRunner.scala:37)org.specs2.Website.createUserGuide$$anonfun$1(Website.scala:57)org.specs2.control.Action.flatMap$$anonfun$1$$anonfun$1(Action.scala:30)scala.concurrent.impl.Promise$Transformation.run(Promise.scala:470)java.base/java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1395)java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373)java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182)java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655)java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622)java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165)
And when it goes very wrong (like throwing an error("very wrong")
), there will be red cells and stacktraces:
a | b | a + b | a - b | ||||||||
1 | 2 | 3 | -1 | ||||||||
2 | 2 | 4 | java.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.minus$3$$anonfun$3(UseForms.scala:590)org.specs2.control.Property$.apply$$anonfun$1(Property.scala:96)org.specs2.control.Property.execute(Property.scala:77)org.specs2.control.Property.optionalValue(Property.scala:26)org.specs2.execute.ResultExecution.executeProperty$$anonfun$1(ResultExecution.scala:125)org.specs2.execute.ResultExecution.$anonfun$1(ResultExecution.scala:98)org.specs2.control.Exceptions.trye(Exceptions.scala:71)org.specs2.control.Exceptions.trye$(Exceptions.scala:15)org.specs2.control.Exceptions$.trye(Exceptions.scala:106)org.specs2.execute.ResultExecution.executeEither(ResultExecution.scala:98)org.specs2.execute.ResultExecution.executeEither$(ResultExecution.scala:15)org.specs2.execute.ResultExecution$.executeEither(ResultExecution.scala:136)org.specs2.execute.ResultExecution.executeProperty(ResultExecution.scala:125)org.specs2.execute.ResultExecution.executeProperty$(ResultExecution.scala:15)org.specs2.execute.ResultExecution$.executeProperty(ResultExecution.scala:136)org.specs2.form.Prop.actualValue(Prop.scala:49)org.specs2.form.Prop.execute(Prop.scala:57)org.specs2.form.PropCell.executeCell$$anonfun$3(Cells.scala:155)scala.Option.orElse(Option.scala:477)org.specs2.form.PropCell.executeCell(Cells.scala:155)org.specs2.form.Row.executeRow$$anonfun$1(Row.scala:43)scala.collection.immutable.List.map(List.scala:250)org.specs2.form.Row.executeRow(Row.scala:43)org.specs2.form.Form.executeRows$$anonfun$1(Form.scala:87)scala.collection.immutable.Vector1.map(Vector.scala:1886)scala.collection.immutable.Vector1.map(Vector.scala:375)org.specs2.form.Form.executeRows(Form.scala:87)org.specs2.form.Form.executeForm(Form.scala:96)org.specs2.guide.UseForms$.is$$anonfun$1$$anonfun$79(UseForms.scala:546)org.specs2.specification.create.S2StringContext$$anon$4.prepend(S2StringContext.scala:58)org.specs2.specification.create.S2StringContext$.$anonfun$1(S2StringContext.scala:132)scala.collection.immutable.ArraySeq.foldLeft(ArraySeq.scala:222)org.specs2.specification.core.Fragments$.reduce(Fragments.scala:139)org.specs2.specification.create.S2StringContext$.s2(S2StringContext.scala:133)org.specs2.guide.UseForms$.is$$anonfun$1(UseForms.scala:11)org.specs2.specification.core.SpecStructure.fragments(SpecStructure.scala:23)org.specs2.specification.core.SpecStructure.map$$anonfun$1(SpecStructure.scala:26)org.specs2.specification.core.SpecStructure.fragments(SpecStructure.scala:23)org.specs2.specification.core.SpecStructure.map$$anonfun$1(SpecStructure.scala:26)org.specs2.specification.core.SpecStructure.fragments(SpecStructure.scala:23)org.specs2.specification.core.SpecStructure$.selected(SpecStructure.scala:168)org.specs2.specification.core.SpecStructure$.linkedSpecStructuresRefs(SpecStructure.scala:157)org.specs2.specification.core.SpecStructure$.linkedSpecifications$$anonfun$1(SpecStructure.scala:110)org.specs2.specification.core.SpecStructure$.getRefs$1(SpecStructure.scala:129)org.specs2.specification.core.SpecStructure$.$anonfun$3(SpecStructure.scala:144)scala.collection.StrictOptimizedIterableOps.flatMap(StrictOptimizedIterableOps.scala:118)scala.collection.StrictOptimizedIterableOps.flatMap$(StrictOptimizedIterableOps.scala:105)scala.collection.immutable.Vector.flatMap(Vector.scala:113)org.specs2.specification.core.SpecStructure$.getAll$1(SpecStructure.scala:144)org.specs2.specification.core.SpecStructure$.specStructuresRefs$$anonfun$1(SpecStructure.scala:148)org.specs2.control.Operation$OperationMonad$.point$$anonfun$1(Operation.scala:128)org.specs2.control.Operation.org$specs2$control$Operation$$run(Operation.scala:18)org.specs2.control.Operation.attempt$$anonfun$1(Operation.scala:81)org.specs2.control.Operation.org$specs2$control$Operation$$run(Operation.scala:18)org.specs2.control.Operation.$anonfun$1(Operation.scala:89)org.specs2.control.Action$.either(Action.scala:107)org.specs2.control.Operation.toAction(Operation.scala:89)org.specs2.runner.DefaultClassRunner.run(ClassRunner.scala:43)org.specs2.runner.DefaultClassRunner.run(ClassRunner.scala:37)org.specs2.Website.createUserGuide$$anonfun$1(Website.scala:57)org.specs2.control.Action.flatMap$$anonfun$1$$anonfun$1(Action.scala:30)scala.concurrent.impl.Promise$Transformation.run(Promise.scala:470)java.base/java.util.concurrent.ForkJoinTask$RunnableExecuteAction.exec(ForkJoinTask.java:1395)java.base/java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:373)java.base/java.util.concurrent.ForkJoinPool$WorkQueue.topLevelExec(ForkJoinPool.java:1182)java.base/java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1655)java.base/java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1622)java.base/java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:165)
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.