這是一個Restful API自動化測試框架,這是一個能讓你寫出高可讀性測試代碼的測試框架!
項目目標##
話說目前行業內,Restful API自動化測試框架已經不是稀罕物了,各個語言都有自己的實現機制。拿Java的Jersey來講,它本身就提供了一個API測試框架-Jersey Test Framework.能夠幫助我們寫API測試,但是這里我們想做的是另一套。
觀察到Jersey使用了Fluent interface的模式來讓代碼可讀性更高,比如下面:
String responseMsg = target.path("myresource").request().get(String.class);
那么如果我們也使用Fluent Interface模式,是不是也可以讓我們的測試代碼可讀性更高呢?
比如下面的測試的代碼,是不是看起來很清爽,目標更明確呢?
APIRequest.GET(URL).header("Authorization", "Bearer " + token).invoke().assertStatus(200).assertBody(expectedBody);
直接一行代碼,搞定一條Case!
分析需求##
既然是一個API自動化測試框架,那它能做什么呢?
- 能夠發HTTP請求 - Get,Post,Put,Delete,甚至 Head
- 能夠接受HTTP返回,並且能夠方便驗證其返回值
- 能夠打印所有Log,包含Request和Response的所有部分,這樣當Case出錯時,我們容易分析問題所在
- 能夠做好數據分離,用配置文件管理測試數據
用到的工具##
顯然,框架不是工具,它只是對現有工具的組合和再包裝,而這個框架也使用了一些流行的工具:
- 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
類這樣可以方便的管理配置文件,以及獲得一些有用的公共方法。
下面是一些例子:
-
如何發一個Get請求
APIRequest.GET(uri).header("Authorization", token) .invoke().assertStatus(200).assertBodyContains("expectedContent");
-
如何使用XML或者Json格式的Payload
String payload = loadFile("xmlfile.xml");
-
如何運行時定制化Payload填充參數
String payload = String.format(loadFile("jsonfile.json"), "abc", "edf");
-
如何做數據分離,在Property文件管理參數
`String uri = getValue("get.uri");
核心實現##
要想使用Fluent Paragraming Model來寫case,那么就得讓我們所有的包裝方法,都能夠返回期望的Class對象,更重要的是,我們是想讓Request的返回和驗證也能參與到Fluent模式的驗證,所以在最終調用方法時,APIRequest
和APIResponse
就要能和諧的過渡到一起。
所以我們這樣定義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://www.cnblogs.com/jinsdu/]
如果您對本文有意見或者建議,歡迎留言