Showing posts with label BDD. Show all posts
Showing posts with label BDD. Show all posts

Monday, March 4, 2013

Expressive JAX-RS integration testing with Specs2 and client API 2.0

No doubts, JAX-RS is an outstanding piece of technology. And upcoming specification JAX-RS 2.0 brings even more great features, especially concerning client API. Topic of today's post is integration testing of the JAX-RS services.

There are a bunch of excellent test frameworks like REST-assured to help with that, but the way I would like to present it is by using expressive BDD style. Here is an example of what I mean by that:

    Create new person with email <[email protected]>
     Given REST client for application deployed at http://localhost:8080
        When I do POST to rest/api/[email protected]&firstName=Tommy&lastName=Knocker
        Then I expect HTTP code 201

Looks like typical Given/When/Then style of modern BDD frameworks. How close we can get to this on JVM, using statically compiled language? It turns out, very close, thanks to great specs2 test harness.

One thing to mention, specs2 is a Scala framework. Though we are going to write a bit of Scala, we will do it in a very intuitive way, familiar to experienced Java developer. The JAX-RS service under the test is the one we've developed in previous post. Here it is:

package com.example.rs;

import java.util.Collection;

import javax.inject.Inject;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;

import com.example.model.Person;
import com.example.services.PeopleService;

@Path( "/people" ) 
public class PeopleRestService {
    @Inject private PeopleService peopleService;
 
    @Produces( { MediaType.APPLICATION_JSON } )
    @GET
    public Collection< Person > getPeople( @QueryParam( "page") @DefaultValue( "1" ) final int page ) {
        return peopleService.getPeople( page, 5 );
    }

    @Produces( { MediaType.APPLICATION_JSON } )
    @Path( "/{email}" )
    @GET
    public Person getPeople( @PathParam( "email" ) final String email ) {
        return peopleService.getByEmail( email );
    }

    @Produces( { MediaType.APPLICATION_JSON  } )
    @POST
    public Response addPerson( @Context final UriInfo uriInfo,
            @FormParam( "email" ) final String email, 
            @FormParam( "firstName" ) final String firstName, 
            @FormParam( "lastName" ) final String lastName ) {
  
        peopleService.addPerson( email, firstName, lastName );
        return Response.created( uriInfo.getRequestUriBuilder().path( email ).build() ).build();
    }
 
    @Produces( { MediaType.APPLICATION_JSON  } )
    @Path( "/{email}" )
    @PUT
    public Person updatePerson( @PathParam( "email" ) final String email, 
            @FormParam( "firstName" ) final String firstName, 
            @FormParam( "lastName" )  final String lastName ) {
  
        final Person person = peopleService.getByEmail( email );  
        if( firstName != null ) {
            person.setFirstName( firstName );
        }
  
        if( lastName != null ) {
            person.setLastName( lastName );
        }

        return person;     
    }
 
    @Path( "/{email}" )
    @DELETE
    public Response deletePerson( @PathParam( "email" ) final String email ) {
        peopleService.removePerson( email );
        return Response.ok().build();
    }
}
Very simple JAX-RS service to manage people. All basic HTTP verbs are present and backed by Java implementation: GET, PUT, POST and DELETE. To be complete, let me also include some methods of the service layer as these ones raise some exceptions of our interest.
package com.example.services;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

import org.springframework.stereotype.Service;

import com.example.exceptions.PersonAlreadyExistsException;
import com.example.exceptions.PersonNotFoundException;
import com.example.model.Person;

@Service
public class PeopleService {
    private final ConcurrentMap< String, Person > persons = new ConcurrentHashMap< String, Person >(); 
  
    // ...
      
    public Person getByEmail( final String email ) {
        final Person person = persons.get( email );  
        
        if( person == null ) {
            throw new PersonNotFoundException( email );
        }
  
        return person;
    }

    public Person addPerson( final String email, final String firstName, final String lastName ) {
        final Person person = new Person( email );
        person.setFirstName( firstName );
        person.setLastName( lastName );
    
        if( persons.putIfAbsent( email, person ) != null ) {
            throw new PersonAlreadyExistsException( email );
        }
  
        return person;
    }
 
    public void removePerson( final String email ) {
        if( persons.remove( email ) == null ) {
            throw new PersonNotFoundException( email );
        }
    }
}
Very simple but working implementation based on ConcurrentMap. The PersonNotFoundException is being raised in a case when person with requested e-mail doesn't exist. Respectively, the PersonAlreadyExistsException is being raised in a case when person with requested e-mail already exists. Each of those exceptions have a counterpart among HTTP codes: 404 NOT FOUND and 409 CONFLICT. And it's the way we are telling JAX-RS about that:
package com.example.exceptions;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

public class PersonAlreadyExistsException extends WebApplicationException {
    private static final long serialVersionUID = 6817489620338221395L;

    public PersonAlreadyExistsException( final String email ) {
        super(
            Response
                .status( Status.CONFLICT )
                .entity( "Person already exists: " + email )
                .build()
        );
    }
}
package com.example.exceptions;

import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;

public class PersonNotFoundException extends WebApplicationException {
    private static final long serialVersionUID = -2894269137259898072L;
 
    public PersonNotFoundException( final String email ) {
        super(
            Response
                .status( Status.NOT_FOUND )
                .entity( "Person not found: " + email )
                .build()
        );
    }
}
The complete project is hosted on GitHub. Let's finish with boring part and move on to the sweet one: BDD. Not a surprise that specs2 has a nice support for Given/When/Then style, as described in the documentation. So using specs2, our test case becomes something like this:
"Create new person with email <[email protected]>" ^ br^
    "Given REST client for application deployed at ${http://localhost:8080}" ^ client^
    "When I do POST to ${rest/api/people}" ^ post(
        Map(
            "email" -> "[email protected]", 
            "firstName" -> "Tommy", 
            "lastName" -> "Knocker"
        )
    )^
    "Then I expect HTTP code ${201}"  ^ expectResponseCode^
    "And HTTP header ${Location} to contain ${http://localhost:8080/rest/api/people/[email protected]}" ^ expectResponseHeader^
Not bad, but what are those ^, br, client, post, expectResponseCode and expectResponseHeader? The ^, br is just some sugar specs2 brings to support Given/When/Then chain. Others, post, expectResponseCode and expectResponseHeader are just couple of functions/variables we define to do actual work. For example, client is a new JAX-RS 2.0 client, which we create like that (using Scala syntax):
val client: Given[ Client ] = ( baseUrl: String ) => 
    ClientBuilder.newClient( new ClientConfig().property( "baseUrl", baseUrl ) )    
The baseUrl is taken from Given definition itself, it's enclosed into ${...} construct. Also, we can see that Given definition has a strong type: Given[ Client ]. Later we will see that same is true for When and Then, they both do have respective strong types When[ T, V ] and Then[ V ].
The flow looks like this:
  • start from Given definition, which returns Client.
  • continue with When definition, which accepts Client from Given and returns Response
  • end up with number of Then definitions, which accept Response from When and check actual expectations
Here is how post definition looks like (which itself is When[ Client, Response ]):
def post( values: Map[ String, Any ] ): When[ Client, Response ] = ( client: Client ) => ( url: String ) =>  
    client
        .target( s"${client.getConfiguration.getProperty( "baseUrl" )}/$url" )
        .request( MediaType.APPLICATION_JSON )
        .post( 
            Entity.form( values.foldLeft( new Form() )( 
                ( form, param ) => form.param( param._1, param._2.toString ) ) 
            ),
            classOf[ Response ] 
        )
And finally expectResponseCode and expectResponseHeader, which are very similar and have the same type Then[ Response ]:
val expectResponseCode: Then[ Response ] = ( response: Response ) => ( code: String ) => 
    response.getStatus() must_== code.toInt                           

val expectResponseHeader: Then[ Response ] = ( response: Response ) => ( header: String, value: String ) =>        
    response.getHeaderString( header ) should contain( value ) 
Yet another example, checking response content against JSON payload:
"Retrieve existing person with email <[email protected]>" ^ br^
    "Given REST client for application deployed at ${http://localhost:8080}" ^ client^
    "When I do GET to ${rest/api/people/[email protected]}" ^ get^
    "Then I expect HTTP code ${200}" ^ expectResponseCode^
    "And content to contain ${JSON}" ^ expectResponseContent(
    """
        {
            "email": "[email protected]", 
            "firstName": "Tommy", 
            "lastName": "Knocker" 
        }            
    """
    )^
This time we are doing GET request using following get implementation:
val get: When[ Client, Response ] = ( client: Client ) => ( url: String ) =>  
    client
        .target( s"${client.getConfiguration.getProperty( "baseUrl" )}/$url" )
        .request( MediaType.APPLICATION_JSON )
        .get( classOf[ Response ] )
Though specs2 has rich set of matchers to perform different checks against JSON payloads, I am using spray-json, a lightweight, clean and simple JSON implementation in Scala (it's true!) and here is the expectResponseContent implementation:
def expectResponseContent( json: String ): Then[ Response ] = ( response: Response ) => ( format: String ) => {
    format match { 
        case "JSON" => response.readEntity( classOf[ String ] ).asJson must_== json.asJson
        case _ => response.readEntity( classOf[ String ] ) must_== json
    }
}            

And the last example (doing POST for existing e-mail):

"Create yet another person with same email <[email protected]>" ^ br^
    "Given REST client for application deployed at ${http://localhost:8080}" ^ client^
    "When I do POST to ${rest/api/people}" ^ post(
        Map( 
            "email" -> "[email protected]" 
        )
    )^
    "Then I expect HTTP code ${409}" ^ expectResponseCode^
    "And content to contain ${Person already exists: [email protected]}" ^ expectResponseContent^
Looks great! Nice, expressive BDD, using strong types and static compilation! For sure, JUnit integrations is available and works great with Eclipse.

Not to forget about own specs2 reports (generated by maven-specs2-plugin): mvn clean test

Please, look for complete project on GitHub. Also, please note, as I am using the latest JAX-RS 2.0 milestone (final draft), the API may change a bit when be released.

I am still learning along the way but I like it so far.

Sunday, September 28, 2008

BDD in Java: practical example

Continuing last post about BDD, I would like to demonstrate an example with both xUnit and BDD tests. Because right now I'm developing mostly on Java, the JUnit and easyb are my choices for this small demo. So, let's start with our domain model.

Let's assume, we're modeling bank transfer subsystem. Needless to say, classes Bank and Account jibe well with our domain.

public class Account {
public void credit( final int amount ) throw NoEnoughMoneyException { ... }
public void debit( final int amount ) { ... }
public int getBalance() { ... }
}

public class Bank {
public void transfer( final Account from, final Account to, final int amount ) { ... }
}

These classes are so simple that I omitted all the code. Contracts only matter. The respective test case for money transfer looks like:

public class BankTestCase {
@Test( expected = NoEnoughMoneyException.class )
public void testTransfer() {
Bank bank = new Bank();
Account from = new Account( 100 ), to = new Account( 200 );
bank.transfer( from, to, 150 );
}
}

This test code is trying to provoke NoEnoughMoneyException by means of debit of the unallowed amount of money. Everything seems to be OK here. Now let's take a look how same test case looks in BDD:

scenario "Bank Transfer", {
given "an specific bank", {
bank = new Bank();
}

given "bank accounts", {
from = new Account( 100 );
to = new Account( 200 );
}

when "no enough money for transfer", {
transfer = {
bank.transfer( from, to, 150 )
}
}

then "exception should be thrown", {
ensureThrows( NoEnoughMoneyException ) {
transfer();
}
}
}


It's great, isn't it? You read it as a story.
And in fact output of this test scenario is real story:

Scenario Bank transfer
Given an specific bank
Given bank accounts
When no enough money for transfer
Then exception should be thrown

I like it. It's clear and friendly.

Wednesday, September 24, 2008

Is BDD good addition to TDD?

Recently I've opened for myself newest modern testing approach - BDD or behavior-driven development. Unit tests are quite familiar for most of the developers (but how many developers use item?). BDD just adds some new, powerful features by means of descriptive test stories.

I guess, everyone agrees that test cases (xUnit, ...) mostly should contain simple and straightforward code (like compare call this method, compare this value with that one, etc.). But in my practice the complex test methods are not so rare (probably, I'm doing something wrong?) and because of this complexity it is often unclear what test method is actually for.

That's where BDD can help. I've found it very useful. It's easy to tell a test story first and then decorate it with test code either write a large test case surrounded by bunch of comments (I personally don't like comments and use them very rarely).

Because right now I'm developing on Java, I would like to recommend few BDD frameworks which still in development but are ready for use.

First is easyb: http://easyb.org/
And second one is JBehave: http://jbehave.org/

Both can be easily integrated in build process (Maven, Ant, ...).
So ... I'm looking forward to increase quality of my code with help of BDD. I'm going to post some practical BDD aspects in future posts.

Cheers.