What LEGO Blocks Teach Us About REST API Testing

My son loves LEGO® blocks. He is continually fascinated about the ways he can use and reuse various LEGO blocks to build different kinds of towers and other edifices.  He also likes to use the same larger LEGO blocks as a foundation for a multitude of different towers.  Why should REST API testing be any different?  Herein, I refer to two main patterns for REST API testing:

1. Classes in your REST API test infrastructure should reflect a Separation of Concerns.  Each class is like a LEGO block that contributes its own particular set of functionality.

2. Features for REST API performance testing should be tightly integrated with your test infrastructure from the beginning and utilize the same foundation upon which the functional tests are built.

This is best explained by the code below.  This code is in Groovy and represents these patterns but the patterns are applicable across languages.  One functional test method and one performance test method is shown.

/**
* Example Pattern to use to quickly create continuous Performance tests along side Functional Acceptance Tests for REST APIs
*/
package example
import static org.junit.Assert.assertEquals

import org.junit.Test

/**
* Always good to have a parent class for test plans to centralize code across all test plans
*/
abstract class AbstractTestPlan {
  protected PersonClient personClient

  AbstractTestPlan() {
    personClient = new PersonClient()
  }
}

class PerformanceTestPlan extends AbstractTestPlan {
  def maxSecondsToWaitForAllResults = 60
  def concurrencyLevel = 1000

  /**
  * Send in concurrent requests and validate only correct response code received for all responses
  */
  @Test
  void getPersonPerf() throws Exception {
    // Notice no overhead for unmarshalling response payload to reduce cpu and memory overhead
    personClient.getPerson(concurrencyLevel, HTTPResponseCodes.OK, maxSecondsToWaitForAllResults)
  }
}

class FunctionalTestPlan extends AbstractTestPlan {
  def maxSecondsToWaitForAllResults = 2
  def concurrencyLevel = 1

  /**
  * Send in a single GET Person request and validate correct response code and payload
  */
  @Test
  void getPerson() throws Exception {
    def mockExpectedPerson = new Person();
    def personReceived = personClient.getPersonAndUnmarshall(concurrencyLevel, HTTPResponseCodes.OK, maxSecondsToWaitForAllResults)
    assertEquals("Validating Person received from GET request is the one expected",mockExpectedPerson, personReceived)
  }
}

/**
* The Person domain object (DTO)
*/
class Person {
  //Imagine code for fields such as name, address etc.
}

enum HTTPResponseCodes {
  OK("200"),
  BAD_REQUEST("404"),
  //and all the other response codes used in tests too
}

/**
* Contains logic to send concurrent (ideally non-blocking) HTTP requests and receive responses. This class could call into other classes
* to generate performance statistics (average response time, average requests/second, etc.) across all the requests
* and, in turn, store these in a database for reporting and trending.
*/
abstract class RestClient {
  List doGet (long concurrencyLevel, String url, HTTPResponseCodes expectedResponseCode, long maxSecondsToWaitForAllResults) {
    //Imagine code to send http requests to specified url and return responses. Imagine a loop waiting for up to maxSecondsToWaitForAllResults for responses to finish
    String mockResponse = "some response returned from http get";
    def mockResponseCode = HTTPResponseCodes.OK
    //Do response code assert here so that we aren't duplicating response code asserts in tests -- DRY Principle
    assertEquals("Validating response code", expectedResponseCode, mockResponseCode)
    // Optionally store performance statistics in a database for trending purposes (not shown)
    return [mockResponse]
  }
}

/**
* Holds the business logic for requests to send and responses to receive. This is what is called from the test plan classes.
*/
class PersonClient extends RestClient {
  private static final String PERSON_URL = "https://get/person"

  private ListunmarshallResponse(List stringResponses) {
    //Imagine code to unmarshall string responses into List of Persons
    def mockPersonResponse = new Person()
    return [mockPersonResponse];
  }

  def List getPersonAndUnmarshall(long concurrencyLevel, HTTPResponseCodes expectedResponseCode, long maxSecondsToWaitForAllResults) {
    List stringResponses = doGet(concurrencyLevel, PERSON_URL, expectedResponseCode, maxSecondsToWaitForAllResults)
    return unmarshallResponse(stringResponses)
  }

  def List getPerson(long concurrencyLevel, HTTPResponseCodes expectedResponseCode, long maxSecondsToWaitForAllResults) {
    return doGet(concurrencyLevel, PERSON_URL, expectedResponseCode, maxSecondsToWaitForAllResults)
  }
}

Regarding pattern #1, like building blocks that fit nicely together, each class is responsible for its own “separate concern.” When we separate out the behavior as shown, we prevent ourselves from repeating code needlessly across different tests.  This leads to the tests having VERY FEW lines of code and being VERY READABLE.

Regarding pattern #2, there are many different types of performance testing but one thing this type of testing commonly necessitates is concurrent connections.  This is often overlooked when test infrastructure is built around single request-response scenarios or utilizes some off-the-shelf tool that has a proprietary language that only one or a few on a team will understand.  (As a side note here, I also strongly recommend the http client utilize non-blocking connections so that it isn’t using one thread per connection and thereby unable to scale to higher concurrency levels due to client resources skewing metrics.)  Why not use the same code (read: LEGO blocks) that was the foundation for the functional tests and apply that to the performance tests as well?  Doing so nicely applies the DRY Principle to your test infrastructure. This pattern allows for continuously running performance tests that can be created quickly and which can provide metrics that can be captured in a database for reporting and trending purposes.

Continue the conversation by sharing your comments here on the blog and by following us on Twitter @CTCT_API.

 

Comments

  1. hi
    I’m getting this error while running the junit tests

    java.lang.ExceptionInInitializerError
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:169)
    at com.yojib.core.FunctionalTestPlan.class$(AbstractTestPlan.groovy)
    at com.yojib.core.FunctionalTestPlan.$get$$class$com$yojib$core$HTTPResponseCodes(AbstractTestPlan.groovy)
    at com.yojib.core.FunctionalTestPlan.getPerson(AbstractTestPlan.groovy:46)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
    Caused by: groovy.lang.GroovyRuntimeException: Could not find matching constructor for: com.yojib.core.HTTPResponseCodes(java.lang.String, java.lang.Integer, java.lang.String)
    at groovy.lang.MetaClassImpl.selectConstructorAndTransformArguments(MetaClassImpl.java:1393)
    at org.codehaus.groovy.runtime.ScriptBytecodeAdapter.selectConstructorAndTransformArguments(ScriptBytecodeAdapter.java:234)
    at com.yojib.core.HTTPResponseCodes.$INIT(AbstractTestPlan.groovy)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.codehaus.groovy.reflection.CachedMethod.invoke(CachedMethod.java:90)
    at groovy.lang.MetaMethod.doMethodInvoke(MetaMethod.java:233)
    at org.codehaus.groovy.runtime.callsite.StaticMetaMethodSite.invoke(StaticMetaMethodSite.java:43)
    at org.codehaus.groovy.runtime.callsite.StaticMetaMethodSite.callStatic(StaticMetaMethodSite.java:99)
    at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCallStatic(CallSiteArray.java:50)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callStatic(AbstractCallSite.java:157)
    at org.codehaus.groovy.runtime.callsite.AbstractCallSite.callStatic(AbstractCallSite.java:173)
    at com.yojib.core.HTTPResponseCodes.(AbstractTestPlan.groovy)
    … 28 more

    • Jason Weden says:

      Hello,

      I would recommend trying to stand up the code in an IDE like eclipse with the groovy plugin or Intellij. That way you can see any problems with compiling before you run it. I think when you are running it you are missing the dependent jar like the junit jar or the groovy jar.

Leave a Comment