Platinum Partner
java,agile,groovy,frameworks,tdd,bdd,easyb

Is easyb Easy?

I was introduced to easyb by none other than the creator of easyb: Andrew Glover. In spite of hearing and reading a lot about easyb from Andy, I never had a chance to actually work on easyb. So, I spent a couple of hours last weekend to dig deep into this framework and to see if easyb was any easy at all.

easyb is a BDD framework for the Java platform. If you have no clue what BDD is, here is a quote from the easyb web site:

Behavior driven development (or BDD) isn't anything new or revolutionary-- it's just an evolutionary offshoot of test driven development, in which the word test is replaced by the word should. Semantics aside, a lot of people have found that the concept of should is a much more natural development driver than the concept of testing. In fact, when you think in terms of behavior (i.e. shoulds) you'll find that writing specifications is easier to do first, which is the intent of test driven development in the first place.

With easyb you express your story and specification using Groovy based domain specific language(DSL). You can use easyb for both Java and Groovy applications.

What is a story? A story can contain any number of scenarios. Each scenario has:

    * Given (a context)
    * When (something happens)
    * Then (something else happens)

I am using a simple login application which was written in both Spring 2.5 and EJB 3.0. In order to test the login functionality, we can write three simple scenarios here:

  • User enters valid credentials.
  • User enters invalid credentials.
  • Invalid login with a null password.


Let me list the Business interfaces and my Domain Class here:
 

1. AccountService

package com.stelligent.easyb.samples.service;

import com.stelligent.easyb.samples.domain.Account;
/**
*
* @author msubbarao
*/

public interface AccountService {

public Account createAccount(Account info);

public Account findAccount(String userid);


}

2. LoginService

package com.stelligent.easyb.samples.service;

import com.stelligent.easyb.samples.domain.Account;
import com.stelligent.easyb.samples.exception.BusinessException;
/**
*
* @author msubbarao
*/

public interface LoginService {

public Account login(String userid, String password) throws BusinessException;

}

3. Account domain class:

package com.stelligent.easyb.samples.domain;

import java.io.Serializable;

/**
*
* @author msubbarao
*/
public class Account implements Serializable {

private String userid;

private String email;

private String firstname;

private String lastname;

private String password;



public Account() {
}

public Account(String userid, String email, String firstname, String lastname, String password) {
this.userid = userid;
this.email = email;
this.firstname = firstname;
this.lastname = lastname;
this.password = password;

}

public String getUserid() {
return userid;
}

public void setUserid(String userid) {
this.userid = userid;
}

......
}

In order to actually start using easyb, you need to download the libraries from here.

Next, create a folder called stories where you will be placing all your easyb stories as shown below:


 

The stories in easyb should and must be in a file ending with an extension of .story. So our login story would be placed in a file called LoginServiceTest.story.

If you have a story named  AccountServiceTest.groovy, you will get an exception as such:

Buildfile: /Users/meerasubbarao/Development/easyb-samples/build.xml
init:
run.easyb.stores:
    [easyb] easyb is preparing to process 2 file(s)
    [easyb] Easyb behavior file must end in Story.groovy, .story, Specification.groovy or .specification. See easyb documentation for more details.
    [easyb] easyb execution FAILED
BUILD SUCCESSFUL
Total time: 1 second

In the above case, as the stack trace reflects, the story should end with 'Story.groovy', and so here, AccountServiceTest.groovy should be AccountServiceTestStory.groovy.

Lets write the scenario for 1 above: User enters valid credentials

scenario "User enters valid credentials", {
given "user account already exists",{

}
when "user logins",{
}
then "the system should return a valid account",{

}
}

In the scenario above, we have a given, when and then. That’s pretty easy, don’t you think? Anyone in the team can write those scenarios. My next step is to implement the next two scenarios.

scenario "User enters invalid credentials", {
given "user account already exists",{

}
when "user logins with invalid password",{
}
then "a null account should be returned",{

}
}

and the last one:

scenario "Invalid login with a null password", {
given "user account already exists",{
}

when "user logins with null password", {
}

then "an exception should be thrown", {
}
}

The above scenarios are just bare bones, lets add some actual code and try to run them.

As I mentioned earlier, I have a simple mock implementations for the AccountService and LoginService.

The three scenarios with valid code would look like:

scenario "User enters valid credentials", {
given "user account already exists",{
accountService = new AccountServiceImpl()
loginService = new LoginServiceImpl()
loginService.setAccountService(accountService)

}
when "user logins",{
account = loginService.login("meera", "password")
}
then "the system returns a valid account",{
account.getUserid().shouldBe "meera"
account.getPassword().shouldBe "password"

}
}

scenario "User enters invalid credentials", {
given "user account already exists",{
accountService = new AccountServiceImpl()
loginService = new LoginServiceImpl()
loginService.setAccountService(accountService)
}
when "user logins with invalid password",{
account = loginService.login("meera", "meera")
}
then "a null account should be returned",{
account.shouldBe null
}
}


scenario "Invalid login with a null password", {
given "user account already exists",{
accountService = new AccountServiceImpl()
loginService = new LoginServiceImpl()
loginService.setAccountService(accountService)
}

when "user logins with null password", {
enternull = {
loginService.login("meera", null)
}
}

then "an exception should be thrown", {
ensureThrows(BusinessException.class){
enternull()
}
}

}

Once you have the story written, we need to run these. easyb can be invoked via:

    * the command line
    * Ant
    * Maven
    * IntelliJ

I am a big fan of automation, so I chose the easy way;Ant.

It’s quite easy to run easyb from Ant. Here is what you need to do:

1. Define a task:

<taskdef name="easyb" classname="org.disco.easyb.ant.BehaviorRunnerTask">
<classpath>
<path refid="easyb.classpath" />
</classpath>
</taskdef>

 2. Run the task:

<easyb>
<classpath>
<path refid="easyb.classpath" />
<pathelement path="${test.classes.dir}" />
<pathelement path="${build.classes.dir}" />
</classpath>
<report location="build/story.txt" format="txtstory" />
<behaviors dir="stories">
</behaviors>
</easyb>

3. Verify the output:

Buildfile: /Users/meerasubbarao/Development/easyb-samples/build.xml
init:
run.easyb.stores:
[easyb] easyb is preparing to process 1 file(s)
[easyb] Running login service test story (LoginServiceTest.story)
[easyb] Scenarios run: 3, Failures: 0, Pending: 0, Time Elapsed: 0.486 sec
[easyb] 3 total behaviors run with no failures
[easyb] easyb execution passed
BUILD SUCCESSFUL
Total time: 1 second

 

4. Story Printing:

In the Ant task in 2, we created a report element with its format attribute set to txtstory as follows:

<report location="build/story.txt" format="txtstory" />

If you open up the story.txt file within the build folder, it conatins a report of the scenarios run as shown below:

3 scenarios (including 0 pending) executed successfully

  Story: login service test

    scenario User enters valid credentials
      given user account already exists
      when user logins
      then the system returns a valid account

    scenario User enters invalid credentials
      given user account already exists
      when user logins with invalid password
      then  a null account should be returned

    scenario Invalid login with a null password
      given user account already exists
      when user logins with null password
      then an exception should be thrown

 

The report above looks like something even a 5th grader could understand, right?

If you want to use easyb from within Maven, or from the Command Line, or even IntelliJ, refer to the easyb web site which has examples for all of them. 

You can have as many givens, whens, and thens, with ands linking them within your Scenarios.

easyb supports the following syntax for should and ensure. Remember, you can also check that an exception is thrown by your code using the ensureThrows variant of the ensure closure; like we did in our 3rd scenario.

Here is a list of the should and ensure syntax:

And now finally, the verdict. Is easyb Easy? In my opinion, it was very

 What do you think? Give it a try, it is easy

Update: Jochen Bedersdorfer, commented that it would be interesting to see what happens when a test fails. So, here is what happens:

In my case, I changed the password for the Account. The scenario one was supposed to validate that the user name was "meera" and password was "password". When you run the same:

Buildfile: /Users/meerasubbarao/Development/easyb-samples/build.xml
init:
run.easyb.stores:
    [easyb] easyb is preparing to process 1 file(s)
    [easyb] Running login service test story (LoginServiceTest.story)
    [easyb] FAILURE Scenarios run: 3, Failures: 1, Pending: 0, Time Elapsed: 0.529 sec
    [easyb]     "the system returns a valid account" -- expected password1 but was password
    [easyb] 3 total behaviors run with 1 failure
    [easyb] easyb execution FAILED
BUILD SUCCESSFUL
Total time: 1 second

 

And within the Story.txt file, you will also see the output as such for the scenario which failed:

    scenario User enters valid credentials
      given user account already exists
      when user logins
      then the system returns a valid account [FAILURE: expected password1 but was password]

 

{{ tag }}, {{tag}},

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}
{{ parent.authors[0].realName || parent.author}}

{{ parent.authors[0].tagline || parent.tagline }}

{{ parent.views }} ViewsClicks
Tweet

{{parent.nComments}}