Testing Network Communication with DukeScript

Since the post on Test Driven Development in DukeScript we know that DukeScript is excellent when it comes to testing the UI behavior. However a typical DukeScript application not only shows a UI, but also talks to a server. This post is going to explain how easy is to mock such server communication and make it fully unit testable.

@OnReceive with Plain Text

The DukeScript network communication is typically done with the help of the @OnReceive annotation that is provided as part of HTML/Java API. It has been carefully designed to express intentions with as little Java code as possible. Handling an asynchronous REST call and processing its result fits in the following twelve lines:

@Model(className = "Data", properties = {
  @Property(name = "message", type = String.class),
})
final class DataModel {
    @OnReceive(url = "{protocol}://{server}/{path}")
    static void readMessage(Data model, String data) {
        model.setMessage(data);
    }
    @ModelOperation static void obtainMessage(Data model) {
        model.readMessage("https", "api.github.com", "users/dukescript/repos");
    }
}

The question, of course, is how do we test the behavior of such a method without really connecting to the server? Luckily, every API in the DukeScript ecosystem comes with an SPI which allows us to create a mock implementation and inject it in the Test:

import java.io.IOException;
import java.io.InputStream;
import net.java.html.BrwsrCtx;
import net.java.html.json.Models;
import org.netbeans.html.context.spi.Contexts;
import org.netbeans.html.json.spi.JSONCall;
import org.netbeans.html.json.spi.Transfer;
import static org.testng.Assert.*;
import org.testng.annotations.Test;

public class DataModelTest {
    @Test public void plainTextRESTCall() {
        class MockTrans implements Transfer {
            @Override
            public void extract(Object obj, String[] props, Object[] values) {
                fail("Not implemented");
            }

            @Override
            public Object toJSON(InputStream is) throws IOException {
                fail("Not implemented");
                return null;
            }

            @Override
            public void loadJSON(JSONCall call) {
                assertFalse(call.isJSONP(), "Regular JSON call, not JSONP");
                assertEquals(call.composeURL(null), "https://api.github.com/users/dukescript/repos", "The expected URL queried");
                call.notifySuccess("Mocked reply!");
            }
        }
        MockTrans mock = new MockTrans();
        
        BrwsrCtx ctx = Contexts.newBuilder()
            .register(Transfer.class, mock, 1)
            .build();
        
        Data data = Models.bind(new Data(), ctx);
        data.obtainMessage();
        assertEquals(data.getMessage(), "Mocked reply!");
    }
}

The test basically checks whether -after calling method obtainMessage- the application connects to the specified server URL and then uses the reply as a value of its own message property.

To do that check (and to avoid connecting to a real server), we register our own implementation of the SPI interface Transfer. The implementation is registered into the BrwsrCtx, and our Data instance is then bound to the context. As a result our MockTrans implementation is injected into the Data instance and used whenever the instance needs to perform a network operation.

Our MockTrans implementation handles only the loadJSON method and checks its input parameters and also verifies the correctness of the passed in URL. The mock implementation then completes the network operation by calling the notifySuccess method. Usually the call to notifySuccess is asynchronous, but for unit testing, it is more handy to make it synchronous. Like this we can check the value of data.getMessage() immediately after returning from the obtainMessage call.

Testing network communication is so easy with DukeScript!

Type-safe Access to Received JSON Data

As we discussed in our blog entry on type-safe parsing the biggest power of the DukeScript APIs is enabling type-safe access to JSON structures. Rather than accepting simple text we can define a Repo model class and let the @OnReceive method do the parsing automatically. Here is the improved example that defines an additional Repo model:

@Model(className = "Data", properties = {
  @Property(name = "message", type = String.class),
})
final class DataModel {
  @Model(className = "Repo", properties = {
    @Property(name = "name", type = String.class),
    @Property(name = "id", type = int.class)
  })
  static class RepoModel {
  }
    
  @OnReceive(url = "{protocol}://{server}/{path}")
  static void readMessage(Data model, Repo data) {
    model.setMessage(data.getName());
  }
  @ModelOperation static void obtainMessage(Data model) {
    model.readMessage("https", "api.github.com", "users/dukescript/repos");
  }
}

How can we mock such a network communication with parsing in a test? The approach is the same, but we need to also implement the extract method and make sure the value passed to notifySuccess can be understood by the extract method as well. Here is the code of our new testing method:

@Test public void jsonRESTCall() {
    class MockTrans implements Transfer {
        @Override
        public void extract(Object obj, String[] props, Object[] values) {
            assertEquals(props.length, 2, "Repo model class has two properties");
            assertEquals(values.length, 2, "Repo model class is requesting two values");
            assertEquals(props[0], "name", "Name of first one is 'name'");
            assertEquals(props[1], "id", "Name of second is 'id'");

            List<?> notifySuccessData = (List<?>)obj;
            values[0] = notifySuccessData.get(0);
            values[1] = notifySuccessData.get(1);
        }

        @Override
        public Object toJSON(InputStream is) throws IOException {
            fail("Not implemented");
            return null;
        }

        @Override
        public void loadJSON(JSONCall call) {
            assertFalse(call.isJSONP(), "Regular JSON call, not JSONP");
            assertEquals(call.composeURL(null), "https://api.github.com/users/dukescript/repos", "The expected URL queried");
            call.notifySuccess(Arrays.asList("Mocked Repo Name", 42));
        }
    }
    MockTrans mock = new MockTrans();

    BrwsrCtx ctx = Contexts.newBuilder()
        .register(Transfer.class, mock, 1)
        .build();

    Data data = Models.bind(new Data(), ctx);
    data.obtainMessage();
    assertEquals(data.getMessage(), "Mocked Repo Name");
}

We use List to hold the data passed from notifySuccess and extract it in the extract method after verifying that we are really queried for two properties in the order specified when declaring the Repo model class.

And that’s all. The rest of the test remains the same.

Dealing with JSON Arrays

Of course, if you click at the real REST URL you find out it returns an array of JSON objects. So we should also modify our code to accept a list of Repo instances:

@OnReceive(url = "{protocol}://{server}/{path}")
static void readMessage(Data model, List<Repo> data) {
    model.setMessage(data.get(0).getName());
}

What needs to be changed in the test? Almost nothing: just call the notifySuccess method with an array of objects! The internal infrastructure will recognize the array and do the necessary processing itself:

public void loadJSON(JSONCall call) {
    assertFalse(call.isJSONP(), "Regular JSON call, not JSONP");
    assertEquals(call.composeURL(null), "https://api.github.com/users/dukescript/repos", "The expected URL queried");
    call.notifySuccess(new Object[] {
        Arrays.asList("Mocked Repo Name", 42),
        Arrays.asList("Another Mocked Repo", 84)
    });
}

Done. We can now use TDD to verify the network communication of our DukeScript based application in unit tests.

Good luck creating your own portable, robust applications in Java!