Writing a load test with Tsung for a Ruby on Rails app for the first time was a little bit confusing. There are some tutorials, but I could not find a tutorial on testing a login session or HTTP post with an authenticity token.

First, I will briefly describe why we chose Tsung, and then I will provide a step-by-step instruction for how to implement a Tsung load test for a login and post session. Don’t we all love XML? ;)

Why Tsung?

Tsung is “an open-source multi-protocol distributed load testing tool which can simulate users in order to test the scalability and performance of IP based client/server applications”.

It is written in Erlang - but don’t be afraid, I don’t have a clue about Erlang and still got it working - thanks to my colleagues who I bombarded with questions…:)

We used Tsung because:

Using Tsung for a Login and Post Session

Installation for Mac OS X

Please see the Tsung Docs if you are not using Mac. To install Tsung, use Homebrew and this prompt: brew install tsung.

For graphical output, you need to install Template Toolkit which is used for HTML reports. This step is optional. If you have CPAN, use cpan: sudo cpan Template. If not, install from the source code.

Configuration

The configuration files your are creating should be located in yourPath/.tsung. Log files and recorded sessions are saved in ~/.tsung/log/, and a new subdirectory is created for each test using the current date and time for the directory name, e.g. ~/.tsung/log/20180717-0940.

Creating your first config Tsung file

You can now create your config file:

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE tsung SYSTEM "/usr/local/Cellar/tsung/1.7.0/share/tsung/tsung-1.0.dtd" [
<!ENTITY login_post_session SYSTEM "tsung_recorder20180717.xml">
]>

<tsung loglevel="debug">

  <clients>
    <client host="localhost" use_controller_vm="true" maxusers='10000'/>
  </clients>

  <servers>
    <server host="yourWebsite.de" port="443" type="ssl"/>
  </servers>

  <!-- test is finished after 2 minutes -->
  <load duration="2" unit="minute">
    <!-- during the first second of this test 1 new user per second is created,
    maximum of users is 1 -->
    <arrivalphase phase="1" duration="2" unit="minute">
      <users arrivalrate="1" unit="second" maxnumber="1"></users>
    </arrivalphase>
  </load>

  <options>
  <!-- adding csv file with user-email and user-password for login -->
  <option name="file_server" id="userlist" value=".tsung/example_userlist.csv"/>
  </options>

  <sessions>
    &login_post_session;
  </sessions>

</tsung>

Next, you need to adapt the following code:

Example csv-list:

user1;password1
user2;password2

Creating a session

You should now be using the Tsung proxy recorder which allows you to record your login and post session to a configuration file. A session defines the content of the scenario itself. The proxy is listening to port 8090, and you can change the port with -L portnumber.

To start, set the manual browser configuration (see connection settings in your preferred browser via preferences) for SSL or HTTP to localhost (or your url) and port 8090. Now, start the recorder with: tsung-recorder start and start clicking through your application. To stop the recorder just type tsung-recorder stop.

Keep in mind that there will be a lot of overhead in your recording session. You can delete and adjust many GET requests for your usage.

Changing your session

To run this session repeatedly with different users from your csv file and with different authentication tokens, the file needs to be adjusted. What I will cover is how to handle authenticity tokens, user-email and user-password. If the content-type of your POST request is application/x-www-form-urlencoded, these need to be changed into dynamic variables, and then parsed to url-encoded variables which were used for login and post requests.

First, you have to set dynamic variables for user-email and user-password from your csv file directly after the session tag in the beginning.

<session name='rec20180924-1355' probability='100'  type='ts_http'>

<!-- sets dynamic variables from csv list with id userlist -->
<setdynvars sourcetype="file" fileid="userlist" delimiter=";" order="iter">
  <var name="user_email" />
  <var name="user_password" />
</setdynvars>

If you are also using an authenticity token in a hidden form field on your site, you can grab it and just put it in as a child element directly after your request: <dyn_variable name="name_of_your_hidden_field_form"></dyn_variable> and before the http tag.

Now there is a need for parsing those variables. Otherwise your dynamic variables could have an empty space or something else which would not work for our content-type.

Therefore, you have to set new dynamic variables with <setdynvars> for authenticity-token, user-email and user-password, and parse those to url-encoded variables. You can do this by using a tiny bit of Erlang code and this method: http_uri:encode(example_var).

<request>
  <!-- sets dynamic variable from hidden form field authenticity_token -->
  <dyn_variable name="authenticity_token"></dyn_variable>
  <http url='/login' version='1.1' method='GET'>
    <http_header name='Accept-Encoding' value='gzip, deflate' />
  </http>
</request>

<!-- parses dynamic variables user_email, user_password and authenticity_token to url-encoded variables -->
<setdynvars sourcetype="eval"
            code="fun({Pid,DynVars})->
                      {ok,Val}=ts_dynvars:lookup(user_email,DynVars),
                      http_uri:encode(Val) end.">
  <var name="encoded_user_email" />
</setdynvars>

<setdynvars sourcetype="eval"
            code="fun({Pid,DynVars})->
                      {ok,Val}=ts_dynvars:lookup(user_password,DynVars),
                      http_uri:encode(Val) end.">
  <var name="encoded_user_password" />
</setdynvars>

<setdynvars sourcetype="eval"
            code="fun({Pid,DynVars})->
                      {ok,Val}=ts_dynvars:lookup(authenticity_token,DynVars),
                      http_uri:encode(Val) end.">
  <var name="encoded_authenticity_token" />
</setdynvars>

Now, you have to integrate your dynamic variables in your POST request. First append subst="true" to the <request> for a dynamic variable request.

Afterwards you adapt the content of your POST and integrate the variables like this: authenticity_token=%%_encoded_authenticity_token%%&amp;. One thing to remember is that all dynamic variables must be prefixed with an underscore in the requests. When you define a variable with the name test_var, you need to call it via %%_test_var%% in your request.

This giant query string example shows what it could look like:

contents='authenticity_token=%%_encoded_authenticity_token%%&amp;user_session%5Bemail%5D=%%_encoded_user_email%%&amp;user_session%5Bpassword%5D=%%_encoded_user_password%%&amp;commit=Log+in'

<!-- for using dynamic variables the request has to be subst="true"-->
<request subst="true">
  <!-- sets encoded dynamic variables for authenticity_token, user-email and user-password-->
  <http url='/user_startpage' version='1.1'
    contents='authenticity_token=%%_encoded_authenticity_token%%&amp;user_session%5Bemail%5D=%%_encoded_user_email%%&amp;user_session%5Bpassword%5D=%%_encoded_user_password%%&amp;commit=Log+in'
    content_type='application/x-www-form-urlencoded'
    method='POST'>
    <http_header name='Accept-Encoding' value='gzip, deflate' />
  </http>
</request>

If you have to use an authenticity token again, you are doing the same procedure again and again.

Congrats! You now have adapted your session file into a properly working file. You just need to run it :). See the full session here:

<session name='rec20180924-1355' probability='100'  type='ts_http'>

<!-- sets dynamic variables from csv list with id userlist -->
<setdynvars sourcetype="file" fileid="userlist" delimiter=";" order="iter">
  <var name="user_email" />
  <var name="user_password" />
</setdynvars>

<request>
  <http url='yourWebsite.de' version='1.1' method='GET'>
    <http_header name='Accept-Encoding' value='gzip, deflate' />
  </http>
</request>

<!-- thinktime of user is random but between 2 and 5 seconds -->
<thinktime random="true" min="2" max="5"/>

<request>
  <!-- sets dynamic variable from hidden form field authenticity_token -->
  <dyn_variable name="authenticity_token"></dyn_variable>
  <http url='/login' version='1.1' method='GET'>
    <http_header name='Accept-Encoding' value='gzip, deflate' />
  </http>
</request>

<thinktime random="true" min="2" max="8"/>

<!-- parses dynamic variables to url-encoded variables -->
<setdynvars sourcetype="eval"
            code="fun({Pid,DynVars})->
                      {ok,Val}=ts_dynvars:lookup(user_email,DynVars),
                      http_uri:encode(Val) end.">
  <var name="encoded_user_email" />
</setdynvars>

<setdynvars sourcetype="eval"
            code="fun({Pid,DynVars})->
                      {ok,Val}=ts_dynvars:lookup(user_password,DynVars),
                      http_uri:encode(Val) end.">
  <var name="encoded_user_password" />
</setdynvars>

<setdynvars sourcetype="eval"
            code="fun({Pid,DynVars})->
                      {ok,Val}=ts_dynvars:lookup(authenticity_token,DynVars),
                      http_uri:encode(Val) end.">
  <var name="encoded_authenticity_token" />
</setdynvars>

<!-- for using dynamic variables the request has to be subst="true"-->
<request subst="true">
  <!-- sets encoded dynamic variables for authenticity_token, user-email and user-password-->
  <http url='/user_startpage' version='1.1' contents='authenticity_token=%%_encoded_authenticity_token%%&amp;user_session%5Bemail%5D=%%_encoded_user_email%%&amp;user_session%5Bpassword%5D=%%_encoded_user_password%%&amp;commit=Log+in'
  content_type='application/x-www-form-urlencoded'
  method='POST'>
    <http_header name='Accept-Encoding' value='gzip, deflate' />
  </http>
</request>

<thinktime random="true" min="5" max="10"/>

<request>
  <http url='/my_overview' version='1.1' method='GET'>
    <http_header name='Accept-Encoding' value='gzip, deflate' />
  </http>
</request>

<thinktime random="true" min="4" max="6"/>

<request>
  <!-- sets dynamic variable from hidden form field authenticity_token -->
  <dyn_variable name="authenticity_token"></dyn_variable>
  <http url='/my_posts' version='1.1' method='GET'>
    <http_header name='Accept-Encoding' value='gzip, deflate' />
  </http>
</request>

<thinktime random="true" min="4" max="10"/>

<!-- parses dynamic variable authenticity_token to url-encoded variable -->
<setdynvars sourcetype="eval"
            code="fun({Pid,DynVars})->
                      {ok,Val}=ts_dynvars:lookup(authenticity_token,DynVars),
                      http_uri:encode(Val) end.">
  <var name="encoded_post_authenticity_token" />
</setdynvars>

<request subst="true">
  <http url='/posts' version='1.1'
  contents='authenticity_token=%%_encoded_post_authenticity_token%%&amp;post%5Btitle%5D=Yeah&amp;post%5Bmessage%5D=HelloWorld&amp;commit=Post'
  content_type='application/x-www-form-urlencoded'
  method='POST'>
    <http_header name='Accept-Encoding' value='gzip, deflate' />
  </http>
</request>

<request>
  <http url='/my_posts/1' version='1.1' method='GET'>
    <http_header name='Accept-Encoding' value='gzip, deflate' />
  </http>
</request>

<thinktime random="true" min="4" max="6"/>

</session>

Running and Reporting

Step 1: Run your chosen configuration file: cd to the folder where this configuration file is saved e.g. yourPath/.tsung and start the file with: tsung -f nameOfYourLoadtest.xml start (tsung -h for man page)

Step 2: Go to log directory: cd ~/.tsung/log/nameOfYourLog

Step 3: Generate graphical report: execute /usr/local/Cellar/tsung/1.7.0/lib/tsung/bin/tsung_stats.pl to generate static graph (you might adapt this to your system).

Step 4: Open report in browser: e.g firefox report.html

I hope this tutorial can help you to set up your Tsung load test.