Objectives

Create Test Project

In Eclipse, create a new standard Java project called 'coffeemate-test'.

The following archive contains a set of jar files to be used by this project:

Download and unzip this archive somewhere convenient.

In Eclipse, create a new 'folder' called lib (not a 'source folder') in your project, and drag and drop (copy) all of the jar files into that folder.

Still in eclipse, select all of the jar files, right click, and select 'add to build path'. This will place all of these jars on the path for the project.

In addition to the above libraries, also add JUnit 4 to your project. This can be done through Project->Properties->Build Path->Libraries->Add Library.

Your project should now look like this:

These are a set of libraries which will greatly simplify the task of testing our API. They include the following:

plus other libraries that these components rely on.

Coffeemate Facade

Create two packages in the src folder called:

In ie.cm.api, create a new class called Rest, this is the complete source for this class:

package ie.cm.api;

import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.BasicResponseHandler;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.HttpConnectionParams;
import org.apache.http.params.HttpParams;

public class Rest
{
  private static DefaultHttpClient httpClient = null;
  private static final String URL = "http://localhost:9000";

  private static DefaultHttpClient httpClient()
  {
    if (httpClient == null)
    {
      HttpParams httpParameters = new BasicHttpParams();
      HttpConnectionParams.setConnectionTimeout(httpParameters, 2000);
      HttpConnectionParams.setSoTimeout(httpParameters, 2000);
      httpClient = new DefaultHttpClient(httpParameters);
    }
    return httpClient;
  }

  public static String get(String path) throws Exception
  {
    HttpGet getRequest = new HttpGet(URL + path);
    getRequest.setHeader("accept", "application/json");
    HttpResponse response = httpClient().execute(getRequest);
    return new BasicResponseHandler().handleResponse(response);
  }
}

Still in ie.cm.api, create the following three classes:

Coffee

package ie.cm.api;

import com.google.common.base.Objects;

public class Coffee
{
  public Long   id;
  public String name;
  public String shop;
  public double rating;
  public double price;
  public int    favourite;

  public Coffee()
  {   
  }

  public Coffee(String coffeeName, String shop, double rating, double price, int favourite)
  {
    this.name      = coffeeName;
    this.shop      = shop;
    this.rating    = rating;
    this.price     = price;
    this.favourite = favourite;
  }

  @Override
  public boolean equals(final Object obj)
  {
    if (obj instanceof Coffee)
    {
      final Coffee other = (Coffee) obj;
      return Objects.equal(name,      other.name) 
          && Objects.equal(shop,      other.shop)
          && Objects.equal(rating,    other.rating)
          && Objects.equal(price,     other.price)           
          && Objects.equal(favourite, other.favourite);                     
    }
    else
    {
      return false;
    }
  }  

  public String toString()
  {
    return Objects.toStringHelper(this)
        .add("id",        id)
        .add("name",      name)
        .add("shop",      shop)
        .add("rating",    rating)
        .add("price",     price)
        .add("favourite", favourite).toString();
  } 
}

JsonParsers

package ie.cm.api;

import java.lang.reflect.Type;
import java.util.List;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;

public class JsonParsers
{
  static Gson gson = new Gson(); 

  public static Coffee json2Coffee(String json)
  {
    return gson.fromJson(json, Coffee.class);    
  }

  public static String coffee2Json(Object obj)
  {
    return gson.toJson(obj);
  }  

  public static List<Coffee> json2Coffees(String json)
  {
    Type collectionType = new TypeToken<List<Coffee>>() {}.getType();
    return gson.fromJson(json, collectionType);    
  }  
}

CoffeeAPI

package ie.cm.api;


public class CoffeeAPI
{

  public static Coffee getCoffee (Long id) throws Exception
  {
    String coffeeStr = Rest.get("/api/coffee/" + id);
    Coffee coffee = JsonParsers.json2Coffee(coffeeStr);
    return coffee;
  }
}

Note that the first two class are (almost) identical to two of the classes in the companion coffeemate-service play project. Check these similarities now.

First Test

Still in coffeemate-test project, in the test package, bring in this class here:

package test;

import org.junit.Test;

import coffeeservice.Coffee;
import coffeeservice.CoffeeAPI;

import utils.Rest;

public class CoffeeTest
{
  @Test
  public void testGetCoffee () throws Exception
  {
     String coffeeStr = Rest.get("/api/coffee/" + 1);
     System.out.println(coffeeStr);
  }
}

Now launch the current version of the coffemate-service play project. In order to make sure the project is properly loaded, browse to the usual web interface:

Now in the coffeemate-test project, select the CoffeeTest class, right click, and select 'Run as -> JUnit Test'.

This should display the Eclipse JUnit test runner:

In addition - we should see in the 'Console' view in eclipse, the output of the test we have written above:

Can you see what it happening here? We are issuing a request - over http - to this url:

http://localhost:9000/api/coffee/1

and retrieving the coffee object with ID 1.

Comment out the two lines in the above code - and replace them with the following:

    Coffee coffee = CoffeeAPI.getCoffee(2l);
    System.out.println(coffee);

Run the test again. The result should be almost identical. Uncomment the test such that it looks like this:

    String coffeeStr = Rest.get("/api/coffee/" + 1);
    System.out.println(coffeeStr);
    Coffee coffee = CoffeeAPI.getCoffee(2l);
    System.out.println(coffee);

Running the test again, you should see this:

{"favourite":0,"id":1,"name":"one","price":2.0,"rating":3.5,"shop":"here"}
Coffee{id=2, name=two, shop=there, rating=4.5, price=3.0, favourite=1}

What is the difference between the two lines output?

Complete CoffeeTest

The test we have written is not actually a test, as it contains no assert statements. It merely prints some data to the console. We should rewrite it such that it does a proper test. i.e. verify that we receive a specific, agreed object from the coffemate-service.

Replace CoffeeTest with the following version:

package test;

import static org.junit.Assert.*;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import coffeeservice.Coffee;
import coffeeservice.CoffeeAPI;

public class CoffeeTest
{
  private Coffee localCoffee;

  @Before
  public void setup()
  {
    localCoffee = new Coffee ("two", "there", 4.5, 3.0, 1);
  }

  @After
  public void teardown()
  {
    localCoffee = null; 
  }

  @Test
  public void testGetCoffee () throws Exception
  {
    Coffee coffee = CoffeeAPI.getCoffee(2l);
    assertEquals (localCoffee, coffee);
  }
}

and run it in the same way. This time the JUnit test runner should report success - with a 'green bar' state.

Carefully read the test itself:

    Coffee coffee = CoffeeAPI.getCoffee(2l);
    assertEquals (localCoffee, coffee);

We are requesting a coffee object with id 2, and, when we get it, verifying that it equals another (test) object we have created just for this purpose.

Just to confirm everything is behaving as expected, change the localCoffee object slightly (say setting favorite to 0). Run the test. It should fail. Correct the test to run successfully again.

Improving the Service

Our service interface - implemented in CoffeeServiceAPI is still fairly simple:

public class CoffeeServiceAPI extends Controller
{
  public static void getCoffees()
  {
    List<Coffee> coffees = Coffee.findAll();
    renderText(coffees.get(0));
  }

  public static void coffee (Long id)
  {
   Coffee coffee = Coffee.findById(id);
   renderJSON (JsonParsers.coffee2Json(coffee));
  }
}

... and is routed via these entries in the routes file:

GET     /api/coffees                           CoffeeServiceAPI.getCoffees
GET     /api/coffee/{id}                       CoffeeServiceAPI.coffee

The second route is adequate - but we delete the first method and route. Our ServiceAPI should look like this:

public class CoffeeServiceAPI extends Controller
{
  public static void coffee (Long id)
  {
   Coffee coffee = Coffee.findById(id);
   renderJSON (JsonParsers.coffee2Json(coffee));
  }
}

.. and our route. Note we have made it plural:

GET     /api/coffees/{id}                       CoffeeServiceAPI.coffee

Now introduce the following new route:

GET     /api/coffees                            CoffeeServiceAPI.coffees

Look carefully at the change - both routes begin with /api/coffees (note the plural coffeeS), but route to different methods. We already have coffee implemented, here is a first attempt at coffees:

  public static void coffees()
  {
    List<Coffee> coffees = Coffee.findAll();
    renderText(JsonParsers.coffee2Json(coffees));
  }

Run the app again and browse to:

The browser should return:

[{"favourite":0,"id":1,"name":"one","price":2.0,"rating":3.5,"shop":"here"},{"favourite":1,"id":2,"name":"two","price":3.0,"rating":4.5,"shop":"there"}]

This is a little hard to read. Chrome has a browser extension that can make this task more effective. You may have installed this last week. If not, on chrome, search for "Postman Rest Client". You may get this link here:

Install the package - and when you launch it you should see something like this:

Now enter our request http://localhost:9000/api/coffees - and we should see this:

Pressing the Json button presents the data in a more readable format:

This is a very useful tool for experimentally verifying that our API is behaving as expected.

Creating coffee objects

We can read individual coffee objects, or a collection of coffees. How about Creating a coffee object?

In order to do this we will need an additional utility class in our coffemate-service Play project. In the utils package, import this class here:

package utils;

import com.google.gson.JsonElement;
import com.google.gson.JsonParser;
import play.data.binding.Global;
import play.data.binding.TypeBinder;

import java.lang.annotation.Annotation;
import java.lang.reflect.Type;

@Global
public class GsonBinder implements TypeBinder<JsonElement>
{
  public Object bind(String name, Annotation[] notes, String value, Class toClass, Type toType) throws Exception
  {
    return new JsonParser().parse(value);
  }
}

The purpose of this class is rather opaque. Its task is to help deliver a Json payload to our controllers. Dont worry about how this is done - we will never need to change this class - so we can merely include it as is.

Introduce this new route here:

POST    /api/coffees                           CoffeeServiceAPI.createCoffee

and the method in the controller to handle this route:

  public static void createCoffee(JsonElement body)
  {
    Coffee coffee = JsonParsers.json2Coffee(body.toString());
    coffee.save();
    renderJSON (JsonParsers.coffee2Json(coffee));
  } 

You will need to import JsonElement at the top of the file:

import com.google.gson.JsonElement

Before writing a test - which would be the logical next step - we can use Postman to manually drive the api and generate the appropriate http request.

This time it is a bit more complicated however - as we are generating a POST request, not a single GET like last time. In addition, we need to include a payload, which is to contain a json version of a coffee object.

First we need to specify 2 headers:

Content-Type: application/json

These are specified using the "Header" button on the right. If you figure it out, it should look like this:

Then we need to specify the payload - also called 'form data':

In the above, we selected 'raw' and 'json' and entered the following data:

{
  "name" : "mocha",
  "shop" : "costa"
}

Note carefully the ',' separating the two values. Before running the coffeemate-service, disable the loading for data.yaml first:

@OnApplicationStart
public class Bootstrap extends Job 
{ 
  public void doJob()
  {
   //Fixtures.loadModels("data.yml");
  }
}

Run the app again, and press 'Send' in Postman. If all goes correctly, then coffeemate-service should respond with a new coffeemate object:

See if you can verify that this has in fact been created in the database.

Create a few more coffee objects - give them different names and process. After each one is sent, check the database for these new values.

Notice that you can 'save' requests in postman. This allows you to construct different requests and run them again later. They appear in the list on the left in postman.

Testing Create Coffee

Now that we have proved we can create coffee objects using POST - we should write a test in Java that does just that.

Returning to the coffeemate-test project, we need a new method in the Rest class:

  public static String post(String path, String json) throws Exception
  {
    HttpPost putRequest = new HttpPost(URL + path);
    putRequest.setHeader("Content-type", "application/json");
    putRequest.setHeader("accept", "application/json");

    StringEntity s = new StringEntity(json);
    s.setContentEncoding("UTF-8");
    s.setContentType("application/json");
    putRequest.setEntity(s);

    HttpResponse response = httpClient().execute(putRequest);
    return new BasicResponseHandler().handleResponse(response);
  }

This will be used to formulate a POST request.

In CoffeeAPI class (in the coffeemate-test project), introduce these two new methods:

  public static Coffee createCoffee(String coffeeJson) throws Exception
  {
    String response = Rest.post ("/api/coffees", coffeeJson);
    return JsonParsers.json2Coffee(response);
  }

  public static Coffee createCoffee(Coffee coffee) throws Exception
  {
    return createCoffee(JsonParsers.coffee2Json(coffee));
  }

These provide a higher level mechanism for creating coffee objects using the API.

Now, in CoffeeTest, we can write an elegant test:

  @Test
  public void testCreateCoffee () throws Exception
  {
    Coffee coffee =  new Coffee ("cappucino", "starbucks", 1.5, 4.0, 0);
    Coffee returnedCoffee = CoffeeAPI.createCoffee(coffee);

    assertEquals (coffee, returnedCoffee);
  }

Our existing test should be commented out now, as it is no longer reliable. Run this new test - it should pass.

Inspect the database - you should see the 'cappucino' created. If you run the test a few times, then you will get multiple cappuccino coffees created.

Sources

The two projects should look something like this:

These are the respective archives:

Here are (slightly revised) version of the sources:

coffeemate-service

Coffee

@Entity
public class Coffee extends Model
{
  public String name;
  public String shop;
  public double rating;
  public double price;
  public int    favourite;

  public Coffee(String coffeeName, String shop, double rating, double price, int favourite)
  {
    this.name      = coffeeName;
    this.shop      = shop;
    this.rating    = rating;
    this.price     = price;
    this.favourite = favourite;
  }

  @Override
  public boolean equals(final Object obj)
  {
    if (obj instanceof Coffee)
    {
      final Coffee other = (Coffee) obj;
      return Objects.equal(name,      other.name) 
          && Objects.equal(shop,      other.shop)
          && Objects.equal(rating,    other.rating)
          && Objects.equal(price,     other.price)           
          && Objects.equal(favourite, other.favourite);                     
    }
    else
    {
      return false;
    }
  }  

  public String toString()
  {
    return Objects.toStringHelper(this)
        .add("Id", id)
        .add("name",      name)
        .add("shop",      shop)
        .add("rating",    rating)
        .add("price",     price)
        .add("favourite", favourite).toString();
  } 
}

CoffeServiceAPI

public class CoffeeServiceAPI extends Controller
{
  public static void coffees()
  {
    List<Coffee> coffees = Coffee.findAll();
    renderText(JsonParsers.coffee2Json(coffees));
  }

  public static void coffee (Long id)
  {
   Coffee coffee = Coffee.findById(id);
   renderJSON (JsonParsers.coffee2Json(coffee));
  }

  public static void createCoffee(JsonElement body)
  {
    Coffee coffee = JsonParsers.json2Coffee(body.toString());
    coffee.save();
    renderJSON (JsonParsers.coffee2Json(coffee));
  }  
}

routes

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET     /                                      Application.index

GET     /api/coffees                           CoffeeServiceAPI.coffees
GET     /api/coffees/{id}                      CoffeeServiceAPI.coffee
POST    /api/coffees                           CoffeeServiceAPI.createCoffee

# Ignore favicon requests
GET     /favicon.ico                            404

# Map static resources from the /app/public folder to the /public path
GET     /public/                                staticDir:public

# Catch all
*       /{controller}/{action}                  {controller}.{action}

coffeemate-test

Coffee

public class Coffee
{
  public Long   id;
  public String name;
  public String shop;
  public double rating;
  public double price;
  public int    favourite;

  public Coffee()
  {   
  }

  public Coffee(String coffeeName, String shop, double rating, double price, int favourite)
  {
    this.name      = coffeeName;
    this.shop      = shop;
    this.rating    = rating;
    this.price     = price;
    this.favourite = favourite;
  }

  @Override
  public boolean equals(final Object obj)
  {
    if (obj instanceof Coffee)
    {
      final Coffee other = (Coffee) obj;
      return Objects.equal(name,      other.name) 
          && Objects.equal(shop,      other.shop)
          && Objects.equal(rating,    other.rating)
          && Objects.equal(price,     other.price)           
          && Objects.equal(favourite, other.favourite);                     
    }
    else
    {
      return false;
    }
  }  

  public String toString()
  {
    return Objects.toStringHelper(this)
        .add("id",        id)
        .add("name",      name)
        .add("shop",      shop)
        .add("rating",    rating)
        .add("price",     price)
        .add("favourite", favourite).toString();
  } 
}

CoffeeAPI

public class CoffeeAPI
{  
  public static Coffee getCoffee (Long id) throws Exception
  {
    String coffeeStr = Rest.get("/api/coffees/" + id);
    Coffee coffee = JsonParsers.json2Coffee(coffeeStr);
    return coffee;
  }

  public static Coffee createCoffee(String coffeeJson) throws Exception
  {
    String response = Rest.post ("/api/coffees", coffeeJson);
    return JsonParsers.json2Coffee(response);
  }

  public static Coffee createCoffee(Coffee coffee) throws Exception
  {
    return createCoffee(JsonParsers.coffee2Json(coffee));
  }

  public static List<Coffee> getCoffees () throws Exception
  {
    String response =  Rest.get("/api/coffees");
    List<Coffee> coffeeList = JsonParsers.json2Coffees(response);
    return coffeeList;
  } 
}

Rest

public class Rest
{
  private static DefaultHttpClient httpClient = null;
  private static final String URL = "http://localhost:9000";

  private static DefaultHttpClient httpClient()
  {
    if (httpClient == null)
    {
      HttpParams httpParameters = new BasicHttpParams();
      HttpConnectionParams.setConnectionTimeout(httpParameters, 2000);
      HttpConnectionParams.setSoTimeout(httpParameters, 2000);
      httpClient = new DefaultHttpClient(httpParameters);
    }
    return httpClient;
  }

  public static String get(String path) throws Exception
  {
    HttpGet getRequest = new HttpGet(URL + path);
    getRequest.setHeader("accept", "application/json");
    HttpResponse response = httpClient().execute(getRequest);
    return new BasicResponseHandler().handleResponse(response);
  }

  public static String post(String path, String json) throws Exception
  {
    HttpPost putRequest = new HttpPost(URL + path);
    putRequest.setHeader("Content-type", "application/json");
    putRequest.setHeader("accept", "application/json");

    StringEntity s = new StringEntity(json);
    s.setContentEncoding("UTF-8");
    s.setContentType("application/json");
    putRequest.setEntity(s);

    HttpResponse response = httpClient().execute(putRequest);
    return new BasicResponseHandler().handleResponse(response);
  }
}

CoffeeTest

public class CoffeeTest
{
  private Coffee mocha;
  private long   mochaId;

  @Before
  public void setup() throws Exception
  {
    Coffee returnedCoffee;

    mocha     = new Coffee ("mocha",     "costa",  4.5, 3.0, 1);
    returnedCoffee = CoffeeAPI.createCoffee(mocha);
    mochaId = returnedCoffee.id;
  }

  @After
  public void teardown()
  {
    mocha     = null; 
  }

  @Test
  public void testCreateCoffee () throws Exception
  {
    assertEquals (mocha, CoffeeAPI.getCoffee(mochaId));
  }
}

This file is shared by both projects:

JsonParsers

public class JsonParsers
{
  static Gson gson = new Gson();

  public static String user2Json(Object obj)
  {
    return gson.toJson(obj);
  }  

  public static Coffee json2Coffee(String json)
  {
    return gson.fromJson(json, Coffee.class);    
  }

  public static String coffee2Json(Object obj)
  {
    return gson.toJson(obj);
  }  

  public static List<Coffee> json2Coffees(String json)
  {
    Type collectionType = new TypeToken<List<Coffee>>() {}.getType();
    return gson.fromJson(json, collectionType);    
  }  
}

Exercises

Test Coffee create and get

Rework CoffeeTest class to be a more comprehensive test. i.e. it should create a number of coffee objects and, once created, check to see if they exist.

Delete coffee

How would we go about deleting a coffee? It might have some similarities to the getCoffee route?

Test coffee list get

Our list coffees api should still be working nicely:

How would we approach writing a test for this?

Cloudbees

See if you can publish the coffeemate-service to cloudbees. Can you then run the test to use the Cloudbees version instead of local one?

Solutions

The solutions to the above exercises will be the subject of next weeks lab.