使用Specs2和客户端API 2.0进行富有表现力的JAX-RS集成测试
毫無(wú)疑問(wèn), JAX-RS是一項(xiàng)杰出的技術(shù)。 即將發(fā)布的規(guī)范JAX-RS 2.0帶來(lái)了更多的強(qiáng)大功能,尤其是在客戶(hù)端API方面。 今天的帖子的主題是JAX-RS服務(wù)的集成測(cè)試。 有很多出色的測(cè)試框架,例如REST可以確保提供幫助,但是我要展示的方式是使用富有表現(xiàn)力的BDD風(fēng)格。 這是我的意思的示例:
看起來(lái)像現(xiàn)代BDD框架的典型Given / When / Then風(fēng)格。 使用靜態(tài)編譯語(yǔ)言,我們?cè)贘VM上能達(dá)到多近的距離? 事實(shí)證明,這要?dú)w功于良好的specs2測(cè)試工具。
值得一提的是, specs2是一個(gè)Scala框架。 盡管我們將編寫(xiě)一些Scala ,但是我們將以非常有經(jīng)驗(yàn)的Java開(kāi)發(fā)人員熟悉的非常直觀(guān)的方式來(lái)完成它。 測(cè)試中的JAX-RS服務(wù)是我們?cè)谏弦黄恼轮虚_(kāi)發(fā)的 。 這里是:
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();} }非常簡(jiǎn)單的JAX-RS服務(wù)來(lái)管理人員。 Java實(shí)現(xiàn)提供并支持所有基本的HTTP動(dòng)詞 : GET , PUT , POST和DELETE 。 為了完整起見(jiàn),讓我也包括服務(wù)層的一些方法,因?yàn)檫@些方法引起我們關(guān)注的一些例外。
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的非常簡(jiǎn)單但可行的實(shí)現(xiàn)。 如果請(qǐng)求的電子郵件的人不存在, 則會(huì)引發(fā)PersonNotFoundException 。 分別在已經(jīng)存在帶有請(qǐng)求電子郵件的人的情況下引發(fā)PersonAlreadyExistsException 。 這些異常中的每一個(gè)在HTTP代碼中都有一個(gè)對(duì)應(yīng)項(xiàng): 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());} }完整的項(xiàng)目托管在GitHub上 。 讓我們結(jié)束無(wú)聊的部分,然后轉(zhuǎn)到最甜蜜的部分: BDD 。 如文檔中所述, specs2對(duì)Given / When / Then樣式提供了很好的支持也就不足為奇了。 因此,使用specs2 ,我們的測(cè)試用例將變成這樣:
'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^還不錯(cuò),但是那些^ , br , client , post , ExpectResponseCode和ExpectResponseHeader是什么? ^ , br只是用于支持Given / When / Then鏈的一些specs2糖。 其他的post , ExpectResponseCode和ExpectResponseHeader只是我們定義用于實(shí)際工作的幾個(gè)函數(shù)/變量。 例如, 客戶(hù)端是一個(gè)新的JAX-RS 2.0客戶(hù)端,我們以此創(chuàng)建(使用Scala語(yǔ)法):
val client: Given[ Client ] = ( baseUrl: String ) => ClientBuilder.newClient( new ClientConfig().property( 'baseUrl', baseUrl ) )baseUrl來(lái)自Given定義本身,它包含在$ {…}構(gòu)造中。 此外,我們可以看到Given定義具有很強(qiáng)的類(lèi)型: Given [Client] 。 稍后我們將看到, When和Then的情況相同 ,它們確實(shí)具有相應(yīng)的強(qiáng)類(lèi)型when [T,V]和Then [V] 。
流程如下所示:
- 從給定定義開(kāi)始,該定義返回Client 。
- 繼續(xù)何時(shí)定義,該定義接受來(lái)自給定的 客戶(hù)并返回響應(yīng)
- 最后是數(shù)量的Then定義,這些定義接受來(lái)自When的 響應(yīng)并檢查實(shí)際期望
這是帖子定義的樣子(本身就是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 ,它們非常相似,并且具有相同的類(lèi)型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 )又一個(gè)示例,根據(jù)JSON有效負(fù)載檢查響應(yīng)內(nèi)容:
'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實(shí)現(xiàn)來(lái)執(zhí)行GET請(qǐng)求:
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具有豐富的匹配器集,可對(duì)JSON有效負(fù)載執(zhí)行不同的檢查,但我在Scala中使用了spray-json ,這是一種輕量級(jí),干凈且簡(jiǎn)單的JSON實(shí)現(xiàn)(的確如此!),這是ExpectResponseContent實(shí)現(xiàn):
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} }最后一個(gè)示例(對(duì)現(xiàn)有電子郵件進(jìn)行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^看起來(lái)很棒! 好的,富有表現(xiàn)力的BDD ,使用強(qiáng)類(lèi)型和靜態(tài)編譯! 當(dāng)然,可以使用JUnit集成,并且可以與Eclipse一起很好地工作。
不要忘記自己的specs2報(bào)告(由maven-specs2-plugin生成): mvn clean test
請(qǐng)?jiān)贕itHub上查找完整的項(xiàng)目。 另外,請(qǐng)注意,由于我使用的是最新的JAX-RS 2.0里程碑(最終草案),因此發(fā)布API時(shí)可能會(huì)有所變化。
參考: Andriy Redko {devmind}博客上的JCG合作伙伴 Andrey Redko 使用Specs2和客戶(hù)端API 2.0進(jìn)行了富有表現(xiàn)力的JAX-RS集成測(cè)試 。
翻譯自: https://www.javacodegeeks.com/2013/03/expressive-jax-rs-integration-testing-with-specs2-and-client-api-2-0.html
總結(jié)
以上是生活随笔為你收集整理的使用Specs2和客户端API 2.0进行富有表现力的JAX-RS集成测试的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: oppo手机读取u盘教程(oppo手机如
- 下一篇: linux如何查看路由表linux如何查