[{"data":1,"prerenderedAt":60},["ShallowReactive",2],{"blog-post-load-testing-put-endpoints-with-apache-jmeter":3,"blog-sidebar-tags":44,"blog-sidebar-popular":50},{"overview":4,"post":33},{"slug":5,"title":6,"subtitle":7,"opener":8,"tags":9,"date":15,"headerImage":16,"author":21,"images":31,"__typename":32},"load-testing-put-endpoints-with-apache-jmeter","Load testing PUT endpoints","With Apache JMeter and minimal APIs","Load testing PUT endpoints with Apache JMeter using ASP.NET Core minimal APIs as an example.",[10,11,12,13,14],"Performance","Apache JMeter","C#","Minimal APIs","ASP.NET","2023-01-03T00:00:00Z",[17],{"fileName":18,"url":19,"__typename":20},"julian-hochgesang-3-y9vq8uoxk-unsplash.jpg","https:\u002F\u002Fcms.spatial-focus.net\u002Fapi\u002Fassets\u002Fspatialfocus\u002Fad44fc84-935c-4ab4-ab0e-9fbcb84fb2c1\u002F","Asset",[22],{"id":23,"flatData":24,"__typename":30},"0152e82d-a848-44f8-b0b8-fd8aec4b874a",{"name":25,"image":26,"__typename":29},"Christopher Dresel",[27],{"url":28,"__typename":20},"https:\u002F\u002Fcms.spatial-focus.net\u002Fapi\u002Fassets\u002Fspatialfocus\u002F305bcd01-b811-4147-8947-e025b7966355\u002F","AuthorFlatDataDto","Author",null,"PostsFlatDataDto",{"slug":5,"title":6,"subtitle":7,"headerImage":34,"opener":8,"text":36,"tags":37,"date":15,"repo":38,"author":39,"images":31,"__typename":32},[35],{"fileName":18,"url":19,"__typename":20},"# The story\n\nOne of our customers wanted us to execute an isolated load test for a specific ASP.NET Core Boilerplate PUT endpoint. Based on the requirements, we decided to use [Apache JMeter](https:\u002F\u002Fjmeter.apache.org\u002F). Apache JMeter is an open-source tool for load testing and performance testing of web applications. It can be used to simulate a heavy load on a server, network, or application to test its performance under different load types. JMeter can send requests to a server and measures the response time, throughput, and other performance metrics.\n\nThe challenge here is that resources are verified via Entity Framework Core [optimistic concurrency](https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Fef\u002Fcore\u002Fsaving\u002Fconcurrency). So sending the same resource via threads \u002F loops was not possible (because the initial save would fail) and might not make too much sense anyway because caching could dilute the results. Instead, we went for a different approach: for the test preparation step we loaded a separate resource for each executed PUT request beforehand. Let's see how this could be done.\n\n# A simple demo backend\n\nLet's pretend we have the following endpoints:\n\n* `\u002Fcustomer` [GET] - a paginable endpoint which returns customer DTOs\n* `\u002Fcustomer\u002F{id}` [GET] - returns the detailed customer DTO which we load and store for testing the PUT endpoint\n* `\u002Fcustomer\u002F{id}` [PUT] - the put endpoint which is the target of the load test\n\nWe used [Bogus](https:\u002F\u002Fgithub.com\u002Fbchavez\u002FBogus) for some nice and easy test data. Bogus is a .NET library that can be used to generate fake data for testing purposes such as names, addresses, emails, and more.\n\nTo keep things simple, let's use [minimal APIs](https:\u002F\u002Flearn.microsoft.com\u002Fen-us\u002Faspnet\u002Fcore\u002Ffundamentals\u002Fminimal-apis\u002Foverview):\n\n```csharp\nWebApplicationBuilder builder = WebApplication.CreateBuilder(args);\nWebApplication app = builder.Build();\n\nRandomizer.Seed = new Random(123456789);\n\nFaker\u003CCustomer> customerData = new Faker\u003CCustomer>().RuleFor(u => u.Id, f => f.Random.Guid())\n\t.RuleFor(u => u.Gender, f => f.PickRandom\u003CGender>())\n\t.RuleFor(u => u.FirstName, (f, u) => f.Name.FirstName(u.Gender.Map()))\n\t.RuleFor(u => u.LastName, (f, u) => f.Name.LastName(u.Gender.Map()));\n\nList\u003CCustomer> customers = customerData.Generate(1000);\n\napp.MapGet(\"\u002Fcustomers\", async (int? pageIndex, int? pageSize) =>\n{\n\tawait Task.Delay(300); \u002F\u002F Simulate some processing :)\n\treturn customers.Skip((pageIndex ?? 0) * (pageSize ?? 50)).Take(pageSize ?? 50);\n});\n\napp.MapGet(\"\u002Fcustomers\u002F{id}\", async (Guid id) =>\n{\n\tawait Task.Delay(100); \u002F\u002F Simulate some processing :)\n\treturn customers.SingleOrDefault(x => x.Id == id);\n});\n\napp.MapPut(\"\u002Fcustomers\u002F{id}\", async (Guid id, Customer customer) =>\n{\n\tawait Task.Delay(500); \u002F\u002F Simulate some processing :)\n});\n\napp.Run();\n```\n\nFor reference, this is how the models look like:\n\n```csharp\npublic class Customer\n{\n\tpublic string FirstName { get; init; } = null!;\n\n\tpublic Gender Gender { get; set; }\n\n\tpublic Guid Id { get; init; }\n\n\tpublic string LastName { get; init; } = null!;\n}\n\n\npublic enum Gender\n{\n\tFemale,\n\n\tMale,\n\n\tNonBinary,\n\n\tUnknown,\n}\n```\n\nAlthough simplified, that's more or less how the customer endpoints looked like. Now comes the interesting part.\n\n# The Apache JMeter test plan\n\nThe test can be split into three parts:\n\n* Collecting customer ids from the paginable endpoint (let's assume overview DTOs are returned)\n* Querying complete customer DTOs from the `\u002Fcustomers\u002F{id}` GET endpoint\n* Load testing the PUT endpoint with previously stored customer DTOs\n\nFor easier configuration, these are the global variables used for each of these steps:\n```\napi.domain = localhost\napi.port = 5555\nnumber_of_threads = 10\nnumber_of_loops = 100\n```\n\nApache JMeter introduces the concept of Thread Groups which defines a \"pool of user\" aka threads and specifies a loop count setting the number of iterations for each thread.\n\nSo a Thread Group with the above configuration will result into `10 x 100 = 1000` runs (requests).\n\n## Thread Group: GetCustomerIds\n\nEach thread should retrieve a different page set to ensure unique ids. This is the path passed to the HTTP request:\n\n`customers?pageIndex=${page_index}&pageSize=${number_of_loops}`\n\nIf the page size is limited by your API, you would have to split this into multiple subrequests. `page_index` corresponds to `__threadNum` - 1, set by a JSR223 PreProcessor.\n\nThe customer ids are extracted by a JSON Extractor with the following path expression: `$[*].id`. Each id is stored in a variable in the form of `customer_ids_\u003Cindex>`. First thread is storing `customer_ids_1` to `customer_ids_100`, second one starting from `customer_ids_101` to `customer_ids_200` and so on.\n\n## Thread Group: GetCustomer\n\nThe trickiest part of this Thread Group is finding the customer variable index for a specific thread \u002F loop iteration. This is calculated by a JSR223 PreProcessor like this (note that thread number is 1-based and the loop index is 0-based):\n\n`int customerIndex = (${__threadNum} - 1) * ${number_of_loops} + (vars.get('__jm__GetCustomers__idx') as Integer) + 1`\n\nThe HTTP Request path is rather simple:\n\n`customers\u002F${customer_id}`\n\nThe customer result is stored in the form of `customer_\u003Cindex>`.\n\n## Thread Group: PutCustomers\n\nNow this is the Thread Group everything was setup for. We have to calculate the index again for the current thread \u002F loop iteration, load the customer id and object and send it back to the PUT endpoint.\n\n# Results & Summary\n\nThe put endpoint is simulating 500ms of processing time, so for 10 parallel threads we could expect about 20 requests per second. As shown in the following image, we are not far off the expectation. Note that we run the test in the UI mode, which is not something you should do for real runs :)\n\n![Results of the Apache JMeter test run](https:\u002F\u002Fgithub.com\u002FSpatialFocus\u002Fsample-load-testing-with-jmeter\u002Fblob\u002Fmaster\u002Fresults.png?raw=true 'results.png')\n\nThis blog post gave a quick overview of a simplified example for load testing with Apache JMeter. If you want to try it out, clone the repository, start the minimal api, open the `test\\testplan.jmx` with Apache JMeter and give it a shot.",[10,11,12,13,14],"https:\u002F\u002Fgithub.com\u002FSpatialFocus\u002Fsample-load-testing-with-jmeter",[40],{"id":23,"flatData":41,"__typename":30},{"name":25,"image":42,"__typename":29},[43],{"url":28,"__typename":20},[12,45,46,47,48,49],"DDD","EF","Clean code","Geo","web",[51,57],{"slug":52,"title":53,"headerImage":54,"__typename":32},"radial-progress-css-animation","Radial progress with CSS",[55],{"url":56,"__typename":20},"https:\u002F\u002Fcms.spatial-focus.net\u002Fapi\u002Fassets\u002Fspatialfocus\u002Fb7cdc0ef-364e-4444-8dd3-b85f19e87820\u002F",{"slug":5,"title":6,"headerImage":58,"__typename":32},[59],{"url":19,"__typename":20},1783525743749]