DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports Events Over 2 million developers have joined DZone. Join Today! Thanks for visiting DZone today,
Edit Profile Manage Email Subscriptions Moderation Admin Console How to Post to DZone Article Submission Guidelines
View Profile
Sign Out
Refcards
Trend Reports
Events
Zones
Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones AWS Cloud
by AWS Developer Relations
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Partner Zones
AWS Cloud
by AWS Developer Relations
The Latest "Software Integration: The Intersection of APIs, Microservices, and Cloud-Based Systems" Trend Report
Get the report
  1. DZone
  2. Coding
  3. Languages
  4. Developing and Testing GWT Client Services

Developing and Testing GWT Client Services

Matt Raible user avatar by
Matt Raible
·
Oct. 22, 09 · Interview
Like (0)
Save
Tweet
Share
8.07K Views

Join the DZone community and get the full member experience.

Join For Free
Earlier this week, Hiram Chirino released RestyGWT, a GWT generator for REST services and JSON encoded data transfer objects. You can read more about it in Hiram's post RestyGWT, a Better GWT RPC??. First of all, I'm impressed with RestyGWT because provides something I've always wanted with GWT: the ability to call RESTful services and get a populated POJO in your callback, much like AsyncCallback provides for RPC services.

RestyGWT also allows you to easily create services using only interfaces and JAX-RS annotations. For example:

import javax.ws.rs.POST;
...
public interface PizzaService extends RestService {
@POST
public void order(PizzaOrder request, MethodCallback<OrderConfirmation> callback);
}

After taking a brief look at RestyGWT, I thought it'd be interesting to share how I develop and test GWT client services.

Developing GWT Client Services
Writing services in a GWT application can be helpful when you're using MVP, especially since you can EasyMock them in a test. On my GWT projects, I've often used overlay types because they allow me to write less code and they make parsing JSON super simple. I've had issues testing my presenters when using overlay types. The good news is I think I've figured out a reasonable solution, but it does require using GWTTestCase. If RestyGWT supported overlay types, there's a good chance I'd use it, especially since its integration tests seem to require GWTTestCase too.

Rather than using callbacks in my presenters, I try to only use them in my service implementations. That way, my presenters don't have to worry about overlay types and can be tested in a JUnit-only fashion. The callbacks in my services handle JSON parsing/object population and fire events with the populated objects.

GWT's RequestBuilder is one option for communicating with RESTful services. The Development Guide for HTTP Requests explains how to use this class. To simplify REST requests and allow multiple callbacks, I'm using a RestRequest class, and a number of other utility classes that make up a small GWT REST framework (created by a former colleague). RestRequest wraps RequestBuilder and provides a Fluent API for executing HTTP requests. Another class, Deferred, is a GWT implementation of Twisted's Deferred.

As part of my service implementation, I inject an EventBus (with GIN) into the constructor and then proceed to implement callbacks that fire Events to indicate loading, saving and deleting has succeeded. Here's an example service:

public class ConversationServiceImpl implements ConversationService {
private EventBus eventBus;

@Inject
public ConversationServiceImpl(EventBus eventBus) {
this.eventBus = eventBus;
}

public void getConversation(String name) {
Deferred<Representation> d =
RestRequest.get(URLs.CONVERSATION + "/" + URL.encode(name)).build();

d.addCallback(new Callback<Representation>() {
public void onSuccess(Representation result) {
Conversation conversation = convertResultToConversation(result);
eventBus.fireEvent(new ResourceLoadedEvent<Conversation>(conversation));
}
});

d.run();
}

public void saveConversation(Conversation conversation) {
Deferred<Representation> d = RestRequest.post(URLs.CONVERSATION)
.setRequestData(conversation.toJson()).build();

d.addCallback(new Callback<Representation>() {
public void onSuccess(Representation result) {
Conversation conversation = convertResultToConversation(result);
eventBus.fireEvent(new ResourceSavedEvent<Conversation>(conversation));
}
});

d.run();
}

public void deleteConversation(Long id) {
Deferred<Representation> d =
RestRequest.post(URLs.CONVERSATION + "/" + id).build();

d.addCallback(new Callback<Representation>() {
public void onSuccess(Representation result) {
eventBus.fireEvent(new ResourceDeletedEvent());
}
});

d.run();
}

/**
* Convenience method to populate object in one location
*
* @param result the result of a resource request.
* @return the populated object.
*/
private Conversation convertResultToConversation(Representation result) {
JSOModel model = JSOModel.fromJson(result.getData());
return new Conversation(model);
}
}

In the saveConversation() method you'll notice the conversation.toJson() method call. This method uses a JSON class that loops through an objects properties and constructs a JSON String.

public JSON toJson() {
return new JSON(getMap());
}

Testing Services
In my experience, the hardest part about using overlay types is writing your objects so they get populated correctly. I've found that writing tests which read JSON from a file can be a great productivity boost. However, because of overlay types, you have to write a test that extends GWTTestCase. When using GWTTestCase, you can't simply read from the filesystem. The good news is there is a workaround where you can subclass GWTShellServlet and overwrite GWT's web.xml to have your own servlet that can read from the filesystem. A detailed explanation of how to do this was written by Alex Moffat in Implementing a -noserver flag for GWTTestCase.

Once this class is in place, I've found you can easily write services using TDD and the server doesn't even have to exist. When constructing services, I've found the following workflow to be the most productive:

  1. Create a file with the expected JSON in src/test/resources/resource.json where resource matches the last part of the URL for your service.
  2. Create a *ServiceGwtTest.java and write tests.
  3. Run tests to make sure they fail.
  4. Implement the service and run tests to ensure JSON is getting consumed/produced properly to/from model objects.

Below is the code for my JsonReaderServlet.java:

public class JsonReaderServlet extends GWTShellServlet {

public void service(ServletRequest servletRequest, ServletResponse servletResponse)
throws ServletException, IOException {

HttpServletRequest req = (HttpServletRequest) servletRequest;
HttpServletResponse resp = (HttpServletResponse) servletResponse;

String uri = req.getRequestURI();
if (req.getQueryString() != null) {
uri += "?" + req.getQueryString();
}

if (uri.contains("/services")) {
String method = req.getMethod();
String output;

if (method.equalsIgnoreCase("get")) {
// use the part after the last slash as the filename
String filename = uri.substring(uri.lastIndexOf("/") + 1, uri.length()) + ".json";
System.out.println("loading: " + filename);
String json = readFileAsString("/" + filename);
System.out.println("loaded json: " + json);
output = json;
} else {
// for posts, return the same body content
output = getBody(req);
}

PrintWriter out = resp.getWriter();
out.write(output);
out.close();

resp.setStatus(HttpServletResponse.SC_OK);
} else {
super.service(servletRequest, servletResponse);
}
}

private String readFileAsString(String filePath) throws IOException {
filePath = getClass().getResource(filePath).getFile();
BufferedReader reader = new BufferedReader(new FileReader(filePath));
return getStringFromReader(reader);
}

private String getBody(ServletRequest request) throws IOException {
BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
return getStringFromReader(reader);
}

private String getStringFromReader(Reader reader) throws IOException {
StringBuilder sb = new StringBuilder();
char[] buf = new char[1024];
int numRead;
while ((numRead = reader.read(buf)) != -1) {
sb.append(buf, 0, numRead);
}
reader.close();
return sb.toString();
}
}

This servlet is mapped to <url-pattern>/*</url-pattern> in a web.xml file in src/test/resources/com/google/gwt/dev/etc/tomcat/webapps/ROOT/WEB-INF.

My Service Test starts by getting an EventBus from GIN and registering itself to handle the fired events.

public class ConversationServiceGwtTest extends AbstractGwtTestCase
implements ResourceLoadedEvent.Handler, ResourceSavedEvent.Handler, ResourceDeletedEvent.Handler {
ConversationService service;
ResourceLoadedEvent<Conversation> loadedEvent;
ResourceSavedEvent<Conversation> savedEvent;
ResourceDeletedEvent deletedEvent;

@Override
public void gwtSetUp() throws Exception {
super.gwtSetUp();
DesigntimeGinjector injector = GWT.create(MyGinjector.class);
EventBus eventBus = injector.getEventBus();
service = new ConversationServiceImpl(eventBus);
eventBus.addHandler(ResourceLoadedEvent.TYPE, this);
eventBus.addHandler(ResourceSavedEvent.TYPE, this);
eventBus.addHandler(ResourceDeletedEvent.TYPE, this);
}

@SuppressWarnings("unchecked")
public void onLoad(ResourceLoadedEvent event) {
this.loadedEvent = event;
}

@SuppressWarnings("unchecked")
public void onSave(ResourceSavedEvent event) {
this.savedEvent = event;
}

public void onDelete(ResourceDeletedEvent event) {
this.deletedEvent = event;
}
}

After this groundwork has been done, a test can be written that loads up the JSON file and verifies the objects are populated correctly.

public void testGetConversation() {

service.getConversation("test-conversation");

Timer t = new Timer() {
public void run() {
assertNotNull("ResourceLoadedEvent not received", loadedEvent);
Conversation conversation = loadedEvent.getResource();
assertEquals("Conversation name is incorrect","Test Conversation", conversation.getName());

assertNotNull("Conversation has no channel", conversation.getChannel());
assertEquals("Conversation has incorrect task size", 3, conversation.getTasks().size());

convertToAndFromJson(conversation);
finishTest();
}
};

delayTestFinish(3000);
t.schedule(100);
}

private void convertToAndFromJson(Conversation fromJsonModel) {
Representation json = fromJsonModel.toJson();
assertNotNull("Cannot convert empty JSON", json.getData());

// change back into model
JSOModel data = JSOModel.fromJson(json.getData());
Conversation toJsonModel = new Conversation(data);
verifyModelBuiltCorrectly(toJsonModel);
}

private void verifyModelBuiltCorrectly(Conversation model) {
assertEquals("Conversation name is incorrect", "Test Conversation", model.getString("name"));
assertEquals("Conversation has incorrect task size", 3, model.getTasks().size());
assertEquals("Conversation channel is incorrect", "Web", model.getChannel().getString("type"));
}

Summary
This article has shown you how I develop and test GWT Client Services. If RestyGWT supported overlay types, there's a good chance I could change my service implementation to use it and I wouldn't have to change my test. Robert Cooper, author of GWT in Practice, claims he has a framework that does this. Here's to hoping this article stimulates the GWT ecosystem and we get a GWT REST framework that's as easy to use as GWT RPC.

Update: Today I enhanced this code to use Generics-based classes (inspired by Don't repeat the DAO!) for the boiler-plate CRUD code in a service. In a nutshell, a service interface can now be written as:

public interface FooService extends GenericService<Foo, String> {

}

The implementation class is responsible for the URL and converting the JSON result to an object:

public class FooServiceImpl extends GenericServiceImpl<Foo, String> implements FooService {

public FooServiceImpl(EventBus eventBus) {
super(eventBus, "/services/foo");
}

@Override
protected Foo convertResultToModel(Representation result) {
return new Foo(JSOModel.fromJson(result.getData()));
}
}

I'm sure this can be further enhanced to get rid of the need to create classes altogether, possibly leveraging GIN or some sort of factory. The parent classes referenced in this code can be viewed at the following URLs:

  • GenericService.java
  • GenericServiceImpl.java

There's also a GenericServiceGwtTest.java that proves it all works as expected.

From http://raibledesigns.com/rd/entry/developing_and_testing_gwt_client

Testing JSON

Opinions expressed by DZone contributors are their own.

Popular on DZone

  • DZone's Article Submission Guidelines
  • How to Submit a Post to DZone
  • The 5 Books You Absolutely Must Read as an Engineering Manager
  • How To Handle Secrets in Docker

Comments

Partner Resources

X

ABOUT US

  • About DZone
  • Send feedback
  • Careers
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 600 Park Offices Drive
  • Suite 300
  • Durham, NC 27709
  • support@dzone.com
  • +1 (919) 678-0300

Let's be friends: