Java Fluent Restful API自動化測試框架

這是一個Restful API自動化測試框架,這是一個能讓你寫出高可讀性測試代碼的測試框架!html

項目目標

話說目前行業內,Restful API自動化測試框架已經不是稀罕物了,各個語言都有本身的實現機制。拿Java的Jersey來說,它自己就提供了一個API測試框架-Jersey Test Framework.可以幫助咱們寫API測試,可是這裏咱們想作的是另外一套。java

觀察到Jersey使用了Fluent interface的模式來讓代碼可讀性更高,好比下面:git

String responseMsg = target.path("myresource").request().get(String.class);

那麼若是咱們也使用Fluent Interface模式,是否是也可讓咱們的測試代碼可讀性更高呢?
好比下面的測試的代碼,是否是看起來很清爽,目標更明確呢?github

APIRequest.GET(URL).header("Authorization", "Bearer " + token).invoke().assertStatus(200).assertBody(expectedBody);

直接一行代碼,搞定一條Case!web

分析需求

既然是一個API自動化測試框架,那它能作什麼呢?json

  • 可以發HTTP請求 - Get,Post,Put,Delete,甚至 Head
  • 可以接受HTTP返回,而且可以方便驗證其返回值
  • 可以打印全部Log,包含Request和Response的全部部分,這樣當Case出錯時,咱們容易分析問題所在
  • 可以作好數據分離,用配置文件管理測試數據

用到的工具

顯然,框架不是工具,它只是對現有工具的組合和再包裝,而這個框架也使用了一些流行的工具:app

  • Jersey Client 2.18 咱們要使用它來幫助咱們發HTTP Request
  • Junit4 測試框架,用它來寫Case
  • Apache Commons IO 提供Common API幫助讀寫文件
  • SLF4J,打印log怎能少了它

如何使用

最終,全部的HTTP Request都從APIRequest這個類出發,一步步構建,最終調用Invoke方法發送HTTP 請求。框架

APIResoponse來管理HTTP的返回,這個方法提供一些公共的方法來驗證API的返回。工具

建議全部的TestCase都繼承與APITest類這樣能夠方便的管理配置文件,以及得到一些有用的公共方法。測試

下面是一些例子:

  1. 如何發一個Get請求

    APIRequest.GET(uri).header("Authorization", token) .invoke().assertStatus(200).assertBodyContains("expectedContent");

  2. 如何使用XML或者Json格式的Payload

    String payload = loadFile("xmlfile.xml");

  3. 如何運行時定製化Payload填充參數

    String payload = String.format(loadFile("jsonfile.json"), "abc", "edf");

  4. 如何作數據分離,在Property文件管理參數

    `String uri = getValue("get.uri");

核心實現

要想使用Fluent Paragraming Model來寫case,那麼就得讓咱們全部的包裝方法,都可以返回指望的Class對象,更重要的是,咱們是想讓Request的返回和驗證也能參與到Fluent模式的驗證,因此在最終調用方法時,APIRequestAPIResponse就要能和諧的過渡到一塊兒。
因此咱們這樣定義APIRequest類:

/**
 * General Class to make HTTP calls
 * 
 * @author Carl Ji
 */

public class APIRequest {

private UriBuilder uri;
private Map<String, String> params = new HashMap<String, String>();
private Map<String, String> headers = new HashMap<String, String>();
private MediaType contentType = MediaType.APPLICATION_XML_TYPE;
private MediaType acceptType;
private String httpMethod;
private String body;

private APIRequest(String uri, String method)
{
    this.uri=UriBuilder.fromUri(uri);
    this.httpMethod = method;
}

/**
 * Build a HTTP Get request
 * 
 * @param uri
 *        The URI on which a HTTP get request will be called
 * @return
 *        {@link APIRequest} 
 */
public static APIRequest GET(String uri)
{
    return new APIRequest(uri, HttpMethod.GET);
}

/**
 * Build a HTTP Post request
 * 
 * @param uri
 *        The URI on which a POST request will be called
 * @return
 *        {@link APIRequest}
 */
public static APIRequest POST(String uri)
{
    return new APIRequest(uri, HttpMethod.POST);
}

/**
 * Build a HTTP Put request
 * 
 * @param uri
 *        The URI on which a PUT request will be called
 * @return
 *        {@link APIRequest}
 */
public static APIRequest PUT(String uri)
{
    return new APIRequest(uri, HttpMethod.PUT);
}

/**
 * Build a HTTP Delete request
 * 
 * @param uri
 *        The URI that the Delete Request will be called
 * @return
 *        {@link APIRequest}
 */
public static APIRequest DELETE(String uri)
{
    return new APIRequest(uri, HttpMethod.DELETE);
}

/**
 * Build a HTTP HEAD request
 * 
 * @param uri
 *        The URI that the Head request will be called
 * @return
 *        {@link APIRequest}
 */
public static APIRequest HEAD(String uri)
{
    return new APIRequest(uri, HttpMethod.HEAD);
}

/**
 * Add the {@code value} to the end of URI to build the final URI
 * 
 * @param value
 *        The value that will be appended to the URI
 * @return
 *        {@link APIRequest}
 */
public APIRequest path(String value)
{
    this.uri.path(value);
    return this;
}

/**
 * Build the parameter in the request URI
 * 
 * @param key
 *        The request URI parameter key
 * @param value
 *        The request URI parameter value
 * @return
 *        {@link APIRequest}
 */
public APIRequest param(String key, String value)
{
    params.put(key, value);
    return this;
}

/**
 * Set the content type in the request body
 * 
 * @param type
 *        The content type {@link MediaType} 
 * @return
 *        {@link APIRequest}
 */
public APIRequest type(MediaType type)
{
    this.contentType = type;
    return this;
}

/**
 * Set the accepted type for the HTTP response when calling the specific HTTP request
 * 
 * @param type
 *        The accepted type for the response of this request
 * @return
 *        {@link APIRequest}
 */
public APIRequest accept(MediaType type)
{
    this.acceptType = type;
    return this;
}

/**
 * Set the HTTP request headers parameter
 * 
 * @param key
 *        The header name
 * @param value
 *        The corresponding value for the header 
 * @return
 *        {@link APIRequest}
 */
public APIRequest header(String key, String value)
{
    headers.put(key, value);
    return this;
}

/**
 * Set the request body
 * 
 * @param body
 *        The body of the request
 * @return
 *        {@link APIRequest}
 */
public APIRequest body(String body)
{
    this.body = body;
    return this;
}

/**
 * Invoke jersey client to send HTTP request
 * 
 * @return {@link APIResponse}
 */
public APIResponse invoke()
{
    ClientConfig config = new ClientConfig();

    /**
     * Important: Jersey Invocation class will check "Entity must be null for http method DELETE."
     * so we can not send DELETE request with entity in payload, 
     * here we suppress this check
     */
    config.property(ClientProperties.SUPPRESS_HTTP_COMPLIANCE_VALIDATION, true);
    
    Client client = ClientBuilder.newClient(config);
    //Print all logs for each request and response
    client.register(new LoggingFilter(Logger.getLogger(APIResponse.class.getName()), true));
    
    WebTarget webTarget = client.target(uri);
    if(!params.isEmpty())
    {
        for(Entry<String, String> key: params.entrySet())
        {
            webTarget = webTarget.queryParam(key.getKey(), key.getValue());
        }
    }
    
    Invocation.Builder invocationBuilder= webTarget.request();
    if(acceptType != null)
    {
        invocationBuilder = invocationBuilder.accept(acceptType);
    }

    if(!headers.isEmpty())
    {
        for(String key: headers.keySet())
        {
            invocationBuilder.header(key, headers.get(key));
        }
    }
    
    Response response;
    
    if(body == null)
    {
        response= invocationBuilder.method(httpMethod, Response.class);
    }
    else
    {
        response = invocationBuilder.method(httpMethod, Entity.entity(body, contentType), Response.class);
    }
    
    return new APIResponse(response);
}
}

`

源碼地址

源碼已上傳Github:https://github.com/CarlJi/RestfulAPITests
歡迎你們分享討論,提意見!

未完待續

下一步打算結合個人Junit Extension工具,給框架添加靈活管理Case的能力,這樣當Case變多時,就能夠按需執行咱們須要的Case。

參考資料

  • Jersey Client使用 https://jersey.java.net/documentation/latest/client.html
  • Jersey Test Framework: http://jersey.java.net/nonav/documentation/latest/test-framework.html
  • Fluent Interface 相關:
    • http://stackoverflow.com/questions/17937755/what-is-the-difference-between-a-fluent-interface-and-the-builder-pattern
    • https://en.wikipedia.org/wiki/Fluent_interface#Problems

若是您看了本篇博客,以爲對您有所收穫,請點擊下面的 [推薦]
若是您想轉載本博客,請註明出處大卡的博客[http://www.cnblogs.com/jinsdu/] 若是您對本文有意見或者建議,歡迎留言

相關文章
相關標籤/搜索