Building a Simple Load Testing Tool in CSharp

C# tool box
Why did the load testing tool break up with the server?
Because it couldn't handle the stress of the relationship!

Load testing is a crucial aspect of performance testing that helps ensure an application can handle expected user loads. By simulating multiple users interacting with your application, you can identify performance bottlenecks and understand how your system behaves under stress. In this article, we’ll explore how to build a simple load testing tool using C#.

Prerequisites

To follow along, you’ll need:

  • Visual Studio or any C# IDE
  • Basic knowledge of C# and .NET
  • Familiarity with HTTP requests and responses

Step 1: Setting Up Your Project

First, create a new Console Application in Visual Studio:

  1. Open Visual Studio and select Create a new project.
  2. Choose Console App (.NET Core) and click Next.
  3. Name your project SimpleLoadTester and click Create.

Step 2: Adding Necessary Packages

For our load testing tool, we’ll use the HttpClient class to send HTTP requests. Ensure that your project references System.Net.Http, which is available in .NET Core by default.

If you’re using .NET Framework, you may need to install the System.Net.Http NuGet package:

  1. Right-click on your project in Solution Explorer.
  2. Select Manage NuGet Packages.
  3. Search for System.Net.Http and install the latest version.

Step 3: Designing the Load Testing Tool

Our simple load testing tool will:

  • Accept a target URL
  • Allow configuration of the number of requests and the concurrency level (number of simultaneous users)
  • Measure and display the total time taken, average response time, and the number of successful and failed requests

Let’s break down the code step-by-step.

3.1. Define the Main Program Structure

In your Program.cs file, define the structure of the program:

using System;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;

namespace SimpleLoadTester
{
    class Program
    {
        static async Task Main(string[] args)
        {
            if (args.Length < 3)
            {
                Console.WriteLine("Usage: SimpleLoadTester <url> <number of requests> <concurrency level>");
                return;
            }

            string url = args[0];
            int numberOfRequests = int.Parse(args[1]);
            int concurrencyLevel = int.Parse(args[2]);

            await RunLoadTest(url, numberOfRequests, concurrencyLevel);
        }

        static async Task RunLoadTest(string url, int numberOfRequests, int concurrencyLevel)
        {
            // Implementation will go here
        }
    }
}

This code snippet defines the entry point of the application, parses command-line arguments, and calls the RunLoadTest method.

3.2. Implementing the Load Testing Logic

Now, let’s implement the core logic of the RunLoadTest method:

static async Task RunLoadTest(string url, int numberOfRequests, int concurrencyLevel)
{
    using HttpClient client = new HttpClient();

    int successfulRequests = 0;
    int failedRequests = 0;
    long totalResponseTime = 0;

    SemaphoreSlim concurrencySemaphore = new SemaphoreSlim(concurrencyLevel);
    var tasks = new Task[numberOfRequests];

    for (int i = 0; i < numberOfRequests; i++)
    {
        await concurrencySemaphore.WaitAsync();

        tasks[i] = Task.Run(async () =>
        {
            try
            {
                var startTime = DateTime.Now;
                HttpResponseMessage response = await client.GetAsync(url);
                var endTime = DateTime.Now;

                if (response.IsSuccessStatusCode)
                {
                    Interlocked.Increment(ref successfulRequests);
                }
                else
                {
                    Interlocked.Increment(ref failedRequests);
                }

                Interlocked.Add(ref totalResponseTime, (endTime - startTime).Milliseconds);
            }
            catch (Exception)
            {
                Interlocked.Increment(ref failedRequests);
            }
            finally
            {
                concurrencySemaphore.Release();
            }
        });
    }

    await Task.WhenAll(tasks);

    Console.WriteLine($"Total Requests: {numberOfRequests}");
    Console.WriteLine($"Successful Requests: {successfulRequests}");
    Console.WriteLine($"Failed Requests: {failedRequests}");
    Console.WriteLine($"Average Response Time: {(double)totalResponseTime / numberOfRequests} ms");
}
Explanation of the Code
  1. HttpClient: We use HttpClient to send HTTP GET requests to the target URL.
  2. Concurrency Control: We use SemaphoreSlim to control the concurrency level, ensuring no more than the specified number of tasks run simultaneously.
  3. Task Creation: For each request, we create a Task that:
  • Measures the response time
  • Tracks success or failure
  • Increments counters using Interlocked methods for thread-safe operations
  1. Awaiting Tasks: We use Task.WhenAll to wait for all tasks to complete before displaying the results.

Step 4: Running the Load Test

To run the load test, open a terminal in the project directory and use the following command:

dotnet run <url> <number of requests> <concurrency level>

For example:

dotnet run https://example.com 100 10

This command will send 100 requests to https://example.com with a concurrency level of 10.

Step 5: Analyzing Results

After the load test completes, the tool displays:

  • Total Requests: The total number of requests sent
  • Successful Requests: The number of requests that received a successful HTTP status code
  • Failed Requests: The number of requests that failed
  • Average Response Time: The average response time across all requests

Building a simple load testing tool in C# is a great way to learn about HTTP requests, multithreading, and performance testing. This basic tool can be extended with additional features such as support for different HTTP methods, request headers, payloads, and more sophisticated result analysis.

By understanding the fundamentals of load testing, you can better prepare your applications for production and ensure they deliver a reliable and responsive user experience.