Learning center
Lua load script tutorial
— Lua load script tutorial —
1. Introduction
In order to create much more complex behaviour from the simulated clients in a load test, you might want to take a look at the scripting functionality of Load Impact. By using the modern, high-level programming language Lua together with the Load Impact API you can make your simulated clients perform very complex tasks on the site being tested. A typical example of complex tasks you might want to emulate is a “shopping scenario” on an e-commerce site where the simulated users should browse the inventory, place random products in their shopping cart, then eventually checking out and performing a payment transaction.
The load script is a small (or sometimes not so small) program that describes what each simulated client should do during the load test. Every simulated client will run its own execution thread, executing the load script until it runs to completion. After that, the client will run the load script again and again, repeating it infinitely until the load test is finished and/or the client is shut down (the client may be shut down before the load test is completed in case the user configures the test to ramp down the load level at some point during the test).
In this guide we will provide you with a set of examples on how to use the Load Impact API and the Lua language to create load scripts that perform several common actions.
We assume that you already have some basic programming skills (Lua knowledge is not necessary).
2. Lua gotchas
The programming language Lua is widely accepted as simple, elegant and easy to learn. It is also light-weight and very suitable for resource-intensive applications such as load testing. But, as with any other language, it has its own pitfalls. Here is a short list of gotchas that are most often encountered by people coming to Lua from other languages. (Note that in several code examples we use the function log.log() to print out text. This is the API function provided by the Load Impact API to log text messages during execution of a load script)
- Conversion to boolean
Any value except `nil` and `false` is considered to be `true` in Lua. Note that this means that `0` is `true` as well:
if 0 then
log.info("zero is true")
else
log.info("zero is false")
end
--> Prints "zero is true"
- Variables are global by default
You have to explicitly declare local variables using keyword `local`. This means that a typo in variable name may potentially bring you trouble:
local has_color = true
if has_colour then -- Note typo
log.info("in color")
else
log.info("in monochrome")
end
--> Unexpectedly prints "in monochrome" since has_colour is nil
It is considered a good practice to declare all variables as local and to limit their scope as much as possible.
- Arrays / tables
Lua has single all-powerful data structuring type, called “table”. All common data stuructures may be represented as a Lua table. Linear arrays in Lua are also represented as tables.
- Arrays indices start at one, not zero
Keep in mind that array indices in Lua start at one (1):
local t = { "one", "two", "three" }
log.info(tostring(t[0])) --> nil
log.info(tostring(t[1])) --> one
Key `0` does not belong to the array part of the table:
t = { }
t[0] = "zero"
log.info(tostring(#t)) --> 0
t[1] = "one"
log.info(tostring(#t)) --> 1
- Holes in arrays
The table length (i.e. array size) definition in Lua is a bit unusual and worth reading carefully:
The length of a table `t` is defined to be any integer index `n` such that `t[n]` is not `nil` and `t[n+1]` is `nil`; moreover, if `t[1]` is `nil`, `n` can be zero. For a regular array, with non-nil values from `1` to a given `n`, its length is exactly that `n`, the index of its last value. If the array has "holes" (that is, `nil` values between other non-nil values), then `#t` can be any of the indices that directly precedes a `nil` value (that is, it may consider any such `nil` value as the end of the array).
From the Lua manual – http://www.lua.org/manual/5.1/manual.html#2.5.5
This affects not only length operator `#`, but any code that uses it directly or indirectly, like functions `unpack()` and `table.concat()`.
local t = { "one", "two", "three", "four" }
log.info(tostring(#t)) --> 4
t[3] = nil -- Make a hole
log.info(tostring(#t)) -- May print either 2 or 4
In general, avoid making “holes” in tables that you use as a linear arrays‚ unless you know what you’re doing.
3. Where to find more information
Information available at loadimpact.com
- The Load Impact load script API: http://loadimpact.com/learning-center/load-script-api
Official Lua documentation
- Lua Reference Manual: http://www.lua.org/manual/5.1/
- Programming in Lua 2nd ed.: http://www.lua.org/docs.html#pil
Lua community
- Lua Unofficial Frequently Asked Questions (FAQ): http://batbytes.com/luafaq/
- Lua users wiki: http://lua-users.org/wiki/
4. Examples
4.1. Load test a single resource
The simplest possible behavior for the client is to issue a HTTP request for a just one single resource (URL) on your site. Lets try it out:
http.get("http://google.com")
That’s it. Each client will issue a single GET request (via `http.get()`) to the given URL every time it executes the load script. In the example the URL is `http://google.com`, you should of course replace it with an URL that accesses your site.
4.2. Using HTTPS
Note that you may use either HTTP or HTTPS when fetching resources. To use HTTPS you just change the URL so it starts with `https://`:
http.get("https://www.amazon.com")
4.3. GET request parameters
If you need to pass GET request parameters, just add them to the URL string:
http.get("http://test.loadimpact.com/pi.php?decimals=18")
4.4. POST requests
To pass POST data, use `http.post()` and supply the post data as the fourth parameter:
http.post(
"http://test.loadimpact.com/login.php",
nil, -- use default IP
nil, -- use default headers
"login=test_user&password=123"
)
See also:
- The Load Impact load script API: http://loadimpact.com/learning-center/load-script-api
- http.post(): http://loadimpact.com/learning-center/load-script-api#http.post
4.5. Load-test multiple resources
Your website visitors might commonly visit more than one page on your site. This means that load testing a single page is not going to be a very realistic load test. To create a more realistic “user scenario”, we want our load test to simulate the behaviour of our site visitors, i.e. load multiple pages.
(Note to the advanced reader: we will simplify things a bit here, and pretend, that you’re only interested in loading the main HTML code for a page, without all the page dependencies)
Let’s try out a simple case first. Say, you notice that your users usually visit two URLs during a session: the news page and the page with contacts. You then have to fetch these two URLs in the client script:
http.get("http://test.loadimpact.com/news.php")
http.get("http://test.loadimpact.com/contacts.php")
4.6. Random delays between requests
Now, in the example above, the two pages would be loaded one after the other, with absolutely no pause in between. Just like it is uncommon for a visitor to load one single page and then leave your site, it is probably very uncommon that a user loads one page and then immediately loads another page, with no time to read the content on the first page. Humans need a little while to process information on a page before moving on. It is often called “think time” or “page view time”. To simulate this behavour of real, human visitors, we have to add delays to our load script:
http.get("http://test.loadimpact.com/news.php")
client.sleep(
math.random(1, 15) -- Sleeps between 1 and 15 seconds before continuing
)
http.get("http://test.loadimpact.com/contacts.php")
See also:
- The Load Impact load script API: http://loadimpact.com/learning-center/load-script-api
- client.sleep(): http://loadimpact.com/learning-center/load-script-api#client.sleep
- math.random(): http://www.lua.org/manual/5.1/manual.html#pdf-math.random
4.7. Read HTTP headers from a response
You have a direct access to the HTTP headers in the response from the server:
local response = http.get("http://google.com")
if client.get_id() 1 and client.get_repetition() 1 then -- Let client 1 do it, and only once
log.info(
"google.com uses `" .. tostring(response.headers["Server"][1]) .. "' as a server software"
)
end
See also:
- The Load Impact load script API: http://loadimpact.com/learning-center/load-script-api
- The http.Response object: http://loadimpact.com/learning-center/load-script-api#http.Response
4.8. Setting headers for a request
Sometimes you need to set custom HTTP headers for your request. One reason is to see if your server handles client-side caching.
If the web browser already has the requested resource in its cache, it may send a If-Modified-Since header, telling the server that it only wants the server to return the resource if it has been changed since last time the browser fetched it. When server sees this header, it may respond “304 Not modified”, instead of returning the actual content, thus saving bandwidth.
(Note that some web servers use an exact match for the modification date check instead of a “less-than” operation (this is explicitly allowed by the standard). So you have to send the exact date you received in the “Last-Modified” header from the server last time you requested the resource)
In the example we assume that you do know the exact page modification date.
local response = http.get(
"http://test.loadimpact.com/",
nil, -- Use default IP
{
["If-Modified-Since"] = "Tue, 27 Apr 2010 03:30:00 GMT";
}
)
if response.status_code==304 then
log.info("not modified")
elseif response.status_code==200 then
log.info("modified")
else
log.error("unexpected code" .. tostring(response.status_code))
end
(Note that you may want to additionally test the support for
“If-None-Match” header)
See also:
- The Load Impact load script API: http://loadimpact.com/learning-center/load-script-api
- The http.Response object: http://loadimpact.com/learning-center/load-script-api#http.Response
- If-Modified-Since header: http://en.wikipedia.org/wiki/List_of_HTTP_headers
4.9. Verifying resource contents
When your website gets stressed it may at some point start to return errors instead of expected page content. Sometimes these errors can be in the form of HTTP error codes (500-codes), which are easy to detect on our side. 500-errors constitute “unsuccessful” HTTP transactions where the server returned no useful content and for those transactions you get separate statistics in our user interface (you can plot separate graphs for them etc).
However, another common outcome is that you get valid HTML code back, and a 200-response (indicating a successful HTTP transaction), but where the actual content returned is just an HTML page with an error message. A human user will read the error message and note that it was not the content they were expecting, but how does a computer program (our simulated user running the load script) do that? This is where you need to write content verification code.
Here is a script that verifies that it is getting the expected content, and if it doesn’t, it will log an error message:
local request = http.get(
"http://test.loadimpact.com/pi.php?decimals=18",
nil,
nil,
nil,
nil,
100 -- Tell the system it should store up to 100 bytes of body data for us
)
if request.body ~= "3.141592653589793238" then
log.error(
"PI calculator returned unexpected result when stressed with "
.. test.get_clients() .. " simulated clients: " .. request.body
)
end
IMPORTANT! – don’t forget to add the last parameter (100) – response_body_bytes – to the get request if you want to examine the reply from the server. Without this extra parameter, response.body will be empty. This is a very common mistake many make. It might seem like a silly parameter to require, but many server responses can be quite big, consuming a lot of memory on the load generator host if we have to save them all, and most of the time, they are not used at all by the client load script.
See also:
- The Load Impact load script API: http://loadimpact.com/learning-center/load-script-api
- http.get(): http://loadimpact.com/learning-center/load-script-api#http.get
4.10. Conditional logic in load scripts
What if your client behaviour depends on the results of previous requests?
Let’s imagine a coin tossing game. Our client bets on head or tails. The server tosses the coin and returns the result as one of two possible text messages:
Toss result: heads!or
Toss result: tails!If the client wins, he should then request `won.php`, otherwise `lost.php`. This means that the client has to make a request to the server to make it perform the coin flip, then read the returned result from the server and act upon its contents, either loading “won.php” or “lost.php”.
-- Define a list of coin sides
local coin_sides = { "heads", "tails" }
-- Pick a random coin side
local coin_side = coin_sides[math.random(#coin_sides)]
-- Tell server what we're betting on.
local response = http.get(
"http://test.loadimpact.com/flip_coin.php?bet=" .. coin_side
)
-- Extract a coin toss result from server response
local toss_result = response.body:find("Toss result: (%s+)!")
-- Check that toss_result contains expected data
if not (toss_result "heads" or toss_result "tails") then
-- Print error to log
log.error(
"server returned unexpected response: "
.. (toss_result or response.body) -- toss_result would be nil if find() fails
)
else -- Toss result is valid, go on
-- If coin toss result matches our bet, request a victory page
if toss_result == coin_side then
http.get("http://test.loadimpact.com/won.php")
else -- Otherwise we've lost
http.get("http://test.loadimpact.com/lost.php")
end
end
See also:
- The Load Impact load script API: http://loadimpact.com/learning-center/load-script-api
- math.random(): http://www.lua.org/manual/5.1/manual.html#pdf-math.random
- string.find(): http://www.lua.org/manual/5.1/manual.html#pdf-string.find
- Lua string patterns: http://www.lua.org/manual/5.1/manual.html#5.4.1
4.11. Simulating browser behavior
When a browser loads a HTML page it usually requests a whole series of resources/files – it loads first the main HTML code, then CSS files, images, Javascript files, and so on. It can even load other HTML files in iframe tags or via AJAX calls.
Modern browsers load all these resources asynchronuously, using multiple network connections over which they issue several requests in parallel. This gets the whole web page loaded faster, and also puts a lot of extra stress on the web server, which has to deliver a lot of different resources in a short amount of time, across several network connections. If you want your load test to be realistic, you should try and make sure that your simulated clients emulate this browser behaviour as closely as possible.
You can use `http.request()` (or its wrappers, `http.get()` and `http.post()`) to issue a blocking HTTP request call that will load only a single resource, but if you want to take advantage of the ability of the Load Impact load generator to load multiple resources at once, across multiple network connections, you should use `http.request_batch()` to issue multiple request in parallel.
(Tip: You can use a browser plugin like Firebug, `http://getfirebug.com/` or HTTP debugging proxy like Charles, `http://www.charlesproxy.com/` to get an idea about how your browser issues requests)
Here is an example that shows a load script that loads all the content on the page `http://test.loadimpact.com/` in a way that emulates a modern browser:
http.get("http://test.loadimpact.com") -- Get the main HTML code for the page
-- Get resources mentioned in the HTML code we just loaded
http.request_batch({
{ "GET", "http://test.loadimpact.com/style.css" },
{ "GET", "http://test.loadimpact.com/favicon.ico" }
})
-- Get all the resources that are mentioned in the CSS
http.request_batch({
{ "GET", "http://test.loadimpact.com/images/logo.png" }
})
4.12. Verifying batch request content
The `http.request_batch()` returns a table of `http.Response` objects, one for each request:
-- Create a table with correct request sizes
local sizes = {
103,
3533,
4,
}
-- Request resources
local responses = http.request_batch({
{ "GET", "http://test.loadimpact.com/style.css"},
{ "GET", "http://test.loadimpact.com/images/logo.png"},
{ "GET", "http://test.loadimpact.com/pi.php"},
})
-- All requested URLs are for static resources, so we verify size.
for i = 1, #responses do
if responses[i].body_size ~= sizes[i] then
log.error(
"Unexpected body size for " .. responses[i].request_url
.. " when stressed with with " .. test.get_clients()
.. " simulated clients: got " .. responses[i].body_size
.. " expected " .. sizes[i]
)
end
end
IMPORTANT! – don’t forget to add the 8th parameter – response_body_bytes – to the batch requests if you want to examine the reply from the server. Without this extra parameter, response.body will be empty. See the earlier example 4.9 – Verifying resource contents for more information.
See also:
- The Load Impact load script API: http://loadimpact.com/learning-center/load-script-api
4.13. Simulating user login
To load test certain pages on your website you may require to create a session for an user account on that site.
It is not very hard to do:
-- Authorize user
local response = http.post(
"http://test.loadimpact.com/login.php",
nil, -- use default IP
nil, -- use default headers
"login=test_user&password=1234"
)
-- Get User ID and Session ID from cookies
local uid, sid = response.cookies["uid"], response.cookies["sid"]
if not uid or not sid then
log.error("authorization failed")
return -- Login failed, nothing to do
end
-- Get user's message list
response = http.get(
"http://test.loadimpact.com/my_messages.php",
nil, -- use default IP
{
["Cookie"] = "uid=" .. uid .. "; sid=" .. sid .. ";"
}
)
See also:
- The Load Impact load script API: http://loadimpact.com/learning-center/load-script-api
4.14. Simulating complex user behavior
If you do not want to write a complex scenario for your clients, you may be able to approximate it by picking URLs randomly.
-- List URLs we want to randomly load
local urls =
{
"http://test.loadimpact.com";
"http://test.loadimpact.com/news.php";
"http://test.loadimpact.com/contacts.php";
}
-- Get a random URL from the list
http.get(urls[math.random(#urls)])
4.15. Weights
If you know that your average user visits some pages more often than another, you may use that knowledge to improve the load profile approximation. The simplest way to do this is to mention the same URL several times in the list:
-- URLs we want to randomly load
local INDEX = "http://test.loadimpact.com"
local NEWS = "http://test.loadimpact.com/news.php"
local CONTACTS = "http://test.loadimpact.com/contacts.php"
-- Index page is seven times more popular than contacts.
-- News page is three times more popular than contacts.
local urls =
{
INDEX, INDEX, INDEX, INDEX, INDEX, INDEX, INDEX, -- x7
NEWS, NEWS, NEWS, -- x3
CONTACTS -- x1
}
-- Get a random URL from the list
http.get(urls[math.random(#urls)])
4.16. Behavior table
To get even more flexibility, instead of using a table of URL strings, you may use a table of Lua functions, where each function would describe the behavior of one “kind” of your website’s clients.
You may code whole user stories this way:
-- URLs we want to load local INDEX = "http://test.loadimpact.com" local NEWS = "http://test.loadimpact.com/news.php" local CONTACTS = "http://test.loadimpact.com/contacts.php"
-- This client gets index and then closes the browser local get_index = function() http.get(INDEX) end
-- This one gets index and then goes to read the news
local get_index_then_news = function()
http.get(INDEX)
-- Sleep a bit
client.sleep(
math.random(1, 15)
)
-- Go read news
http.get(NEWS)
end
-- This one refreshes the news page few times
-- because he can't wait for the new product announcement,
-- and then goes to contacts to find sales' phone number.
local refresher = function()
local number_of_refreshes = math.random(1, 10)
-- Come on! I can't wait for the new iThing announcement!
for i = 1, number_of_refreshes do
-- Is it ready?!
http.get(NEWS)
-- Sleep a bit
client.sleep(
math.random(1, 5)
)
end
-- Sleep more
client.sleep(
math.random(5, 15)
)
-- I should phone them!
http.get(CONTACTS)
end
-- List of our scenarios (scenario-functions)
local scenarios =
{
get_index;
get_index_then_news;
refresher;
}
-- Pick random scenarios function... local scenario = scenarios[math.random(#scenarios)] -- ...and execute it scenario()
See also:
- The Load Impact load script API: http://loadimpact.com/learning-center/load-script-api
- math.random(): http://www.lua.org/manual/5.1/manual.html#pdf-math.random