My favorites | Sign in
Logo
                
Search
for
Updated Nov 05, 2009 by etorreborre
Forms  
Forms for HtmlSpecifications

Alpha version!

Forms

Forms are the preferred way in specs to create examples in a table format, so that they are readable by business users. You can have a look at some examples here. You can also read the blog post about it.

Fields

A form is a set of Fields or Properties. Here is a simple example of a Form:

class Person extends Form {
  val firstName = field("First name", "Eric")
  val lastName = field("Last name", "Torreborre")

  tr(firstName)
  tr(lastName)
}

This form declares 2 fields attached to the Person form (with the field method). They have a distinct label and an initial value. They are displayed on 2 separate rows with the tr method.

The following html is produced with this literate specification:

class fieldsFormSpec extends LiterateSpecification with Html {
 class Person extends Form {
   val firstName = field("First name", "Eric")
   val lastName = field("Last name", "Torreborre")
   tr(firstName)
   tr(lastName)
 }
 "A form with fields" is <textile>
   { new Person().toHtml }  
  </textile>
}

Clearly this Form is not doing much, this is just a way to create a table with input data in the LiterateSpecification and to be able to get its values afterwards.

Properties

Now if we want to be able to set expectations on data we need to declare properties. A property is declared like a field, but with the prop method (inside a Form object):

  // "Name" is the property label, "Eric" is its actual value
  val name = prop("Name", "Eric")

Then it is possible to set the expected value of this property with its apply method (see the complete example below):

  name("Bob") // the enclosing Form object will check that actual value == expected value

Property matcher

The default matcher used to check if the actual value is ok according to the expected value is beEqual(_). However, you can change this matcher to another one. For example:

  val name = prop("Name", "Eric").matchesWith(equalToIgnoringCase(_))
  name("eric").execute.isOk // true

Value formatters

The default formatter for Double values is new DecimalFormat("#.###############"), however if you want to have a specific display for some of your values, you can declare a new value formatter on a property. For example, you may want to display Iterable properties (created with the propIterable method) with \ as a separator:

  p = PropIterable("", List("1.2", "3.4"))
  p.formatIterableWith((list:Option[Iterable[Double]]) => list match { 
     case None => "x/x/x" // this is for the case where there are no values
     case Some(l) => l.map(p.formatValue(_)).mkString("/") // using the default formatter for Doubles, then separating with "/"
  })

The available formatting methods are:

Label and Value decorators

On any Field, Prop or Form you can set decorators for labels or values:

  // this will add bold tags around the formatted value
  myField.decorateValueWith((s:String) => <b>{s}</b>)

  // this will add italic tags around the formatted label
  myField.decorateLabelWith((s:String) => <i>{s}</i>)

  // and there are convenient shortcuts for italics, bold and strike
  myField.italicValue    // the value is italic
  myField.strikeLabel    // the label is strike
  myField.bold           // all is bold
  myField.boldLabel.strikeValue    // the label is bold, the value is strike

  // there are other shortcuts to set the style attributes on cells too
  myField.successValue  // the value has a success style (green)

Nested forms

Let's have a look at a complete example, which will also demonstrate that forms can also be nested to create complex business objects:

First of all, let's define some application-level objects modeling a Person and his address:

trait PersonBusinessEntities {
  case class Person(firstName: String, lastName: String, address: Address, friends: List[String]) {
    def initials = firstName(0).toString + lastName(0)
  }
  case class Address(number: Int, street: String)
}

Then let's create a PersonForm which can be instantiated from an actual Person object (retrieved from a database for example):

trait PersonForms extends HtmlSpecification with PersonBusinessEntities {

  case class PersonForm(t: String, p: Person) extends Form(t) {
    def this(p: Person) = this("Customer", p)
    val firstName = prop("First Name", p.firstName)
    val lastName = prop("Last Name", p.lastName)
    val initials = prop("Initials", p.initials).matchesWith(beEqualToIgnoringCase(_))
    val friends =  propIterable("Friends", p.friends)
    val address = form(AddressForm("Home", p.address))

    tr(firstName, address)
    tr(lastName, initials)
    tr(friends)
  }
  case class AddressForm(t: String, address: Address) extends Form(t) {
    def this(a: Address) = this("Home", a)
    val number = prop("Number", address.number)
    val street = prop("Street", address.street)
    tr(number)
    tr(street)
  }
}

In the 2 forms above, we declare:

Finally, we can create a literate specification with this form, like this:

class formSampleSpec extends PersonForms with Html {
  "Forms can be used in a Literate specificatins" is <textile>

This is a Person form, checking that the initials are set properly on a Person object. { 
  val address = Address(37, "Nando-cho")
  val person = Person("Eric", "Torreborre", address, List("Jerome", "Olivier"))

  "Initials are automatically populated" inForm
   new PersonForm(person) {
    firstName("Eric")       
    initials("et")
    friends("Jerome", "Olivier")
    address.set { a =>
                  a.number(37)
                  a.street("Nando-cho") }
    lastName("Torreborre")
   }
}

  </textile>
}

When we use the form, we bind it with an actual person object and for each property, we declare what is the expected value. For example, we expect the initials property to be properly populated with the initials of the first name and the last name of the Person.

You can also notice the set method on a form which provides a way to set a nested form properties in the same block.

When the specification is executed, the result will look like this.

Forms execution

There are 2 way to relate the form successes/failures to its specification:

Forms layout

Predefined layouts

Some special methods can be used to help the layout of Forms (see the scaladoc for the precise methods description):

class tabsSpec extends HtmlSpecification("Tabs sample") with JUnit {
 class ClubMember extends Form {
   new tabs() {
     new tab("Contact details") {
       tr(field("First name", "Eric"))
       tr(field("Last name", "Torreborre"))
     }
     new tab("Sports") {
       th2("Sport", "Years of practice")
       tr(field("Squash", 10))
       tr(field("Tennis", 5))
       tr(field("Windsurf", 2))
     }
   }
 }
 "A form with tabs" is <textile>
   { new ClubMember().toHtml }  
  </textile>
}

Tabs helper methods

See here for the corresponding Html report.

Override an existing layout

When working with an existing form you may wish to:

Special forms

DataTable form

DataTables can also be used in a Form. Here is an example:

  new TradePrice {
    
    "Value date"    | "NPV_PAY_NOTIONAL"    | "NPV_REC_NOTIONAL"    |
    "6/1/2007"      ! -1732.34              ! 0.0                   |
    "4/30/2008"     ! -580332.88            ! 0.0                   | { (valueDate: String, pay: Double, rec: Double) =>

      tr(valueDate, prop(pricePay(valueDate))(pay), prop(priceRec(valueDate))(rec))
    }
  }.report

In this example:

Table forms

Actually a DataTableForm is just a special case of a TableForm. A TableForm is a form containing only LineForms. And a LineForm is just a set of properties which should be displayed on a row, without their labels (the labels will be used to create the table header).

The DataTableForm example above could then be rewritten as:

  class PayLine(vDate: String, p: Double, r: Double) extends LineForm {
    val valueDate = field("Value date", vDate)
    val pay = prop("NPV_PAY_NOTIONAL", pricePay(valueDate))(p)
    val rec = prop("NPV_REC_NOTIONAL", priceRec(valueDate))(r)
  }
  new TableForm {
    tr(PayLine("6/1/2007", -1732.34, 0.0))
    tr(PayLine("4/30/2008", -580332.88, 0.0))
  }.report

You can note in this example that there is also an implicit conversion between a Field and its value so you can write pricePay(valueDate) instead of pricePay(valueDate.get)

SeqForm and EntityLine form

Those 2 forms are to be used when you want to declare a form where each row is a separate "Entity" (modeling a business object for example) and where the whole table declares a sorted sequence of expected entities. Upon execution, the missing or supplementary rows will be reported. Here is an example:

  case class CustomerLine(name: String, age: Int) extends EntityLineForm[Customer] {

    // the prop method accepts a function here, taking the proper attribute on the "Entity"
    prop("Name", (_:Customer).getName)(name)
    prop("Age", (_:Customer).getAge)(age)
  }
  class Customers(actualCustomers: Seq[Customer]) extends SeqForm(actualCustomers)

  // example usage
  new Customers(listFromTheDatabase) {
    tr(CustomerLine("Eric", 36))
    tr(CustomerLine("Bob", 27))
  }

BagForm and EntityLine form

The BagForm is similar to a SeqForm but it doesn't expect a sequence of expected rows to match a sequence of actual entities. It tries instead to find the best match between the set of expected rows and the set of actual entities based on the number of failing properties for each row.

Note however that the underlying algorithm for finding the best match is a bit naive and may not find the best combination based on the expected and actual rows order. This will evolve in subsequent releases.

You can see an example of it here

Missing actual rows

By default, a BagForm will report missing actual rows and a failure if some actual rows can't be matched against expected raws.

You can switch this behavior off with isIncomplete:

  // there are 2 actual persons to match but one only is ok
  val form = new DataTableBagForm("Persons", actual) {
    "Name" | "Age" |
    "Bob"  ! 40    | { (name:String, age:Int) =>
      tr(PersonLine(name, age))
    }
  }
  form.isIncomplete.execute.isOk must be(true)

Sign in to add a comment
Hosted by Google Code