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 fieldsWhen the form is displayed, here's how the fields are displayed:
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 seq 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)) }
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:
Addresses | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
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 readibility 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
:
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 = 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 failuresOrder(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 failuresOrder(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 failuresOrder(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 failuresOrder(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:
import Form._
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:
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 | 2 | '0' is not equal to '2' |
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 | 2 | very wrong |
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.
Total for specification FormsPage | |
---|---|
Finished in | 1 ms |
Results | 11 examples, 170 expectations, 0 failure, 0 error |