A Step-By-Step Script Tutorial
Introduction
The is a step-by-step tutorial of how to write a number of dynamic HTTP tests using various aspects of The Grinder and Jython APIs. The test script contains a number of tests that are requests to the same URL. For each request, a different XML parameter is specified. The resulting HTML data is checked on return and if the test was not successful, the statistics API is used to mark that test as failed.
Richard Perks
Script Imports
import string import random from java.lang import String from java.net import URLEncoder from net.grinder.script import Test from net.grinder.plugin.http import HTTPRequest from net.grinder.common import GrinderException
Firstly when writing a script come the import statements. These include imports of standard Python modules such as string and random, and other Java imports including some language and network classes. Finally there are imports for Grinder specific methods. A powerful feature of the Jython scripts that are used with The Grinder is the ability to take a mix and match approach to script programming. In some cases using a Python API is quicker and easier than always using the corresponding Java API calls, so feel free to use whichever API makes most sense.
Test Definition
tests = { "News01" : Test(1, "News 1 posting"), "Sport01" : Test(2, "Sport 1 posting"), "Sport02" : Test(3, "Sport 2 posting"), "Trading01" : Test(4, "Trading 1 query"), "LifeStyle01" : Test(5, "LifeStyle 1 posting"), }
To keep the script code easy to read, we next define all the tests we are going to be running within this script. These are created as a Python dictionary and are name-value pairs. The name is the name of the test and the value is a Test object with a test numeric identifier and description.
Bread crumbs
log = grinder.logger.info # Server Properties SERVER = "http://serverhost:7001" URI = "/myServlet"
We next define some variables such as Grinder helper methods and server properties. The log variable is used to hold a reference to The Grinder logging mechanism and is used throughout the script.
The Test Interface
class TestRunner: def __call__(self):
Here is the definition of our test class and the method called by The Grinder by each test thread. All scripts must define this class and method. Whilst we are discussing classes and methods, an important point to remember when new to Jython script development is that Jython/Python code is scoped by indentation, rather than using braces like in a language like C or Java. The colon is used to delimit the start scope such as an if or method definition.
Using the Dictionary and Random Python Modules
for idx in range(len(tests)): testId = random.choice(tests.keys()) log("Reading XML file %s " % testId)
As discussed earlier, the use of Python modules is encouraged during Grinder script development and I have used a few examples above when performing the test run. Within the test run, each of the tests defined in the test dictionary is looped round so that each Grinder thread executes five separate tests. Within the loop, a test is chosen randomly from one of the five tests. This prevents all threads of executing all the tests in the same order and helps simulate a more random load on the server.
Within the dictionary defined as tests, there are a number of useful methods such as are keys(), items() and sort(). We use the keys returned from the tests dictionary as the parameter to the choice() method in the random module. This randomly selects one of the tests keys as the current test identifier.
Forget the Java IO Package when Handling Files
file = open("./CAAssets/"+testId+".xml", 'r') fileStr = URLEncoder.encode(String(file.read())) file.close() requestString = "%s%s%s%s" % (SERVER, URI, "?xmldata=", fileStr)
When having to retrieve the contents of files using Jython script, the use of the file operations blitz's Java IO for pure script development speed. In the code above, we need to open an XML document that has the name of a test, for example News01.xml. This will be used as a request parameter for the News01 test. The file is opened for reading and encoded using the Java URLEncoder.
We next construct the request string to the server by concatenating the server, URI and XML documents together. Tip: if you need to remove spaces from within a string, you can use a method like the following:
requestString = string.join(requestString.split(), "")
Sending the Request and the Statistics API
grinder.statistics.delayReports = 1 request = HTTPRequest() tests[testId].record(request) log("Sending request %s " % requestString) result = request.GET(requestString)
As part of the test execution, we want the ability to check the result of the HTTP request. If the response back from the server is not one that we except, we want to mark the test as unsuccessful and not include the statistics in the test times. To do this, the delayReports variable can be set to 1. Doing so will delay the reporting back of the statistics until after the test has completed and we have had chance to check its operation. The default is to report back when the test returns control back to the script, i.e. immediately after a test has executed.
Next we instrument the HTTPRequest with the test being executed. This enables any calls through HTTPRequest to be monitored by the Grinder. Any other time spent within the script will not be recorded by The Grinder. Be careful not to include extra script processing within a test; doing so will not give the correct statistics. Only test what is required.
The test itself is next executed which is a HTTP GET to the server using our previously constructed test string. Remember - these tests execute in a loop for the number of tests we have defined, using a random test each time.
if string.find(result.getText(), "SUCCESS") < 1: grinder.statistics.forLastTest.setSuccess(0) writeToFile(result.getText(), testId)
On return from the HTTP GET, we check the result for the string "SUCCESS". If the test has failed, this value will not be returned and the statistics object can be marked as unsuccessful. In the case of an unsuccessful test, we write the HTML output to a file for later analysis:
def writeToFile(text, testId): filename = "%s-%d-page-%d.html" % (grinder.processName, testId, grinder.runNumber) file = open(filename, "w") print >> file, text file.close()
Full Script Listing
# Send an HTTP request to the server with XML request values import string import random from java.lang import String from java.net import URLEncoder from net.grinder.script import Test from net.grinder.plugin.http import HTTPRequest from net.grinder.common import GrinderException tests = { "News01" : Test(1, "News 1 posting"), "Sport01" : Test(2, "Sport 1 posting"), "Sport02" : Test(3, "Sport 2 posting"), "Trading01" : Test(4, "Trading 1 query"), "LifeStyle01" : Test(5, "LifeStyle 1 posting"), } log = grinder.logger.info out = grinder.logger.TERMINAL # Server Properties SERVER = "http://serverhost:7001" URI = "/myServlet" class TestRunner: def __call__(self): for idx in range(len(tests)): testId = random.choice(tests.keys()) log("Reading XML file %s " % testId) file = open("./CAAssets/"+testId+".xml", 'r') fileStr = URLEncoder.encode(String(file.read())) file.close() # Send the request to the server requestString = "%s%s%s%s" % (SERVER, URI, "?xmldata=", fileStr) requestString = string.join(requestString.split(), "") grinder.statistics.delayReports = 1 request = HTTPRequest() tests[testId].record(request) log("Sending request %s " % requestString) result = request.GET(requestString) if string.find(result.getText(), "SUCCESS") < 1: grinder.statistics.forLastTest.setSuccess(0) writeToFile(result.getText(), testId) # Write the response def writeToFile(text, testId): filename = "%s-%d-page-%d.html" % (grinder.processName, testId, grinder.runNumber) file = open(filename, "w") print >> file, text file.close()