-
Notifications
You must be signed in to change notification settings - Fork 477
The New nGrinder HTTP Client
Until nGrinder 3.5.3, legacy HTTP client has not updated and exposed on many vulnerabilities. Also, modern HTTP specs were not supported like HTTP2.
From nGrinder 3.5.4, nGrinder provides new HTTP client that supports HTTP2, latest cookie specs, and HTTP methods PATCH.
The new HTTP client for nGrinder must be light-weight so the agent can focus on performance test. The new HTTP client is implemented based on Apache httpcomponents-core.
The new HTTP client supports both HTTP1 and HTTP2. You can enforce protocol version with invoking setVersionPolicy()
. Available options are FORCE_HTTP_1
, FORCE_HTTP_2
and NEGOTIATE
. Default version policy is NEGOTIATE
. So, HTTP client negotiates the protocol version with target server.
import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPResponse
import org.apache.hc.core5.http2.HttpVersionPolicy
@RunWith(GrinderRunner)
class TestRunner {
public static GTest test
public static HTTPRequest request
public static Map<String, String> headers = [:]
public static Map<String, String> params = [:]
@BeforeProcess
public static void beforeProcess() {
test = new GTest(1, "sample")
request = new HTTPRequest()
grinder.logger.info("before process.");
}
@BeforeThread
public void beforeThread() {
test.record(this, "test")
grinder.statistics.delayReports = true;
grinder.logger.info("before thread.");
}
@Before
public void before() {
request.setHeaders(headers)
request.setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_1) // or HttpVersionPolicy.FORCE_HTTP_2
grinder.logger.info("before. init headers and cookies");
}
@Test
public void test(){
HTTPResponse result = request.GET("{input_your_api_url}", params)
grinder.logger.info("protocol version: {}", result.version);
grinder.logger.info("body: {}", result.bodyText);
if (result.statusCode == 301 || result.statusCode == 302) {
grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode);
} else {
assertThat(result.statusCode, is(200));
}
}
}
Legacy nGrinder HTTP client supports only NVPair
as a parameter. It was enough for setting request parameters or headers but, in case you have to use bunch of parameters, you also had to write more boilerplate codes.
Now, the new HTTP client supports org.apache.hc.core5.http.NameValuePair
and org.apache.hc.core5.http.Header
as a parameter. But, you don't have to worry about instantiating implementation class. Map
is also supported as a parameter. If you pass a parameter as Map
, new HTTP client converts it to appropriate parameter type and request is executed. The NVPair
is also supported for easy to migrate existing performance test scripts.
import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPResponse
@RunWith(GrinderRunner)
class TestRunner {
public static GTest test
public static HTTPRequest request
@BeforeProcess
public static void beforeProcess() {
test = new GTest(1, "sample")
request = new HTTPRequest()
grinder.logger.info("before process.");
}
@BeforeThread
public void beforeThread() {
test.record(this, "test")
grinder.statistics.delayReports = true;
grinder.logger.info("before thread.");
}
@Test
public void test(){
// You can pass a parameter with Groovy Map literal
HTTPResponse result = request.GET("{input_your_api_url}", [ "param1": "value1" ], [ "header1": "value1" ])
if (result.statusCode == 301 || result.statusCode == 302) {
grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode);
} else {
assertThat(result.statusCode, is(200));
}
}
}
Or you can pass a parameter with instantiating a list of org.apache.hc.core5.http.message.BasicHeader
or org.apache.hc.core5.http.message.BasicNameValuePair
yourself.
import org.apache.hc.core5.http.Header;
import org.apache.hc.core5.http.NameValuePair;
import org.apache.hc.core5.http.message.BasicHeader;
import org.apache.hc.core5.http.message.BasicNameValuePair;
import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPResponse
@RunWith(GrinderRunner)
class TestRunner {
public static GTest test
public static HTTPRequest request
public List<Header> headers = [ new BasicHeader("header1", "value1") ]
public List<NameValuePair> params = [ new BasicNameValuePair("param1", "value1") ]
@BeforeProcess
public static void beforeProcess() {
test = new GTest(1, "sample")
request = new HTTPRequest()
grinder.logger.info("before process.");
}
@BeforeThread
public void beforeThread() {
test.record(this, "test")
grinder.statistics.delayReports = true;
grinder.logger.info("before thread.");
}
@Test
public void test(){
HTTPResponse result = request.GET("{input_your_api_url}", params, headers)
if (result.statusCode == 301 || result.statusCode == 302) {
grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode);
} else {
assertThat(result.statusCode, is(200));
}
}
}
In case the test target API produces a large response body, the performance test will derive humongous network traffic and it's almost same as DDOS attack. You can reduce network traffic by reading only a part of response body. The partial response reading feature is only supported on HTTP1 protocol currently.
import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPResponse
import org.apache.hc.core5.http2.HttpVersionPolicy
@RunWith(GrinderRunner)
class TestRunner {
public static GTest test
public static HTTPRequest request
public static Map<String, String> headers = [:]
public static Map<String, String> params = [:]
@BeforeProcess
public static void beforeProcess() {
test = new GTest(1, "sample")
request = new HTTPRequest()
grinder.logger.info("before process.");
}
@BeforeThread
public void beforeThread() {
test.record(this, "test")
grinder.statistics.delayReports = true;
request.setReadBytes(1024) // Set how many bytes will you read from response body
request.setVersionPolicy(HttpVersionPolicy.FORCE_HTTP_1)
grinder.logger.info("before thread.");
}
@Before
public void before() {
request.setHeaders(headers)
grinder.logger.info("before. init headers and cookies");
}
@Test
public void test(){
HTTPResponse result = request.GET("{input_your_api_url}", params)
grinder.logger.info("protocol version: {}", result.version);
grinder.logger.info("body: {}", result.bodyText);
if (result.statusCode == 301 || result.statusCode == 302) {
grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", result.statusCode);
} else {
assertThat(result.statusCode, is(200));
}
}
}
The new HTTP client provides the CookieManager, so you can handle cookies with it. Some APIs may require you to log in first to access them properly. Let's see the login sample script using the CookieManager.
import org.ngrinder.http.cookie.CookieManager;
import org.ngrinder.http.cookie.Cookie;
import org.ngrinder.http.HTTPRequest
import org.ngrinder.http.HTTPResponse
/**
* A simple example test script using login cookie to access target API
* with new nGrinder HTTP client.
*/
@RunWith(GrinderRunner)
class TestRunner {
public static GTest test
public static HTTPRequest request
public static HTTPResponse response
public static Map<String, String> headers = [:]
public static Map<String, Object> params = [:]
public static List<Cookie> cookies = []
@BeforeProcess
public static void beforeProcess() {
test = new GTest(1, "sample")
request = new HTTPRequest()
}
@BeforeThread
public void beforeThread() {
test.record(this, "test")
grinder.statistics.delayReports = true
}
@Before
public void before() {
headers.put("Content-Type", "application/x-www-form-urlencoded")
params.put("username", "admin")
params.put("password", "admin")
// You can add a cookie explicitly
cookies.add(new Cookie("cookie_name1", "cookie_value1"))
cookies.add(new Cookie("cookie_name2", "cookie_value2", "please_modify_this.com", "/", Integer.MAX_VALUE))
CookieManager.addCookies(cookies)
}
@Test
public void test(){
response = request.GET("http://please_modify_this.com/api")
if (response.statusCode == 301 || response.statusCode == 302) {
grinder.logger.info("You may have to log in first.")
} else {
assertThat(response.getStatusCode(), not(is(200)))
}
// After login request, all the requests performed in current thread contains login cookie
request.POST("http://please_modify_this.com/login", params, headers)
grinder.logger.info("cookies: {}", CookieManager.getCookies())
response = request.GET("http://please_modify_this.com/api")
assertThat(response.statusCode, is(200));
}
}
The new HTTP client supports request with multipart form data. To use resources in performance test script context, have to be located in ${PROJECT_ROOT}/resources/
in normal project, ${PROJECT_ROOT}/src/main/java/resources/
in groovy gradle project.
import org.ngrinder.http.multipart.MultipartEntityBuilder
@RunWith(GrinderRunner)
class TestRunner {
public static GTest test
public static HTTPRequest request
@BeforeProcess
public static void beforeProcess() {
HTTPRequestControl.setConnectionTimeout(300000)
test = new GTest(1, "sample")
request = new HTTPRequest()
grinder.logger.info("before process.")
}
@BeforeThread
public void beforeThread() {
test.record(this, "test")
grinder.statistics.delayReports = true
grinder.logger.info("before thread.")
}
@Test
public void test(){
def multipart = MultipartEntityBuilder.create()
.addEntity("message", "Hello, nGrinder!")
.addEntity("file", new File("resources/sample.jpg")) // if you're working on groovy gradle project, path will be just "sample.jpg"
.build();
HTTPResponse response = request.POST("http://please_modify_this.com/", multipart)
if (response.statusCode == 301 || response.statusCode == 302) {
grinder.logger.warn("Warning. The response may not be correct. The response code was {}.", response.statusCode)
} else {
assertThat(response.statusCode, is(200))
}
}
}
You can get response body as JSON(Map) with user defined converter.
import groovy.json.JsonSlurper
def toJSON = { new JsonSlurper().parseText(it) }
@RunWith(GrinderRunner)
class TestRunner {
public static GTest test
public static HTTPRequest request
@BeforeProcess
public static void beforeProcess() {
test = new GTest(1, "Test1")
request = new HTTPRequest()
}
@BeforeThread
public void beforeThread() {
test.record(this, "test")
grinder.statistics.delayReports = true
}
@Test
public void test(){
HTTPResponse response = request.GET("http://please_modify_this.com")
assertThat(response.getBodyText(), is("{\"hello\":\"world!\"}"))
assertThat(response.getBody(toJSON), is(["hello":"world!"]))
assertThat(response.getBody(toJSON).hello, is("world!"))
}
}