Recently I wanted to load test a simple API I have developed for a personal project. Ideally, I wanted to go with a tool/framework that would allow me to create scenarios directly into my codebase using C#.

That is where NBomber came in which is an open-source library that helps us create various load-testing scenarios and generate a report with the results. Scenarios include steps that are executed sequentially to simulate the expected behavior. Concurrent execution of multiple scenarios is supported.

Getting started is easy and only requires a console project and references to the relevant packages.

dotnet new console -n NBomberTest
dotnet add package NBomber
dotnet add package NBomber.http

Consider a scenario that consists of three requests

Request Description
GET /states returns a static list
GET /stateEntry returns a dynamic list
POST /stateEntry adds a new entry

To avoid duplication I have added a function that accepts a few inputs and returns the step

IStep CreateStep(IClientFactory<HttpClient> clientFactory, string name, string url, HttpMethod method, string body=null)
{
	string uri = $"http://localhost:5000/api{url}";
	var step = Step.Create(name,
		clientFactory: clientFactory,
		execute: context =>
		{
			var request = Http.CreateRequest(method.Method, uri);
			
			if (!string.IsNullOrEmpty((body)))
			{
				var stringContent = new StringContent(body, Encoding.UTF8, "application/json");
				request = request.WithBody(stringContent);
			}
			var resp=  Http.Send(request, context);
			return resp;
		});
	return step;
}
CreateStep helper method

And this is the method that creates the scenario. I'm using the ClientFactory to return an HttpClient and then the CreateStep method from above to create the three steps that I want to include in the scenario. Steps are executed in the order they are defined.

void LoadTesting()
{
	var httpFactory = ClientFactory.Create(
		name: "http_factory",                         
		clientCount: 1,
		initClient: (number,context) => Task.FromResult(new HttpClient())
	);
    
	var stateEntryBody = JsonSerializer.Serialize(new  {StateId = 1, Date = DateTime.Now, Note = "welcome to the jungle"});
	
	var steps = new[]
	{
		CreateStep(httpFactory,"getStates", "/state", HttpMethod.Get),
		CreateStep(httpFactory,"getEntries", "/stateEntry", HttpMethod.Get),
		CreateStep(httpFactory,"addStateEntry", "/stateEntry", HttpMethod.Post,stateEntryBody),
	};
	
	var scenario = ScenarioBuilder
		.CreateScenario("addEntry", steps)
		.WithWarmUpDuration(TimeSpan.FromSeconds(5))
		.WithLoadSimulations(
			Simulation.KeepConstant(10, during: TimeSpan.FromSeconds(30))
		);

	// creates ping plugin that brings additional reporting data
	var pingPluginConfig = PingPluginConfig.CreateDefault(new[] { "localhost" });
	var pingPlugin = new PingPlugin(pingPluginConfig);

	NBomberRunner
		.RegisterScenarios(scenario)
		.WithWorkerPlugins(pingPlugin)
		.WithReportFormats(ReportFormat.Html)
		.Run();
}

Once finished with the step creation, the ScenarioBuilder is used to build the scenario by passing all the required parameters. In this test, WithWarmupDuration() is used which will simply start a scenario with a specified duration as specified in the documentation.

Next is the WithLoadSimulations() method. NBomber provides the following methods to simulate various request patterns which can be used together in a scenario similar to

Start with a ramp up to 1000 requests over 1 minute for 20 minutes, maintain a constant load of 750 for the next 10 minutes and eventually ramp down to 0.
Methods see more
RampConstant(int copies, TimeSpan during)
KeepConstant(int copies, TimeSpan during)
RampPerSec(int rate, TimeSpan during)
InjectPerSec(int rate, TimeSpan during)
InjectPerSecRandom(int minRate,int maxRate, TimeSpan during)

Next is the ping plugin which is pretty boilerplate and is used to send a PING request to the system under test once on the startup end and the configuration of NBomberRunner which will actually run the scenarios. If not specified NBomber will generate the report in .txt, .csv, .md, and HTML files.

using System.Text;
using System.Text.Json;
using NBomber.Contracts;
using NBomber.Contracts.Stats;
using NBomber.CSharp;
using NBomber.Plugins.Http.CSharp;
using NBomber.Plugins.Network.Ping;

try
{
	LoadTesting();
}
catch (Exception e)
{
	Console.WriteLine(e);
	throw;
}
Program.cs

Now once the scenario's set, time to run it. Below are screenshots from the execution and the HTML report generated. In the generated report you will find statistics for the scenario as a whole but also for each step.

In my case, I can see that the latency and the data transfer grow as the RPS decline and it makes sense as with every execution the data that needs to be returned grows and no pagination is used. You can see in the image below that the total data transferred for the second step getEntries is almost 32GB for 18458 requests

NBomber startup
Results printed to Console
HTML Generated Report
HTML Generated Report

It is worth heading over to NBomber's documentation to understand more about it and see the full power behind it such as using a data feed, dynamic configuration or real-time reporting, using the response of the previous steps and more.

You can find the code demonstrated here as part of my diathesea project.

thanks for reading.