Sunday, May 29, 2011

Testing client-server communication with Java Scripting API

I would like to share my best practice for client- and server-side testing with Java Scripting API introduced in Java SE 6. As example I want to test simultaneous JSON serialization / deserialization on both sides. I'm going to use json2 from Douglas Crockford on client-side and Gson on server-side. I want to utilize the class Circle from my previous post and write JUnit tests for its serialization / deserialization. At first we need to implement the same interface by Java and JavaScript. It's convenient to implement an Java interface by script functions or methods. By using interfaces we can avoid having to use the javax.script API in many places.

JsonProvide.java
public interface JsonProvider
{
    public Object fromJson(String json);

    public String toJson(Object object);
}
JsonProvider is an interface which is used later to access correspondent JavaScript methods.

jsonTest.js
var jsonProvider = new Object();

// produces an JavaScript object or array from an JSON text.
jsonProvider.fromJson = function(json) {
    var obj = JSON.parse(json);
    return makeTestable(obj);
};

// produces an JSON text from an JavaScript object or array
jsonProvider.toJson = function(object) {
    var obj = makeTestable(object);
    return JSON.stringify(obj);
};

function makeTestable(obj) {
    obj.getValue = function(property) {
        return this[property];
    };

    return obj;
}

// Test object
var circle = {
    uuid: "567e6162-3b6f-4ae2-a171-2470b63dff00",
    x: 10,
    y: 20,
    movedToFront: true,
    rotationDegree: 90,
    radius: 50,
    backgroundColor: "#FF0000",
    borderColor: "#DDDDDD",
    borderWidth: 1,
    borderStyle: "-",
    backgroundOpacity: 1.0,
    borderOpacity: 0.5,
    scaleFactor: 1.2
};
There are two methods fromJson / toJson and a helper function makeTestable in order to get any value of JavaScript objects from Java. The test object in JavaScript is called circle. The corresponding Java class is called Circle and has the same fields with getter / setter. We can write an JUnit test now.
import com.google.gson.Gson;
import com.googlecode.whiteboard.model.Circle;
import org.apache.commons.beanutils.PropertyUtils;
import org.junit.AfterClass;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import java.io.FileNotFoundException;
import java.util.Map;
import java.util.Set;
import java.util.UUID;

public class JsonTest
{
    private static Gson gson;
    private static ScriptEngine engine;
    private static JsonProvider jsonProvider;

    @BeforeClass
    public static void runBeforeClass() {
        // create Gson
        GsonBuilder gsonBilder = new GsonBuilder();
        gson = gsonBilder.serializeNulls().create();

        // create a script engine manager
        ScriptEngineManager factory = new ScriptEngineManager();
        // create JavaScript engine
        engine = factory.getEngineByName("JavaScript");

        try {
            // evaluate JavaScript code from the json2 library and the test file
            engine.eval(new java.io.FileReader("src/main/webapp/resources/js/json2-min.js"));
            engine.eval(new java.io.FileReader("src/test/resources/js/jsonTest.js"));

            // get an implementation instance of the interface JsonProvider from the JavaScript engine,
            // all interface's methods are implemented by script methods of JavaScript object jsonProvider
            Invocable inv = (Invocable) engine;
            jsonProvider = inv.getInterface(engine.get("jsonProvider"), JsonProvider.class);
        } catch (ScriptException e) {
            e.printStackTrace();
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
    }

    @AfterClass
    public static void runAfterClass() {
        gson = null;
        jsonProvider = null;
    }

    @Test
    public void JavaScript2Java() {
        // get JavaScript object
        Object circle1 = engine.get("circle");

        // client-side: make JSON text from JavaScript object
        String json = jsonProvider.toJson(circle1);

        // server-side: convert JSON text to Java object
        Circle circle2 = gson.fromJson(json, Circle.class);

        // compare two objects
        testEquivalence(circle2, circle1);
    }

    @Test
    public void Java2JavaScript() {
        // create Java object
        Circle circle1 = new Circle();
        circle1.setUuid(UUID.randomUUID().toString());
        circle1.setX(100);
        circle1.setY(100);
        circle1.setMovedToFront(false);
        circle1.setRotationDegree(0);
        circle1.setRadius(250);
        circle1.setBackgroundColor("#FFFFFF");
        circle1.setBorderColor("#000000");
        circle1.setBorderWidth(3);
        circle1.setBorderStyle(".");
        circle1.setBackgroundOpacity(0.2);
        circle1.setBorderOpacity(0.8);
        circle1.setScaleFactor(1.0);

        // server-side: convert Java object to JSON text
        String json = gson.toJson(circle1);

        // client-side: make JavaScript object from JSON text
        Object circle2 = jsonProvider.fromJson(json);

        // compare two objects
        testEquivalence(circle1, circle2);
    }

    @SuppressWarnings("unchecked")
    private void testEquivalence(Object obj1, Object obj2) {
        try {
            Map<String, Object> map = PropertyUtils.describe(obj1);
            Set<String> fields = map.keySet();
            Invocable inv = (Invocable) engine;

            for (String key : fields) {
                Object value1 = map.get(key);
                if (!key.equals("class")) {
                    Object value2 = inv.invokeMethod(obj2, "getValue", key);
                    if (value1 instanceof Number && !(value1 instanceof Double)) {
                        // JS number is always converted to Java double ==> only doubles can be compared,
                        // see http://www.mozilla.org/js/liveconnect/lc3_method_overloading.html
                        value1 = new Double(value1.toString());
                    }

                    Assert.assertEquals("Value of property '" + key + "' was wrong converted", value2, value1);
                }
            }
        } catch (Exception e) {
            throw new IllegalStateException("Equivalence test of two objects failed!", e);
        }
    }
}
I create a Gson and a ScriptEngine instances in the method runBeforeClass() and load all needed scripts into the ScriptEngine. After that I get an implementation instance of the interface JsonProvider from the JavaScript engine. Now I'm able to call JavaScript methods from my JsonProvider implementation. There are two tests:

@Test public void JavaScript2Java()

I test here the use case if an JavaScript object (circle) gets converted to an JSON text, sent to the server and converted there to an Java object (Circle). The original JavaScript object and the result Java object are compared afterwards.

@Test public void Java2JavaScript()

I test here the use case if a created Java object (Circle) gets converted to an JSON text, sent to the client and converted there to an JavaScript object (circle). The objects are compared to ensure their equivalence.

You can also use "JavaScript to Java Communication" with Java Scripting API and access Java classes, objects and methods from JavaScript. Pretty cool.

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.