This is my second post regarding Grinder. In this post I'll go over the anatomy of a recorder Grinder test-script. If you haven't read my previous post, please take a look at it. Otherwise this post won't make much sense!
High-level structure of a Grinder script
The high-level structure of a recorded Grinder-test-script looks like this:
[ ... import statements ... ] [ ... header definitions ... ] [ ... url definitions ... ] [ ... request and test definitions ... ] class TestRunner: [ ... method definitions - a method is defined for each recorded page ... ] def __call__(self): [ ... calls to defined methods, which actually runs the requests ... ] [ utility function (I'll go over this later) ] [ ... calls to utility function to wrap/instrument tests (I'll go over this later) ... ]
Let's go over each of these sections one by one:
Import statements
If you know Java, then this section is pretty much self-explanatory. Here you import the stuff you need, to make the tests work. By default, the following libraries are imported:
from net.grinder.script import Test from net.grinder.script.Grinder import grinder from net.grinder.plugin.http import HTTPPluginControl, HTTPRequest from HTTPClient import NVPair
The first two import statements import the Test and grinder objects, which give you access to Grinder's testing framework, and the grinder environment. The next two import statements import some utility classes which perform HTTP Requests. The NVPair class lets you organize parameters and values for POST requests into name-value pairs.
Header definitions
When the recorder runs, it records every request that the browser makes. After that, it goes through all the requests and generates a set of unique headers. These headers are then used to create the request objects (which we'll look at later). The generated code looks like this:
connectionDefaults = HTTPPluginControl.getConnectionDefaults() httpUtilities = HTTPPluginControl.getHTTPUtilities() # To use a proxy server, uncomment the next line and set the host and port. # connectionDefaults.setProxyServer("localhost", 8001) # These definitions at the top level of the file are evaluated once, # when the worker process is started. connectionDefaults.defaultHeaders = \ ( NVPair('Accept-Language', 'en-us,en;q=0.5'), NVPair('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7'), NVPair('Accept-Encoding', 'gzip,deflate'), NVPair('User-Agent', 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.9.0.11) Gecko/2009060308 Ubuntu/9.04 (jaunty) Firefox/3.0.11'), ) headers0= \ ( NVPair('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'), NVPair('Referer', 'https://local.infusiontest.com:8443/?msg=You%27ve+been+logged+out+-+thanks+for+playing%21&notification=You%27ve+been+logged+out+-+thanks+for+playing%21'), ) headers1= \ ( NVPair('Accept', 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'), NVPair('Referer', 'https://local.infusiontest.com:8443/Admin/home.jsp'), )
The first statement creates an HTTPPluginConnection object, which lets you control the behavior of the connection. The next statement creates an instance of an HTTPUtilities object which provides access to various utility methods (you'll see examples of this later). The next two commented lines let you know that you can proxy these requests through a proxy server if you wish. The two comments after that let you know that these definitions are evaluated once for each process. Essentially, all data defined at the top of a script, outside the class, are shared between threads belonging to a single worker process.
In the next few lines, the script actually defines the headers. First, it sets up the default headers that are used by each request (things like the User-Agent, Charset, and Encoding). After that, it goes through and defines the headers. As you can see, these headers are basically tuples of NVPair objects. One NVPair defines the Accept parameter, and the other defines the Referer. Those with an eye for detail will notice that there seems to be an extra comma at the end of the tuple definition. This isn't a problem in Python because the parser will treat the last element as an empty element.
URL definitions
In the next section of the script, you'll find URL definitions, which look like this:
url0 = 'https://local.infusiontest.com:8443'
In this example, there is only one URL. But let's say that the app you're testing hits a few different URL's during the scenario that you're recording. Then the script will have a definition for each unique URL that the recorder encountered. The definitions are of the form urlN = "protocol://recorded.url.here". The combination of a URL and Header is what defines a request (more on that in the next section).
Request and test definitions
In the next section of the script, we define the actual requests that we're going to use for our tests:
# Create an HTTPRequest for each request, then replace the # reference to the HTTPRequest with an instrumented version. # You can access the unadorned instance using request101.__target__. request101 = HTTPRequest(url=url0, headers=headers0) request101 = Test(101, 'POST processLogin.jsp').wrap(request101) request102 = HTTPRequest(url=url0, headers=headers0) request102 = Test(102, 'GET home.jsp').wrap(request102) request201 = HTTPRequest(url=url0, headers=headers1) request201 = Test(201, 'GET popUpTask.jsp').wrap(request201) request301 = HTTPRequest(url=url0, headers=headers2) request301 = Test(301, 'POST calendarBackend.jsp').wrap(request301)
The comments give a terse explanation of what's happening, but allow me to elaborate. First, you create an instance of an HTTPRequest object using the previously defined URL's and Headers. An important thing to note here is that there is a naming scheme for the requests, which is of the form request[Page Number][Request Number]. So, for example, request105 means the fifth request in the first recorded page, and request1101 means the first request in the eleventh recorded page (yes, there is an inherent ambiguity, because 1101 could also mean the one hundred and first request in the first recorded page. But if you have a hundred and one unique requests per page, then you have a problem).
The next statement is the most important one. The way Grinder records test statistics is by proxying whatever you want to test, through the Test object. What this means is that the Test object is a wrapper around the object you want to test. You can still treat the wrapped object like the original object, but under the hood, access to the methods of that object are proxied through the Test object. This way, Grinder can record statistics. The picture might help understand what's going on:
On the top you can see how a regular request behaves when accessing the GET method. It's a straight call. The bottom picture shows you what happens when you wrap your request object with a Test object. Now, the call to GET isn't made on the actual request object. It's proxied through the Test object, which has an attribute called __target__ (which is the actual request object). Once it gets to the actual request object, the actual GET method is called. The reason Grinder does this is so that it can collect statistics on actions performed by an object.
Method definitions
After the request objects have been defined, we come to the actual TestRunner class. This is where you define methods that correspond to each recorded page:
# A method for each recorded page. def page1(self): """GET / (requests 101-104).""" result = request101.GET('/') grinder.sleep(307) # Expecting 302 'Moved Temporarily' request102.GET('/slices/infusion-crm.gif') grinder.sleep(29) # Expecting 302 'Moved Temporarily' request103.GET('/login/defaultLogin.jsp') self.token_msg = \ httpUtilities.valueFromLocationURI('msg') # 'Whoa,+easy+there+tiger.+You\'re+gonna+nee...' grinder.sleep(24) request104.GET('/index.jsp' + '?msg=' + self.token_msg) return result def page2(self): """POST processLogin.jsp (requests 201-202).""" # Expecting 302 'Moved Temporarily' result = request201.POST('/login/processLogin.jsp', ( NVPair('username', 'vivin'), NVPair('password', 'abAB12!@'), NVPair('Login', 'Login'), ), ( NVPair('Content-Type', 'application/x-www-form-urlencoded'), )) grinder.sleep(72) request202.GET('/Admin/home.jsp') return result
In the script, you have a method for each recorded page, which are called page1, page2, and so forth. Within each page, there are one or more GET and/or POST requests made via the request objects, which simulate the transactions made by a single page. Let's take a look at the page1 method. In this method you can see that it makes three requests. The first two requests are simple requests without any parameters. The last request, however, is a GET request with a querystring. The script uses the httpUtilities variable (defined earlier, at the beginning of the script) to get the value of the msg parameter from the URI, and constructs the querystring for the request.
The page2 method provides an example of a POST request. The values to the POST are supplied in name-value pairs using the NVPair object. Also notice the calls to grinder.sleep(). These calls simulate "think time".
In both methods, you can see that the return value from the first request is assigned to a variable called result. The method then returns this variable (which contains statistics from Grinder).
Utility method, and calls to the utility method
I'm going to go out of order here and talk about the instrumentMethod utility method before I talk about the __call__ method. The instrumentMethod and the calls to that method look like this:
def instrumentMethod(test, method_name, c=TestRunner): """Instrument a method with the given Test.""" unadorned = getattr(c, method_name) import new method = new.instancemethod(test.wrap(unadorned), None, c) setattr(c, method_name, method) # Replace each method with an instrumented version. # You can call the unadorned method using self.page1.__target__(). instrumentMethod(Test(100, 'Page 1'), 'page1') instrumentMethod(Test(200, 'Page 2'), 'page2') instrumentMethod(Test(300, 'Page 3'), 'page3') instrumentMethod(Test(400, 'Page 4'), 'page4') instrumentMethod(Test(500, 'Page 5'), 'page5') instrumentMethod(Test(600, 'Page 6'), 'page6') instrumentMethod(Test(700, 'Page 7'), 'page7')
The instrumentMethod method is another way to proxy calls through the Test object, except, instead of proxying an object, it proxies a method. To accomplish this, instrumentMethod performs some metaprogramming. Metaprogramming lets you modify a class's properties and methods during runtime. You can add, remove, and modify existing methods and properties. The instrumentMethod method has three arguments. The first argument is a test object, the second is the method name (as a string), and the third argument is a default argument that is set to TestRunner. First, instrumentMethod gets a reference to a TestRunner class method identified by method_name (assigned to the variable unadorned). Then it creates a new method (using new.instancemethod) which has the test object wrapped around the original method. instrumentMethod then assigns this new method back to the TestRunner class, effectively proxying all calls to the original method through the Test object.
__call__ method
In Python any method that implements the __call__ is _callable_, which basically means that this function is called when you call the class instance as a function. In the context of the this script, it just means that this is where all the testing starts off. In the __call__ method you can see that there are calls to all the recorded page methods (along with think time), and this essentially runs the recorded test.
Conclusion
Now you should have a general idea of what a recorded Grinder test-script looks like. In my next tutorial, I'll go over writing performance tests in Grinder using a testing framework.
Now this is something I can really appreciate…I’ve been looking for info on how to handle the security certificate issue (I’m not a developer) and am looking at using Grinder with Amazon EC2 to do load testing. Thanks for your posts!
@Corazon
Thanks! I’m working on a new post right now that deals with using a testing framework for Grinder. I’ll try and post it tonight.
@vivin
Can you give me the framework that you have developed …?? is it Open source??
@kamalakar
Yes, it’s open source. I’ve got it available for download in the last page of my framework tutorial!
The Grinder is the best tool for performance testing. I’ve working on GrinderStone – IDE for Grinder scripts which allows debug scripts using Eclipse and provides some interesting features for development like modularity and pretty useful logging in debug mode. That project you can download from official project site:
http://code.google.com/p/grinderstone
we also have Eclipse Update site for simple plugin installation into Eclipse platform. All details you can obtain on our site and support group. GrinderStone gives you more power to develope Grinder scripts.
I think it should be usefull to add assertion in a load test, don’t you think ?
But i didn’t find how to increment grinder errors in the console with assertions, do you ?
@Borislav Andruschuk
Thanks Boris. I’ve used Grinderstone once; it was about two years ago. I’d like to give it a try again sometime. I bet it’s come along pretty far! 🙂
@Damien GOUYETTE
Assertions in what sense? I don’t know if a performance test is the right place for an assertion; they might make more sense in a functional test.
I you have several account to login, you should veriffy than you are logger with the right name and not another ont to check problems like code re-entry, no ?
you have several account to login, you should verify than you are logged with the right name and not another one to check problems like code re-entry, no ?
@Damien GOUYETTE
That sort of check is best suited for a functional test (something like Selenium). With a performance test, you’re only checking the performance of the app and not how it functions.
Its a grt article. i wish to test multiple urls concurrently.. Can u give some ideas
Thanks A lot for you great effort.
I want to know how do i log the request and response data and some customize (like first name last name user id password )data ?
what do need to import in
[import statements] section
[header definitions]section
and in other section also
hi,
this article is very useful for us. i have some doubt, is there any way to perform test concurrently ?
how to store the test history?
Hey Krishna, did you ever get a answer to your question about running different scripts concurrently?