How to Perform Load Testing on Authenticated Routes in Rails with k6 and Devise

Load testing is crucial for ensuring that web applications perform well under pressure. This becomes slightly more complex when applications have authenticated routes, especially when using frameworks like Ruby on Rails with authentication handled by Devise. In this blog post, we will go through the process of setting up load tests for such scenarios using K6, a powerful open-source load testing tool.

Setting Up Your Testing Environment

Before diving into the script setup, ensure you have K6 installed. If you're using Docker, you can pull the K6 image from Docker Hub:

docker pull grafana/k6

This ensures that your testing environment is isolated and consistent with your CI/CD pipeline.

Writing the k6 Script

The objective is to simulate a user login through Devise to test authenticated routes. Here’s how you can achieve this with a K6 script:

1. Simulate the Login

Devise handles user sessions, which we need to replicate in our load tests. We start by creating a function in our script to log in a user and capture the session cookie:

import http from 'k6/http';
import { check } from 'k6';

function login() {
    const payload = JSON.stringify({
        user: {
            email: 'user@example.com',
            password: 'password',
        },
    });

    const params = {
        headers: {
            'Content-Type': 'application/json',
        },
    };

    let res = http.post('https://yourdomain.com/users/sign_in', payload, params);
    check(res, {
        'is status 200': (r) => r.status === 200,
    });

    return res.headers['Set-Cookie'];
}

2. Access Authenticated Routes

Once logged in, use the session cookie to make authenticated requests:

export default function () {
    let cookie = login();
    let params = {
        headers: {
            Cookie: cookie,
        },
    };

    let res = http.get('https://yourdomain.com/protected-route', params);
    check(res, {
        'is status 200': (r) => r.status === 200,
    });
}

Load Testing Strategy

Configure your K6 scenarios to reflect real user behavior as closely as possible. This includes ramp-up phases, peak load, and ramp-down phases. Here is a simple configuration:

export let options = {
    stages: [
        { duration: '1m', target: 10 }, // ramp up to 10 users
        { duration: '5m', target: 10 }, // hold at 10 users
        { duration: '1m', target: 0 },  // ramp down
    ],
};

Full script:

import http from 'k6/http';
import { check, sleep } from 'k6';

export let options = {
    stages: [
        { duration: '1m', target: 10 }, // Example: Ramp up to 10 users over 1 minute
        { duration: '5m', target: 10 }, // Stay at 10 users for 5 minutes
        { duration: '1m', target: 0 },  // Ramp down to 0 users over 1 minute
    ],
};

function login() {
    const payload = JSON.stringify({
        user: {
            email: 'user@example.com',
            password: 'password',
        },
    });

    const params = {
        headers: {
            'Content-Type': 'application/json',
        },
    };

    let res = http.post('https://yourdomain.com/users/sign_in', payload, params);
    // Check response to make sure login was successful
    check(res, {
        'is status 200': (r) => r.status === 200,
    });

    // Capture and return the cookie from the response header
    return res.headers['Set-Cookie'];
}

export default function () {
    let cookie = login();
    let params = {
        headers: {
            Cookie: cookie,
        },
    };

    // Perform a request to an authenticated route
    let res = http.get('https://yourdomain.com/protected-route', params);
    check(res, {
        'is status 200': (r) => r.status === 200,
    });

    sleep(1);
}

Run K6 container

To run K6 from a Docker container, mount your test scripts into the container and direct K6 to execute them. Here’s an example command:

docker run -v ${PWD}/tests:/tests grafana/k6 run /tests/my-test-script.js

Share a localhost network with docker container

If your ruby on rails installation is done in Mac or Ubuntu machine directly without docker but you setup K6 using docker container and your script is using http://localhost:3000 as an endpoint, running above command might give you connection refused error, to fix that issue we can simply share the host machine's network host by --network="host" flag in the docker run command

docker run --network="host" -v ${PWD}/tests:/tests grafana/k6 run /tests/my-test-script.js

Best Practices and Considerations

  • Rate Limiting and Lockouts: Ensure your test accounts are exempt from these features to avoid false negatives during testing.

  • Security: Use environment variables to handle sensitive data like usernames and passwords securely.

  • Session Management: If your application uses token-based authentication or has specific headers, adjust the script accordingly.

  • Custom Headers or CSRF Tokens: If your Rails app uses CSRF tokens or other custom headers for security, make sure to capture and use them as required by your application.

Conclusion

Load testing authenticated routes in a Rails application with Devise is straightforward with k6. By simulating real user behaviors, including logging in and accessing protected routes, you can uncover potential bottlenecks and ensure that your application can handle real-world usage. Remember to keep your scripts secure and handle user data responsibly.

With these guidelines, you're well-equipped to set up effective load testing for your Rails applications, helping to ensure robust performance and a great user experience.