Статьи

RESTClient Groovy с расширениями Спока

Groovy имеет расширение к классу HTTPBuilder под названием RESTClient, которое позволяет довольно легко протестировать веб-сервис RESTful. В этом посте будет описано, как использовать RESTClient , как внедрить его в тестовый класс Groovy Spock с помощью пользовательских аннотаций, как поменять внедренный клиент между реальным и поддельным с помощью Mockito и как его протестировать.

Класс RESTClient предоставляет методы для выполнения HTTP-методов GET, POST, PUT и DELETE. RESTClient экземпляр может быть построен с помощью строки URL — адреса и , возможно, типа контента. Методы HTTP можно затем вызывать с помощью списка именованных параметров. Например, приведенный ниже код создаст экземпляр RESTClient и вызовет метод get, передавая путь и параметр пути:

def client = new RESTClient("http://localhost:8080/testWebService/")
def resp = client.get(path : "server/status/ServerOne")

Возвращенный ответ является экземпляром  HttpResponseDecorator . Это оборачивает HttpResponse, чтобы обеспечить легкий доступ к данным, которые анализируются в зависимости от типа контента. Приведенный ниже класс показывает, как он используется в рамках каркасного теста Spock, и как проанализированные данные можно извлечь из объекта ответа. Возвращенный XML:

<serverstatus>
  <servername>ServerOne</servername>
  <isrunning>true</isrunning>
</serverstatus>

затем

<serverstatus>
  <servername>ServerTwo</servername>
  <isrunning>false</isrunning>
</serverstatus>
import groovy.util.slurpersupport.GPathResult
import groovyx.net.http.RESTClient
import spock.lang.*
import groovyx.net.http.ContentType

class InjectedRestServiceTest extends Specification {

 @RESTWebClient
 def client

 def "Test Server Statuses"() {

  when: "retrieve server status"
  def resp1 = client.get(path : "server/status/ServerOne")
  def resp2 = client.get(path : "server/status/ServerTwo")

  then: "test server one response"
  assert resp1.data.serverName.text() == "ServerOne"
  assert resp1.data.isRunning.text() == "true"

  then: "test server two response"
  assert resp2.data.serverName.text() == "ServerTwo"
  assert resp2.data.isRunning.text() == "false"

 }

}

Тестовые утверждения приведены исключительно в этом примере, чтобы доказать, что ответ от реального веб-сервиса такой же, как и от поддельного веб-сервиса. На самом деле вы будете тестировать некоторые другие функции, вызовом которых является вызов веб-службы RESTful. Вы бы не стали утверждать ответ от поддельного RESTClient, хотя вы можете захотеть убедиться, что он был вызван n раз.

В классе InjectedRestServiceTest RESTClient внедряется с использованием пользовательской аннотации @RESTWebClient . Есть три класса, которые включают эту функцию. Сначала интерфейс, который помечен как видимый только на уровне поля, содержит @ExtensionAnnotationуказание класса, который в основном говорит Споку, что делать, когда он встречает аннотацию @RESTClient :

import java.lang.annotation.ElementType
import java.lang.annotation.Retention
import java.lang.annotation.RetentionPolicy
import java.lang.annotation.Target
import org.spockframework.runtime.extension.ExtensionAnnotation

@Retention(RetentionPolicy.RUNTIME)
@Target([ ElementType.FIELD ])
@ExtensionAnnotation(RESTWebClientAnnotationDrivenExtension)

public @interface RESTWebClient{}

Класс RESTWebClientAnnotationDrivenExtension в этом примере должен только переопределить метод visitFieldAnnotation класса AbstractAnnotationDrivenExtension из-за типа целевого элемента аннотации:

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

class RESTWebClientAnnotationDrivenExtension extends
 AbstractAnnotationDrivenExtension<RESTWebClient> {

 private static final String url =
  System.getProperties().getProperty("url")
 private static final boolean useMock =
  Boolean.parseBoolean(System.getProperties().getProperty("useMock"))

 @Override
 void visitFieldAnnotation(RESTWebClient annotation, FieldInfo field) {
  def methodInterceptor = new RESTWebClientMethodInterceptor(field, url, useMock)
  methodInterceptor.install(field.parent)
 }

}

Вышеупомянутое расширение сообщает Споку, что когда он встречает @RESTClient , он должен создать перехватчик с аргументами, обозначающими поле, в которое должен быть установлен экземпляр RESTClient, URL-адрес веб-службы и логическое значение, чтобы указать, следует ли использовать макет. используется или реальный веб-сервис должен быть использован. (Значение двух значений можно объединить, чтобы получить только URL, и если он установлен, то должен использоваться реальный веб-сервис.)

Перехватчик создаст либо фиктивный RESTClient, либо реальный. Это будет сделано, когда поле клиента должно быть установлено в классе InjectedRestServiceTest. Если требуется настоящий RESTClient, то экземпляр, использующий URL, будет создан и установлен с использованием fieldInfo.writeValue.метод. Если макет требуется, то создается макет Mockito RESTClient, и ожидаемое поведение макетируется с использованием экземпляров класса HttpResponseDecorator (хотя это могут быть и макеты.) Опять же, это устанавливается с  помощью метода fieldInfo.writeValue :

import groovy.mock.interceptor.MockFor
import groovyx.net.http.HttpResponseDecorator
import groovyx.net.http.RESTClient
import org.spockframework.runtime.extension.AbstractMethodInterceptor;
import org.spockframework.runtime.extension.IMethodInvocation;
import org.spockframework.runtime.model.FieldInfo;
import org.spockframework.runtime.model.SpecInfo;

import static org.mockito.Mockito.*
import static org.mockito.Matchers.*

class RESTWebClientMethodInterceptor extends AbstractMethodInterceptor {

 private final FieldInfo fieldInfo
 private final String url
 private final boolean useMock

 RESTWebClientMethodInterceptor(FieldInfo fieldInfo, String url, boolean useMock) {
  this.fieldInfo = fieldInfo
  this.url = url
  this.useMock = useMock
 }

 @Override
 void interceptSetupMethod(IMethodInvocation methodInvocation) {
  setupRESTClient(methodInvocation.target)
  methodInvocation.proceed()
 }

 @Override
 void install(SpecInfo specInfo) {
  specInfo.setupMethod.addInterceptor this
 }

 private void setupRESTClient(target) {

  if (useMock) {

   def xmlServerOne =
     """
      <serverStatus>
        <serverName>ServerOne</serverName>
        <isRunning>true</isRunning>
      </serverStatus>
      """

   def xmlServerTwo =
     """
      <serverStatus>
        <serverName>ServerTwo</serverName>
        <isRunning>false</isRunning>
      </serverStatus>
      """

   def httpResponseDecoratorServerOne = new HttpResponseDecorator(
     null, new XmlParser().parseText(xmlServerOne))
   def httpResponseDecoratorServerTwo = new HttpResponseDecorator(
     null, new XmlParser().parseText(xmlServerTwo))
   def mapServerOne = [path:"server/status/ServerOne"]
   def mapServerTwo = [path:"server/status/ServerTwo"]

   RESTClient mockRESTClient = org.mockito.Mockito.mock(RESTClient)
   when(mockRESTClient.get(mapServerOne)).thenReturn(httpResponseDecoratorServerOne);
   when(mockRESTClient.get(mapServerTwo)).thenReturn(httpResponseDecoratorServerTwo);

   fieldInfo.writeValue(target, mockRESTClient)
  }
  else {
   fieldInfo.writeValue(target, new RESTClient(url))
  }
 }

}

Whilst there are different ways to mock and test RESTful web services, the above classes do show how easily RESTful web services can be called and their responses parsed, how annotations can be created and how objects can be mocked. Similar examples could be constructed for different aspects of a Spock test eg features, fixtures etc..

More can be found on extensions at http://code.google.com/p/spock/wiki/SpockBasics#Extensions and on RESTClient at http://groovy.codehaus.org/modules/http-builder/doc/rest.html