Spock and testing RESTful API services
Join the DZone community and get the full member experience.
Join For FreeOur first RESTful API Test
package com.wolfware.integration
import groovyx.net.http.RESTClient
import spock.lang.*
import spock.lang.Specification
import com.movideo.spock.extension.APIVersion
import com.movideo.spock.extension.EnvironmentEndPoint
@APIVersion(minimimApiVersion="1.0.0.0")
class GetAuthenticationToken extends Specification {
@EnvironmentEndPoint
protected def environmentHost
def "Get authentication token XML from API for valid account"() {
given: "a valid account"
def authenticationTokenRequestParams = ['key':"AAABBBCCC123", 'user':"myauthemail@bla.com"]
and: "a client to get the authentication token XML"
def client = new RESTClient(environmentHost)
when: "we attempt to retrieve authentication token XML"
def resp = client.get(path : "/authenticate", query : authenticationTokenRequestParams)
then: "we should get a valid authentication token XML response"
assert resp.data.token.isEmpty() == false
// lots more asserts
}
}
As you can see, apart from the @APIVersion and @EnvironmentEndPoint annotations (these are Spock extensions as explained later), the spec is a fairly simple Spock test.
- The url parameters required to get a authentication token from the RESTful service
- using the Groovy RestClient to call the RESTful service for the authentication token details
- We can assert all the details of the response.
Trying to test any environment RESTful service
- Hard coded environment details and the code branched for each environment making it near impossible to keep code in sync as merge hell becomes the norm.
- Config files that define the environment are used to define environment details, again checked into each branch for each environment.
Spock Extensions
The @EnvironmentEndPoint Extension
package com.movideo.runtime.extension.custom
import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.spockframework.runtime.extension.AbstractAnnotationDrivenExtension
import org.spockframework.runtime.extension.AbstractMethodInterceptor
import org.spockframework.runtime.extension.IMethodInvocation
import org.spockframework.runtime.model.FieldInfo
import org.spockframework.runtime.model.SpecInfo
/**
* Spock Environment Annotation Extension
*/
class EnvironmentEndPointExtension extends AbstractAnnotationDrivenExtension<EnvironmentEndPoint> {
private static final Log LOG = LogFactory.getLog(getClass());
private static def config = new ConfigSlurper().parse(new File('src/test/resources/SpockConfig.groovy').toURL())
/**
* env environment variable
* <p>
* Defaults to {@code LOCAL_END_POINT}
*/
private static final String envString = System.getProperties().getProperty("env", config.envHost);
static {
LOG.info("Environment End Point [" + envString + "]")
}
/**
* {@inheritDoc}
*/
@Override
void visitFieldAnnotation(EnvironmentEndPoint annotation, FieldInfo field) {
def interceptor = new EnvironmentInterceptor(field, envString)
interceptor.install(field.parent.getTopSpec())
}
}
/**
*
* Environment Intercepter
*
*/
class EnvironmentInterceptor extends AbstractMethodInterceptor {
private final FieldInfo field
private final String envString
EnvironmentInterceptor(FieldInfo field, String envString) {
this.field = field
this.envString = envString
}
private void injectEnvironmentHost(target) {
field.writeValue(target, envString)
}
@Override
void interceptSetupMethod(IMethodInvocation invocation) {
injectEnvironmentHost(invocation.target)
invocation.proceed()
}
@Override
void install(SpecInfo spec) {
spec.setupMethod.addInterceptor this
}
}
- config: is a ConfigSlurper that parses a config file 'SpockConfig.groovy' that is used to define the default environment host (envHost)
- envString: gets the value of 'env' from all System Properties (these include run-time properties) and defaults to config.envHost
A note on Gradle builds.
/*
* Required to pass all system properties to Test tasks.
* Not default for Gradle to pass system properties through to forked processes.
*/
tasks.withType(Test) {
def config = new ConfigSlurper().parse(new File('src/test/resources/SpockConfig.groovy').toURL())
systemProperty 'env', System.getProperty('env', config.envHost)
}
The @APIVersion Extension
package com.movideo.runtime.extension.custom
import groovyx.net.http.RESTClient
import java.lang.annotation.Annotation
import java.util.regex.Pattern
import org.apache.commons.logging.Log
import org.apache.commons.logging.LogFactory
import org.spockframework.runtime.extension.AbstractAnnotationDrivenExtension
import org.spockframework.runtime.model.FeatureInfo
import org.spockframework.runtime.model.SpecInfo
/**
* API Version Extension
*
*/
class APIVersionExtension extends AbstractAnnotationDrivenExtension<APIVersion> {
/**
* Logger
*/
private static final Log LOG = LogFactory.getLog(getClass());
/**
*
*/
private static def config = new ConfigSlurper().parse(new File('src/test/resources/SpockConfig.groovy').toURL())
/**
* env environment variable
* <p>
* Defaults to {@code LOCAL_END_POINT}
*/
private static final String envString = System.getProperties().getProperty("env", config.envHost);
/**
* Version REGX pattern
*/
private static final def VERSION_PATTERN = Pattern.compile(".", Pattern.LITERAL);
/**
* Max version length
*/
private static final def MAX_VERSION_LENGTH = 4;
/**
* Current API Version
*/
private static final def CURRENT_API_VERSION = getDeployedAPIVersion();
/**
* {@inheritDoc}
*/
@Override
void visitFeatureAnnotation(APIVersion annotation, FeatureInfo feature) {
if(!isApiVersionGreaterThanMinApiVersion(annotation, feature.name)) {
feature.setSkipped(true)
}
}
/**
* {@inheritDoc}
*/
@Override
public void visitSpecAnnotation(APIVersion annotation, SpecInfo spec) {
if(!isApiVersionGreaterThanMinApiVersion(annotation, spec.name)) {
spec.setSkipped(true)
}
}
/**
* Get the current deployed API version
* <p>
* Performs a HTTP request to the current deployed API version. Parses the returned data and get the {@code version} node data.
* @return current deployed API version
*/
private static String getDeployedAPIVersion() {
def apiVersion = null
try {
def client = new RESTClient(envString)
def resp = client.get(path : config.versionServiceUri)
apiVersion = resp.data.version
LOG.info("Current deployed API version [" + apiVersion + "]");
} catch (ex) {
APIVersionError apiVersionError = new APIVersionError("Error occurred attempting to get current deployed API version from %s", envString + config.versionServiceUri);
apiVersionError.setStackTrace(ex.stackTrace);
throw apiVersionError;
}
return apiVersion
}
* @param annotation
* @param infoName
* @return
*/
private boolean isApiVersionGreaterThanMinApiVersion(APIVersion annotation, String infoName) {
def isApiVersionGreaterThanMinApiVersion = true
def minApiVersionRequired = annotation.minimimApiVersion();
// normalise both version id's
def apiVersionNormalised = normaliseVersion(CURRENT_API_VERSION);
def minApiVersionRequiredNormalised = normaliseVersion(minApiVersionRequired);
// compare version id's
int cmp = apiVersionNormalised.compareTo(minApiVersionRequiredNormalised);
// if the comparison is less than 0, min API version is greater than the deployed API version
if(cmp < 0) {
LOG.info("min api version [" + minApiVersionRequired + "] greater than api version [" + CURRENT_API_VERSION + "], skipping [" + infoName + "]")
isApiVersionGreaterThanMinApiVersion = false
}
return isApiVersionGreaterThanMinApiVersion
}
* @param version
* @return
*/
private String normaliseVersion(String version) {
String[] split = VERSION_PATTERN.split(version);
StringBuilder sb = new StringBuilder();
for (String s : split) {
sb.append(String.format("%" + MAX_VERSION_LENGTH + 's', s));
}
return sb.toString();
}
}
<ServiceInformation> <serviceName>Media API</serviceName> <version>1.51.1</version> </ServiceInformation>
@APIVersion(minimimApiVersion="1.0.0.0")
The SpockConfig.groovy file
versionServiceUri="/public/serviceInformation" envHost="http://api.preview.movideo.com"
- The 'versionServiceUri' is required for @APIVersion extension as the URI for the RESTful API version
- The 'envHost' is required for both @APIVersion and @EnvironmentEndPoint extensions as the host of the RESTful API
Go and start testing
References and really helpful links
Opinions expressed by DZone contributors are their own.
Comments