Skip to content

Rough Book

random musings

Menu
  • About Me
  • Contact
  • Projects
    • bAdkOde
    • CherryBlossom
    • FXCalendar
    • Sulekha
Menu

Anatomy of a Grinder test-script

Posted on August 14, 2009September 16, 2009 by vivin

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:

[sourcecode language="python"]
[
...
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)
...
]
[/sourcecode]

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:

[sourcecode language="python"]
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
[/sourcecode]

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:

[sourcecode language="python"]
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'), )
[/sourcecode]

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:

[sourcecode language="python"]
url0 = 'https://local.infusiontest.com:8443'
[/sourcecode]

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:

[sourcecode language="python"]
# 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)
[/sourcecode]

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:

Proxying through Test object

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:

[sourcecode language="python"]
# 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', '[email protected]'),
NVPair('Login', 'Login'), ),
( NVPair('Content-Type', 'application/x-www-form-urlencoded'), ))

grinder.sleep(72)
request202.GET('/Admin/home.jsp')

return result
[/sourcecode]

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:

[sourcecode language="python"]
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')
[/sourcecode]

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.

15 thoughts on “Anatomy of a Grinder test-script”

  1. Corazon says:
    September 15, 2009 at 9:14 pm

    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!

    Reply
  2. vivin says:
    September 15, 2009 at 9:22 pm

    @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.

    Reply
  3. Pingback: Rough Book » Writing Performance Tests in Grinder using a Framework
  4. kamalakar says:
    September 29, 2009 at 2:06 am

    @vivin
    Can you give me the framework that you have developed …?? is it Open source??

    Reply
  5. vivin says:
    September 29, 2009 at 12:25 pm

    @kamalakar
    Yes, it’s open source. I’ve got it available for download in the last page of my framework tutorial!

    Reply
  6. Borislav Andruschuk says:
    September 28, 2010 at 2:06 pm

    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.

    Reply
  7. Damien GOUYETTE says:
    November 16, 2010 at 5:36 am

    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 ?

    Reply
  8. vivin says:
    November 16, 2010 at 9:05 pm

    @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.

    Reply
  9. Damien GOUYETTE says:
    November 16, 2010 at 11:23 pm

    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 ?

    Reply
  10. Damien GOUYETTE says:
    November 16, 2010 at 11:24 pm

    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 ?

    Reply
  11. vivin says:
    November 23, 2010 at 10:20 am

    @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.

    Reply
  12. harine says:
    March 14, 2012 at 3:42 am

    Its a grt article. i wish to test multiple urls concurrently.. Can u give some ideas

    Reply
  13. Arvind says:
    July 8, 2012 at 10:29 pm

    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

    Reply
  14. Krishna says:
    January 17, 2013 at 10:20 pm

    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?

    Reply
    1. Chris says:
      June 26, 2015 at 2:47 pm

      Hey Krishna, did you ever get a answer to your question about running different scripts concurrently?

      Reply

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Meta

  • Log in
  • Entries feed
  • Comments feed
  • WordPress.org

Archives

  • February 2023
  • April 2020
  • February 2020
  • January 2020
  • December 2019
  • November 2019
  • September 2019
  • August 2019
  • July 2019
  • June 2019
  • May 2019
  • March 2019
  • February 2019
  • January 2019
  • December 2018
  • November 2018
  • September 2018
  • August 2018
  • July 2018
  • June 2018
  • May 2018
  • April 2018
  • March 2018
  • February 2018
  • January 2018
  • December 2017
  • November 2017
  • October 2017
  • June 2017
  • March 2017
  • November 2016
  • August 2016
  • July 2016
  • June 2016
  • February 2016
  • August 2015
  • July 2014
  • June 2014
  • March 2014
  • December 2013
  • November 2013
  • September 2013
  • July 2013
  • June 2013
  • March 2013
  • February 2013
  • January 2013
  • October 2012
  • July 2012
  • June 2012
  • January 2012
  • December 2011
  • November 2011
  • October 2011
  • September 2011
  • July 2011
  • June 2011
  • May 2011
  • February 2011
  • January 2011
  • December 2010
  • November 2010
  • October 2010
  • September 2010
  • July 2010
  • June 2010
  • May 2010
  • April 2010
  • March 2010
  • January 2010
  • December 2009
  • November 2009
  • October 2009
  • September 2009
  • August 2009
  • July 2009
  • May 2009
  • April 2009
  • March 2009
  • February 2009
  • January 2009
  • December 2008
  • November 2008
  • October 2008
  • August 2008
  • March 2008
  • February 2008
  • November 2007
  • July 2007
  • June 2007
  • May 2007
  • March 2007
  • December 2006
  • October 2006
  • September 2006
  • August 2006
  • June 2006
  • April 2006
  • March 2006
  • January 2006
  • December 2005
  • November 2005
  • October 2005
  • September 2005
  • August 2005
  • July 2005
  • June 2005
  • May 2005
  • April 2005
  • February 2005
  • October 2004
  • September 2004
  • August 2004
  • July 2004
  • June 2004
  • May 2004
  • April 2004
  • March 2004
  • February 2004
  • January 2004
  • December 2003
  • November 2003
  • October 2003
  • September 2003
  • July 2003
  • June 2003
  • May 2003
  • March 2003
  • February 2003
  • January 2003
  • December 2002
  • November 2002
  • October 2002
  • September 2002
  • August 2002
  • July 2002
  • June 2002
  • May 2002
  • April 2002
  • February 2002
  • September 2001
  • August 2001
  • April 2001
  • March 2001
  • February 2001
  • January 2001
  • December 2000
  • November 2000
  • October 2000
  • August 2000
  • July 2000
  • June 2000
  • May 2000
  • March 2000
  • January 2000
  • December 1999
  • November 1999
  • October 1999
  • September 1999
©2023 Rough Book | Built using WordPress and Responsive Blogily theme by Superb
All original content on these pages is fingerprinted and certified by Digiprove