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 |