Jan. 3rd, 2018

How to Run Locust with Different Users

Locust is a great performance framework because it provides powerful capabilities, if you are comfortable with programming and know some basic Python scripting. But even if you know Python well, it might be tricky to find a better approach to all the challenges you might encounter at the beginning of your Locust experience. In contrast to JMeter, Locust doesn’t have a user-friendly UI for test creation and detailed documentation for all its facilities. However, as soon as you find the workarounds and steps that you need to proceed to resolve your tasks, you will be paid back with Locust performance capabilities and other benefits of keeping your tests in code format.

 

In this article, we are going to help you and show 3 different workarounds for one of the most common challenges you might encounter when performance testing web applications - using different user credentials to test login action simulating unique users.

 

Those of you who have used JMeter before, know how easy it is to perform such an operation, by using the CSV Data set config element. In Locust this action is also relatively easy, but until you’ve done it for the first time, it might be not so straightforward. That’s why we are going to show you some handy ways to do it.

 

Creating an Example Locust Script with a Login Action

 

To make our example meaningful, we need to create a test script that simulates users login to a web application. http://blazedemo.com is a great option as it has login functionality available through the http://blazedemo.com/login endpoint, which takes the email and password as arguments and can be triggered by using the POST() HTTP method.

 

First, let’s create a script that performs a login with one user, to get the main idea of the script:

 

from locust import HttpLocust, TaskSet, task

class LoginWithUniqueUsersSteps(TaskSet):
    @task
    def login(self):
        self.client.post("/login", {
            'email': 'LocustPerformanceUser1@gmail.com', 'password': '123456'
        })

class LoginWithUniqueUsersTest(HttpLocust):
    task_set = LoginWithUniqueUsersSteps
    host = "http://blazedemo.com"
    sock = None

    def __init__(self):
        super(LoginWithUniqueUsersTest, self).__init__()

 

If you have some basic experience with Locust, the script should be very clear for you. If not, then it might be useful to go over the “Quick Start” section of the Locust web site. Briefly, this script simulates a login to the http://blazedemo.com by using a specific user.

 

This script works well until we have a requirement to test the login operation for different unique users. Of course, you can create separate functions for each user, but please never do that. All configurable parameters including user credentials should always be externalised (meaning that you should be able to provide parameters outside the code definition), and you should never hardcode specific user credentials inside your test functions. In a few sections below we are going to help you and show a couple of handy examples for that.

 

Keeping Credentials in the Locust Script Itself

 

First, if the list of credentials is relatively small, you can always store these credentials inside your performance script. For this we just need to make a separate array that stores all the credentials:

 

USER_CREDENTIALS = [
    ("LocustPerformanceUser5@gmail.com", "5678901"),
    ("LocustPerformanceUser4@gmail.com", "456789"),
    ("LocustPerformanceUser3@gmail.com", "345678"),
    ("LocustPerformanceUser2@gmail.com", "234567"), 
    ("LocustPerformanceUser1@gmail.com", "123456")
]

 

Next, you should make sure that each new thread takes unique user credentials and that we don’t have separate users running with the same one. I have seen some engineers who were trying to use a global variable with a counter of active users. They were using this counter trying to get the unique user credentials by index.

 

My advice - don’t do it in this way. First, there is a much simpler way. Second, you need to keep in mind that you are going to run performance tests that can spin up many users in parallel. That’s why you always need to be sure that you don’t have situations when two separate users are trying to get credentials by the same index. Of course, different options keeping you safe from these mistakes do exist, but this adds an additional complexity to the script. Instead, you can easily keep the separate array with credentials by using the atomic function pop(), which is available for Python arrays. This function takes one element out of the array and after that, this same array doesn’t have that element anymore.

 

You also need to find the best place in the script to specify this pop() function. We need to allocate unique credentials only once for each user. Also, it makes sense that we need to allocate credentials at the beginning of the script. Therefore, we need to find a place to place this function  so it is executed at the beginning of each thread. Luckily, Locust provides such an option. You can use the “def on_start(self):” function, which is automatically called by the Locust script executor, before any Locust task starts.

 

In addition to that, you need to think about a situation when the user tries to spin up more users than we have credentials for. If we want to run a performance script with 10 unique users, but we have provided 5 unique credentials, then it doesn't make sense. Therefore, we can add a separate line to check if the array with credentials doesn’t have anymore elements:

 

  def on_start(self):
            if len(USER_CREDENTIALS) > 0:
                self.email, self.password = USER_CREDENTIALS.pop()

 

It is better to specify some default values for credentials to continue the script in case the credentials run out, that will help us notice that we have put more users to spin up than the number of credentials we specified. I usually put the “NOT_FOUND” value for both. In this case we will immediately see that the response is failed and the reason is that we sent “NOT_FOUND” values instead of an email and password, which basically means that we ran out of the credentials.

 

Let’s also add the command that allows us to verify which user is being used for login, just to verify if the script works as expected. For this we can add the logging function this way:

 

logging.info('Login with %s email and %s password', self.email, self.password)

 

As a result, you will have such a script:

 

from locust import HttpLocust, TaskSet, task import logging, sys USER_CREDENTIALS = [ ("LocustPerformanceUser5@gmail.com", "5678901"), ("LocustPerformanceUser4@gmail.com", "456789"), ("LocustPerformanceUser3@gmail.com", "345678"), ("LocustPerformanceUser2@gmail.com", "234567"), ("LocustPerformanceUser1@gmail.com", "123456") ] class LoginWithUniqueUsersSteps(TaskSet): email = "NOT_FOUND" password = "NOT_FOUND" def on_start(self): if len(USER_CREDENTIALS) > 0: self.email, self.password = USER_CREDENTIALS.pop() @task def login(self): self.client.post("/login", { 'email': self.email, 'password': self.password }) logging.info('Login with %s email and %s password', self.email, self.password) class LoginWithUniqueUsersTest(HttpLocust): task_set = LoginWithUniqueUsersSteps host = "http://blazedemo.com" sock = None def __init__(self): super(LoginWithUniqueUsersTest, self).__init__()

 

Let’s try to run this script in the non-GUI mode with 5 users who are spinning up right away at the same time:

 

running a locust test with different users

 

As you can see, the provided solution works well, as expected. Each new login action is triggered by unique user credentials that are picked up from the array that is located in the Locust script.

 

Keeping Credentials in a Separate Python Script

 

Sometimes, you might want to keep credentials in a separate file. This might be useful when you have too many credentials (let’s say for 200 users). In this case, our script file will be overcrowded with users credentials and it will be very inconvenient to use such a Python script file further. Moreover, it is quite common that you would need to use the same credentials across different performance scripts. Therefore, if you prefer keeping credentials inside the performance scripts, you will end up with a huge amount of duplicated code in each performance script. And what if some user credentials are changed? Yes, right… You need to go over each performance script and fix the credentials at lots of different places. Sounds not so wise.

 

The easiest workaround that you can use to avoid such issues is to keep the credentials in a separate Python file in the same format. This will help you reuse the same file across different test scripts, instead of overcrowding the main performance script with unnecessary code. Let’s create a separate Python file with credentials and call it “credentials.py”:

 

USER_CREDENTIALS = [
    ("LocustPerformanceUser5@gmail.com", "5678901"),
    ("LocustPerformanceUser4@gmail.com", "456789"),
    ("LocustPerformanceUser3@gmail.com", "345678"),
    ("LocustPerformanceUser2@gmail.com", "234567"),
    ("LocustPerformanceUser1@gmail.com", "123456")
]

 

Now, all we need is to import this separate python file in the main performance script. This can be easily done by using this import operation:

 

from credentials import *

 

As a result, you should get the script:

 

from locust import HttpLocust, TaskSet, task
import logging, sys
from credentials import *

class LoginWithUniqueUsersSteps(TaskSet):
    email = "NOT_FOUND"
    password = "NOT_FOUND"

    def on_start(self):
            if len(USER_CREDENTIALS) > 0:
                self.email, self.password = USER_CREDENTIALS.pop()

    @task
    def login(self):
        self.client.post("/login", {
            'email': self.email, 'password': self.password
        })
        logging.info('Login with %s email and %s password', self.email, self.password)

class LoginWithUniqueUsersTest(HttpLocust):
    task_set = LoginWithUniqueUsersSteps
    host = "http://blazedemo.com"
    sock = None

    def __init__(self):
        super(LoginWithUniqueUsersTest, self).__init__()

 

As you can see, we can easily solve the issues mentioned at the beginning of the paragraph, and our performance script looks clean and still does the job well.

 

Keeping Credentials in a Separate CSV File

 

There is even a third and maybe the most common way to store user credentials - by using a CSV file. First of all, this approach is the most habitual for those who worked with JMeter before and have a clear idea that this type of data is handy to store in a separate CSV file.

 

There is one more argument for storing the credentials in this way. It is quite a common situation that the same data set might be used in different tools. I have had situations when I used both Locust and Apache JMeter™ performance scripts on the same application. Moreover, sometimes I used the same data set for my regression and functional tests. Therefore, it doesn’t make sense to keep user credentials in Java classes for Java tests, in Python scripts for Locust scripts and in CSV for JMeter scripts, if the data set is the same. In this case, it is wise to use a format that is acceptable for all of these languages and tools. And of course, this format is CSV. All the tools and languages above can easily work with CSV files, which makes this approach preferable.

 

Let’s create a separate CSV file that holds all our credentials and call it ‘credentials.csv’:

 

LocustPerformanceUser5@gmail.com,5678901
LocustPerformanceUser4@gmail.com,456789
LocustPerformanceUser3@gmail.com,345678
LocustPerformanceUser2@gmail.com,234567
LocustPerformanceUser1@gmail.com,123456

 

Next, all we need to do is to write a small code snippet that can parse the CSV file and put it inside the array at the beginning of the test. In this case, we will have almost the same scenario which we had before, with only on the additional step: we are creating the array with credentials dynamically parsing the CSV file, instead of creating this array inside the code. As we want to perform this action at the beginning of the whole script, it makes sense to put it inside the “def __init__(self):” function of the main Locust class. But we need to keep in mind that this function will be triggered once for each user. As we are not going to edit the file during script execution, it doesn't make sense to read this file again and again, and it is more efficient to read it only once at the beginning of script execution. Therefore we need to be sure that we will trigger it only once for all users. You can get the idea from this result script which you will finally get:

 

from locust import HttpLocust, TaskSet, task
import logging, sys
import csv

USER_CREDENTIALS = None

class LoginWithUniqueUsersSteps(TaskSet):
    email = "NOT_FOUND"
    password = "NOT_FOUND"

    def on_start(self):
            if len(USER_CREDENTIALS) > 0:
                self.email, self.password = USER_CREDENTIALS.pop()

    @task
    def login(self):
        self.client.post("/login", {
            'email': self.email, 'password': self.password
        })
        logging.info('Login with %s email and %s password', self.email, self.password)

class LoginWithUniqueUsersTest(HttpLocust):
    task_set = LoginWithUniqueUsersSteps
    host = "http://blazedemo.com"
    sock = None

    def __init__(self):
        super(LoginWithUniqueUsersTest, self).__init__()
        global USER_CREDENTIALS
        if (USER_CREDENTIALS == None):
            with open('credentials.csv', 'rb') as f:
                reader = csv.reader(f)
                USER_CREDENTIALS = list(reader)

 

If we run this script, we will see exactly the same results as in the all other options:

 

running a local test with unique users

 

Final Thoughts

 

As you can see, there is nothing that you can not do in Locust, as it is just native Python. It is up to you to decide which functionality to add your scripts. In this article, we showed you how to implement credentials storage separately from the main script, and how to use different credentials to simulate unique users to test your web application. Let us know what functionality you miss out of the box in the Locust framework and we can give you nice tips to implement on your own!

 

As always, you can find all the test scripts in the example tests repository.

 

You can also automate your Locust tests through Taurus. You can also easily scale your Locust framework tests, run them in the cloud and analyze the results on BlazeMeter.

Interested in writing for our Blog? Send us a pitch!