毫无疑问, JAX-RS是一项杰出的技术。 即将发布的规范JAX-RS 2.0带来了更多的强大功能,尤其是在客户端API方面。 今天的帖子的主题是JAX-RS服务的集成测试。 有很多出色的测试框架,例如REST可以确保提供帮助,但是我要展示的方式是使用富有表现力的BDD风格。 这是我的意思的示例:
Create new person with email <a@b.com>Given REST client for application deployed at http://localhost:8080When I do POST to rest/api/people?email=a@b.com&firstName=Tommy&lastName=KnockerThen I expect HTTP code 201
看起来像现代BDD框架的典型Given / When / Then风格。 使用静态编译语言,我们在JVM上能达到多近的距离? 事实证明,这要归功于良好的specs2测试工具。
值得一提的是, specs2是一个Scala框架。 尽管我们将编写一些Scala ,但是我们将以非常有经验的Java开发人员熟悉的非常直观的方式来完成它。 测试中的JAX-RS服务是我们在上一篇文章中开发的 。 这里是:
package com.example.rs;import java.util.Collection;import javax.inject.Inject;
import javax.ws.rs.DELETE;
import javax.ws.rs.DefaultValue;
import javax.ws.rs.FormParam;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriInfo;import com.example.model.Person;
import com.example.services.PeopleService;@Path( '/people' )
public class PeopleRestService {@Inject private PeopleService peopleService;@Produces( { MediaType.APPLICATION_JSON } )@GETpublic Collection< Person > getPeople( @QueryParam( 'page') @DefaultValue( '1' ) final int page ) {return peopleService.getPeople( page, 5 );}@Produces( { MediaType.APPLICATION_JSON } )@Path( '/{email}' )@GETpublic Person getPeople( @PathParam( 'email' ) final String email ) {return peopleService.getByEmail( email );}@Produces( { MediaType.APPLICATION_JSON } )@POSTpublic Response addPerson( @Context final UriInfo uriInfo,@FormParam( 'email' ) final String email, @FormParam( 'firstName' ) final String firstName, @FormParam( 'lastName' ) final String lastName ) {peopleService.addPerson( email, firstName, lastName );return Response.created( uriInfo.getRequestUriBuilder().path( email ).build() ).build();}@Produces( { MediaType.APPLICATION_JSON } )@Path( '/{email}' )@PUTpublic Person updatePerson( @PathParam( 'email' ) final String email, @FormParam( 'firstName' ) final String firstName, @FormParam( 'lastName' ) final String lastName ) {final Person person = peopleService.getByEmail( email ); if( firstName != null ) {person.setFirstName( firstName );}if( lastName != null ) {person.setLastName( lastName );}return person; }@Path( '/{email}' )@DELETEpublic Response deletePerson( @PathParam( 'email' ) final String email ) {peopleService.removePerson( email );return Response.ok().build();}
}
非常简单的JAX-RS服务来管理人员。 Java实现提供并支持所有基本的HTTP动词 : GET , PUT , POST和DELETE 。 为了完整起见,让我也包括服务层的一些方法,因为这些方法引起我们关注的一些例外。
package com.example.services;import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;import org.springframework.stereotype.Service;import com.example.exceptions.PersonAlreadyExistsException;
import com.example.exceptions.PersonNotFoundException;
import com.example.model.Person;@Service
public class PeopleService {private final ConcurrentMap< String, Person > persons = new ConcurrentHashMap< String, Person >(); // ...public Person getByEmail( final String email ) {final Person person = persons.get( email ); if( person == null ) {throw new PersonNotFoundException( email );}return person;}public Person addPerson( final String email, final String firstName, final String lastName ) {final Person person = new Person( email );person.setFirstName( firstName );person.setLastName( lastName );if( persons.putIfAbsent( email, person ) != null ) {throw new PersonAlreadyExistsException( email );}return person;}public void removePerson( final String email ) {if( persons.remove( email ) == null ) {throw new PersonNotFoundException( email );}}
}
基于ConcurrentMap的非常简单但可行的实现。 如果请求的电子邮件的人不存在, 则会引发PersonNotFoundException 。 分别在已经存在带有请求电子邮件的人的情况下引发PersonAlreadyExistsException 。 这些异常中的每一个在HTTP代码中都有一个对应项: 404 NOT FOUND和409 CONFLICT 。 这就是我们告诉JAX-RS的方式:
package com.example.exceptions;import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;public class PersonAlreadyExistsException extends WebApplicationException {private static final long serialVersionUID = 6817489620338221395L;public PersonAlreadyExistsException( final String email ) {super(Response.status( Status.CONFLICT ).entity( 'Person already exists: ' + email ).build());}
}
package com.example.exceptions;import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;public class PersonNotFoundException extends WebApplicationException {private static final long serialVersionUID = -2894269137259898072L;public PersonNotFoundException( final String email ) {super(Response.status( Status.NOT_FOUND ).entity( 'Person not found: ' + email ).build());}
}
完整的项目托管在GitHub上 。 让我们结束无聊的部分,然后转到最甜蜜的部分: BDD 。 如文档中所述, specs2对Given / When / Then样式提供了很好的支持也就不足为奇了。 因此,使用specs2 ,我们的测试用例将变成这样:
'Create new person with email <a@b.com>' ^ br^'Given REST client for application deployed at ${http://localhost:8080}' ^ client^'When I do POST to ${rest/api/people}' ^ post(Map('email' -> 'a@b.com', 'firstName' -> 'Tommy', 'lastName' -> 'Knocker'))^'Then I expect HTTP code ${201}' ^ expectResponseCode^'And HTTP header ${Location} to contain ${http://localhost:8080/rest/api/people/a@b.com}' ^ expectResponseHeader^
还不错,但是那些^ , br , client , post , ExpectResponseCode和ExpectResponseHeader是什么? ^ , br只是用于支持Given / When / Then链的一些specs2糖。 其他的post , ExpectResponseCode和ExpectResponseHeader只是我们定义用于实际工作的几个函数/变量。 例如, 客户端是一个新的JAX-RS 2.0客户端,我们以此创建(使用Scala语法):
val client: Given[ Client ] = ( baseUrl: String ) => ClientBuilder.newClient( new ClientConfig().property( 'baseUrl', baseUrl ) )
baseUrl来自Given定义本身,它包含在$ {…}构造中。 此外,我们可以看到Given定义具有很强的类型: Given [Client] 。 稍后我们将看到, When和Then的情况相同 ,它们确实具有相应的强类型when [T,V]和Then [V] 。
流程如下所示:
- 从给定定义开始,该定义返回Client 。
- 继续何时定义,该定义接受来自给定的 客户并返回响应
- 最后是数量的Then定义,这些定义接受来自When的 响应并检查实际期望
这是帖子定义的样子(本身就是When [Client,Response] ):
def post( values: Map[ String, Any ] ): When[ Client, Response ] = ( client: Client ) => ( url: String ) => client.target( s'${client.getConfiguration.getProperty( 'baseUrl' )}/$url' ).request( MediaType.APPLICATION_JSON ).post( Entity.form( values.foldLeft( new Form() )( ( form, param ) => form.param( param._1, param._2.toString ) ) ),classOf[ Response ] )
最后是ExpectResponseCode和ExpectResponseHeader ,它们非常相似,并且具有相同的类型Then [Response] :
val expectResponseCode: Then[ Response ] = ( response: Response ) => ( code: String ) => response.getStatus() must_== code.toInt val expectResponseHeader: Then[ Response ] = ( response: Response ) => ( header: String, value: String ) => response.getHeaderString( header ) should contain( value )
又一个示例,根据JSON有效负载检查响应内容:
'Retrieve existing person with email <a@b.com>' ^ br^'Given REST client for application deployed at ${http://localhost:8080}' ^ client^'When I do GET to ${rest/api/people/a@b.com}' ^ get^'Then I expect HTTP code ${200}' ^ expectResponseCode^'And content to contain ${JSON}' ^ expectResponseContent('''{'email': 'a@b.com', 'firstName': 'Tommy', 'lastName': 'Knocker' } ''')^
这次我们使用以下get实现来执行GET请求:
val get: When[ Client, Response ] = ( client: Client ) => ( url: String ) => client.target( s'${client.getConfiguration.getProperty( 'baseUrl' )}/$url' ).request( MediaType.APPLICATION_JSON ).get( classOf[ Response ] )
尽管specs2具有丰富的匹配器集,可对JSON有效负载执行不同的检查,但我在Scala中使用了spray-json ,这是一种轻量级,干净且简单的JSON实现(的确如此!),这是ExpectResponseContent实现:
def expectResponseContent( json: String ): Then[ Response ] = ( response: Response ) => ( format: String ) => {format match { case 'JSON' => response.readEntity( classOf[ String ] ).asJson must_== json.asJsoncase _ => response.readEntity( classOf[ String ] ) must_== json}
}
最后一个示例(对现有电子邮件进行POST ):
'Create yet another person with same email <a@b.com>' ^ br^'Given REST client for application deployed at ${http://localhost:8080}' ^ client^'When I do POST to ${rest/api/people}' ^ post(Map( 'email' -> 'a@b.com' ))^'Then I expect HTTP code ${409}' ^ expectResponseCode^'And content to contain ${Person already exists: a@b.com}' ^ expectResponseContent^
看起来很棒! 好的,富有表现力的BDD ,使用强类型和静态编译! 当然,可以使用JUnit集成,并且可以与Eclipse一起很好地工作。
不要忘记自己的specs2报告(由maven-specs2-plugin生成): mvn clean test
请在GitHub上查找完整的项目。 另外,请注意,由于我使用的是最新的JAX-RS 2.0里程碑(最终草案),因此发布API时可能会有所变化。
参考: Andriy Redko {devmind}博客上的JCG合作伙伴 Andrey Redko 使用Specs2和客户端API 2.0进行了富有表现力的JAX-RS集成测试 。
翻译自: https://www.javacodegeeks.com/2013/03/expressive-jax-rs-integration-testing-with-specs2-and-client-api-2-0.html