Unit Testing Network Calls
Using NanoHttpd to isolate network call testing
Setup
Gradle
if you are only using nanohttp for testing then use testCompile, otherwise compile
dependencies {
testCompile 'org.nanohttpd:nanohttpd:2.3.1'
}
Simple MockHttpServer
class MockHttpServer extends NanoHTTPD {
private Context mContext;
private Context mTestContext;
static private Object initlock= new Object();
/**
* Constructs an HTTP server on given port.
*
* @param port
*/
public MockHttpServer( Context context, int port ) {
super(port);
mContext = context;
mTestContext = null;
}
public MockHttpServer(Context context, Context testContext, int port) {
super(port);
mContext = context.getApplicationContext();
mTestContext = testContext;
}
@Override
public Response serve(IHTTPSession session) {
String route = session.getUri();
List<String> segs = Uri.parse(route).getPathSegments();
JSONObject jsonBody = new JSONObject();
switch(segs.get(0)){
case "asset":
if (segs.size()<2 ) {
String asset = segs.get(1);
try {
return newChunkedResponse(
Response.Status.OK,
"application/octet-stream",
mContext.getAssets().open(asset)
);
} catch (IOException e) {
String responseContents = null;
responseContents = "<html><body><h1>Request Error</h1>\n";
responseContents += "<p>Uri: " + session.getUri() + " </p>";
responseContents += "<p>Message: " + e.getMessage() + " </p>";
return newFixedLengthResponse(responseContents);
}
}
break;
case "echo":
String msg = segs.size()>1 ? segs.get(1) : "";
return newFixedLengthResponse(msg);
//return newFixedLengthResponse(Response.Status.OK, "plain/text", , segs.get(1).length() );
case "delay":
return super.serve(session);
case "get":
default:
}
// 404: Not Found error
return super.serve(session);
}
}
Unit test setup
public class HttpUrlConnectionNetworkTests extends AndroidTestCase {
NanoHTTPD httpServer;
Network network;
@Override
public void setUp() throws Exception {
super.setUp();
RenamingDelegatingContext context = new RenamingDelegatingContext(getContext(), "test_");
mTestContext = InstrumentationRegistry.getContext();
httpServer = new MockHttpServer(getContext(), mTestContext, 7078);
httpServer.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
}
@Override
public void tearDown() throws Exception {
httpServer.stop();
super.tearDown();
}
}
serving assets with NanoHttp
Passing in the TestContext allows you to isolate assets to the androidTest apk.
mstevens@lt-mstevens-mac2:bztsitecore$ cd src/androidTest/
mstevens@lt-mstevens-mac2:androidTest$ tree
..
|-- assets
| |-- soap_GetSiteConfigurationRequest.xml
| |-- soap_GetSiteConfigurationResponse.xml
| |-- soap_GetSiteInfoRequest.xml
| |-- soap_GetSiteInfoResponse.xml
| |-- soap_GetTabletConfigurationRequest.xml
| |-- soap_GetTabletConfigurationResponse.xml
| |-- soap_GetTabletSiteDeviceListRequest.xml
| |-- soap_GetTabletSiteDeviceListResponse.xml
| |-- w3_xml_to_json.xml
| |-- xml_to_json.xml
| |-- xml_to_json2.xml
| |-- xslt_identity.xml
| |-- xslt_outline.xml
| `-- xslt_strip_comments.xml
`-- java
`-- com
`-- buzztime
|-- net
| `-- volley
| |-- BTNetVolleyDefaultStackTests.java
| |-- BTNetVolleyTests.java
| |-- HttpUrlConnectionNetworkTests.java
| |-- MockHttpServer.java
| `-- SoapRequestTests.java
|-- site
| `-- core
| |-- ApplicationTest.java
| |-- BTSiteContentProviderTests.java
| |-- BTSiteDBHelperTests.java
| |-- BTSiteRoutesTests.java
| `-- services
| `-- BtStartupServiceTests.java
`-- test
|-- Repeat.java
`-- RepeatRule.java
Simple synchronous network call
public void testSimpleEcho() throws Exception {
URL url = new URL("http://localhost:7078/echo/hello_world");
String responseContents = "";
HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
try {
InputStream in = new BufferedInputStream(urlConnection.getInputStream());
java.util.Scanner s = new java.util.Scanner(in).useDelimiter("\\A");
if (s.hasNext()) {
responseContents = s.next();
}
} finally {
urlConnection.disconnect();
}
assertEquals("hello_world", responseContents );
}
Using a Semaphore to test network responses
Semaphore is an easy way to deal with async calls. A semaphore tracks a permit count, release( ) increments the permit count, aquire( ) decrements the permit if it was non zero.
The pattern is to initialize the Semaphore with no permits. after the async call.. wait with tryAcquire() for a timeout period When the listener is called, it releases a permit and tryAcquire() will return.
public void testStringRequest() throws Exception {
final Semaphore semaphore = new Semaphore(0);
URL url = new URL("http://localhost:7078/echo/hello_world");
StringRequest stringRequest = new StringRequest(Request.Method.GET, url.toString(),
new Response.Listener<String>() {
@Override
public void onResponse(String response) {
assertNotNull(response);
assertEquals("hello_world", response);
semaphore.release();
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
}
});
stringRequest.setShouldCache(false);
BTNetVolley.with(getContext())
.getRequestQueue()
.add(stringRequest);
assertTrue(semaphore.tryAcquire(10000, TimeUnit.MILLISECONDS));
}
Using Future<> to test network requests
Volley supports getting a Future<> object which can be used to inline a network for unit testing
public void testFutureStringRequest() throws Exception {
URL url = new URL("http://localhost:7078/echo/hello_world");
final RequestFuture<String> futureRequest = RequestFuture.newFuture();
StringRequest stringRequest = new StringRequest(Request.Method.GET, url.toString(),futureRequest, futureRequest);
stringRequest.setShouldCache(false);
BTNetVolley.with(getContext())
.getRequestQueue()
.add(stringRequest);
String result = futureRequest.get(5, TimeUnit.SECONDS);
assertNotNull(result);
assertEquals("hello_world", result);
}
Separate Request testing from Response testing
public void testSoapRequest() throws Exception {
GetTabletConfigurationRequest gtcr = new GetTabletConfigurationRequest(
"{340A98F5-F71E-4625-81BC-0A38371213A7}",
"0000211600069",//serialNumber
"5ab6b64123925dc", //deviceSerialNumber
"5ab6b64123925dc", //deviceIdentifier
"1.0.1.1036 release-keys", //fw disp
"Buzztime", // fw manu
"BZT-T101", // fw model
"5.1.1", // fw os
22, //fw sdk
"1.40.00", // sw version
3560206336l, // data free
6199967744l // data total
);
assertEquals("http://services.buzztime.com/2011/01/ISiteService/GetTabletConfiguration", gtcr.getAction());
assertEquals("urn:uuid:a6058e8e-279a-4313-a210-58f8deb3d015", gtcr.getMessageId());
Uri uri = Uri.parse("https://services.dev.buzztime.com/Operations/Constituent/Site/SiteService/SiteService.svc");
SoapRequest soapRequest = new SoapRequest<>(
uri.toString(),
gtcr, GetTabletConfigurationResponse.class, null, null );
InputStream expectedXmlIn = new BufferedInputStream( mTestContext.getAssets()
.open("soap_GetTabletConfigurationRequest.xml") );
String expectedXmlOutline = getXmlTransformed( expectedXmlIn, "xslt_outline.xml");
String actualXml = new String(soapRequest.getBody());//.replaceAll("\'","\"");
String actualXmlOutline = getXmlTransformed(actualXml, "xslt_outline.xml");
System.out.println("ExpectedXML:");
System.out.println(expectedXmlOutline);
System.out.println("SerializedXML:");
System.out.println(actualXmlOutline);
assertEquals(expectedXmlOutline, actualXmlOutline);
}
public void testSoapResponse() throws Exception {
GetTabletConfigurationResponse response = new GetTabletConfigurationResponse();
InputStream expectedXmlIn = new BufferedInputStream( mTestContext.getAssets()
.open("soap_GetTabletConfigurationResponse.xml") );
XmlPullParser xpp = Xml.newPullParser();
xpp.setInput(new InputStreamReader( expectedXmlIn ));
response.deserialize(xpp, response.getClass(), new SoapEnvelopeFilter() );
assertNotNull(response.mTabletConfiguration);
TabletConfiguration tc = response.mTabletConfiguration;
System.out.println("TabletConfiguration:"+tc);
assertEquals("GMTOffset", "-0800", tc.mGMTOffset);
assertEquals("RouterSSID", "Buzztime161594909190", tc.mRouterSSID);
assertEquals("SiteChain", "NONE", tc.mSiteChain);
assertEquals("SiteLocation.City", "CARLSBAD", tc.mSiteLocation.mAddress.getLocality());
assertEquals("SiteLocation.Country", "US", tc.mSiteLocation.mAddress.getCountryName());
assertEquals("SiteLocation.State", "CA", tc.mSiteLocation.mAddress.getAdminArea());
assertEquals("SiteLocation.Address1", "5966 La Place Court, Suite 100", tc.mSiteLocation.mAddress.getAddressLine(0));
assertEquals("SiteLocation.Address2", "", tc.mSiteLocation.mAddress.getAddressLine(1));
assertEquals("SiteLocation.ZipCode", "92008", tc.mSiteLocation.mAddress.getPostalCode());
assertEquals("SiteLocation.Id", "1097", tc.mSiteLocation.mSiteId);
assertEquals("SiteLocation.Name", "NTN BUZZTIME 1097", tc.mSiteLocation.mSiteName);
assertEquals("SitePhone", "(123) 438-7400", tc.mSitePhone);
assertTrue(tc.mProperties.size() > 0);
assertEquals("SitePCIPAddress", "172.23.59.193", tc.mSitePCIPAddress);
assertEquals("SpreadPort", "4803", tc.mSpreadPort);
assertEquals("StoreNumber", "-1", tc.mStoreNumber);
assertEquals("NcrAlohaOnlineSiteId", "-1", tc.mNcrAlohaOnlineSiteId);
assertEquals("NcrCloudConnectCompanyCode", "", tc.mNcrCloudConnectCompanyCode);
assertEquals("PointOfSaleProvider", "EmptyString", tc.mPointOfSaleProvider);
}