├── Abstracta.JmeterDsl.Tests ├── Usings.cs ├── Core │ ├── Configs │ │ ├── data.csv │ │ └── DslCsvDataSetTest.cs │ ├── Controllers │ │ ├── DslTransactionControllerTest.cs │ │ ├── ForLoopControllerTest.cs │ │ └── DslSimpleControllerTest.cs │ ├── Timers │ │ ├── DslConstantTimerTest.cs │ │ └── DslUniformRandomTimerTest.cs │ ├── Samplers │ │ └── DslFlowControlActionTest.cs │ ├── PreProcessors │ │ └── DslJsr223PreProcessorTest.cs │ ├── Listeners │ │ ├── ResponseFileSaverTest.cs │ │ └── JtlWriterTest.cs │ ├── ThreadGroups │ │ └── DslThreadGroupTest.cs │ ├── Assertions │ │ └── DslResponseAssertionTest.cs │ └── PostProcessors │ │ └── DslRegexExtractorTest.cs ├── Http │ └── sample.xml ├── log4j2.xml ├── Abstracta.JmeterDsl.Tests.csproj └── WireMockAssertionsExtensions.cs ├── Abstracta.JmeterDsl.Azure.Tests ├── Usings.cs ├── log4j2.xml ├── Abstracta.JmeterDsl.Azure.Tests.csproj └── AzureEngineTest.cs ├── Abstracta.JmeterDsl.BlazeMeter.Tests ├── Usings.cs ├── log4j2.xml ├── Abstracta.JmeterDsl.BlazeMeter.Tests.csproj └── BlazeMeterEngineTests.cs ├── logo.png ├── docs ├── guide │ ├── protocols │ │ ├── index.md │ │ └── http │ │ │ ├── redirects.md │ │ │ ├── cookies-and-cache.md │ │ │ ├── index.md │ │ │ ├── methods-and-body.md │ │ │ ├── multipart.md │ │ │ ├── headers.md │ │ │ └── parameters.md │ ├── request-generation │ │ ├── loops │ │ │ ├── index.md │ │ │ └── forloop-controller.md │ │ ├── timers │ │ │ ├── index.md │ │ │ ├── synchronized-samples.png │ │ │ ├── not-synchronized-samples.png │ │ │ ├── synchronizing-timer.md │ │ │ └── constant-and-random.md │ │ ├── index.md │ │ ├── jsr223-pre-processor.md │ │ ├── csv-dataset.md │ │ └── group-requests.md │ ├── scale │ │ ├── azure.png │ │ ├── blazemeter.png │ │ ├── index.md │ │ ├── azure.md │ │ └── blazemeter.md │ ├── debugging │ │ ├── images │ │ │ ├── test-plan-gui.png │ │ │ ├── view-results-tree.png │ │ │ ├── intellij-remote-jvm-debug.png │ │ │ └── jmeter-http-sampler-debugging.png │ │ ├── index.md │ │ ├── show-in-gui.md │ │ ├── debug-jmeter.md │ │ ├── dummy-sampler.md │ │ └── view-results-tree.md │ ├── response-processing │ │ ├── index.md │ │ ├── correlation │ │ │ ├── index.md │ │ │ └── regex-extractor.md │ │ └── response-assertion.md │ ├── thread-groups │ │ ├── images │ │ │ ├── ultimate-thread-group-gui.png │ │ │ └── ultimate-thread-group-timeline.png │ │ ├── index.md │ │ └── ramps-and-holds.md │ ├── reporting │ │ ├── index.md │ │ └── logging.md │ ├── setup.md │ ├── index.md │ └── simple-test-plan.md ├── support │ ├── azure-logo.png │ ├── octoperf-logo.png │ ├── abstracta-logo.png │ ├── blazemeter-logo.png │ └── index.md ├── .vuepress │ ├── public │ │ └── favicon.ico │ ├── plugins │ │ ├── repoLinkSolverPlugin.ts │ │ └── includedRelativeLinkSolverPlugin.ts │ ├── client.ts │ ├── components │ │ ├── HomeFeatures.vue │ │ ├── NavbarBrand.vue │ │ ├── HomeHero.vue │ │ └── AutoLink.vue │ ├── styles │ │ └── index.scss │ └── config.ts ├── package.json └── index.md ├── .github ├── next-minor-alpha.sh ├── nuget-deploy.sh ├── fix-docs-version.sh ├── vuepress-deploy.sh ├── update-sample-version.sh ├── semver-check.sh ├── fix-project-version.sh └── workflows │ ├── build.yml │ └── release.yml ├── .envrc ├── Abstracta.JmeterDsl ├── Core │ ├── Bridge │ │ ├── YamlTypeAttribute.cs │ │ ├── TestPlanExecution.cs │ │ ├── IDslProperty.cs │ │ ├── JvmException.cs │ │ ├── EnumConverter.cs │ │ ├── TimeSpanConverter.cs │ │ └── BridgedObjectConverter.cs │ ├── Engines │ │ ├── EmbeddedJmeterEngine.cs │ │ └── BaseJmeterEngine.cs │ ├── Samplers │ │ ├── ISamplerChild.cs │ │ ├── DslFlowControlAction.cs │ │ ├── BaseSampler.cs │ │ └── DslDummySampler.cs │ ├── Listeners │ │ ├── BaseListener.cs │ │ ├── ResponseFileSaver.cs │ │ └── ResultsTreeVisualizer.cs │ ├── ThreadGroups │ │ ├── IThreadGroupChild.cs │ │ └── BaseThreadGroup.cs │ ├── ITestPlanChild.cs │ ├── Timers │ │ ├── BaseTimer.cs │ │ ├── DslSynchronizingTimer.cs │ │ ├── DslConstantTimer.cs │ │ └── DslUniformRandomTimer.cs │ ├── Configs │ │ └── BaseConfigElement.cs │ ├── IDslTestElement.cs │ ├── TestElements │ │ ├── IMultiLevelTestElement.cs │ │ ├── BaseTestElement.cs │ │ ├── DslJsr223TestElement.cs │ │ ├── TestElementContainer.cs │ │ └── DslScopedTestElement.cs │ ├── Stats │ │ ├── CountMetricSummary.cs │ │ ├── TimeMetricSummary.cs │ │ └── StatsSummary.cs │ ├── IDslJmeterEngine.cs │ ├── PostProcessors │ │ ├── DslVariableExtractor.cs │ │ └── DslRegexExtractor.cs │ ├── Controllers │ │ ├── ForLoopController.cs │ │ ├── DslSimpleController.cs │ │ ├── BaseController.cs │ │ └── DslTransactionController.cs │ ├── TestPlanStats.cs │ ├── PreProcessors │ │ └── DslJsr223PreProcessor.cs │ └── DslTestPlan.cs ├── pom.xml ├── Http │ ├── DslHttpCache.cs │ ├── DslHttpCookies.cs │ └── HttpHeaders.cs └── Abstracta.JmeterDsl.csproj ├── .editorconfig ├── devbox.json ├── pom.xml ├── Abstracta.JmeterDsl.Azure ├── pom.xml └── Abstracta.JmeterDsl.Azure.csproj ├── devbox.lock ├── Abstracta.JmeterDsl.BlazeMeter ├── pom.xml ├── Abstracta.JmeterDsl.BlazeMeter.csproj └── BlazeMeterLocation.cs ├── Abstracta.JmeterDsl.sln └── README.md /Abstracta.JmeterDsl.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using NUnit.Framework; -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Azure.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using NUnit.Framework; -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.BlazeMeter.Tests/Usings.cs: -------------------------------------------------------------------------------- 1 | global using NUnit.Framework; -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/HEAD/logo.png -------------------------------------------------------------------------------- /docs/guide/protocols/index.md: -------------------------------------------------------------------------------- 1 | ## Protocols 2 | 3 | 4 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/Core/Configs/data.csv: -------------------------------------------------------------------------------- 1 | VAR1,VAR2 2 | val1,"val2" 3 | "val,3",val4 4 | -------------------------------------------------------------------------------- /docs/guide/request-generation/loops/index.md: -------------------------------------------------------------------------------- 1 | ### Loops 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/guide/scale/azure.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/HEAD/docs/guide/scale/azure.png -------------------------------------------------------------------------------- /docs/support/azure-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/HEAD/docs/support/azure-logo.png -------------------------------------------------------------------------------- /docs/support/octoperf-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/HEAD/docs/support/octoperf-logo.png -------------------------------------------------------------------------------- /docs/guide/scale/blazemeter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/HEAD/docs/guide/scale/blazemeter.png -------------------------------------------------------------------------------- /docs/support/abstracta-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/HEAD/docs/support/abstracta-logo.png -------------------------------------------------------------------------------- /docs/support/blazemeter-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/HEAD/docs/support/blazemeter-logo.png -------------------------------------------------------------------------------- /docs/.vuepress/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/HEAD/docs/.vuepress/public/favicon.ico -------------------------------------------------------------------------------- /docs/guide/debugging/images/test-plan-gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/HEAD/docs/guide/debugging/images/test-plan-gui.png -------------------------------------------------------------------------------- /docs/guide/request-generation/timers/index.md: -------------------------------------------------------------------------------- 1 | ### Timers 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/guide/debugging/images/view-results-tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/HEAD/docs/guide/debugging/images/view-results-tree.png -------------------------------------------------------------------------------- /docs/guide/response-processing/index.md: -------------------------------------------------------------------------------- 1 | ## Response processing 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /docs/guide/debugging/images/intellij-remote-jvm-debug.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/HEAD/docs/guide/debugging/images/intellij-remote-jvm-debug.png -------------------------------------------------------------------------------- /docs/guide/debugging/images/jmeter-http-sampler-debugging.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/HEAD/docs/guide/debugging/images/jmeter-http-sampler-debugging.png -------------------------------------------------------------------------------- /docs/guide/request-generation/timers/synchronized-samples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/HEAD/docs/guide/request-generation/timers/synchronized-samples.png -------------------------------------------------------------------------------- /docs/guide/thread-groups/images/ultimate-thread-group-gui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/HEAD/docs/guide/thread-groups/images/ultimate-thread-group-gui.png -------------------------------------------------------------------------------- /docs/guide/request-generation/timers/not-synchronized-samples.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/HEAD/docs/guide/request-generation/timers/not-synchronized-samples.png -------------------------------------------------------------------------------- /docs/guide/thread-groups/images/ultimate-thread-group-timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/HEAD/docs/guide/thread-groups/images/ultimate-thread-group-timeline.png -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/Http/sample.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tested 5 | 6 | 7 | -------------------------------------------------------------------------------- /.github/next-minor-alpha.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script solves next minor version 4 | set -eo pipefail 5 | 6 | VERSION="$1" 7 | MAJOR="${VERSION%%.*}" 8 | VERSION="${VERSION#*.}" 9 | MINOR="${VERSION%%.*}" 10 | echo "${MAJOR}.$((MINOR + 1))-alpha1" 11 | -------------------------------------------------------------------------------- /docs/guide/request-generation/index.md: -------------------------------------------------------------------------------- 1 | ## Requests generation 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.envrc: -------------------------------------------------------------------------------- 1 | # Automatically sets up your devbox environment whenever you cd into this 2 | # directory via our direnv integration: 3 | 4 | eval "$(devbox generate direnv --print-envrc)" 5 | 6 | # check out https://www.jetpack.io/devbox/docs/ide_configuration/direnv/ 7 | # for more details 8 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Bridge/YamlTypeAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Abstracta.JmeterDsl.Core.Bridge 4 | { 5 | [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] 6 | public class YamlTypeAttribute : Attribute 7 | { 8 | public string TagName { get; set; } 9 | } 10 | } -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Engines/EmbeddedJmeterEngine.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.Engines 2 | { 3 | /// 4 | /// Allows running test plans in an embedded JMeter instance. 5 | /// 6 | public class EmbeddedJmeterEngine : BaseJmeterEngine 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Samplers/ISamplerChild.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.Samplers 2 | { 3 | /// 4 | /// Test elements which can be nested as children of a sampler in JMeter, should implement this interface. 5 | /// 6 | public interface ISamplerChild : IDslTestElement 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/guide/protocols/http/redirects.md: -------------------------------------------------------------------------------- 1 | #### Redirects 2 | 3 | When JMeter DSL (using JMeter logic) detects a redirection, it will automatically do a request to the redirected URL and register the redirection as a sub-sample of the main request. 4 | 5 | If you want to disable such logic, you can just call `.FollowRedirects(false)` in a given `HttpSampler`. 6 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Listeners/BaseListener.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.TestElements; 2 | 3 | namespace Abstracta.JmeterDsl.Core.Listeners 4 | { 5 | public abstract class BaseListener : BaseTestElement, IMultiLevelTestElement 6 | { 7 | protected BaseListener() 8 | : base(null) 9 | { 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/ThreadGroups/IThreadGroupChild.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.ThreadGroups 2 | { 3 | /// 4 | /// Test elements that can be added as direct children of a thread group in jmeter should implement this interface. 5 | /// 6 | public interface IThreadGroupChild : IDslTestElement 7 | { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /docs/guide/response-processing/correlation/index.md: -------------------------------------------------------------------------------- 1 | ### Use part of a response in a subsequent request (aka correlation) 2 | 3 | It is a usual requirement while creating a test plan for an application to be able to use part of a response (e.g.: a generated ID, token, etc.) in a subsequent request. This can be easily achieved using JMeter extractors and variables. 4 | 5 | 6 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/ITestPlanChild.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core 2 | { 3 | /// 4 | /// Test elements that can be added directly as test plan children in JMeter should implement this interface. 5 | /// Check for an example. 6 | /// 7 | public interface ITestPlanChild : IDslTestElement 8 | { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docs/.vuepress/plugins/repoLinkSolverPlugin.ts: -------------------------------------------------------------------------------- 1 | import mditPlugin from 'markdown-it-replace-link' 2 | 3 | export const repoLinkSolverPlugin = ({repoUrl} : {repoUrl: String}) => ({ 4 | name: 'repoLinkSolverPlugin', 5 | extendsMarkdown: (md) => { 6 | md.use(mditPlugin, { 7 | replaceLink: (link, env) => link.startsWith('/') ? repoUrl + '/tree/master' + link : link 8 | }) 9 | } 10 | }); 11 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Timers/BaseTimer.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.TestElements; 2 | 3 | namespace Abstracta.JmeterDsl.Core.Timers 4 | { 5 | /// 6 | /// Contains common logic for all timers. 7 | /// 8 | public abstract class BaseTimer : BaseTestElement, IMultiLevelTestElement 9 | { 10 | public BaseTimer(string name) 11 | : base(name) 12 | { 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/nuget-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script takes care of deploying packages to nuget 4 | # 5 | # Required environment variables: NUGET_API_KEY 6 | 7 | set -xeo pipefail 8 | 9 | push_package() { 10 | dotnet nuget push "$1" --api-key "${NUGET_API_KEY}" --source https://api.nuget.org/v3/index.json --skip-duplicate 11 | } 12 | 13 | find . -name "*.nupkg" -or -name "*.snupkg" | sort | while read PACKAGE; do 14 | push_package ${PACKAGE} 15 | done 16 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Bridge/TestPlanExecution.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.Bridge 2 | { 3 | public class TestPlanExecution 4 | { 5 | private readonly IDslJmeterEngine _engine; 6 | private readonly DslTestPlan _testPlan; 7 | 8 | public TestPlanExecution(IDslJmeterEngine engine, DslTestPlan testPlan) 9 | { 10 | _engine = engine; 11 | _testPlan = testPlan; 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Timers/DslSynchronizingTimer.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.Timers 2 | { 3 | /// 4 | /// Uses JMeter Synchronizing Timer to allow sending a batch of requests simultaneously to a system 5 | /// under test. 6 | /// 7 | public class DslSynchronizingTimer : BaseTimer 8 | { 9 | public DslSynchronizingTimer() 10 | : base(null) 11 | { 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /docs/.vuepress/client.ts: -------------------------------------------------------------------------------- 1 | import { defineClientConfig } from '@vuepress/client' 2 | import { library } from '@fortawesome/fontawesome-svg-core' 3 | import { faDiscord, faGithub } from '@fortawesome/free-brands-svg-icons' 4 | import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome' 5 | 6 | library.add(faDiscord, faGithub) 7 | 8 | export default defineClientConfig({ 9 | enhance({ app }) { 10 | app.component('font-awesome-icon', FontAwesomeIcon) 11 | }, 12 | }) -------------------------------------------------------------------------------- /docs/guide/debugging/index.md: -------------------------------------------------------------------------------- 1 | ## Test plan debugging 2 | 3 | A usual requirement while building a test plan is to be able to review requests and responses and debug the test plan for potential issues in the configuration or behavior of the service under test. With JMeter DSL you have several options for this purpose. 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/guide/protocols/http/cookies-and-cache.md: -------------------------------------------------------------------------------- 1 | #### Cookies & caching 2 | 3 | JMeter DSL automatically adds a cookie manager and cache manager for automatic HTTP cookie and caching handling, emulating a browser behavior. If you need to disable them you can use something like this: 4 | 5 | ```cs 6 | TestPlan( 7 | HttpCookies().Disable(), 8 | HttpCache().Disable(), 9 | ThreadGroup(2, 10, 10 | HttpSampler("http://my.service") 11 | ) 12 | ) 13 | ``` 14 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Azure.Tests/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.BlazeMeter.Tests/log4j2.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Configs/BaseConfigElement.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.TestElements; 2 | 3 | namespace Abstracta.JmeterDsl.Core.Configs 4 | { 5 | /// 6 | /// Contains common logic for config elements defined by the DSL. 7 | /// 8 | public abstract class BaseConfigElement : BaseTestElement, IMultiLevelTestElement 9 | { 10 | public BaseConfigElement(string name) 11 | : base(name) 12 | { 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.github/fix-docs-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script takes care of setting proper JMeter DSL version in docs. 4 | # 5 | set -eo pipefail 6 | 7 | VERSION=$1 8 | 9 | update_file_versions() { 10 | local VERSION="$1" 11 | local FILE="$2" 12 | sed -i "s/--version [0-9.]\+/--version ${VERSION}/g" "${FILE}" 13 | } 14 | 15 | update_file_versions ${VERSION} README.md 16 | 17 | find docs -name "*.md" -not -path "*/node_modules/*" | while read DOC_FILE; do 18 | update_file_versions ${VERSION} ${DOC_FILE} 19 | done -------------------------------------------------------------------------------- /docs/guide/reporting/index.md: -------------------------------------------------------------------------------- 1 | ## Reporting 2 | 3 | Once you have a test plan you would usually want to be able to analyze the collected information. This section contains a few ways to achieve this, but in the future, we plan to support more ([as Java DSL does](https://abstracta.github.io/jmeter-java-dsl/guide/#reporting)). If you are interested in some not yet covered feature, please ask for it by creating an [issue in the repository](https://github.com/abstracta/jmeter-dotnet-dsl/issues). 4 | 5 | 6 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/IDslTestElement.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core 2 | { 3 | /// 4 | /// Interface to be implemented by all elements composing a JMeter test plan. 5 | /// 6 | public interface IDslTestElement 7 | { 8 | /// 9 | /// Shows the test element in it's defined GUI in a popup window. 10 | ///
11 | /// This might be handy to visualize the element as it looks in JMeter GUI. 12 | ///
13 | void ShowInGui(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/TestElements/IMultiLevelTestElement.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.Samplers; 2 | using Abstracta.JmeterDsl.Core.ThreadGroups; 3 | 4 | namespace Abstracta.JmeterDsl.Core.TestElements 5 | { 6 | /// 7 | /// This is just a simple interface to avoid code duplication for test elements that apply at 8 | /// different levels of a test plan(at test plan, thread group or as sampler child). 9 | /// 10 | public interface IMultiLevelTestElement : ITestPlanChild, IThreadGroupChild, ISamplerChild 11 | { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/ThreadGroups/BaseThreadGroup.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.TestElements; 2 | 3 | namespace Abstracta.JmeterDsl.Core.ThreadGroups 4 | { 5 | /// 6 | /// Contains common logic for all Thread Groups. 7 | /// 8 | public abstract class BaseThreadGroup : TestElementContainer, ITestPlanChild 9 | where T : BaseThreadGroup 10 | { 11 | protected BaseThreadGroup(string name, IThreadGroupChild[] children) 12 | : base(name, children) 13 | { 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Stats/CountMetricSummary.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.Stats 2 | { 3 | /// 4 | /// Provides summary data for a set of count values. 5 | /// 6 | public class CountMetricSummary 7 | { 8 | /// 9 | /// Provides the total count (the sum). 10 | /// 11 | public long Total { get; set; } 12 | 13 | /// 14 | /// Provides the average count per second for the given metric. 15 | /// 16 | public double PerSecond { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /docs/.vuepress/plugins/includedRelativeLinkSolverPlugin.ts: -------------------------------------------------------------------------------- 1 | import mditPlugin from 'markdown-it-replace-link' 2 | 3 | export const includedRelativeLinkSolverPlugin = ({}) => ({ 4 | name: 'includedRelativeLinkSolverPlugin', 5 | extendsMarkdown: (md) => { 6 | md.use(mditPlugin, { 7 | replaceLink: (link, env) => { 8 | if (link.startsWith('.')) { 9 | let hashPos = link.indexOf('#') 10 | return hashPos > 0 ? link.substring(hashPos) : link 11 | } 12 | return link 13 | } 14 | }) 15 | } 16 | }); 17 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/TestElements/BaseTestElement.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.Bridge; 2 | 3 | namespace Abstracta.JmeterDsl.Core.TestElements 4 | { 5 | /// 6 | /// Provides the basic logic for all . 7 | /// 8 | public abstract class BaseTestElement : IDslTestElement 9 | { 10 | protected readonly string _name; 11 | 12 | public BaseTestElement(string name) 13 | { 14 | _name = name; 15 | } 16 | 17 | public void ShowInGui() => 18 | new BridgeService().ShowTestElementInGui(this); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /docs/guide/setup.md: -------------------------------------------------------------------------------- 1 | ## Setup 2 | 3 | To use the DSL just include it in your project: 4 | 5 | ```powershell 6 | dotnet add package Abstracta.JmeterDsl --version 0.8 7 | ``` 8 | 9 | ::: tip 10 | [Here](https://github.com/abstracta/jmeter-dotnet-dsl-sample) is a sample project in case you want to start one from scratch. 11 | ::: 12 | 13 | ::: warning 14 | JMeter .Net DSL uses existing JMeter Java DSL which in turn uses JMeter. JMeter Java DSL and JMeter are Java based tools. So, **Java 8+ is required** for the proper execution of DSL test plans. One option is downloading a JVM from [Adoptium](https://adoptium.net/) if you don't have one already. 15 | ::: -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*.cs] 2 | 3 | csharp_style_expression_bodied_methods=when_on_single_line 4 | dotnet_analyzer_diagnostic.severity = error 5 | dotnet_diagnostic.CS1591.severity = none 6 | dotnet_diagnostic.CA1822.severity = none 7 | dotnet_diagnostic.IDE0008.severity = none 8 | dotnet_diagnostic.IDE0039.severity = none 9 | dotnet_diagnostic.IDE0052.severity = none 10 | dotnet_diagnostic.IDE0058.severity = none 11 | dotnet_diagnostic.IDE0063.severity = none 12 | dotnet_diagnostic.IDE0065.severity = none 13 | dotnet_diagnostic.IDE0090.severity = none 14 | dotnet_diagnostic.IDE0270.severity = none 15 | dotnet_diagnostic.JSON002.severity = none 16 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Bridge/IDslProperty.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.Bridge 2 | { 3 | /// 4 | /// This is just a marker interface to properly serialize properties that can include multiple values. 5 | ///
6 | /// Such properties are added to a __propList c# class property and a class implementing IDslProperty is used to define the name of the property. 7 | ///
8 | /// and for examples of classes using __propList and IDslProperty interface. 9 | ///
10 | public interface IDslProperty 11 | { 12 | } 13 | } -------------------------------------------------------------------------------- /.github/vuepress-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script takes care of building and deploying the vuepress documentation to github pages. 4 | # 5 | set -eo pipefail 6 | 7 | cd docs 8 | 9 | pnpm install && pnpm build 10 | 11 | cd .vuepress/dist 12 | 13 | EMAIL="$(git log --format='%ae' HEAD^!)" 14 | USERNAME="$(git log --format='%an' HEAD^!)" 15 | git init 16 | git config --local user.email "$EMAIL" 17 | git config --local user.name "$USERNAME" 18 | git add . 19 | git commit -m '[skip ci] Deploy docs to GitHub pages' 20 | 21 | git push -f https://git:${ACCESS_TOKEN}@github.com/abstracta/jmeter-dotnet-dsl.git master:gh-pages 22 | 23 | cd $GITHUB_WORKSPACE 24 | -------------------------------------------------------------------------------- /devbox.json: -------------------------------------------------------------------------------- 1 | { 2 | "packages": [ 3 | "maven@latest", 4 | "nodejs@latest", 5 | "nodePackages.pnpm@latest", 6 | "temurin-bin-8@latest", 7 | "dotnet-sdk@latest" 8 | ], 9 | "shell": { 10 | "init_hook": [ 11 | "echo 'Welcome to devbox!' > /dev/null" 12 | ], 13 | "scripts": { 14 | "setup": [ 15 | "pnpm --dir docs install" 16 | ], 17 | "docs": [ 18 | "pnpm --dir docs dev" 19 | ], 20 | "clean": [ 21 | "dotnet clean" 22 | ], 23 | "build": [ 24 | "dotnet build" 25 | ], 26 | "test": [ 27 | "dotnet test" 28 | ] 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/guide/protocols/http/index.md: -------------------------------------------------------------------------------- 1 | ### HTTP 2 | 3 | Throughout this guide, several examples have been shown for simple cases of HTTP requests (mainly how to do gets and posts), but the DSL provides additional features that you might need to be aware of. 4 | 5 | Here we show some of them, but check [JmeterDsl](/Abstracta.JmeterDsl/JmeterDsl.cs) and [DslHttpSampler](/Abstracta.JmeterDsl/Http/DslHttpSampler.cs) to explore all available features. 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Bridge/JvmException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Abstracta.JmeterDsl.Core.Bridge 4 | { 5 | /// 6 | /// Exception thrown when there is some problem interacting with Java Virutal Machine or executing a particular command. 7 | ///
8 | /// This might be to some exception generated in test plan execution, showing element in GUI, or event issues with JVM initialization. 9 | ///
10 | public class JvmException : Exception 11 | { 12 | public JvmException() 13 | : base("JVM execution failed. Check stderr and stdout for additional info.") 14 | { 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /docs/guide/protocols/http/methods-and-body.md: -------------------------------------------------------------------------------- 1 | #### Methods & body 2 | 3 | As previously seen, you can do simple gets and posts like in the following snippet: 4 | 5 | ```cs 6 | HttpSampler("http://my.service") // A simple get 7 | HttpSampler("http://my.service") 8 | .Post("{\"field\":\"val\"}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) // simple post 9 | ``` 10 | 11 | But you can also use additional methods to specify any HTTP method and body: 12 | 13 | ```cs 14 | HttpSampler("http://my.service") 15 | .Method(HttpMethod.Put.Method) 16 | .ContentType(new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) 17 | .Body("{\"field\":\"val\"}") 18 | ``` 19 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/Core/Controllers/DslTransactionControllerTest.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.Controllers 2 | { 3 | using static JmeterDsl; 4 | 5 | public class DslTransactionControllerTest 6 | { 7 | [Test] 8 | public void ShouldIncludeTransactionSampleInResultsWhenTestPlanWithTransaction() 9 | { 10 | TestPlanStats stats = TestPlan( 11 | ThreadGroup(1, 1, 12 | Transaction("My Transaction", 13 | DummySampler("ok") 14 | ) 15 | ) 16 | ).Run(); 17 | Assert.That(stats.Overall.SamplesCount, Is.EqualTo(2)); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/Core/Controllers/ForLoopControllerTest.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.Controllers 2 | { 3 | using static JmeterDsl; 4 | 5 | public class ForLoopControllerTest 6 | { 7 | [Test] 8 | public void ShouldGetExpectedSamplerCountWhenSamplerInsideLoopController() 9 | { 10 | var loopIterations = 3; 11 | var stats = TestPlan( 12 | ThreadGroup(1, 1, 13 | ForLoopController(loopIterations, 14 | DummySampler("ok") 15 | ) 16 | )).Run(); 17 | Assert.That(stats.Overall.SamplesCount, Is.EqualTo(loopIterations)); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/Core/Timers/DslConstantTimerTest.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.Timers 2 | { 3 | using System; 4 | using static JmeterDsl; 5 | 6 | public class DslConstantTimerTest 7 | { 8 | [Test] 9 | public void ShouldLastAtLeastConfiguredTimeWhenUsingConstantTimer() 10 | { 11 | var timerDuration = TimeSpan.FromSeconds(5); 12 | var stats = TestPlan( 13 | ThreadGroup(1, 1, 14 | ConstantTimer(timerDuration), 15 | DummySampler("OK") 16 | ) 17 | ).Run(); 18 | Assert.That(stats.Duration, Is.GreaterThan(timerDuration)); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/IDslJmeterEngine.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core 2 | { 3 | /// 4 | /// Interface to be implemented by classes allowing to run a DslTestPlan in different engines. 5 | /// 6 | public interface IDslJmeterEngine 7 | { 8 | /// 9 | /// Runs the given test plan obtaining the execution metrics. 10 | ///
11 | /// This method blocks execution until the test plan execution ends. 12 | ///
13 | /// to run in the JMeter engine. 14 | /// the metrics associated to the run. 15 | TestPlanStats Run(DslTestPlan testPlan); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/Core/Samplers/DslFlowControlActionTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Abstracta.JmeterDsl.Core.Samplers 4 | { 5 | using static JmeterDsl; 6 | 7 | public class DslFlowControlActionTest 8 | { 9 | [Test] 10 | public void ShouldLastAtLeastConfiguredTimeWhenUsingConstantTimer() 11 | { 12 | var timerDuration = TimeSpan.FromSeconds(5); 13 | var stats = TestPlan( 14 | ThreadGroup(1, 1, 15 | ThreadPause(timerDuration), 16 | DummySampler("OK") 17 | ) 18 | ).Run(); 19 | Assert.That(stats.Duration, Is.GreaterThan(timerDuration)); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /docs/guide/scale/index.md: -------------------------------------------------------------------------------- 1 | ## Run test at scale 2 | 3 | Running a load test from one machine is not always enough, since you are limited to the machine's hardware capabilities. Sometimes, is necessary to run the test using a cluster of machines to be able to generate enough load for the system under test. Currently, the .Net DSL only provides two ways to run tests at scale, but in the future, we plan to support more ([as Java DSL does](https://abstracta.github.io/jmeter-java-dsl/guide/#run-test-at-scale)). If you are interested in some not yet covered feature, please ask for it by creating an [issue in the repository](https://github.com/abstracta/jmeter-dotnet-dsl/issues). 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/Core/Timers/DslUniformRandomTimerTest.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.Timers 2 | { 3 | using System; 4 | using static JmeterDsl; 5 | 6 | public class DslUniformRandomTimerTest 7 | { 8 | [Test] 9 | public void ShouldLastAtLeastMinimumTimeWhenUsingRandomUniformTimer() 10 | { 11 | var minimum = TimeSpan.FromSeconds(5); 12 | var stats = TestPlan( 13 | ThreadGroup(1, 1, 14 | UniformRandomTimer(minimum, TimeSpan.FromSeconds(7)), 15 | DummySampler("OK") 16 | ) 17 | ).Run(); 18 | Assert.That(stats.Duration, Is.GreaterThan(minimum)); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/update-sample-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script takes care of updating JMeter DSL version in sample project. 4 | # 5 | set -eo pipefail 6 | 7 | VERSION=$1 8 | 9 | USER_EMAIL="$(git log --format='%ae' HEAD^!)" 10 | USER_NAME="$(git log --format='%an' HEAD^!)" 11 | 12 | cd jmeter-dotnet-dsl-sample 13 | sed -i "s/Include=\"Abstracta.JmeterDsl\" Version=\"[0-9.]\+\"/Include=\"Abstracta.JmeterDsl\" Version=\"${VERSION}\"/g" Abstracta.JmeterDsl.Sample/Abstracta.JmeterDsl.Sample.csproj 14 | 15 | git add . 16 | git config --local user.email "$USER_EMAIL" 17 | git config --local user.name "$USER_NAME" 18 | git commit -m "Updated Abstracta.JmeterDsl version" 19 | git push origin HEAD:master 20 | cd .. 21 | rm -rf jmeter-dotnet-dsl-sample 22 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/Core/PreProcessors/DslJsr223PreProcessorTest.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.PreProcessors 2 | { 3 | using static JmeterDsl; 4 | 5 | public class DslJsr223PreProcessorTest 6 | { 7 | [Test] 8 | public void ShouldUseDefinedLabelWhenPreProcessorSettingLabelInTestPlan() 9 | { 10 | var stats = TestPlan( 11 | ThreadGroup(1, 1, 12 | DummySampler("ok") 13 | .Children( 14 | Jsr223PreProcessor("sampler.name = 'test'") 15 | ) 16 | )).Run(); 17 | Assert.That(stats.Labels["test"].SamplesCount, Is.EqualTo(1)); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/TestElements/DslJsr223TestElement.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.TestElements 2 | { 3 | /// 4 | /// Abstracts common logic used by JSR223 test elements. 5 | /// 6 | public abstract class DslJsr223TestElement : BaseTestElement 7 | where T : DslJsr223TestElement 8 | { 9 | protected readonly string _script; 10 | protected string _language; 11 | 12 | public DslJsr223TestElement(string name, string script) 13 | : base(name) 14 | { 15 | _script = script; 16 | } 17 | 18 | public T Language(string language) 19 | { 20 | _language = language; 21 | return (T)this; 22 | } 23 | } 24 | } -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/PostProcessors/DslVariableExtractor.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.TestElements; 2 | 3 | namespace Abstracta.JmeterDsl.Core.PostProcessors 4 | { 5 | /// 6 | /// Contains common logic for post processors which extract some value into a variable. 7 | /// 8 | public abstract class DslVariableExtractor : DslScopedTestElement 9 | where T : DslVariableExtractor 10 | { 11 | protected readonly string _variableName; 12 | protected int? _matchNumber; 13 | protected string _defaultValue; 14 | 15 | public DslVariableExtractor(string name, string varName) 16 | : base(name) 17 | { 18 | _variableName = varName; 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /.github/semver-check.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script checks that the version number of the release is an expected one, and avoid erroneous releases which don't follow semver 4 | set -eo pipefail 5 | 6 | git fetch --tags --quiet 7 | VERSION="$1" 8 | PREV_VERSION=$(git tag --sort=-creatordate | head -1) 9 | PREV_VERSION=${PREV_VERSION:-v0.0} 10 | PREV_VERSION=${PREV_VERSION#v} 11 | PREV_MAJOR="${PREV_VERSION%%.*}" 12 | PREV_VERSION="${PREV_VERSION#*.}" 13 | PREV_MINOR="${PREV_VERSION%%.*}" 14 | PREV_PATCH="${PREV_VERSION#*.}" 15 | if [[ "$PREV_VERSION" == "$PREV_PATCH" ]]; then 16 | PREV_PATCH="0" 17 | fi 18 | 19 | [[ "$VERSION" == "$PREV_MAJOR.$PREV_MINOR.$((PREV_PATCH + 1))" || "$VERSION" == "$PREV_MAJOR.$((PREV_MINOR + 1))" || "$VERSION" == "$((PREV_MAJOR + 1)).0" ]] 20 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Samplers/DslFlowControlAction.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.Bridge; 2 | 3 | namespace Abstracta.JmeterDsl.Core.Samplers 4 | { 5 | /// 6 | /// Uses JMeter Flow Control Action to allow taking different actions (stop, pause, interrupt). 7 | /// 8 | [YamlType(TagName = "threadPause")] 9 | public class DslFlowControlAction : BaseSampler 10 | { 11 | private readonly string _duration; 12 | 13 | private DslFlowControlAction(string duration) 14 | : base(null) 15 | { 16 | _duration = duration; 17 | } 18 | 19 | public static DslFlowControlAction PauseThread(string duration) => 20 | new DslFlowControlAction(duration); 21 | } 22 | } -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | us.abstracta.jmeter.dotnet 7 | jmeter-dotnet-dsl-parent 8 | 1.0-SNAPSHOT 9 | pom 10 | 11 | This pom is only needed to be able to copy jmeter-java-dsl jars and dependencies with dependency:copy-dependencies maven plugin goal in child projects 12 | 13 | 14 | 1.29.1 15 | 16 | -------------------------------------------------------------------------------- /docs/guide/thread-groups/index.md: -------------------------------------------------------------------------------- 1 | ## Advanced threads configuration 2 | 3 | JMeter DSL provides two simple ways of creating thread groups which are used in most scenarios: 4 | 5 | * specifying threads and the number of iterations each thread should execute before ending the test plan 6 | * specifying threads and duration for which each thread should execute before the test plan ends 7 | 8 | This is how they look in code: 9 | 10 | ```cs 11 | ThreadGroup(10, 20, ...) // 10 threads for 20 iterations each 12 | ThreadGroup(10, TimeSpan.FromSeconds(20), ...) // 10 threads for 20 seconds each 13 | ``` 14 | 15 | But these options are not good when working with many threads or when trying to configure some complex test scenarios (like when doing incremental or peak tests). 16 | 17 | 18 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/Core/Configs/DslCsvDataSetTest.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.Configs 2 | { 3 | using static JmeterDsl; 4 | 5 | public class DslCsvDataSetTest 6 | { 7 | [Test] 8 | public void ShouldGetExpectedSamplesWhenTestPlanWithSampleNamesFromCsvDataSet() 9 | { 10 | var stats = TestPlan( 11 | CsvDataSet("Core/Configs/data.csv"), 12 | ThreadGroup(1, 2, 13 | DummySampler("${VAR1}-${VAR2}", "ok") 14 | )).Run(); 15 | Assert.Multiple(() => 16 | { 17 | Assert.That(stats.Labels["val1-val2"].SamplesCount, Is.EqualTo(1)); 18 | Assert.That(stats.Labels["val,3-val4"].SamplesCount, Is.EqualTo(1)); 19 | }); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Bridge/EnumConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | using YamlDotNet.Core; 4 | using YamlDotNet.Core.Events; 5 | using YamlDotNet.Serialization; 6 | 7 | namespace Abstracta.JmeterDsl.Core.Bridge 8 | { 9 | public class EnumConverter : IYamlTypeConverter 10 | { 11 | public bool Accepts(Type type) 12 | => type.IsEnum; 13 | 14 | public object ReadYaml(IParser parser, Type type) 15 | => throw new NotImplementedException(); 16 | 17 | public void WriteYaml(IEmitter emitter, object value, Type type) 18 | => emitter.Emit(new Scalar(ToSnakeCase(value.ToString()))); 19 | 20 | private string ToSnakeCase(string val) 21 | => Regex.Replace(val, @"([a-z0-9])([A-Z])", "$1_$2").ToUpper(); 22 | } 23 | } -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/Core/Controllers/DslSimpleControllerTest.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.Controllers 2 | { 3 | using static JmeterDsl; 4 | 5 | public class DslSimpleControllerTest 6 | { 7 | [Test] 8 | public void ShouldApplyAssertionToSimpleControllerScopedElements() 9 | { 10 | var body = "FAIL"; 11 | var stats = TestPlan( 12 | ThreadGroup(1, 1, 13 | SimpleController( 14 | ResponseAssertion() 15 | .ContainsSubstrings("OK"), 16 | DummySampler(body), 17 | DummySampler(body) 18 | ), 19 | DummySampler(body) 20 | ) 21 | ).Run(); 22 | Assert.That(stats.Overall.ErrorsCount, Is.EqualTo(2)); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /docs/.vuepress/components/HomeFeatures.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 25 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Controllers/ForLoopController.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.ThreadGroups; 2 | 3 | namespace Abstracta.JmeterDsl.Core.Controllers 4 | { 5 | /// 6 | /// Allows running part of a test plan a given number of times inside one thread group iteration. 7 | ///
8 | /// Internally this uses JMeter Loop Controller. 9 | ///
10 | /// JMeter automatically creates a variable named __jm__<controllerName>__idx which contains 11 | /// the index of the iteration starting with zero. 12 | ///
13 | public class ForLoopController : BaseController 14 | { 15 | private readonly string _count; 16 | 17 | public ForLoopController(string name, string count, IThreadGroupChild[] children) 18 | : base(name, children) 19 | { 20 | _count = count; 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /.github/fix-project-version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # This script takes care of setting proper JMeter DSL version in projects. 4 | # 5 | set -eo pipefail 6 | 7 | VERSION=$1 8 | 9 | update_file_versions() { 10 | local VERSION="$1" 11 | local FILE="$2" 12 | local BASE_VERSION="${VERSION%%-*}" 13 | local ASSEMBLY_VERSION="${BASE_VERSION}.0" 14 | if [ $(echo "${BASE_VERSION}" | awk -F"." '{print NF-1}') == 1 ]; then 15 | ASSEMBLY_VERSION="${ASSEMBLY_VERSION}.0" 16 | fi 17 | sed -i "s/[^<]\+<\/AssemblyVersion>/${ASSEMBLY_VERSION}<\/AssemblyVersion>/g" "${FILE}" 18 | sed -i "s/[^<]\+<\/Version>/${VERSION}<\/Version>/g" "${FILE}" 19 | sed -i "s/[^<]\+<\/FileVersion>/${BASE_VERSION}<\/FileVersion>/g" "${FILE}" 20 | } 21 | 22 | find . -name "*.csproj" | while read DOC_FILE; do 23 | update_file_versions ${VERSION} ${DOC_FILE} 24 | done -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Controllers/DslSimpleController.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.ThreadGroups; 2 | 3 | namespace Abstracta.JmeterDsl.Core.Controllers 4 | { 5 | /// 6 | /// Builds a Simple Controller that allows defining new JMeter scope for other elements to apply. 7 | ///
8 | /// This is handy for example to apply timers, configs, listeners, assertions, pre- and 9 | /// post-processors to only some samplers in the test plan. 10 | ///
11 | /// It has a similar functionality as the transaction controller, but it doesn't add any additional 12 | /// sample results (statistics) to the test plan. 13 | ///
14 | public class DslSimpleController : BaseController 15 | { 16 | public DslSimpleController(string name, IThreadGroupChild[] children) 17 | : base(name, children) 18 | { 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Engines/BaseJmeterEngine.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.Bridge; 2 | using YamlDotNet.Serialization; 3 | 4 | namespace Abstracta.JmeterDsl.Core.Engines 5 | { 6 | public abstract class BaseJmeterEngine : IDslJmeterEngine 7 | where T : BaseJmeterEngine 8 | { 9 | [YamlIgnore] 10 | protected string _jvmArgs = string.Empty; 11 | 12 | /// 13 | /// Specifies arguments to be added to JVM command line. 14 | ///
15 | /// This is helpful, for example, when debugging JMeter DSL or JMeter code. 16 | ///
17 | public T JvmArgs(string args) 18 | { 19 | _jvmArgs = args; 20 | return (T)this; 21 | } 22 | 23 | public TestPlanStats Run(DslTestPlan testPlan) => 24 | new BridgeService() 25 | .JvmArgs(_jvmArgs) 26 | .RunTestPlanInEngine(testPlan, this); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/guide/debugging/show-in-gui.md: -------------------------------------------------------------------------------- 1 | ### Test plan review un JMeter GUI 2 | 3 | A usual requirement for new DSL users that are used to Jmeter GUI, is to be able to review Jmeter DSL generated test plan in the familiar JMeter GUI. For this, you can use the `ShowInGui()` method in a test plan to open JMeter GUI with the preloaded test plan. 4 | 5 | This can be also used to debug the test plan, by adding elements (like view results tree, dummy samplers, etc.) in the GUI and running the test plan. 6 | 7 | Here is a simple example using the method: 8 | 9 | ```cs 10 | using static Abstracta.JmeterDsl.JmeterDsl; 11 | 12 | public class PerformanceTest 13 | { 14 | [Test] 15 | public void LoadTest() 16 | { 17 | var stats = TestPlan( 18 | ThreadGroup(2, 10, 19 | HttpSampler("http://my.service") 20 | ) 21 | ).ShowInGui(); 22 | } 23 | } 24 | ``` 25 | 26 | Which ends up opening a window like this one: 27 | 28 | ![Test plan in JMeter GUI](./images/test-plan-gui.png) -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Timers/DslConstantTimer.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.Timers 2 | { 3 | /// 4 | /// Allows using JMeter Constant Timers which pause the thread for a given period. 5 | ///
6 | /// The pause calculated by the timer will be applied after samplers pre-processors execution and 7 | /// before actual sampling. 8 | ///
9 | /// Take into consideration that timers applies to all samplers in their scope: if added at test plan 10 | /// level, it will apply to all samplers in test plan; if added at thread group level, it will apply 11 | /// only to samples in such thread group; if added as child of a sampler, it will only apply to that 12 | /// sampler. 13 | ///
14 | public class DslConstantTimer : BaseTimer 15 | { 16 | private readonly string _duration; 17 | 18 | public DslConstantTimer(string duration) 19 | : base(null) 20 | { 21 | _duration = duration; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/TestElements/TestElementContainer.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace Abstracta.JmeterDsl.Core.TestElements 5 | { 6 | /// 7 | /// Abstracts logic for that can nest other test elements. 8 | /// 9 | /// is type of sub classes. 10 | /// is type of test elements that can be nested by this class. 11 | public abstract class TestElementContainer : BaseTestElement 12 | where T : TestElementContainer 13 | { 14 | protected List _children = new List(); 15 | 16 | protected TestElementContainer(string name, C[] children) 17 | : base(name) 18 | { 19 | _children = children.ToList(); 20 | } 21 | 22 | protected T Children(params C[] chilren) 23 | { 24 | _children.AddRange(chilren); 25 | return (T)this; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /docs/guide/protocols/http/multipart.md: -------------------------------------------------------------------------------- 1 | #### Multipart requests 2 | 3 | When you need to upload files to an HTTP server or need to send a complex request body, you will in many cases require sending multipart requests. To send a multipart request just use `BodyPart` and `BodyFilePart` methods like in the following example: 4 | 5 | ```cs 6 | using static Abstracta.JmeterDsl.JmeterDsl; 7 | using System.Net.Http; 8 | using System.Net.Http.Headers; 9 | using System.Net.Mime; 10 | 11 | public class PerformanceTest 12 | { 13 | [Test] 14 | public void LoadTest() 15 | { 16 | TestPlan( 17 | ThreadGroup(1, 1, 18 | HttpSampler("https://myservice.com/report"), 19 | .Method(HttpMethod.Post.Method) 20 | .BodyPart("myText", "Hello World", new MediaTypeHeaderValue(MediaTypeNames.Text.Plain)) 21 | .BodyFilePart("myFile", "myReport.xml", new MediaTypeHeaderValue(MediaTypeNames.Text.Xml)) 22 | ) 23 | ).Run(); 24 | } 25 | } 26 | ``` 27 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: run tests 2 | on: 3 | push: 4 | tags-ignore: 5 | - "*" 6 | branches: 7 | - "**" 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | concurrency: azure_test 12 | steps: 13 | - uses: actions/checkout@v4 14 | - name: Setup .NET Core 15 | uses: actions/setup-dotnet@v4 16 | with: 17 | dotnet-version: 6 18 | - uses: actions/setup-java@v4 19 | with: 20 | distribution: temurin 21 | java-version: 11 22 | cache: maven 23 | - name: Install dependencies 24 | run: dotnet restore 25 | - name: Build 26 | run: dotnet build --configuration Release --no-restore 27 | - name: Test 28 | # avoid running tests in parallel since it may happen that two tests try to modify .jmeter-dsl directory 29 | run: dotnet test -m:1 --no-build --configuration Release --verbosity normal 30 | env: 31 | AZURE_CREDS: ${{ secrets.AZURE_CREDS }} 32 | BZ_TOKEN: ${{ secrets.BZ_TOKEN }} 33 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/Core/Listeners/ResponseFileSaverTest.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Abstracta.JmeterDsl.Core.Listeners 4 | { 5 | using static JmeterDsl; 6 | 7 | public class ResponseFileSaverTest 8 | { 9 | [Test] 10 | public void ShouldWriteFileWithResponseContentWhenResponseFileSaverInPlan() 11 | { 12 | var workDir = new DirectoryInfo("responses"); 13 | try 14 | { 15 | var body = "ok"; 16 | TestPlan( 17 | ThreadGroup(1, 1, 18 | DummySampler(body), 19 | ResponseFileSaver(Path.Combine(workDir.FullName, "response")) 20 | )).Run(); 21 | var responseFileBody = File.ReadAllText(Path.Combine(workDir.FullName, "response1.unknown")); 22 | Assert.That(responseFileBody, Is.EqualTo(body)); 23 | } 24 | finally 25 | { 26 | workDir.Delete(true); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/Core/ThreadGroups/DslThreadGroupTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Abstracta.JmeterDsl.Core.ThreadGroups 4 | { 5 | using static JmeterDsl; 6 | 7 | public class DslThreadGroupTest 8 | { 9 | [Test] 10 | public void ShouldMakeOneRequestWhenOneThreadAndIteration() 11 | { 12 | var stats = TestPlan( 13 | ThreadGroup(threads: 1, iterations: 1, 14 | DummySampler("OK") 15 | ) 16 | ).Run(); 17 | Assert.That(stats.Overall.SamplesCount, Is.EqualTo(1)); 18 | } 19 | 20 | [Test] 21 | public void ShouldTakeAtLeastDurationWhenThreadGroupWithDuration() 22 | { 23 | var duration = TimeSpan.FromSeconds(5); 24 | var stats = TestPlan( 25 | ThreadGroup(threads: 1, duration: duration, 26 | DummySampler("OK") 27 | ) 28 | ).Run(); 29 | Assert.That(stats.Duration, Is.GreaterThanOrEqualTo(duration)); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/TestPlanStats.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Abstracta.JmeterDsl.Core.Stats; 4 | 5 | namespace Abstracta.JmeterDsl.Core 6 | { 7 | /// 8 | /// Contains all statistics collected during the execution of a test plan. 9 | ///
10 | /// When using different samples, specify different names on them to be able to get each sampler 11 | /// specific statistics after they run. 12 | ///
13 | public class TestPlanStats 14 | { 15 | /// 16 | /// Provides the time taken to run the test plan. 17 | /// 18 | public TimeSpan Duration { get; set; } 19 | 20 | /// 21 | /// Provides statistics for the entire test plan. 22 | /// 23 | public StatsSummary Overall { get; set; } 24 | 25 | /// 26 | /// Provides statistics for each label (usually, samplers labels). 27 | /// 28 | public Dictionary Labels { get; set; } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Azure.Tests/Abstracta.JmeterDsl.Azure.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | false 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | PreserveNewest 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/PreProcessors/DslJsr223PreProcessor.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.TestElements; 2 | 3 | namespace Abstracta.JmeterDsl.Core.PreProcessors 4 | { 5 | /// 6 | /// Allows running custom logic before executing a sampler. 7 | ///
8 | /// This is a very powerful and flexible component that allows you to modify variables, sampler, 9 | /// context, etc., before running a sampler (for example to generate dynamic requests 10 | /// programmatically). 11 | ///
12 | /// By default, provided script will be interpreted as groovy script, which is the default setting 13 | /// for JMeter. If you need, you can use any of JMeter provided scripting languages (beanshell, 14 | /// javascript, jexl, etc.) by setting the property. 15 | ///
16 | public class DslJsr223PreProcessor : DslJsr223TestElement, IMultiLevelTestElement 17 | { 18 | public DslJsr223PreProcessor(string name, string script) 19 | : base(name, script) 20 | { 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | us.abstracta.jmeter.dotnet 8 | jmeter-dotnet-dsl-parent 9 | 1.0-SNAPSHOT 10 | ../pom.xml 11 | 12 | jmeter-dotnet-dsl 13 | pom 14 | 15 | This pom is only needed to be able to copy jmeter-java-dsl-bridge jar and its dependencies with dependency:copy-dependencies maven plugin goal 16 | 17 | 18 | 19 | us.abstracta.jmeter 20 | jmeter-java-dsl-bridge 21 | ${jmeter-java-dsl.version} 22 | 23 | 24 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.BlazeMeter.Tests/Abstracta.JmeterDsl.BlazeMeter.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net6.0 5 | false 6 | true 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | PreserveNewest 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/Core/Listeners/JtlWriterTest.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Abstracta.JmeterDsl.Core.Listeners 4 | { 5 | using static JmeterDsl; 6 | 7 | public class JtlWriterTest 8 | { 9 | [Test] 10 | public void ShouldWriteResultsToFileWhenJtlWriterAtTestPlan() 11 | { 12 | DirectoryInfo workDir = new DirectoryInfo("jtls"); 13 | try 14 | { 15 | TestPlan( 16 | ThreadGroup(1, 1, 17 | DummySampler("ok"), 18 | JtlWriter(workDir.FullName) 19 | )).Run(); 20 | Assert.That(GetFileLinesCount(FindJtlFileInDirectory(workDir)), Is.EqualTo(2)); 21 | } 22 | finally 23 | { 24 | workDir.Delete(true); 25 | } 26 | } 27 | 28 | private FileInfo FindJtlFileInDirectory(DirectoryInfo workDir) => 29 | workDir.GetFiles("*.jtl")[0]; 30 | 31 | private int GetFileLinesCount(FileInfo jtlFile) => 32 | File.ReadAllLines(jtlFile.FullName).Length; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /docs/guide/request-generation/timers/synchronizing-timer.md: -------------------------------------------------------------------------------- 1 | #### Requests synchronization 2 | 3 | Usually, samples generated by different threads in a test plan thread group start deviating from each other according to the different durations each of them may experience. 4 | 5 | Here is a diagram depicting this behavior, extracted from [this nice example](https://github.com/abstracta/jmeter-java-dsl/discussions/204) provided by one of JMeter DSL users: 6 | 7 | ![not synchronized samples in 2 threads and 3 iterations](./not-synchronized-samples.png) 8 | 9 | In most cases this is ok. But, if you want to generate batches of simultaneous requests to a system under test, this variability will prevent you from getting the expected behavior. 10 | 11 | So, to synchronize requests, by holding some of them until all are in sync, like in this diagram: 12 | 13 | ![synchronized samples in 2 threads and 3 iterations](./synchronized-samples.png) 14 | 15 | You can use `SynchronizingTimer` like in the following example: 16 | 17 | ```cs 18 | TestPlan( 19 | ThreadGroup(2, 3, 20 | HttpSample("https://mysite"), 21 | SynchronizingTimer() 22 | ) 23 | ) 24 | ``` 25 | -------------------------------------------------------------------------------- /docs/guide/response-processing/correlation/regex-extractor.md: -------------------------------------------------------------------------------- 1 | #### Regular expressions extraction 2 | 3 | Here is an example with JMeter DSL using regular expressions: 4 | 5 | ```cs 6 | using System.Net.Http.Headers; 7 | using System.Net.Mime; 8 | 9 | using static Abstracta.JmeterDsl.JmeterDsl; 10 | 11 | public class PerformanceTest 12 | { 13 | [Test] 14 | public void LoadTest() 15 | { 16 | var stats = TestPlan( 17 | ThreadGroup(2, 10, 18 | HttpSampler("http://my.service/accounts") 19 | .Post("{\"name\": \"John Doe\"}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) 20 | .Children( 21 | RegexExtractor("ACCOUNT_ID", "\"id\":\"([^\"]+)\"") 22 | ), 23 | HttpSampler("http://my.service/accounts/${ACCOUNT_ID}") 24 | ) 25 | ).Run(); 26 | Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); 27 | } 28 | } 29 | ``` 30 | 31 | Check [DslRegexExtractor](/Abstracta.JmeterDsl/Core/PostProcessors/DslRegexExtractor.cs) for more details and additional options. 32 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Listeners/ResponseFileSaver.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.Listeners 2 | { 3 | /// 4 | /// Generates one file for each response of a sample/request. 5 | ///
6 | /// This element is dependant of the scope: this means that if you add it at test plan level it will 7 | /// generate files for all samplers in test plan, if added at thread group level then it will 8 | /// generate files for samplers only in the thread group, and if you add it at sampler level it will 9 | /// generate files only for the associated sampler. 10 | ///
11 | /// By default, it will generate one file for each response using the given (which might include the 12 | /// directory location) prefix to create the files and adding an incremental number to each response 13 | /// and an extension according to the response mime type. 14 | ///
15 | public class ResponseFileSaver : BaseListener 16 | { 17 | private readonly string _fileNamePrefix; 18 | 19 | public ResponseFileSaver(string fileNamePrefix) 20 | { 21 | _fileNamePrefix = fileNamePrefix; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Timers/DslUniformRandomTimer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Abstracta.JmeterDsl.Core.Timers 4 | { 5 | /// 6 | /// Allows specifying JMeter Uniform Random Timers which pause the thread with a random time with 7 | /// uniform distribution. 8 | ///
9 | /// The pause calculated by the timer will be applied after samplers pre-processors execution and 10 | /// before actual sampling. 11 | ///
12 | /// Take into consideration that timers applies to all samplers in their scope: if added at test plan 13 | /// level, it will apply to all samplers in test plan; if added at thread group level, it will apply 14 | /// only to samples in such thread group; if added as child of a sampler, it will only apply to that 15 | /// sampler. 16 | ///
17 | public class DslUniformRandomTimer : BaseTimer 18 | { 19 | private TimeSpan _minimum; 20 | private TimeSpan _maximum; 21 | 22 | public DslUniformRandomTimer(TimeSpan minimum, TimeSpan maximum) 23 | : base(null) 24 | { 25 | _minimum = minimum; 26 | _maximum = maximum; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Http/DslHttpCache.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.Configs; 2 | 3 | namespace Abstracta.JmeterDsl.Http 4 | { 5 | /// 6 | /// Allows configuring caching behavior used by HTTP samplers. 7 | ///
8 | /// This element can only be added as child of test plan, and currently allows only to disable HTTP 9 | /// caching which is enabled by default (emulating browser behavior). 10 | ///
11 | /// This element has to be added before any http sampler to be considered, and if you add multiple 12 | /// instances of cache manager to a test plan, only the first one will be considered. 13 | ///
14 | public class DslHttpCache : BaseConfigElement 15 | { 16 | protected bool? _disable; 17 | 18 | public DslHttpCache() 19 | : base(null) 20 | { 21 | } 22 | 23 | /// 24 | /// disables HTTP caching for the test plan. 25 | /// 26 | /// the DslHttpCache to allow fluent API usage. 27 | public DslHttpCache Disable() 28 | { 29 | _disable = true; 30 | return this; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Azure.Tests/AzureEngineTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Abstracta.JmeterDsl.Azure.Tests 5 | { 6 | using static JmeterDsl; 7 | 8 | public class AzureEngineTest 9 | { 10 | 11 | private TextWriter originalConsoleOut; 12 | 13 | // Redirecting output to progress to get live stdout with nunit. 14 | // https://github.com/nunit/nunit3-vs-adapter/issues/343 15 | // https://github.com/nunit/nunit/issues/1139 16 | [SetUp] 17 | public void SetUp() 18 | { 19 | originalConsoleOut = Console.Out; 20 | Console.SetOut(TestContext.Progress); 21 | } 22 | 23 | [TearDown] 24 | public void TearDown() => 25 | Console.SetOut(originalConsoleOut!); 26 | 27 | [Test] 28 | public void TestInAzure() 29 | { 30 | var stats = TestPlan( 31 | ThreadGroup(1, 1, 32 | HttpSampler("http://localhost") 33 | ) 34 | ).RunIn(new AzureEngine(Environment.GetEnvironmentVariable("AZURE_CREDS"))); 35 | Assert.That(stats.Overall.ErrorsCount, Is.EqualTo(1)); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jmeter-dotnet-dsl", 3 | "version": "1.0.0", 4 | "description": "Simple JMeter performance tests API", 5 | "main": "index.js", 6 | "repository": "https://github.com/abstracta/jmeter-dotnet-dsl", 7 | "license": "Apache-2.0", 8 | "devDependencies": { 9 | "@fortawesome/fontawesome-svg-core": "^6.4.0", 10 | "@fortawesome/free-brands-svg-icons": "^6.4.0", 11 | "@fortawesome/vue-fontawesome": "^3.0.3", 12 | "@vuepress/cli": "2.0.0-beta.64", 13 | "@vuepress/client": "2.0.0-beta.64", 14 | "@vuepress/plugin-container": "2.0.0-beta.64", 15 | "@vuepress/plugin-medium-zoom": "2.0.0-beta.64", 16 | "@vuepress/plugin-search": "2.0.0-beta.64", 17 | "@vuepress/shared": "2.0.0-beta.64", 18 | "@vuepress/theme-default": "2.0.0-beta.64", 19 | "@vuepress/utils": "2.0.0-beta.64", 20 | "markdown-it-replace-link": "^1.2.0", 21 | "vue": "^3.3.4", 22 | "vue-router": "^4.2.3", 23 | "vuepress": "2.0.0-beta.64", 24 | "vuepress-plugin-copy-code2": "2.0.0-beta.230", 25 | "vuepress-plugin-md-enhance": "2.0.0-beta.230" 26 | }, 27 | "scripts": { 28 | "dev": "vuepress dev . --clean-cache --clean-temp", 29 | "build": "vuepress build . --clean-cache --clean-temp" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /docs/guide/protocols/http/headers.md: -------------------------------------------------------------------------------- 1 | #### Headers 2 | 3 | You might have already noticed in some of the examples that we have shown, some ways to set some headers. For instance, in the following snippet, `Content-Type` header is being set in two different ways: 4 | 5 | ```cs 6 | HttpSampler("http://my.service") 7 | .Post("{\"field\":\"val\"}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) 8 | HttpSampler("http://my.service") 9 | .ContentType(new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) 10 | ``` 11 | 12 | These are handy methods to specify the `Content-Type` header, but you can also set any header on a particular request using provided `Header` method, like this: 13 | 14 | ```cs 15 | HttpSampler("http://my.service") 16 | .Header("X-First-Header", "val1") 17 | .Header("X-Second-Header", "val2") 18 | ``` 19 | 20 | Additionally, you can specify headers to be used by all samplers in a test plan, thread group, transaction controllers, etc. For this, you can use `HttpHeaders` like this: 21 | 22 | ```cs 23 | TestPlan( 24 | ThreadGroup(2, 10, 25 | HttpHeaders() 26 | .Header("X-Header", "val1"), 27 | HttpSampler("http://my.service"), 28 | HttpSampler("http://my.service/users") 29 | ) 30 | ).Run(); 31 | ``` 32 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/Core/Assertions/DslResponseAssertionTest.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.Assertions 2 | { 3 | using static JmeterDsl; 4 | 5 | public class DslResponseAssertionTest 6 | { 7 | [Test] 8 | public void ShouldNotFailAssertionWhenResponseAssertionWithMatchingCondition() 9 | { 10 | var stats = TestPlan( 11 | ThreadGroup(1, 1, 12 | DummySampler("OK") 13 | .Children( 14 | ResponseAssertion().ContainsSubstrings("OK") 15 | ) 16 | )).Run(); 17 | Assert.That(stats.Overall.ErrorsCount, Is.EqualTo(0)); 18 | } 19 | 20 | [Test] 21 | public void ShouldFailAssertionWhenResponseAssertionWithNotMatchingCondition() 22 | { 23 | var stats = TestPlan( 24 | ThreadGroup(1, 1, 25 | DummySampler("OK") 26 | .Children( 27 | ResponseAssertion().ContainsSubstrings("FAIL") 28 | ) 29 | )).Run(); 30 | Assert.That(stats.Overall.ErrorsCount, Is.EqualTo(1)); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.BlazeMeter.Tests/BlazeMeterEngineTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | 4 | namespace Abstracta.JmeterDsl.BlazeMeter.Tests 5 | { 6 | using static JmeterDsl; 7 | 8 | public class BlazeMeterEngineTests 9 | { 10 | private TextWriter originalConsoleOut; 11 | 12 | // Redirecting output to progress to get live stdout with nunit. 13 | // https://github.com/nunit/nunit3-vs-adapter/issues/343 14 | // https://github.com/nunit/nunit/issues/1139 15 | [SetUp] 16 | public void SetUp() 17 | { 18 | originalConsoleOut = Console.Out; 19 | Console.SetOut(TestContext.Progress); 20 | } 21 | 22 | [TearDown] 23 | public void TearDown() => 24 | Console.SetOut(originalConsoleOut!); 25 | 26 | [Test] 27 | public void TestInBlazeMeter() 28 | { 29 | var stats = TestPlan( 30 | ThreadGroup(1, 1, 31 | HttpSampler("http://localhost") 32 | ) 33 | ).RunIn(new BlazeMeterEngine(Environment.GetEnvironmentVariable("BZ_TOKEN")) 34 | .UseDebugRun()); 35 | Assert.That(stats.Overall.ErrorsCount, Is.EqualTo(1)); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Controllers/BaseController.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.TestElements; 2 | using Abstracta.JmeterDsl.Core.ThreadGroups; 3 | 4 | namespace Abstracta.JmeterDsl.Core.Controllers 5 | { 6 | /// 7 | /// Contains common logic for logic controllers defined by the DSL. 8 | /// 9 | public abstract class BaseController : TestElementContainer, IThreadGroupChild 10 | where T : BaseController 11 | { 12 | protected BaseController(string name, IThreadGroupChild[] children) 13 | : base(name, children) 14 | { 15 | } 16 | 17 | /// 18 | /// Allows specifying children elements that are affected by this controller. 19 | ///
20 | /// This method is helpful to keep general controller settings at the beginning and specify 21 | /// children at last. 22 | ///
23 | /// set of elements to be included in the controller. This list is appended to any children defined in controller builder method. 24 | /// a new controller instance for further configuration or usage. 25 | public new T Children(params IThreadGroupChild[] children) 26 | => base.Children(children); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Azure/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | us.abstracta.jmeter.dotnet 8 | jmeter-dotnet-dsl-parent 9 | 1.0-SNAPSHOT 10 | ../pom.xml 11 | 12 | jmeter-dotnet-dsl-azure 13 | pom 14 | 15 | This pom is only needed to be able to copy jmeter-java-dsl-azure jar and its dependencies with dependency:copy-dependencies maven plugin goal 16 | 17 | 18 | 19 | us.abstracta.jmeter 20 | jmeter-java-dsl-azure 21 | ${jmeter-java-dsl.version} 22 | 23 | 24 | us.abstracta.jmeter 25 | jmeter-java-dsl 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /devbox.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfile_version": "1", 3 | "packages": { 4 | "dotnet-sdk@latest": { 5 | "last_modified": "2023-09-10T10:53:27Z", 6 | "resolved": "github:NixOS/nixpkgs/78058d810644f5ed276804ce7ea9e82d92bee293#dotnet-sdk", 7 | "source": "devbox-search", 8 | "version": "6.0.413" 9 | }, 10 | "maven@latest": { 11 | "last_modified": "2023-09-10T10:53:27Z", 12 | "resolved": "github:NixOS/nixpkgs/78058d810644f5ed276804ce7ea9e82d92bee293#maven", 13 | "source": "devbox-search", 14 | "version": "3.9.4" 15 | }, 16 | "nodePackages.pnpm@latest": { 17 | "last_modified": "2023-09-10T10:53:27Z", 18 | "resolved": "github:NixOS/nixpkgs/78058d810644f5ed276804ce7ea9e82d92bee293#nodePackages.pnpm", 19 | "source": "devbox-search", 20 | "version": "8.6.12" 21 | }, 22 | "nodejs@latest": { 23 | "last_modified": "2023-09-10T10:53:27Z", 24 | "plugin_version": "0.0.2", 25 | "resolved": "github:NixOS/nixpkgs/78058d810644f5ed276804ce7ea9e82d92bee293#nodejs_20", 26 | "source": "devbox-search", 27 | "version": "20.6.1" 28 | }, 29 | "temurin-bin-8@latest": { 30 | "last_modified": "2023-09-10T10:53:27Z", 31 | "resolved": "github:NixOS/nixpkgs/78058d810644f5ed276804ce7ea9e82d92bee293#temurin-bin-8", 32 | "source": "devbox-search", 33 | "version": "8.0.372" 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.BlazeMeter/pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | us.abstracta.jmeter.dotnet 8 | jmeter-dotnet-dsl-parent 9 | 1.0-SNAPSHOT 10 | ../pom.xml 11 | 12 | jmeter-dotnet-dsl-blazemeter 13 | pom 14 | 15 | This pom is only needed to be able to copy jmeter-java-dsl-blazmeter jar and its dependencies with dependency:copy-dependencies maven plugin goal 16 | 17 | 18 | 19 | us.abstracta.jmeter 20 | jmeter-java-dsl-blazemeter 21 | ${jmeter-java-dsl.version} 22 | 23 | 24 | us.abstracta.jmeter 25 | jmeter-java-dsl 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /docs/guide/request-generation/loops/forloop-controller.md: -------------------------------------------------------------------------------- 1 | #### Iterating a fixed number of times 2 | 3 | In simple scenarios where you just want to execute a fixed number of times, within a thread group iteration, a given part of the test plan, you can just use `ForLoopController` (which uses [JMeter Loop Controller component](https://jmeter.apache.org/usermanual/component_reference.html#Loop_Controller)) as in the following example: 4 | 5 | ```cs 6 | using static Abstracta.JmeterDsl.JmeterDsl; 7 | 8 | public class PerformanceTest 9 | { 10 | [Test] 11 | public void LoadTest() 12 | { 13 | var stats = TestPlan( 14 | ThreadGroup(2, 10, 15 | ForLoopController(5, 16 | HttpSampler("http://my.service/accounts") 17 | ) 18 | ) 19 | ).Run(); 20 | Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); 21 | } 22 | } 23 | ``` 24 | 25 | This will result in 10 * 5 = 50 requests to the given URL for each thread in the thread group. 26 | 27 | ::: tip 28 | JMeter automatically generates a variable `__jm____idx` with the current index of for loop iteration (starting with 0) which you can use in children elements. The default name for the for loop controller, when not specified, is `for`. 29 | ::: 30 | 31 | Check [ForLoopController](/Abstracta.JmeterDsl/Core/Controllers/ForLoopController.cs) for more details. 32 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Samplers/BaseSampler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Abstracta.JmeterDsl.Core.TestElements; 3 | using Abstracta.JmeterDsl.Core.ThreadGroups; 4 | 5 | namespace Abstracta.JmeterDsl.Core.Samplers 6 | { 7 | /// 8 | /// Hosts common logic to all samplers. 9 | ///
10 | /// In particular, it specifies that samplers are and containing . 11 | /// For an example of an implementation of a sampler check 12 | ///
13 | public abstract class BaseSampler : TestElementContainer, IThreadGroupChild 14 | where T : BaseSampler 15 | { 16 | protected BaseSampler(string name) 17 | : base(name, Array.Empty()) 18 | { 19 | } 20 | 21 | /// 22 | /// Allows specifying children test elements for the sampler, which allow for example extracting 23 | /// information from response, alter request, assert response contents, etc. 24 | /// 25 | /// list of test elements to add as children of this sampler. 26 | /// the altered sampler to allow for fluent API usage. 27 | public new T Children(params ISamplerChild[] children) 28 | => base.Children(children); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /docs/guide/response-processing/response-assertion.md: -------------------------------------------------------------------------------- 1 | ### Check for expected response 2 | 3 | By default, JMeter marks any HTTP request with a fail response code (4xx or 5xx) as failed, which allows you to easily identify when some request unexpectedly fails. But in many cases, this is not enough or desirable, and you need to check for the response body (or some other field) to contain (or not) a certain string. 4 | 5 | This is usually accomplished in JMeter with the usage of Response Assertions, which provides an easy and fast way to verify that you get the proper response for each step of the test plan, marking the request as a failure when the specified condition is not met. 6 | 7 | Here is an example of how to specify a response assertion in JMeter DSL: 8 | 9 | ```cs 10 | using static Abstracta.JmeterDsl.JmeterDsl; 11 | 12 | public class PerformanceTest 13 | { 14 | [Test] 15 | public void LoadTest() 16 | { 17 | var stats = TestPlan( 18 | ThreadGroup(2, 10, 19 | HttpSampler("http://my.service") 20 | .Children( 21 | ResponseAssertion().ContainsSubstrings("OK") 22 | ) 23 | ) 24 | ).Run(); 25 | Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); 26 | } 27 | } 28 | ``` 29 | 30 | Check [Response Assertion](/Abstracta.JmeterDsl/Core/Assertions/DslResponseAssertion.cs) for more details and additional options. 31 | -------------------------------------------------------------------------------- /docs/guide/protocols/http/parameters.md: -------------------------------------------------------------------------------- 1 | #### Parameters 2 | 3 | In many cases, you will need to specify some URL query string parameters or URL encoded form bodies. For these cases, you can use `Param` method as in the following example: 4 | 5 | ```cs 6 | using static Abstracta.JmeterDsl.JmeterDsl; 7 | using System.Net.Http; 8 | 9 | public class PerformanceTest 10 | { 11 | [Test] 12 | public void LoadTest() 13 | { 14 | var baseUrl = "https://myservice.com/products"; 15 | TestPlan( 16 | ThreadGroup(1, 1, 17 | // GET https://myservice.com/products?name=iron+chair 18 | HttpSampler("GetIronChair", baseUrl) 19 | .Param("name", "iron chair"), 20 | /* 21 | * POST https://myservice.com/products 22 | * Content-Type: application/x-www-form-urlencoded 23 | * 24 | * name=wooden+chair 25 | */ 26 | HttpSampler("CreateWoodenChair", baseUrl) 27 | .Method(HttpMethod.Post.Method) // POST 28 | .Param("name", "wooden chair") 29 | ) 30 | ).Run(); 31 | } 32 | } 33 | ``` 34 | 35 | ::: tip 36 | JMeter automatically URL encodes parameters, so you don't need to worry about special characters in parameter names or values. 37 | 38 | If you want to use some custom encoding or have an already encoded value that you want to use, then you can use `RawParam` method instead which does not apply any encoding to the parameter name or value, and send it as is. 39 | ::: -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Listeners/ResultsTreeVisualizer.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.Listeners 2 | { 3 | /// 4 | /// Shows a popup window including live results tree using JMeter built-in View Results Tree 5 | /// element. 6 | ///
7 | /// If resultsTreeVisualizer is added at testPlan level it will show information about all samples in 8 | /// the test plan, if added at thread group level it will only show samples for samplers contained 9 | /// within it, if added as a sampler child, then only that sampler samples will be shown. 10 | ///
11 | public class ResultsTreeVisualizer : BaseListener 12 | { 13 | protected int? _resultsLimit; 14 | 15 | /// 16 | /// Specifies the maximum number of sample results to show. 17 | ///
18 | /// When the limit is reached, only latest sample results are shown. 19 | ///
20 | /// Take into consideration that the greater the number of displayed results, the more system 21 | /// memory is required, which might cause an OutOfMemoryError depending on JVM settings. 22 | ///
23 | /// the maximum number of sample results to show. When not set the default 24 | /// value is 500. 25 | /// the visualizer for further configuration or usage. 26 | public ResultsTreeVisualizer ResultsLimit(int resultsLimit) 27 | { 28 | _resultsLimit = resultsLimit; 29 | return this; 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /docs/guide/debugging/debug-jmeter.md: -------------------------------------------------------------------------------- 1 | ### Debug JMeter code 2 | 3 | You can even add breakpoints to JMeter or JMeter Java DSL code in your IDE and debug the code line by line providing the greatest possible detail. 4 | 5 | Here is an example screenshot debugging HTTP Sampler: 6 | 7 | ![JMeter HTTP Sampler debugging in IDE](./images/jmeter-http-sampler-debugging.png) 8 | 9 | For that, you need to: 10 | 11 | * have a Java IDE with JMeter or JMeter Java DSL code open. 12 | * set proper breakpoints in the code you are interested in debugging. 13 | * can configure Remote JVM Debug like in the following screenshot: 14 | ![IntelliJ Remote JVM Debug](./images/intellij-remote-jvm-debug.png) 15 | * set required JVM arguments in the JMeter .Net DSL test using `EmbeddedJmeterEngine` like in the following example: 16 | ```cs 17 | TestPlan( 18 | ThreadGroup(threads: 1, iterations: 1, 19 | HttpSampler("http://my.service") 20 | ) 21 | ).RunIn(new EmbeddedJmeterEngine() 22 | .JvmArgs("-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005")); 23 | ``` 24 | > Note that we changed the `suspend` flag to `y` to block test execution until Remote JVM Debug is run in IDE. 25 | * run the JMeter .Net DSL test. The test should get stuck until you start Remote JVM Debug in the Java IDE. 26 | * start the Remote JVM Devug in the Java IDE. 27 | * wait for a breakpoint to activate and debug as usual 🙂. 28 | 29 | ::: tip 30 | JMeter class in charge of executing threads logic is `org.apache.jmeter.threads.JMeterThread`. You can check the classes used by each DSL-provided test element by checking the Java DSL code. 31 | ::: 32 | -------------------------------------------------------------------------------- /docs/guide/debugging/dummy-sampler.md: -------------------------------------------------------------------------------- 1 | ### Dummy sampler 2 | 3 | In many cases, you want to be able to test part of the test plan but without directly interacting with the service under test, avoiding any potential traffic to the servers, testing some border cases which might be difficult to reproduce with the actual server, and avoid actual server interactions variability and potential unpredictability. In such scenarios, you might replace actual samplers with `DummySampler` (which uses [Dummy Sampler plugin](https://jmeter-plugins.org/wiki/DummySampler/)) to be able to test extractors, assertions, controllers conditions, and other parts of the test plan under certain conditions/results generated by the samplers. 4 | 5 | Here is an example: 6 | 7 | ```cs 8 | using static Abstracta.JmeterDsl.JmeterDsl; 9 | 10 | public class PerformanceTest 11 | { 12 | [Test] 13 | public void LoadTest() 14 | { 15 | TestPlan( 16 | ThreadGroup(2, 10, 17 | // HttpSampler("http://my.service") 18 | DummySampler("{\"status\" : \"OK\"}") 19 | ) 20 | ).Run(); 21 | } 22 | } 23 | ``` 24 | 25 | ::: tip 26 | The DSL configures dummy samplers by default, in contrast to what JMeter does, with response time simulation disabled. This allows to speed up the debugging process, not having to wait for proper response time simulation (sleeps/waits). If you want a more accurate emulation, you might turn it on through the `ResponseTimeSimulation()` method. 27 | ::: 28 | 29 | Check [DslDummySampler](/Abstracta.JmeterDsl/Core/Samplers/DslDummySampler.cs) for more information o additional configuration and options. 30 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | home: true 3 | heroHeight: 68 4 | heroImage: /logo.svg 5 | actions: 6 | - text: User Guide → 7 | link: /guide/ 8 | features: 9 | - title: 💙 Git, IDE & Programmers Friendly 10 | details: Simple way of defining performance tests that takes advantage of IDEs autocompletion and inline documentation. 11 | - title: 💪 JMeter ecosystem & community 12 | details: Use the most popular performance tool and take advantage of the wide support of protocols and tools. 13 | - title: 😎 Built-in features & extensibility 14 | details: Built-in additional features which ease usage and using it in CI/CD pipelines. 15 | footer: Made by Abstracta with ❤️ | Apache 2.0 Licensed | Powered by Vuepress 16 | footerHtml: true 17 | --- 18 | 19 | ## Example 20 | 21 | Add the package to your project: 22 | 23 | ```powershell 24 | dotnet add package Abstracta.JmeterDsl --version 0.8 25 | ``` 26 | 27 | Create performance test: 28 | 29 | ```cs 30 | using static Abstracta.JmeterDsl.JmeterDsl; 31 | 32 | public class PerformanceTest 33 | { 34 | [Test] 35 | public void LoadTest() 36 | { 37 | var stats = TestPlan( 38 | ThreadGroup(2, 10, 39 | HttpSampler("http://my.service") 40 | ) 41 | ).Run(); 42 | Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); 43 | } 44 | } 45 | ``` 46 | 47 | > **Java 8+ is required** for test plan execution. 48 | 49 | [Here](https://github.com/abstracta/jmeter-dotnet-dsl-sample) is a sample project in case you want to start one from scratch. 50 | 51 | -------------------------------------------------------------------------------- /docs/guide/request-generation/jsr223-pre-processor.md: -------------------------------------------------------------------------------- 1 | ### Provide request parameters programmatically per request 2 | 3 | So far we have seen a how to generate requests with information extracted from CSV, but this is not enough for some scenarios. When you need more flexibility and power you can use `jsr223preProcessor` to specify your own logic to build each request. 4 | 5 | Here is an example: 6 | 7 | ```cs 8 | using System; 9 | using System.Net.Http.Headers; 10 | using System.Net.Mime; 11 | using static Abstracta.JmeterDsl.JmeterDsl; 12 | 13 | public class PerformanceTest 14 | { 15 | [Test] 16 | public void LoadTest() 17 | { 18 | var stats = TestPlan( 19 | ThreadGroup(5, 10, 20 | HttpSampler("http://my.service") 21 | .Post("${REQUEST_BODY}", new MediaTypeHeaderValue(MediaTypeNames.Text.Plain)) 22 | .Children(Jsr223PreProcessor("vars.put('REQUEST_BODY', '{\"time\": \"' + Instant.now() + '\"}')")) 23 | ) 24 | ).Run(); 25 | Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); 26 | } 27 | } 28 | ``` 29 | 30 | ::: tip 31 | For the time being only JSR223 scripts can be used. By default `Groovy` is used, but you can change to others by using the provided `Language()` method. 32 | 33 | We plan in the future to look for alternatives as to be able to use .Net code as pre processor. If you are interested in this you can let us know in [this issue](https://github.com/abstracta/jmeter-dotnet-dsl/issues/3) posting your use case. 34 | ::: 35 | 36 | Check [DslJsr223PreProcessor](/Abstracta.JmeterDsl/Core/PreProcessors/DslJsr223PreProcessor.cs) for more details and additional options. 37 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/DslTestPlan.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.Bridge; 2 | using Abstracta.JmeterDsl.Core.Engines; 3 | using Abstracta.JmeterDsl.Core.TestElements; 4 | 5 | namespace Abstracta.JmeterDsl.Core 6 | { 7 | /// 8 | /// Represents a JMeter test plan, with associated thread groups and other children elements. 9 | /// 10 | public class DslTestPlan : TestElementContainer 11 | { 12 | public DslTestPlan(ITestPlanChild[] children) 13 | : base(null, children) 14 | { 15 | } 16 | 17 | /// 18 | /// Uses to run the test plan. 19 | /// 20 | /// A object containing all statistics of the test plan execution. 21 | public TestPlanStats Run() => 22 | new EmbeddedJmeterEngine().Run(this); 23 | 24 | /// 25 | /// Allows to run the test plan in a given engine. 26 | ///
27 | /// This method is just a simple method which provides fluent API to run the test plans in a given 28 | /// engine. 29 | ///
30 | /// 31 | public TestPlanStats RunIn(IDslJmeterEngine engine) => 32 | engine.Run(this); 33 | 34 | /// 35 | /// Saves the given test plan as JMX, which allows it to be loaded in JMeter GUI. 36 | /// 37 | /// specifies where to store the JMX of the test plan. 38 | public void SaveAsJmx(string filePath) => 39 | new BridgeService().SaveTestPlanAsJmx(this, filePath); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /docs/guide/index.md: -------------------------------------------------------------------------------- 1 | # User guide 2 | 3 | Here we share some tips and examples on how to use the DSL to tackle common use cases. 4 | 5 | Provided examples use [Nunit](https://nunit.org/), but you can use other test libraries. 6 | 7 | Explore the DSL in your preferred IDE to discover all available features, and consider reviewing [existing tests](/Abstracta.JmeterDsl.Tests) for additional examples. 8 | 9 | The .Net DSL currently does not support all use cases supported by the [Java Dsl](https://abstracta.github.io/jmeter-java-dsl/), and currently only focuses on a limited set of features that cover the most commonly used cases. If you identify any particular scenario (or JMeter feature) that you need and is not currently supported, or easy to use, **please let us know by [creating an issue](https://github.com/abstracta/jmeter-dotnet-dsl/issues)** and we will try to implement it as soon as possible. Usually porting JMeter features is quite fast, and porting existing Java DSL features is even faster. 10 | 11 | ::: tip 12 | If you like this project, **please give it a star ⭐ in [GitHub](https://github.com/abstracta/jmeter-dotnet-dsl)!**. This helps the project be more visible, gain relevance and encourages us to invest more effort in new features. 13 | ::: 14 | 15 | For an intro to JMeter concepts and components, you can check [JMeter official documentation](http://jmeter.apache.org/usermanual/get-started.html). 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/.vuepress/styles/index.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | --c-brand: #a502ce; 3 | --c-brand-light: #b55ecb; 4 | 5 | --c-shadow: rgba(0,0,0,0.2); 6 | } 7 | 8 | html.dark { 9 | // brand colors 10 | --c-brand: #b55ecb; 11 | --c-brand-light: #a502ce; 12 | 13 | --c-shadow: rgba(255,255,255,0.2); 14 | } 15 | 16 | /* This is a fix to avoid, when building (does not happen with dev server), 17 | showing two external link icons for external links containing icons.*/ 18 | a.external-link>svg~span:nth-child(2) { 19 | display: none; 20 | } 21 | 22 | .vertical-divider { 23 | display: inline-flex; 24 | width:0; 25 | border: solid; 26 | border-width: 0 thin 0 0; 27 | border-color: var(--c-border); 28 | min-height: 80%; 29 | vertical-align: text-bottom; 30 | margin: 0 0.5rem; 31 | } 32 | 33 | .hero-logo { 34 | margin: 3rem auto 1.5rem; 35 | display: flex; 36 | align-items: center; 37 | justify-content: center; 38 | } 39 | 40 | .hero-logo span { 41 | font-weight:500; 42 | font-size: 3rem; 43 | color: var(--c-text-accent); 44 | margin: 0 0 0 0.7rem; 45 | } 46 | 47 | .grid { 48 | display: flex; 49 | } 50 | 51 | .grid-logo { 52 | display: flex; 53 | align-items: center; 54 | width: 25%; 55 | height: 100px; 56 | margin-right: 10px; 57 | padding: 10px; 58 | border: 1px solid var(--c-border); 59 | border-radius: 5px; 60 | box-shadow: 0 4px 8px 0 var(--c-shadow); 61 | } 62 | 63 | .grid-logo a { 64 | width: 100%; 65 | } 66 | 67 | .grid-logo img { 68 | width: 100%; 69 | -webkit-filter: drop-shadow(1px 0 0 white) 70 | drop-shadow(0 1px 0 white) 71 | drop-shadow(-1px 0 0 white) 72 | drop-shadow(0 -1px 0 white); 73 | -filter: drop-shadow(1px 0 0 white) 74 | drop-shadow(0 1px 0 white) 75 | drop-shadow(-1px 0 0 white) 76 | drop-shadow(0 -1px 0 white); 77 | } 78 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Stats/TimeMetricSummary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Abstracta.JmeterDsl.Core.Stats 4 | { 5 | /// 6 | /// Provides summary data for a set of timing values. 7 | /// 8 | public class TimeMetricSummary 9 | { 10 | /// 11 | /// Gets the minimum collected value. 12 | /// 13 | public TimeSpan Min { get; set; } 14 | 15 | /// 16 | /// Gets the maximum collected value. 17 | /// 18 | public TimeSpan Max { get; set; } 19 | 20 | /// 21 | /// Gets the mean/average of collected values. 22 | /// 23 | public TimeSpan Mean { get; set; } 24 | 25 | /// 26 | /// Gets the median of collected values. 27 | ///
28 | /// The median is the same as percentile 50, and is the value for which 50% of the collected values 29 | /// is smaller/greater. 30 | /// This value might differ from when distribution of values is not symmetric. 31 | ///
32 | public TimeSpan Median { get; set; } 33 | 34 | /// 35 | /// Gets the 90 percentile of samples times. 36 | ///
37 | /// 90% of samples took less or equal to the returned value. 38 | ///
39 | public TimeSpan Perc90 { get; set; } 40 | 41 | /// 42 | /// Gets the 95 percentile of samples times. 43 | ///
44 | /// 95% of samples took less or equal to the returned value. 45 | ///
46 | public TimeSpan Perc95 { get; set; } 47 | 48 | /// 49 | /// Gets the 99 percentile of samples times. 50 | ///
51 | /// 99% of samples took less or equal to the returned value. 52 | ///
53 | public TimeSpan Perc99 { get; set; } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /docs/guide/debugging/view-results-tree.md: -------------------------------------------------------------------------------- 1 | ### View results tree 2 | 3 | One option is using provided `ResultsTreeVisualizer()` like in the following example: 4 | 5 | ```cs 6 | using static Abstracta.JmeterDsl.JmeterDsl; 7 | 8 | public class PerformanceTest 9 | { 10 | [Test] 11 | public void LoadTest() 12 | { 13 | var stats = TestPlan( 14 | ThreadGroup(2, 10, 15 | HttpSampler("http://my.service") 16 | ), 17 | ResultsTreeVisualizer() 18 | ).Run(); 19 | } 20 | } 21 | ``` 22 | 23 | This will display the JMeter built-in View Results Tree element, which allows you to review request and response contents in addition to collected metrics (spent time, sent & received bytes, etc.) for each request sent to the server, in a window like this one: 24 | 25 | ![View Results Tree GUI](./images/view-results-tree.png) 26 | 27 | ::: tip 28 | To debug test plans use a few iterations and threads to reduce the execution time and ease tracing by having less information to analyze. 29 | ::: 30 | 31 | ::: tip 32 | When adding `ResultsTreeVisualizer()` as a child of a thread group, it will only display sample results of that thread group. When added as a child of a sampler, it will only show sample results for that sampler. You can use this to only review certain sample results in your test plan. 33 | ::: 34 | 35 | ::: tip 36 | **Remove `ResultsTreeVisualizer()` from test plans when are no longer needed** (when debugging is finished). Leaving them might interfere with unattended test plan execution (eg: in CI) due to test plan execution not finishing until all visualizers windows are closed. 37 | ::: 38 | 39 | ::: warning 40 | By default, View Results Tree only displays the last 500 sample results. If you need to display more elements, use provided `ResultsLimit(int)` method which allows changing this value. Take into consideration that the more results are shown, the more memory that will require. So use this setting with care. 41 | ::: -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Http/DslHttpCookies.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.Configs; 2 | 3 | namespace Abstracta.JmeterDsl.Http 4 | { 5 | /// 6 | /// Allows configuring cookies settings used by HTTP samplers. 7 | ///
8 | /// This element can only be added as child of test plan, and currently allows only to disable HTTP 9 | /// cookies handling which is enabled by default (emulating browser behavior). 10 | ///
11 | /// This element has to be added before any http sampler to be considered, and if you add multiple 12 | /// instances of cookie manager to a test plan, only the first one will be considered. 13 | ///
14 | public class DslHttpCookies : BaseConfigElement 15 | { 16 | protected bool? _disable; 17 | protected bool? _clearCookiesBetweenIterations; 18 | 19 | public DslHttpCookies() 20 | : base(null) 21 | { 22 | } 23 | 24 | /// 25 | /// Disables HTTP cookies handling for the test plan. 26 | /// 27 | /// the DslHttpCookies to allow fluent API usage. 28 | public DslHttpCookies Disable() 29 | { 30 | _disable = true; 31 | return this; 32 | } 33 | 34 | /// 35 | /// Allows to enable or disable clearing cookies between thread group iterations. 36 | ///
37 | /// Cookies are cleared each iteration by default. If this is not desirable, for instance if 38 | /// logging in once and then iterating through actions multiple times, use this to set to false. 39 | ///
40 | /// clear boolean to set clearing of cookies. By default, it is set to true. 41 | /// the DslHttpCookies for further configuration or usage. 42 | public DslHttpCookies ClearCookiesBetweenIterations(bool clear) 43 | { 44 | _clearCookiesBetweenIterations = clear; 45 | return this; 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/Abstracta.JmeterDsl.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | net6.0 4 | Abstracta.JmeterDsl 5 | false 6 | ../StyleCop.ruleset 7 | true 8 | 9 | 10 | 11 | 12 | runtime; build; native; contentfiles; analyzers; buildtransitive 13 | all 14 | 15 | 16 | 17 | 18 | runtime; build; native; contentfiles; analyzers; buildtransitive 19 | all 20 | 21 | 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | all 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | PreserveNewest 34 | 35 | 36 | PreserveNewest 37 | 38 | 39 | PreserveNewest 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/guide/thread-groups/ramps-and-holds.md: -------------------------------------------------------------------------------- 1 | ### Thread ramps and holds 2 | 3 | When working with many threads, it is advisable to configure a ramp-up period, to avoid starting all threads at once affecting performance metrics and generation. 4 | 5 | You can easily configure a ramp-up with the DSL like this: 6 | 7 | ```cs 8 | ThreadGroup().RampTo(10, TimeSpan.FromSeconds(5)).HoldIterating(20) // ramp to 10 threads for 5 seconds (1 thread every half second) and iterating each thread 20 times 9 | ThreadGroup().RampToAndHold(10, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(20)) //similar as above but after ramping up holding execution for 20 seconds 10 | ``` 11 | 12 | Additionally, you can use and combine these same methods to configure more complex scenarios (incremental, peak, and any other types of tests) like the following one: 13 | 14 | ```cs 15 | ThreadGroup() 16 | .RampToAndHold(10, TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(20)) 17 | .RampToAndHold(100, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30)) 18 | .RampTo(200, TimeSpan.FromSeconds(10)) 19 | .RampToAndHold(100, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30)) 20 | .RampTo(0, TimeSpan.FromSeconds(5)) 21 | .Children( 22 | HttpSampler("http://my.service") 23 | ) 24 | ``` 25 | 26 | Which would translate into the following threads' timeline: 27 | 28 | ![Thread Group Timeline](./images/ultimate-thread-group-timeline.png) 29 | 30 | Check [DslThreadGroup](/jmeter-java-dsl/src/main/java/us/abstracta/jmeter/javadsl/core/threadgroups/DslThreadGroup.java) for more details. 31 | 32 | ::: tip 33 | If you are a JMeter GUI user, you may even be interested in using provided `TestElement.ShowInGui()` method, which shows the JMeter test element GUI that could help you understand what will DSL execute in JMeter. You can use this method with any test element generated by the DSL (not just thread groups). 34 | 35 | For example, for the above test plan you would get a window like the following one: 36 | 37 | ![UltimateThreadGroup GUI](./images/ultimate-thread-group-gui.png) 38 | ::: 39 | 40 | ::: tip 41 | When using multiple thread groups in a test plan, consider setting a name (eg: `ThreadGroup("main", 1, 1, ...)`) on them to properly identify associated requests in statistics & jtl results. 42 | ::: 43 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Azure/Abstracta.JmeterDsl.Azure.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Abstracta.JmeterDsl.Azure 6 | ../StyleCop.ruleset 7 | true 8 | true 9 | Abstracta.JmeterDsl.Azure 10 | Abstracta.JmeterDsl.Azure 11 | Abstracta 12 | Abstracta 13 | Module which allows to easily run Abstracta.JmeterDsl test plans at scale in Azure Load Testing. 14 | true 15 | jmeter,performance,load,test 16 | logo.png 17 | Apache-2.0 18 | https://abstracta.github.io/jmeter-dotnet-dsl 19 | README.md 20 | true 21 | snupkg 22 | true 23 | 0.9.0.0 24 | 0.9-alpha1 25 | 0.9 26 | 27 | 28 | true 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.BlazeMeter/Abstracta.JmeterDsl.BlazeMeter.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | Abstracta.JmeterDsl.BlazeMeter 6 | ../StyleCop.ruleset 7 | true 8 | true 9 | Abstracta.JmeterDsl.BlazeMeter 10 | Abstracta.JmeterDsl.BlazeMeter 11 | Abstracta 12 | Abstracta 13 | Module which allows to easily run Abstracta.JmeterDsl test plans at scale in BlazeMeter. 14 | true 15 | jmeter,performance,load,test 16 | logo.png 17 | Apache-2.0 18 | https://abstracta.github.io/jmeter-dotnet-dsl 19 | README.md 20 | true 21 | snupkg 22 | true 23 | 0.9.0.0 24 | 0.9-alpha1 25 | 0.9 26 | 27 | 28 | 29 | true 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /docs/.vuepress/components/NavbarBrand.vue: -------------------------------------------------------------------------------- 1 | 53 | 54 | 62 | -------------------------------------------------------------------------------- /docs/guide/reporting/logging.md: -------------------------------------------------------------------------------- 1 | ### Log requests and responses 2 | 3 | The main mechanism provided by JMeter (and `Abstracta.JmeterDsl`) to get information about generated requests, responses, and associated metrics is through the generation of JTL files. 4 | 5 | This can be easily achieved by using provided `JtlWriter` like in this example: 6 | 7 | ```cs 8 | using static Abstracta.JmeterDsl.JmeterDsl; 9 | 10 | public class PerformanceTest 11 | { 12 | [Test] 13 | public void LoadTest() 14 | { 15 | var stats = TestPlan( 16 | ThreadGroup(2, 10, 17 | HttpSampler("http://my.service") 18 | ), 19 | JtlWriter("jtls") 20 | ).Run(); 21 | } 22 | } 23 | ``` 24 | 25 | ::: tip 26 | By default, `JtlWriter` will write the most used information to evaluate the performance of the tested service. If you want to trace all the information of each request you may use `JtlWriter` with the `WithAllFields()` option. Doing this will provide all the information at the cost of additional computation and resource usage (fewer resources for actual load testing). You can tune which fields to include or not with `JtlWriter` and only log what you need, check [JtlWriter](/Abstracta.JmeterDsl/Core/Listeners/JtlWriter.cs) for more details. 27 | ::: 28 | 29 | ::: tip 30 | `JtlWriter` will automatically generate `.jtl` files applying this format: ` .jtl`. 31 | 32 | If you need a specific file name, for example for later postprocessing logic (eg: using CI build ID), you can specify it by using `JtlWriter(directory, fileName)`. 33 | 34 | When specifying the file name, make sure to use unique names, otherwise, the JTL contents may be appended to previous existing jtl files. 35 | ::: 36 | 37 | An additional option, specially targeted towards logging sample responses, is `ResponseFileSaver` which automatically generates a file for each received response. Here is an example: 38 | 39 | ```cs 40 | using static Abstracta.JmeterDsl.JmeterDsl; 41 | 42 | public class PerformanceTest 43 | { 44 | [Test] 45 | public void LoadTest() 46 | { 47 | TestPlan( 48 | ThreadGroup(2, 10, 49 | HttpSampler("http://my.service") 50 | ), 51 | ResponseFileSaver(DateTime.Now.ToString("dd-MM-yyyy HH:mm:ss").Replace(":", "-") + "-response") 52 | ).Run(); 53 | } 54 | } 55 | ``` 56 | 57 | Check [ResponseFileSaver](/Abstracta.JmeterDsl/Core/Listeners/ResponseFileSaver.cs) for more details. 58 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Http/HttpHeaders.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Net.Http.Headers; 3 | using Abstracta.JmeterDsl.Core.TestElements; 4 | 5 | namespace Abstracta.JmeterDsl.Http 6 | { 7 | /// 8 | /// Allows specifying HTTP headers (through an underlying JMeter HttpHeaderManager) to be used by 9 | /// HTTP samplers. 10 | ///
11 | /// This test element can be added at different levels (in the same way as HTTPHeaderManager) of a 12 | /// test plan affecting all samplers in the scope were is added.For example if httpHeaders is 13 | /// specified at test plan, then all headers will apply to http samplers; if it is specified on 14 | /// thread group, then only samplers on that thread group would be affected; if specified as a child 15 | /// of a sampler, only the particular sampler will include such headers.Also take into consideration 16 | /// that headers specified at lower scope will overwrite ones specified at higher scope (eg: sampler 17 | /// child headers will overwrite test plan headers). 18 | ///
19 | public class HttpHeaders : BaseTestElement, IMultiLevelTestElement 20 | { 21 | private readonly Dictionary _headers = new Dictionary(); 22 | 23 | public HttpHeaders() 24 | : base(null) 25 | { 26 | } 27 | 28 | /// 29 | /// Allows to set an HTTP header to be used by HTTP samplers. 30 | ///
31 | /// To specify multiple headers just invoke this method several times with the different header 32 | /// names and values. 33 | ///
34 | /// specifies name of the HTTP header. 35 | /// specifies value of the HTTP header. 36 | /// the config element for further configuration or usage. 37 | public HttpHeaders Header(string name, string value) 38 | { 39 | _headers[name] = value; 40 | return this; 41 | } 42 | 43 | /// 44 | /// Allows to easily specify the Content-Type HTTP header. 45 | /// 46 | /// value to use as Content-Type header. 47 | /// the config element for further configuration or usage. 48 | public HttpHeaders ContentType(MediaTypeHeaderValue value) 49 | { 50 | _headers["Content-Type"] = value.ToString(); 51 | return this; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Stats/StatsSummary.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Abstracta.JmeterDsl.Core.Stats 4 | { 5 | /// 6 | /// Contains summary statistics of a group of collected sample results. 7 | /// 8 | public class StatsSummary 9 | { 10 | /// 11 | /// Gets the instant when the first sample started. 12 | ///
13 | /// When associated to a test plan or transaction it gets its start time. 14 | ///
15 | public DateTime FirstTime { get; set; } 16 | 17 | /// 18 | /// Gets the instant when the last sample ended. 19 | ///
20 | /// When associated to a test plan or transaction it gets its end time. 21 | ///
22 | /// Take into consideration that for transactions this time takes not only into consideration the 23 | /// endTime of last sample, but also the time spent in timers and pre and postprocessors. 24 | ///
25 | public DateTime EndTime { get; set; } 26 | 27 | /// 28 | /// Gets metrics for number of samples 29 | ///
30 | /// This counts both failing and passing samples. 31 | ///
32 | public CountMetricSummary Samples { get; set; } 33 | 34 | /// 35 | /// Gets the total number of samples. 36 | /// 37 | public long SamplesCount => Samples.Total; 38 | 39 | /// 40 | /// Gets metrics for number of samples that failed. 41 | /// 42 | public CountMetricSummary Errors { get; set; } 43 | 44 | /// 45 | /// Gets the total number of samples that failed. 46 | /// 47 | public long ErrorsCount => Errors.Total; 48 | 49 | /// 50 | /// Gets metrics for time spent in samples. 51 | /// 52 | public TimeMetricSummary SampleTime { get; set; } 53 | 54 | /// 55 | /// Gets the 99 percentile of samples times. 56 | ///
57 | /// 99% of samples took less or equal to the returned value. 58 | ///
59 | public TimeSpan SampleTimePercentile99 => SampleTime.Perc99; 60 | 61 | /// 62 | /// Gets metrics for received bytes in sample responses. 63 | /// 64 | public CountMetricSummary ReceivedBytes { get; set; } 65 | 66 | /// 67 | /// Gets metrics for sent bytes in samples requests. 68 | /// 69 | public CountMetricSummary SentBytes { get; set; } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/TestElements/DslScopedTestElement.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.TestElements 2 | { 3 | /// 4 | /// Contains common logic for test elements that only process certain samples. 5 | /// 6 | /// is the type of the test element that extends this class (to properly inherit fluent 7 | /// API methods). 8 | public abstract class DslScopedTestElement : BaseTestElement, IMultiLevelTestElement 9 | where T : DslScopedTestElement 10 | { 11 | protected DslScope? _scope; 12 | protected string _scopeVariable; 13 | 14 | protected DslScopedTestElement(string name) 15 | : base(name) 16 | { 17 | } 18 | 19 | public enum DslScope 20 | { 21 | /// 22 | /// Applies the regular extractor to all samples (main and sub samples). 23 | /// 24 | AllSamples, 25 | 26 | /// 27 | /// Applies the regular extractor only to main sample (sub samples, like redirects, are not 28 | /// included). 29 | /// 30 | MainSample, 31 | 32 | /// 33 | /// Applies the regular extractor only to sub samples (redirects, embedded resources, etc.). 34 | /// 35 | SubSamples, 36 | } 37 | 38 | /// 39 | /// Allows specifying if the element should be applied to main sample and/or sub samples. 40 | ///
41 | /// When not specified the element will only apply to main sample. 42 | ///
43 | /// specifying to what sample result apply the element to. 44 | /// the DSL element for further configuration or usage. 45 | /// 46 | public T Scope(DslScope scope) 47 | { 48 | _scope = scope; 49 | return (T)this; 50 | } 51 | 52 | /// 53 | /// Allows specifying that the element should be applied to the contents of a given JMeter 54 | /// variable. 55 | ///
56 | /// This setting overrides any setting on scope and fieldToCheck. 57 | ///
58 | /// specifies the name of the variable to apply the element to. 59 | /// the DSL element for further configuration or usage. 60 | public T ScopeVariable(string scopeVariable) 61 | { 62 | _scopeVariable = scopeVariable; 63 | return (T)this; 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/Core/PostProcessors/DslRegexExtractorTest.cs: -------------------------------------------------------------------------------- 1 | using WireMock.FluentAssertions; 2 | using WireMock.RequestBuilders; 3 | using WireMock.ResponseBuilders; 4 | using WireMock.Server; 5 | 6 | namespace Abstracta.JmeterDsl.Core.PostProcessors 7 | { 8 | using static JmeterDsl; 9 | 10 | public class DslRegexExtractorTest 11 | { 12 | private WireMockServer _wiremock; 13 | 14 | [SetUp] 15 | public void SetUp() 16 | { 17 | _wiremock = WireMockServer.Start(); 18 | _wiremock.Given(Request.Create().WithPath("/")) 19 | .RespondWith(Response.Create() 20 | .WithStatusCode(200)); 21 | } 22 | 23 | [TearDown] 24 | public void TearDown() => 25 | _wiremock.Stop(); 26 | 27 | [Test] 28 | public void ShouldExtractVariableWhenSimpleRegexExtractorMatches() 29 | { 30 | var user = "test"; 31 | var userParam = "user="; 32 | var userVar = "USER"; 33 | TestPlan( 34 | ThreadGroup(1, 1, 35 | DummySampler(userParam + user) 36 | .Children( 37 | RegexExtractor(userVar, userParam + "(.*)") 38 | ), 39 | HttpSampler(_wiremock.Url + "/?" + userParam + "${" + userVar + "}") 40 | )).Run(); 41 | _wiremock.Should().HaveReceivedACall().AtUrl(_wiremock.Url + "/?" + userParam + user); 42 | } 43 | 44 | [Test] 45 | public void ShouldExtractVariableWhenComplexRegexExtractorMatches() 46 | { 47 | var user = "test"; 48 | var userParam = "user="; 49 | var userVar = "USER"; 50 | TestPlan( 51 | ThreadGroup(1, 1, 52 | DummySampler("OK") 53 | .Url("http://localhost/?" + userParam + "user2&" + userParam + user) 54 | .Children( 55 | RegexExtractor(userVar, "([^&?]+)=([^&]+)") 56 | .MatchNumber(2) 57 | .Template("$2$") 58 | .FieldToCheck(DslRegexExtractor.DslTargetField.RequestUrl) 59 | .Scope(TestElements.DslScopedTestElement.DslScope.AllSamples) 60 | ), 61 | HttpSampler(_wiremock.Url + "/?" + userParam + "${" + userVar + "}") 62 | )).Run(); 63 | _wiremock.Should().HaveReceivedACall().AtUrl(_wiremock.Url + "/?" + userParam + user); 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Bridge/TimeSpanConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using YamlDotNet.Core; 3 | using YamlDotNet.Core.Events; 4 | using YamlDotNet.Serialization; 5 | 6 | namespace Abstracta.JmeterDsl.Core.Bridge 7 | { 8 | public class TimespanConverter : IYamlTypeConverter 9 | { 10 | private const string DurationPrefix = "PT"; 11 | 12 | public bool Accepts(Type type) => 13 | type == typeof(TimeSpan); 14 | 15 | public object ReadYaml(IParser parser, Type type) 16 | { 17 | var scalar = parser.Consume(); 18 | var value = scalar.Value; 19 | if (!value.StartsWith(DurationPrefix)) 20 | { 21 | throw new YamlException(scalar.Start, scalar.End, $"No valid duration value '{value}'"); 22 | } 23 | int hours = 0, minutes = 0, seconds = 0, millis = 0; 24 | var lastPos = DurationPrefix.Length; 25 | var unitPos = value.IndexOf('H'); 26 | if (unitPos >= 0) 27 | { 28 | hours = int.Parse(value.Substring(lastPos, unitPos - lastPos)); 29 | lastPos = unitPos + 1; 30 | } 31 | unitPos = value.IndexOf('M'); 32 | if (unitPos >= 0) 33 | { 34 | minutes = int.Parse(value.Substring(lastPos, unitPos - lastPos)); 35 | lastPos = unitPos + 1; 36 | } 37 | unitPos = value.IndexOf('.'); 38 | if (unitPos >= 0) 39 | { 40 | seconds = int.Parse(value.Substring(lastPos, unitPos - lastPos)); 41 | lastPos = unitPos + 1; 42 | var millisStr = value.Substring(lastPos, Math.Min(3, value.Length - 1 - lastPos)); 43 | millis = int.Parse(millisStr.PadLeft(3, '0')); 44 | } 45 | else if (value.Contains("S")) 46 | { 47 | seconds = int.Parse(value.Substring(lastPos, value.Length - lastPos - 1)); 48 | } 49 | return new TimeSpan(0, hours, minutes, seconds, millis); 50 | } 51 | 52 | public void WriteYaml(IEmitter emitter, object value, Type type) 53 | { 54 | var ret = "PT"; 55 | var duration = (TimeSpan)value; 56 | if (duration.Hours > 0) 57 | { 58 | ret += duration.Hours + "H"; 59 | } 60 | if (duration.Minutes > 0) 61 | { 62 | ret += duration.Minutes + "M"; 63 | } 64 | if ((duration.Hours == 0 && duration.Minutes == 0) 65 | || duration.Seconds > 0 || duration.Milliseconds > 0) 66 | { 67 | ret += duration.Seconds; 68 | if (duration.Milliseconds > 0) 69 | { 70 | ret += "." + duration.Milliseconds.ToString().PadLeft(3, '0'); 71 | } 72 | ret += "S"; 73 | } 74 | emitter.Emit(new Scalar(ret)); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /docs/.vuepress/components/HomeHero.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 96 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Abstracta.JmeterDsl.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0 4 | Abstracta.JmeterDsl 5 | ../StyleCop.ruleset 6 | true 7 | true 8 | Abstracta.JmeterDsl 9 | Abstracta.JmeterDsl 10 | Abstracta 11 | Abstracta 12 | Simple API to run JMeter performance tests in an VCS and programmers friendly way. 13 | true 14 | jmeter,performance,load,test 15 | logo.png 16 | Apache-2.0 17 | https://abstracta.github.io/jmeter-dotnet-dsl 18 | README.md 19 | true 20 | snupkg 21 | true 22 | 0.9.0.0 23 | 0.9-alpha1 24 | 0.9 25 | 26 | 27 | true 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | runtime; build; native; contentfiles; analyzers; buildtransitive 55 | all 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | -------------------------------------------------------------------------------- /docs/support/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar: false 3 | --- 4 | 5 | # Support 6 | 7 | ## Community Support 8 | 9 | The JMeter DSL project has a vibrant and active community that provides extensive support, on a best effort basis, to its users. Community support is primarily offered through the following channels: 10 | 11 | * [Discord server]: Join our [Discord server] to engage with fellow JMeter DSL enthusiasts. It's a real-time platform where you can ask questions, share experiences, and participate in discussions. 12 | * [GitHub Issues]: For bug reports, feature requests, or any specific problems you encounter while using JMeter DSL, [GitHub Issues] is the place to go. Create an issue, and the community will jump in to assist you, propose improvements, and collaborate on finding solutions. 13 | * [GitHub Discussions]: If you have open-ended discussions, ideas, or suggestions related to JMeter DSL, head over to [GitHub Discussions]. It's an excellent platform for brainstorming, gathering feedback, and engaging in community-driven conversations. 14 | 15 | The community is actively involved in proposing new improvements, answering questions, assisting in design decisions, and submitting pull requests. Together, we strive to enhance the capabilities and usability of JMeter DSL. 16 | 17 | ## Enterprise Support by Abstracta 18 | 19 | In addition to community support, [Abstracta](https://abstracta.us) offers enterprise-level support for JMeter DSL users. Abstracta is the main supporter of JMeter DSL development and provides specialized professional services to ensure the success of organizations using JMeter DSL. With Abstracta's enterprise support, you can accelerate your JMeter DSL implementation and have access to: 20 | 21 | * Dedicated support team : Get prompt answers and peace of mind from a dedicated support team with the expertise to help you resolve issues faster. 22 | * Customizations: Receive tailored solutions to meet your specific requirements. 23 | * Consulting services: Access a team of experts to fine-tune your JMeter DSL usage, speed up implementation, work on your performance testing strategy and overall testing processes. 24 | 25 | Abstracta is committed to helping organizations succeed with JMeter DSL by providing comprehensive support and specialized services tailored to your enterprise needs. 26 | 27 | To explore Abstracta's enterprise support options or discuss your specific needs, please [contact the Abstracta team](https://abstracta.us/contact-us). 28 | 29 | :::: grid 30 | ::: grid-logo https://abstracta.us 31 | ![Abstracta logo](./abstracta-logo.png) 32 | ::: 33 | :::: 34 | 35 | ## Industry Support 36 | 37 | JMeter DSL has received valuable support from industry-leading companies, contributing to the integration features and promoting the tool. We would like to acknowledge and express our gratitude to the following companies: 38 | 39 | :::: grid 40 | ::: grid-logo https://www.blazemeter.com/ 41 | ![BlazMeter logo](./blazemeter-logo.png) 42 | ::: 43 | ::: grid-logo https://octoperf.com/ 44 | ![OctoPerf logo](./octoperf-logo.png) 45 | ::: 46 | ::: grid-logo https://azure.microsoft.com/en-us/products/load-testing/ 47 | ![Azure Load Testing logo](./azure-logo.png) 48 | ::: 49 | :::: 50 | 51 | [Discord server]:https://discord.gg/WNSn5hqmSd 52 | [GitHub Issues]:https://github.com/abstracta/jmeter-dotnet-dsl/issues 53 | [GitHub Discussions]:https://github.com/abstracta/jmeter-dotnet-dsl/discussions 54 | -------------------------------------------------------------------------------- /docs/guide/request-generation/timers/constant-and-random.md: -------------------------------------------------------------------------------- 1 | #### Emulate user delays between requests 2 | 3 | Sometimes, is necessary to be able to properly replicate users' behavior, and in particular the time the users take between sending one request and the following one. For example, to simulate the time it will take to complete a purchase form. JMeter (and the DSL) provide a few alternatives for this. 4 | 5 | If you just want to add 1 pause between two requests, you can use the `ThreadPause` method like in the following example: 6 | 7 | ```cs 8 | using System; 9 | using System.Net.Http.Headers; 10 | using System.Net.Mime; 11 | using static Abstracta.JmeterDsl.JmeterDsl; 12 | 13 | public class PerformanceTest 14 | { 15 | [Test] 16 | public void LoadTest() 17 | { 18 | TestPlan( 19 | ThreadGroup(2, 10, 20 | HttpSampler("http://my.service/items"), 21 | ThreadPause(TimeSpan.FromSeconds(4)), 22 | HttpSampler("http://my.service/cart/selected-items") 23 | .Post("{\"id\": 1}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) 24 | ) 25 | ).Run(); 26 | } 27 | } 28 | ``` 29 | 30 | Using `ThreadPause` is a good solution for adding individual pauses, but if you want to add pauses across several requests, or sections of test plan, then using a `ConstantTimer` or `UniformRandomTimer` is better. Here is an example that adds a delay of between 4 and 10 seconds for every request in the test plan: 31 | 32 | ```cs 33 | using System; 34 | using System.Net.Http.Headers; 35 | using System.Net.Mime; 36 | using static Abstracta.JmeterDsl.JmeterDsl; 37 | 38 | public class PerformanceTest 39 | { 40 | [Test] 41 | public void LoadTest() 42 | { 43 | TestPlan( 44 | ThreadGroup(2, 10, 45 | UniformRandomTimer(TimeSpan.FromSeconds(4), TimeSpan.FromSeconds(10)), 46 | Transaction("addItemToCart", 47 | HttpSampler("http://my.service/items"), 48 | HttpSampler("http://my.service/cart/selected-items") 49 | .Post("{\"id\": 1}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) 50 | ), 51 | Transaction("checkout", 52 | HttpSampler("http://my.service/cart/chekout"), 53 | HttpSampler("http://my.service/cart/checkout/userinfo") 54 | .poPostst( 55 | "{\"Name\": Dave, \"lastname\": Tester, \"Street\": 1483 Smith Road, \"City\": Atlanta}", 56 | new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) 57 | ) 58 | ).Run(); 59 | } 60 | } 61 | ``` 62 | 63 | ::: tip 64 | As you may have noticed, timer order in relation to samplers, doesn't matter. Timers apply to all samplers in their scope, adding a pause after pre-processor executions and before the actual sampling. 65 | `ThreadPause` order, on the other hand, is relevant, and the pause will only execute when previous samplers in the same scope have run and before following samplers do. 66 | ::: 67 | 68 | ::: warning 69 | `UniformRandomTimer` `minimum` and `maximum` parameters differ from the ones used by JMeter Uniform Random Timer element, to make it simpler for users with no JMeter background. 70 | 71 | The generated JMeter test element uses the `Constant Delay Offset` set to `minimum` value, and the `Maximum random delay` set to `(maximum - minimum)` value. 72 | ::: 73 | -------------------------------------------------------------------------------- /docs/guide/scale/azure.md: -------------------------------------------------------------------------------- 1 | ### Azure Load Testing 2 | 3 | To use [Azure Load Testing](https://azure.microsoft.com/en-us/products/load-testing/) to execute your test plans at scale is as easy as including the following package to your project: 4 | 5 | ```powershell 6 | dotnet add package Abstracta.JmeterDsl.Azure --version 0.8 7 | ``` 8 | 9 | And using the provided engine like this: 10 | 11 | ```cs 12 | using Abstracta.JmeterDsl.Azure; 13 | using static Abstracta.JmeterDsl.JmeterDsl; 14 | 15 | public class PerformanceTest 16 | { 17 | [Test] 18 | public void LoadTest() 19 | { 20 | var stats = TestPlan( 21 | ThreadGroup(2, 10, 22 | HttpSampler("http://my.service") 23 | ) 24 | ).RunIn(new AzureEngine(Environment.GetEnvironmentVariable("AZURE_CREDS")) // AZURE_CREDS=tenantId:clientId:secretId 25 | .TestName("dsl-test") 26 | /* 27 | This specifies the number of engine instances used to execute the test plan. 28 | In this case, means that it will run 2(threads in thread group)x2(engines)=4 concurrent users/threads in total. 29 | Each engine executes the test plan independently. 30 | */ 31 | .Engines(2) 32 | .TestTimeout(TimeSpan.FromMinutes(20))); 33 | Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); 34 | } 35 | } 36 | ``` 37 | > This test is using `AZURE_CREDS`, a custom environment variable containing `tenantId:clientId:clientSecret` with proper values for each. Check at [Azure Portal tenant properties](https://portal.azure.com/#view/Microsoft_AAD_IAM/TenantPropertiesBlade) the proper tenant ID for your subscription, and follow [this guide](https://learn.microsoft.com/en-us/azure/active-directory/develop/howto-create-service-principal-portal) to register an application with proper permissions and secrets generation for tests execution. 38 | 39 | With Azure, you can not only run the test at scale but also get additional features like nice real-time reporting, historic data tracking, etc. Here is an example of how a test looks like in Azure Load Testing: 40 | 41 | ![Azure Load Testing Example Execution Dashboard](./azure.png) 42 | 43 | Check [AzureEngine](/Abstracta.JmeterDsl.Azure/AzureEngine.cs) for details on usage and available settings when running tests in Azure Load Testing. 44 | 45 | ::: warning 46 | By default, the engine is configured to time out if test execution takes more than 1 hour. 47 | This timeout exists to avoid any potential problem with Azure Load Testing execution not detected by the 48 | client, and avoid keeping the test indefinitely running until is interrupted by a user, 49 | which may incur unnecessary expenses in Azure and is especially annoying when running tests 50 | in an automated fashion, for example in CI/CD. 51 | It is strongly advised to **set this timeout properly in each run**, according to the expected test 52 | execution time plus some additional margin (to consider for additional delays in Azure Load Testing 53 | test setup and teardown) to avoid unexpected test plan execution failure (due to timeout) or 54 | unnecessary waits when there is some unexpected issue with Azure Load Testing execution. 55 | ::: 56 | 57 | ::: tip 58 | If you want to get debug logs for HTTP calls to Azure API, you can include the following setting to an existing `log4j2.xml` configuration file: 59 | ```xml 60 | 61 | 62 | ``` 63 | ::: 64 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Controllers/DslTransactionController.cs: -------------------------------------------------------------------------------- 1 | using Abstracta.JmeterDsl.Core.Bridge; 2 | using Abstracta.JmeterDsl.Core.ThreadGroups; 3 | 4 | namespace Abstracta.JmeterDsl.Core.Controllers 5 | { 6 | /// 7 | /// Allows specifying JMeter transaction controllers which group different samples associated to same 8 | /// transaction. 9 | ///
10 | /// This is usually used when grouping different steps of a flow, for example group requests of login 11 | /// flow, adding item to cart, purchase, etc. It provides aggregate metrics of all it's samples. 12 | ///
13 | [YamlType(TagName = "transaction")] 14 | public class DslTransactionController : BaseController 15 | { 16 | private bool _includeTimersAndProcessorsTime; 17 | private bool _generateParentSample; 18 | 19 | public DslTransactionController(string name, IThreadGroupChild[] children) 20 | : base(name, children) 21 | { 22 | } 23 | 24 | /// 25 | /// Specifies to include time spent in timers and pre- and post-processors in sample results. 26 | /// 27 | /// the controller for further configuration or usage. 28 | public DslTransactionController IncludeTimersAndProcessorsTime() => 29 | IncludeTimersAndProcessorsTime(true); 30 | 31 | /// 32 | /// Same as but allowing to enable or disable it. 33 | ///
34 | /// This is helpful when the resolution is taken at runtime. 35 | ///
36 | /// specifies to enable or disable the setting. By default, it is set to false. 37 | /// the controller for further configuration or usage. 38 | /// 39 | public DslTransactionController IncludeTimersAndProcessorsTime(bool enable) 40 | { 41 | _includeTimersAndProcessorsTime = enable; 42 | return this; 43 | } 44 | 45 | /// 46 | /// Specifies to create a sample result as parent of children samplers. 47 | ///
48 | /// It is useful in some scenarios to get transaction sample results as parent of children samplers 49 | /// to focus mainly in transactions and not be concerned about each particular request. Enabling 50 | /// parent sampler helps in this regard, only reporting the transactions in summary reports, and 51 | /// not the transaction children results. 52 | ///
53 | /// the controller for further configuration or usage. 54 | public DslTransactionController GenerateParentSample() => 55 | GenerateParentSample(true); 56 | 57 | /// 58 | /// Same as but allowing to enable or disable it. 59 | ///
60 | /// This is helpful when the resolution is taken at runtime. 61 | ///
62 | /// specifies to enable or disable the setting. By default, it is set to false. 63 | /// the controller for further configuration or usage. 64 | /// 65 | public DslTransactionController GenerateParentSample(bool enable) 66 | { 67 | _generateParentSample = enable; 68 | return this; 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /docs/guide/request-generation/csv-dataset.md: -------------------------------------------------------------------------------- 1 | ### CSV as input data for requests 2 | 3 | Sometimes is necessary to run the same flow but using different pre-defined data on each request. For example, a common use case is to use a different user (from a given set) in each request. 4 | 5 | This can be easily achieved using the provided `CsvDataSet` element. For example, having a file like this one: 6 | 7 | ```csv 8 | USER,PASS 9 | user1,pass1 10 | user2,pass2 11 | ``` 12 | 13 | You can implement a test plan that tests recurrent login with the two users with something like this: 14 | 15 | ```cs 16 | using System; 17 | using System.Net.Http; 18 | using System.Net.Http.Headers; 19 | using System.Net.Mime; 20 | using static Abstracta.JmeterDsl.JmeterDsl; 21 | 22 | public class PerformanceTest 23 | { 24 | [Test] 25 | public void LoadTest() 26 | { 27 | var stats = TestPlan( 28 | CsvDataSet("users.csv"), 29 | ThreadGroup(5, 10, 30 | HttpSampler("http://my.service/login") 31 | .Post("{\"${USER}\": \"${PASS}\"", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)), 32 | HttpSampler("http://my.service/logout") 33 | .Method(HttpMethod.Post.Method) 34 | ) 35 | ).Run(); 36 | Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); 37 | } 38 | } 39 | ``` 40 | 41 | ::: tip 42 | To properly format the data in your CSV, a general rule you can apply is to replace each double quotes with two double quotes and add double quotes to the beginning and end of each CSV value. 43 | 44 | E.g.: if you want one CSV field to contain the value `{"field": "value"}`, then use `"{""field:"": ""value""}"`. 45 | 46 | This way, with a simple search and replace, you can include in a CSV field any format like JSON, XML, etc. 47 | 48 | Note: JMeter uses should be aware that JMeter DSL `CsvDataSet` sets `Allowed quoted data?` flag, in associated `Csv Data Set Config` element, to `true`. 49 | ::: 50 | 51 | ::: tip 52 | By default, the CSV file will be opened once and shared by all threads. This means that when one thread reads a CSV line in one iteration, then the following thread reading a line will continue with the following line. 53 | 54 | If you want to change this (to share the file per thread group or use one file per thread), then you can use the provided `SharedIn` method like in the following example: 55 | 56 | ```java 57 | using static Abstracta.JmeterDsl.Core.Configs.DslCsvDataSet; 58 | ... 59 | var stats = TestPlan( 60 | CsvDataSet("users.csv") 61 | .SharedIn(Sharing.Thread), 62 | ThreadGroup(5, 10, 63 | HttpSampler("http://my.service/login") 64 | .Post("{\"${USER}\": \"${PASS}\"", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)), 65 | HttpSampler("http://my.service/logout") 66 | .Method(HttpMethod.Post.Method) 67 | ) 68 | ).Run(); 69 | Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); 70 | ``` 71 | ::: 72 | 73 | ::: warning 74 | You can use the `RandomOrder()` method to get CSV lines in random order (using [Random CSV Data Set plugin](https://github.com/Blazemeter/jmeter-bzm-plugins/blob/master/random-csv-data-set/RandomCSVDataSetConfig.md)), but this is less performant as getting them sequentially, so use it sparingly. 75 | ::: 76 | 77 | Check [DslCsvDataSet](/Abstracta.JmeterDsl/Core/Configs/DslCsvDataSet.cs) for additional details and options (like changing delimiter, handling files without headers line, stopping on the end of file, etc.). 78 | -------------------------------------------------------------------------------- /docs/guide/simple-test-plan.md: -------------------------------------------------------------------------------- 1 | ## Simple HTTP test plan 2 | 3 | To generate HTTP requests just use provided `HttpSampler`. 4 | 5 | The following example uses 2 threads (concurrent users) that send 10 HTTP GET requests each to `http://my.service`. 6 | 7 | Additionally, it logs collected statistics (response times, status codes, etc.) to a file (for later analysis if needed) and checks that the response time 99 percentile is less than 5 seconds. 8 | 9 | ```cs 10 | using static Abstracta.JmeterDsl.JmeterDsl; 11 | 12 | public class PerformanceTest 13 | { 14 | [Test] 15 | public void LoadTest() 16 | { 17 | var stats = TestPlan( 18 | ThreadGroup(2, 10, 19 | HttpSampler("http://my.service") 20 | ), 21 | //this is just to log details of each request stats 22 | JtlWriter("jtls") 23 | ).Run(); 24 | Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); 25 | } 26 | } 27 | ``` 28 | 29 | ::: tip 30 | When working with multiple samplers in a test plan, specify their names (eg: `HttpSampler("home", "http://my.service")`) to easily check their respective statistics. 31 | ::: 32 | 33 | ::: tip 34 | JMeter .Net DSL uses Java for executing JMeter test plans. If you need to tune JVM parameters, for example for specifying maximum heap memory size, you can use `EmbeddedJMeterEngine` and the `JvmArgs` method like in the following example: 35 | 36 | ```cs 37 | using Abstracta.JmeterDsl.Core.Engines; 38 | ... 39 | var stats = TestPlan( 40 | ThreadGroup(2, 10, 41 | HttpSampler("http://my.service") 42 | ) 43 | ).RunIn(new EmbeddedJmeterEngine() 44 | .JvmArgs("-Xmx4g") 45 | ); 46 | ``` 47 | ::: 48 | 49 | ::: tip 50 | Since JMeter uses [log4j2](https://logging.apache.org/log4j/2.x/), if you want to control the logging level or output, you can use something similar to this [log4j2.xml](Abstracta.JmeterDsl.Tests/log4j2.xml), using "CopyToOutputDirectory" in the project item, so the file is available in dotnet build output directory as well (check [Abstracta.JmeterDsl.Test/Abstracta.JmeterDsl.Tests.csproj]). 51 | ::: 52 | 53 | ::: tip 54 | Depending on the test framework you use, and the way you run your tests, you might be able to see JMeter logs and output in real-time, at the end of the test, or not see them at all. This is not something we can directly control in JMeter DSL, and heavily depends on the dotnet environment and testing framework implementation. 55 | 56 | When using Nunit, to get real-time console output from JMeter you might want to run your tests with something like `dotnet test -v n` and add the following code to your tests: 57 | 58 | ```cs 59 | private TextWriter? originalConsoleOut; 60 | 61 | // Redirecting output to progress to get live stdout with nunit. 62 | // https://github.com/nunit/nunit3-vs-adapter/issues/343 63 | // https://github.com/nunit/nunit/issues/1139 64 | [SetUp] 65 | public void SetUp() 66 | { 67 | originalConsoleOut = Console.Out; 68 | Console.SetOut(TestContext.Progress); 69 | } 70 | 71 | [TearDown] 72 | public void TearDown() 73 | { 74 | Console.SetOut(originalConsoleOut!); 75 | } 76 | ``` 77 | ::: 78 | 79 | ::: tip 80 | Keep in mind that you can use .Net programming to modularize and create abstractions which allow you to build complex test plans that are still easy to read, use and maintain. [Here is an example](https://github.com/abstracta/jmeter-java-dsl/issues/26#issuecomment-953783407) of some complex abstraction built using Java features (you can easily extrapolate to .Net) and the DSL. 81 | ::: 82 | 83 | Check [HTTP performance testing](./protocols/http/index#http) for additional details while testing HTTP services. -------------------------------------------------------------------------------- /docs/guide/request-generation/group-requests.md: -------------------------------------------------------------------------------- 1 | ### Group requests 2 | 3 | Sometimes, is necessary to be able to group requests which constitute different steps in a test. For example, to separate necessary requests to do a login from the ones used to add items to the cart and the ones to do a purchase. JMeter (and the DSL) provide Transaction Controllers for this purpose, here is an example: 4 | 5 | ```cs 6 | using System.Net.Http.Headers; 7 | using System.Net.Mime; 8 | using static Abstracta.JmeterDsl.JmeterDsl; 9 | 10 | public class PerformanceTest 11 | { 12 | [Test] 13 | public void TestTransactions() 14 | { 15 | TestPlan( 16 | ThreadGroup(2, 10, 17 | Transaction("login", 18 | HttpSampler("http://my.service"), 19 | HttpSampler("http://my.service/login") 20 | .Post("user=test&password=test", new MediaTypeHeaderValue(MediaTypeNames.Application.FormUrlEncoded)) 21 | ), 22 | Transaction("checkout", 23 | HttpSampler("http://my.service/items"), 24 | HttpSampler("http://my.service/cart/items") 25 | .poPostst("{\"id\": 1}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) 26 | ) 27 | ).Run(); 28 | } 29 | } 30 | ``` 31 | 32 | This will provide additional sample results for each transaction, which contain the aggregate metrics for containing requests, allowing you to focus on the actual flow steps instead of each particular request. 33 | 34 | If you don't want to generate additional sample results (and statistics), and want to group requests for example to apply a given timer, config, assertion, listener, pre- or post-processor, then you can use `SimpleController` like in following example: 35 | 36 | ```cs 37 | using static Abstracta.JmeterDsl.JmeterDsl; 38 | 39 | public class PerformanceTest 40 | { 41 | [Test] 42 | public void TestTransactions() 43 | { 44 | TestPlan( 45 | ThreadGroup(2, 10, 46 | simpleController("login", 47 | HttpSampler("http://my.service"), 48 | HttpSampler("http://my.service/users") 49 | ResponseAssertion() 50 | .ContainsSubstrings("OK") 51 | ) 52 | ) 53 | ).Run(); 54 | } 55 | } 56 | ``` 57 | 58 | You can even use `TransactionController` and `SimpleController` to easily modularize parts of your test plan into Java methods (or classes) like in this example: 59 | 60 | ```cs 61 | using System.Net.Http.Headers; 62 | using System.Net.Mime; 63 | using static Abstracta.JmeterDsl.JmeterDsl; 64 | 65 | public class PerformanceTest 66 | { 67 | private DslTransactionController Login(string baseUrl) => 68 | Transaction("login", 69 | HttpSampler(baseUrl), 70 | HttpSampler(baseUrl + "/login") 71 | .Post("user=test&password=test", new MediaTypeHeaderValue(MediaTypeNames.Application.FormUrlEncoded)) 72 | ); 73 | 74 | private DslTransactionController AddItemToCart(string baseUrl) => 75 | Transaction("addItemToCart", 76 | HttpSampler(baseUrl + "/items"), 77 | HttpSampler(baseUrl + "/cart/items") 78 | .Post("{\"id\": 1}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) 79 | ); 80 | 81 | [Test] 82 | public void TestTransactions() 83 | { 84 | var baseUrl = "http://my.service"; 85 | TestPlan( 86 | ThreadGroup(2, 10, 87 | Login(baseUrl), 88 | AddItemToCart(baseUrl) 89 | ) 90 | ).Run(); 91 | } 92 | } 93 | ``` 94 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | run-name: release ${{ inputs.version }} 3 | on: 4 | workflow_dispatch: 5 | inputs: 6 | version: 7 | required: true 8 | type: string 9 | jobs: 10 | release: 11 | runs-on: ubuntu-latest 12 | concurrency: remote_test 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup .NET Core 16 | uses: actions/setup-dotnet@v4 17 | with: 18 | dotnet-version: 6 19 | - uses: actions/setup-java@v4 20 | with: 21 | distribution: temurin 22 | java-version: 11 23 | cache: maven 24 | - uses: actions/setup-node@v4 25 | with: 26 | node-version: '20' 27 | - uses: pnpm/action-setup@v3 28 | with: 29 | version: 8 30 | - name: check version 31 | run: .github/semver-check.sh ${{ inputs.version }} 32 | - name: create release draft 33 | uses: ncipollo/release-action@v1 34 | with: 35 | tag: v${{ inputs.version }} 36 | name: ${{ inputs.version }} 37 | draft: true 38 | - name: set project version 39 | run: .github/fix-project-version.sh ${{ inputs.version }} 40 | - name: update docs version 41 | run: .github/fix-docs-version.sh ${{ inputs.version }} 42 | - name: commit release version 43 | uses: stefanzweifel/git-auto-commit-action@v5 44 | with: 45 | commit_message: '[skip ci] Set release version' 46 | branch: main 47 | file_pattern: '*/*.csproj README.md docs/index.md docs/guide/**' 48 | - name: build 49 | run: dotnet build --configuration Release 50 | - name: test 51 | # avoid running tests in parallel since it may happen that two tests try to modify .jmeter-dsl directory 52 | run: dotnet test -m:1 --no-build --configuration Release --verbosity normal 53 | env: 54 | AZURE_CREDS: ${{ secrets.AZURE_CREDS }} 55 | BZ_TOKEN: ${{ secrets.BZ_TOKEN }} 56 | - name: package 57 | run: dotnet pack --no-build --configuration Release 58 | - name: publish to Nuget 59 | run: .github/nuget-deploy.sh 60 | env: 61 | NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} 62 | - name: publish GH release 63 | uses: ncipollo/release-action@v1 64 | with: 65 | tag: v${{ inputs.version }} 66 | allowUpdates: true 67 | omitNameDuringUpdate: true 68 | omitBodyDuringUpdate: true 69 | updateOnlyUnreleased: true 70 | draft: false 71 | - name: get next alpha version 72 | run: echo "ALPHA_VERSION=$(.github/next-minor-alpha.sh ${{ inputs.version }})" >> $GITHUB_ENV 73 | - name: update to next ALPHA version 74 | run: .github/fix-project-version.sh ${{ env.ALPHA_VERSION }} 75 | - name: commit ALPHA version 76 | uses: stefanzweifel/git-auto-commit-action@v5 77 | with: 78 | commit_message: '[skip ci] Set ALPHA version' 79 | branch: main 80 | file_pattern: '*/*.csproj README.md docs/index.md docs/guide/**' 81 | - name: deploy github pages 82 | run: .github/vuepress-deploy.sh 83 | env: 84 | ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 85 | - uses: actions/checkout@v4 86 | with: 87 | repository: abstracta/jmeter-dotnet-dsl-sample 88 | path: jmeter-dotnet-dsl-sample 89 | token: ${{ secrets.ACTIONS_TOKEN }} 90 | - name: update version in sample project 91 | run: .github/update-sample-version.sh ${{ inputs.version }} 92 | - name: Discord notification 93 | env: 94 | DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} 95 | uses: Ilshidur/action-discord@master 96 | with: 97 | args: 'A new release is out! Check it at https://github.com/abstracta/jmeter-dotnet-dsl/releases/tag/v${{ inputs.version }}' 98 | -------------------------------------------------------------------------------- /docs/.vuepress/config.ts: -------------------------------------------------------------------------------- 1 | import { defineUserConfig } from '@vuepress/cli' 2 | import { defaultTheme } from '@vuepress/theme-default' 3 | import { getDirname, path, fs } from '@vuepress/utils' 4 | import { searchPlugin } from '@vuepress/plugin-search' 5 | import { mediumZoomPlugin } from '@vuepress/plugin-medium-zoom' 6 | import { containerPlugin } from '@vuepress/plugin-container' 7 | import { mdEnhancePlugin } from "vuepress-plugin-md-enhance" 8 | import { repoLinkSolverPlugin } from "./plugins/repoLinkSolverPlugin" 9 | import { includedRelativeLinkSolverPlugin } from "./plugins/includedRelativeLinkSolverPlugin" 10 | import { copyCodePlugin } from "vuepress-plugin-copy-code2" 11 | 12 | const __dirname = getDirname(import.meta.url) 13 | 14 | const REPO_LINK = "https://github.com/abstracta/jmeter-dotnet-dsl" 15 | 16 | 17 | export default defineUserConfig({ 18 | lang: 'en-US', 19 | title: 'jmeter-dotnet-dsl', 20 | description: 'Simple JMeter performance tests API', 21 | base: '/jmeter-dotnet-dsl/', 22 | head: [ 23 | ['link', { rel: 'shortcut icon', href: '/jmeter-dotnet-dsl/favicon.ico'}], 24 | // when changing this remember also changing components/NavbarBrand.vue 25 | ['script', {}, `(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start': 26 | new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0], 27 | j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src= 28 | 'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f); 29 | })(window,document,'script','dataLayer','GTM-PHSGKLD'); 30 | `] 31 | ], 32 | // restrict pattern to avoid going into included pages 33 | pagePatterns: ["*.md", "*/index.md", "!.vuepress", "!node_modules"], 34 | markdown: { 35 | headers: { 36 | level: [2, 3, 4] 37 | } 38 | }, 39 | theme: defaultTheme({ 40 | logo: '/logo.svg', 41 | editLink: false, 42 | lastUpdated: false, 43 | contributors: false, 44 | navbar: [ 45 | { 46 | text: 'Guide', 47 | link: '/guide/', 48 | }, 49 | { 50 | text: 'Support', 51 | link: '/support/', 52 | }, 53 | { 54 | text: 'Motivation', 55 | link: 'https://abstracta.github.io/jmeter-java-dsl/motivation/', 56 | }, 57 | { 58 | link: "https://discord.gg/WNSn5hqmSd", 59 | icon: ['fab', 'discord'] 60 | }, 61 | { 62 | link: REPO_LINK, 63 | icon: ['fab', 'github'] 64 | } 65 | ], 66 | sidebarDepth: 3 67 | }), 68 | alias: { 69 | '@theme/NavbarBrand.vue': path.resolve(__dirname, './components/NavbarBrand.vue'), 70 | '@theme/AutoLink.vue': path.resolve(__dirname, './components/AutoLink.vue'), 71 | '@theme/HomeHero.vue': path.resolve(__dirname, './components/HomeHero.vue'), 72 | '@theme/HomeFeatures.vue': path.resolve(__dirname, './components/HomeFeatures.vue'), 73 | }, 74 | plugins: [ 75 | searchPlugin({ maxSuggestions: 10 }), 76 | mdEnhancePlugin({ 77 | include: { 78 | deep: true, 79 | resolveImagePath: true, 80 | resolveLinkPath: true, 81 | resolvePath: (filePath: string, cwd: string | null) => { 82 | let ret = path.join(cwd, filePath) 83 | if (!fs.existsSync(ret)) { 84 | throw new Error(`File ${ret} not found.`) 85 | } 86 | return ret; 87 | } 88 | } 89 | }), 90 | repoLinkSolverPlugin({ repoUrl: REPO_LINK }), 91 | includedRelativeLinkSolverPlugin({}), 92 | copyCodePlugin({ pure: true }), 93 | containerPlugin({ 94 | type: 'grid', 95 | before: (info: string): string => `
\n`, 96 | after: (): string => '
\n' 97 | }), 98 | containerPlugin({ 99 | type: 'grid-logo', 100 | before: (info: string): string => `\n' 102 | }), 103 | mediumZoomPlugin({selector: "*:is(img):not(.card img):not(a img)"}), 104 | ], 105 | }) -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.Tests/WireMockAssertionsExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text.RegularExpressions; 6 | using FluentAssertions; 7 | using FluentAssertions.Execution; 8 | using WireMock; 9 | using WireMock.FluentAssertions; 10 | using WireMock.Types; 11 | 12 | namespace Abstracta.JmeterDsl 13 | { 14 | public static class WireMockAssertionsExtensions 15 | { 16 | public static AndConstraint WithBody(this WireMockAssertions instance, string body) => 17 | WithBody(instance, request => string.Equals(request.Body, body, StringComparison.OrdinalIgnoreCase), body); 18 | 19 | private static AndConstraint WithBody(WireMockAssertions instance, Func predicate, object body) 20 | { 21 | var requestsField = GetPrivateField("_requestMessages", instance); 22 | var requests = (IReadOnlyList)requestsField.GetValue(instance)!; 23 | var callsCount = (int?)GetPrivateField("_callsCount", instance).GetValue(instance); 24 | Func, IReadOnlyList> filter = requests => requests.Where(predicate).ToList(); 25 | Func, bool> condition = requests => (callsCount is null && filter(requests).Any()) || callsCount == filter(requests).Count; 26 | 27 | Execute.Assertion 28 | .BecauseOf(string.Empty, Array.Empty()) 29 | .Given(() => requests) 30 | .ForCondition(requests => callsCount == 0 || requests.Any()) 31 | .FailWith( 32 | "Expected {context:wiremockserver} to have been called using body " + (body is Regex ? "matching " : string.Empty) + "{0}{reason}, but no calls were made.", 33 | body 34 | ) 35 | .Then 36 | .ForCondition(condition) 37 | .FailWith( 38 | "Expected {context:wiremockserver} to have been called using body " + (body is Regex ? "matching " : string.Empty) + "{0}{reason}, but didn't find it among the bodies {1}.", 39 | _ => body, 40 | requests => requests.Select(request => request.Body) 41 | ); 42 | requestsField.SetValue(instance, filter(requests).ToList()); 43 | return new AndConstraint(instance); 44 | } 45 | 46 | private static FieldInfo GetPrivateField(string fieldName, object o) => 47 | o.GetType().GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance)!; 48 | 49 | public static AndConstraint WithoutHeader(this WireMockAssertions instance, string headerName) 50 | { 51 | var headersField = GetPrivateField("_headers", instance); 52 | var headers = (IReadOnlyList>>)headersField.GetValue(instance)!; 53 | using (new AssertionScope("headers from requests sent")) 54 | { 55 | headers.Select(h => h.Key).Should().NotContain(headerName); 56 | } 57 | return new AndConstraint(instance); 58 | } 59 | 60 | public static AndConstraint WithHeader(this WireMockAssertions instance, string headerName, Regex valueRegex) 61 | { 62 | var headersField = GetPrivateField("_headers", instance); 63 | var headers = (IReadOnlyList>>)headersField.GetValue(instance)!; 64 | using (new AssertionScope("headers from requests sent")) 65 | { 66 | headers.Should() 67 | .ContainSingle(h => h.Key == headerName && h.Value.Count == 1 && valueRegex.IsMatch(h.Value[0])); 68 | } 69 | return new AndConstraint(instance); 70 | } 71 | 72 | public static AndConstraint WithBody(this WireMockAssertions instance, Regex bodyRegex) => 73 | WithBody(instance, request => bodyRegex.IsMatch(request.Body), bodyRegex); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /docs/.vuepress/components/AutoLink.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 96 | 97 | -------------------------------------------------------------------------------- /docs/guide/scale/blazemeter.md: -------------------------------------------------------------------------------- 1 | ### BlazeMeter 2 | 3 | By including the following package: 4 | 5 | ```powershell 6 | dotnet add package Abstracta.JmeterDsl.BlazeMeter --version 0.8 7 | ``` 8 | 9 | You can easily run a JMeter test plan at scale in [BlazeMeter](https://www.blazemeter.com/) like this: 10 | 11 | ```cs 12 | using Abstracta.JmeterDsl.BlazeMeter; 13 | using static Abstracta.JmeterDsl.JmeterDsl; 14 | 15 | public class PerformanceTest 16 | { 17 | [Test] 18 | public void LoadTest() 19 | { 20 | var stats = TestPlan( 21 | // number of threads and iterations are in the end overwritten by BlazeMeter engine settings 22 | ThreadGroup(2, 10, 23 | HttpSampler("http://my.service") 24 | ) 25 | ).RunIn(new BlazeMeterEngine(Environment.GetEnvironmentVariable("BZ_TOKEN")) 26 | .TestName("DSL test") 27 | .TotalUsers(500) 28 | .HoldFor(TimeSpan.FromMinutes(10)) 29 | .ThreadsPerEngine(100) 30 | .TestTimeout(TimeSpan.FromMinutes(20)) 31 | .TestName("dsl-test")); 32 | Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); 33 | } 34 | } 35 | ``` 36 | 37 | > This test is using `BZ_TOKEN`, a custom environment variable with `:` format, to get the BlazeMeter API authentication credentials. 38 | 39 | Note that is as simple as [generating a BlazeMeter authentication token](https://guide.blazemeter.com/hc/en-us/articles/115002213289-BlazeMeter-API-keys-) and adding `.RunIn(new BlazeMeterEngine(...))` to any existing JMeter DSL test to get it running at scale in BlazeMeter. 40 | 41 | BlazeMeter will not only allow you to run the test at scale but also provides additional features like nice real-time reporting, historic data tracking, etc. Here is an example of how a test would look in BlazeMeter: 42 | 43 | ![BlazeMeter Example Execution Dashboard](./blazemeter.png) 44 | 45 | Check [BlazeMeterEngine](/Abstracta.JmeterDsl.BlazeMeter/BlazeMeterEngine.cs) for details on usage and available settings when running tests in BlazeMeter. 46 | 47 | ::: warning 48 | By default the engine is configured to timeout if test execution takes more than 1 hour. 49 | This timeout exists to avoid any potential problem with BlazeMeter execution not detected by the 50 | client, and avoid keeping the test indefinitely running until is interrupted by a user, 51 | which may incur in unnecessary expenses in BlazeMeter and is specially annoying when running tests 52 | in automated fashion, for example in CI/CD. 53 | It is strongly advised to **set this timeout properly in each run**, according to the expected test 54 | execution time plus some additional margin (to consider for additional delays in BlazeMeter 55 | test setup and teardown) to avoid unexpected test plan execution failure (due to timeout) or 56 | unnecessary waits when there is some unexpected issue with BlazeMeter execution. 57 | ::: 58 | 59 | ::: warning 60 | `BlazeMeterEngine` always returns 0 as `sentBytes` statistics since there is no efficient way to get it from BlazMeter. 61 | ::: 62 | 63 | ::: tip 64 | By default `BlazeMeterEngine` will run tests from default location (most of the times `us-east4-a`). But in some scenarios you might want to change the location, or even run the test from multiple locations. 65 | 66 | Here is an example how you can easily set this up: 67 | 68 | ```cs 69 | TestPlan( 70 | ThreadGroup(300, TimeSpan.FromMinutes(5), // 300 total users for 5 minutes 71 | HttpSampler(SAMPLE_LABEL, "https://myservice") 72 | ) 73 | ).RunIn(new BlazeMeterEngine(Environment.GetEnvironmentVariable("BZ_TOKEN")) 74 | .Location(BlazeMeterLocation.GCP_SAO_PAULO, 30) // 30% = 90 users will run in Google Cloud Platform at Sao Paulo 75 | .Location("MyPrivateLocation", 70) // 70% = 210 users will run in MyPrivateLocation named private location 76 | .TestTimeout(TimeSpan.FromMinutes(10))); 77 | ``` 78 | 79 | ::: 80 | 81 | ::: tip 82 | In case you want to get debug logs for HTTP calls to BlazeMeter API, you can include the following setting to an existing `log4j2.xml` configuration file: 83 | ```xml 84 | 85 | 86 | ``` 87 | ::: 88 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 25.0.1705.4 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstracta.JmeterDsl", "Abstracta.JmeterDsl\Abstracta.JmeterDsl.csproj", "{52A68F2B-F431-49FF-8518-265238BF62FF}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstracta.JmeterDsl.Tests", "Abstracta.JmeterDsl.Tests\Abstracta.JmeterDsl.Tests.csproj", "{0BC6F8BE-9959-49AA-97BD-1AD3EF2C3FB6}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{D722220C-CD82-4457-984E-DEE739CE632C}" 11 | ProjectSection(SolutionItems) = preProject 12 | .editorconfig = .editorconfig 13 | StyleCop.ruleset = StyleCop.ruleset 14 | EndProjectSection 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstracta.JmeterDsl.Azure", "Abstracta.JmeterDsl.Azure\Abstracta.JmeterDsl.Azure.csproj", "{5F7E31FB-CCC9-4FFC-ACD3-20D47B91A188}" 17 | EndProject 18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstracta.JmeterDsl.Azure.Tests", "Abstracta.JmeterDsl.Azure.Tests\Abstracta.JmeterDsl.Azure.Tests.csproj", "{8EE1AA42-6DD8-4A24-A9E0-0549E9B2F9B5}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstracta.JmeterDsl.BlazeMeter", "Abstracta.JmeterDsl.BlazeMeter\Abstracta.JmeterDsl.BlazeMeter.csproj", "{B998F9F5-DC15-4FE3-9624-F0BB8E6B3591}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Abstracta.JmeterDsl.BlazeMeter.Tests", "Abstracta.JmeterDsl.BlazeMeter.Tests\Abstracta.JmeterDsl.BlazeMeter.Tests.csproj", "{9170BA21-A016-402B-9E30-034937B33DB8}" 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {52A68F2B-F431-49FF-8518-265238BF62FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {52A68F2B-F431-49FF-8518-265238BF62FF}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {52A68F2B-F431-49FF-8518-265238BF62FF}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {52A68F2B-F431-49FF-8518-265238BF62FF}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {0BC6F8BE-9959-49AA-97BD-1AD3EF2C3FB6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {0BC6F8BE-9959-49AA-97BD-1AD3EF2C3FB6}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {0BC6F8BE-9959-49AA-97BD-1AD3EF2C3FB6}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {0BC6F8BE-9959-49AA-97BD-1AD3EF2C3FB6}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {5F7E31FB-CCC9-4FFC-ACD3-20D47B91A188}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {5F7E31FB-CCC9-4FFC-ACD3-20D47B91A188}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {5F7E31FB-CCC9-4FFC-ACD3-20D47B91A188}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {5F7E31FB-CCC9-4FFC-ACD3-20D47B91A188}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {8EE1AA42-6DD8-4A24-A9E0-0549E9B2F9B5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {8EE1AA42-6DD8-4A24-A9E0-0549E9B2F9B5}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {8EE1AA42-6DD8-4A24-A9E0-0549E9B2F9B5}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {8EE1AA42-6DD8-4A24-A9E0-0549E9B2F9B5}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {B998F9F5-DC15-4FE3-9624-F0BB8E6B3591}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {B998F9F5-DC15-4FE3-9624-F0BB8E6B3591}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {B998F9F5-DC15-4FE3-9624-F0BB8E6B3591}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {B998F9F5-DC15-4FE3-9624-F0BB8E6B3591}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {9170BA21-A016-402B-9E30-034937B33DB8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {9170BA21-A016-402B-9E30-034937B33DB8}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {9170BA21-A016-402B-9E30-034937B33DB8}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {9170BA21-A016-402B-9E30-034937B33DB8}.Release|Any CPU.Build.0 = Release|Any CPU 54 | EndGlobalSection 55 | GlobalSection(SolutionProperties) = preSolution 56 | HideSolutionNode = FALSE 57 | EndGlobalSection 58 | GlobalSection(ExtensibilityGlobals) = postSolution 59 | SolutionGuid = {152E808D-363F-4C3D-BDF9-D9DFA65E05AE} 60 | EndGlobalSection 61 | EndGlobal 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](https://raw.githubusercontent.com/abstracta/jmeter-dotnet-dsl/main/docs/.vuepress/public/logo.svg) 2 | 3 | Simple .Net API to run performance tests, using [JMeter](http://jmeter.apache.org/) as engine, in a Git and programmers friendly way. 4 | 5 | If you like this project, **please give it a star :star:!** This helps the project be more visible, gain relevance, and encourage us to invest more effort in new features. 6 | 7 | [Here](https://abstracta.github.io/jmeter-java-dsl), you can find the Java DSL. 8 | 9 | Please join [discord server](https://discord.gg/WNSn5hqmSd) or create GitHub [issues](https://github.com/abstracta/jmeter-dotnet-dsl/issues) and [discussions](https://github.com/abstracta/jmeter-dotnet-dsl/discussions) to be part of the community and clear out doubts, get the latest news, propose ideas, report issues, etc. 10 | 11 | ## Usage 12 | 13 | Add the package to your project: 14 | 15 | ```powershell 16 | dotnet add package Abstracta.JmeterDsl --version 0.8 17 | ``` 18 | 19 | Here is a simple example test using [Nunit](https://nunit.org/)+ with 2 threads/users iterating 10 times each to send HTTP POST requests with a JSON body to `http://my.service`: 20 | 21 | ```cs 22 | using System.Net.Http.Headers; 23 | using System.Net.Mime; 24 | using static Abstracta.JmeterDsl.JmeterDsl; 25 | 26 | public class PerformanceTest 27 | { 28 | [Test] 29 | public void LoadTest() 30 | { 31 | var stats = TestPlan( 32 | ThreadGroup(2, 10, 33 | HttpSampler("http://my.service") 34 | .Post("{\"name\": \"test\"}", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)) 35 | ), 36 | //this is just to log details of each request stats 37 | JtlWriter("jtls") 38 | ).Run(); 39 | Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5))); 40 | } 41 | } 42 | ``` 43 | 44 | > **Java 8+ is required** for test plan execution. 45 | 46 | More examples can be found in [tests](Abstracta.JmeterDsl.Tests) 47 | 48 | [Here](https://github.com/abstracta/jmeter-dotnet-dsl-sample) is a sample project for reference or for starting new projects from scratch. 49 | 50 | > **Tip 1:** When working with multiple samplers in a test plan, specify their names to easily check their respective statistics. 51 | 52 | > **Tip 2:** Since JMeter uses [log4j2](https://logging.apache.org/log4j/2.x/), if you want to control the logging level or output, you can use something similar to the tests included [log4j2.xml](Abstracta.JmeterDsl.Tests/log4j2.xml), using "CopyToOutputDirectory" in the project item so the file is available in dotnet build output directory as well (check [Abstracta.JmeterDsl.Test/Abstracta.JmeterDsl.Tests.csproj]). 53 | 54 | 55 | **Check [here](https://abstracta.github.io/jmeter-dotnet-dsl/) for details on some interesting use cases**, like running tests at scale in [Azure Load Testing](https://azure.microsoft.com/en-us/products/load-testing/), and general usage guides. 56 | 57 | ## Why? 58 | 59 | Check more about the motivation and analysis of alternatives [here](https://abstracta.github.io/jmeter-java-dsl/motivation/) 60 | 61 | ## Support 62 | 63 | Join our [Discord server](https://discord.gg/WNSn5hqmSd) to engage with fellow JMeter DSL enthusiasts, ask questions, and share experiences. Visit [GitHub Issues](https://github.com/abstracta/jmeter-dotnet-dsl/issues) or [GitHub Discussions](https://github.com/abstracta/jmeter-dotnet-dsl/discussions) for bug reports, feature requests and share ideas. 64 | 65 | [Abstracta](https://abstracta.us), the main supporter for JMeter DSL development, offers enterprise-level support. Get faster response times, personalized customizations and consulting. 66 | 67 | For detailed support information, visit our [Support](https://abstracta.github.io/jmeter-dotnet-dsl/support) page. 68 | 69 | ## Articles & Talks 70 | 71 | Check articles and talks mentioning the Java version [here](https://github.com/abstracta/jmeter-java-dsl#articles--talks). 72 | 73 | ## Ecosystem 74 | 75 | * [Jmeter Java DSL](https://abstracta.github.io/jmeter-java-dsl): Java API which is the base of the .Net API. 76 | * [pymeter](https://github.com/eldaduzman/pymeter): Python API based on JMeter Java DSL that allows Python devs to create and run JMeter test plans. 77 | 78 | ## Contributing & Requesting features 79 | 80 | Currently, the project covers some of the most used features of JMeter and JMeter Java DSL test, but not everything, as we keep improving it to cover more use cases. 81 | 82 | We invest in the development of DSL according to the community's (your) interest, which we evaluate by reviewing GitHub stars' evolution, feature requests, and contributions. 83 | 84 | To keep improving the DSL we need you to **please create an issue for any particular feature or need that you have**. 85 | 86 | We also really appreciate pull requests. Check the [CONTRIBUTING](CONTRIBUTING.md) guide for an explanation of the main library components and how you can extend the library. 87 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl.BlazeMeter/BlazeMeterLocation.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.BlazeMeter 2 | { 3 | public static class BlazeMeterLocation 4 | { 5 | public static readonly string AWS_AP_NORTHEAST_1 = "ap-northeast-1", AWS_TOKYO = AWS_AP_NORTHEAST_1, 6 | AWS_AP_NORTHEAST_2 = "ap-northeast-2", AWS_SEOUL = AWS_AP_NORTHEAST_2, 7 | AWS_AP_SOUTH_1 = "ap-south-1", AWS_MUMBAI = AWS_AP_SOUTH_1, 8 | AWS_AP_SOUTHEAST_1 = "ap-southeast-1", AWS_SINGAPORE = AWS_AP_SOUTHEAST_1, 9 | AWS_AP_SOUTHEAST_2 = "ap-southeast-2", AWS_AUSTRALIA = AWS_AP_SOUTHEAST_2, 10 | AWS_CA_CENTRAL_1 = "ca-central-1", AWS_CANADA_CENTRAL = AWS_CA_CENTRAL_1, 11 | AWS_EU_CENTRAL = "eu-central-1", AWS_FRANKFURT = AWS_EU_CENTRAL, 12 | AWS_EU_WEST_1 = "eu-west-1", AWS_IRELAND = AWS_EU_WEST_1, 13 | AWS_EU_WEST_2 = "eu-west-2", AWS_LONDON = AWS_EU_WEST_2, 14 | AWS_EU_WEST_3 = "eu-west-3", AWS_PARIS = AWS_EU_WEST_3, 15 | AWS_SA_EAST_1 = "sa-east-1", AWS_SAO_PAULO = AWS_SA_EAST_1, 16 | AWS_US_EAST_1 = "us-east-1", AWS_NORTHERN_VIRGINIA = AWS_US_EAST_1, 17 | AWS_US_EAST_2 = "us-east-2", AWS_OHIO = AWS_US_EAST_2, 18 | AWS_US_WEST_1 = "us-west-1", AWS_NORTHERN_CALIFORNIA = AWS_US_WEST_1, 19 | AWS_US_WEST_2 = "us-west-2", AWS_OREGON = AWS_US_WEST_2, 20 | AZURE_KOREA_CENTRAL = "azure-ap-northeast-1", AZURE_SEOUL = AZURE_KOREA_CENTRAL, 21 | AZURE_KOREA_SOUTH = "azure-ap-northeast-2", AZURE_BUSAN = AZURE_KOREA_SOUTH, 22 | AZURE_BRAZIL_SOUTH = "azure-brazil-south-1", AZURE_SAO_PAULO = AZURE_BRAZIL_SOUTH, 23 | AZURE_CENTRAL_INDIA = "azure-central-asia-1", AZURE_PUNE = AZURE_CENTRAL_INDIA, 24 | AZURE_WEST_INDIA = "azure-central-asia-2", AZURE_MUMBAI = AZURE_WEST_INDIA, 25 | AZURE_SOUTH_INDIA = "azure-central-asia-3", AZURE_CHENNAI = AZURE_SOUTH_INDIA, 26 | AZURE_CANADA_CENTRAL = "azure-central-ca", AZURE_TORONTO = AZURE_CANADA_CENTRAL, 27 | AZURE_CENTRAL_US = "azure-central-us-1", AZURE_IOWA = AZURE_CENTRAL_US, 28 | AZURE_EAST_ASIA = "azure-east-asia-1", AZURE_HONG_KONG = AZURE_EAST_ASIA, 29 | AZURE_AUSTRALIA_EAST = "azure-east-au-1", AZURE_NEW_SOUTH_WALES = AZURE_AUSTRALIA_EAST, 30 | AZURE_CANADA_EAST = "azure-east-ca", AZURE_QUEBEC = AZURE_CANADA_EAST, 31 | AZURE_US_EAST = "azure-east-us-1", AZURE_VIRGINIA = AZURE_US_EAST, 32 | AZURE_US_EAST_2 = "azure-east-us-2", AZURE_VIRGINIA_2 = AZURE_US_EAST_2, 33 | AZURE_UK_SOUTH = "azure-eu-west-2", AZURE_LONDON = AZURE_UK_SOUTH, 34 | AZURE_UK_WEST = "azure-eu-west-3", AZURE_CARDIFF = AZURE_UK_WEST, 35 | AZURE_JAPAN_EAST = "azure-japan-east-1", AZURE_TOKYO = AZURE_JAPAN_EAST, 36 | AZURE_JAPAN_WEST = "azure-japan-west-1", AZURE_OSAKA = AZURE_JAPAN_WEST, 37 | AZURE_NORTH_CENTRAL_US = "azure-north-central-us-1", AZURE_ILLINOIS = AZURE_NORTH_CENTRAL_US, 38 | AZURE_NORTH_EUROPE = "azure-north-europe-1", AZURE_IRELAND = AZURE_NORTH_EUROPE, 39 | AZURE_SOUTH_CENTRAL_US_STG = "azure-south-central-us-1", 40 | AZURE_TEXAS = AZURE_SOUTH_CENTRAL_US_STG, 41 | AZURE_SOUTHEAST_ASIA = "azure-southeast-asia-1", AZURE_SINGAPORE = AZURE_SOUTHEAST_ASIA, 42 | AZURE_AUSTRALIA_SOUTHEAST = "azure-southeast-au-1", 43 | AZURE_VICTORIA = AZURE_AUSTRALIA_SOUTHEAST, 44 | AZURE_WEST_CENTRAL_US = "azure-us-west-central", AZURE_WYOMING = AZURE_WEST_CENTRAL_US, 45 | AZURE_WEST_EUROPE = "azure-west-europe-1", AZURE_NETHERLANDS = AZURE_WEST_EUROPE, 46 | AZURE_WEST_US = "azure-west-us-1", AZURE_CALIFORNIA = AZURE_WEST_US, 47 | AZURE_WEST_US_2 = "azure-west-us-2", AZURE_WASHINGTON = AZURE_WEST_US_2, 48 | GCP_ASIA_EAST_1 = "asia-east1-a", GCP_TAIWAN = GCP_ASIA_EAST_1, 49 | GCP_ASIA_NORTHEAST_1 = "asia-northeast1-a", GCP_TOKYO = GCP_ASIA_NORTHEAST_1, 50 | GCP_ASIA_NORTHEAST_2 = "asia-northeast2-a", GCP_OSAKA = GCP_ASIA_NORTHEAST_2, 51 | GCP_ASIA_SOUTH_1 = "asia-south1-a", GCP_MUMBAI = GCP_ASIA_SOUTH_1, 52 | GCP_ASIA_SOUTHEAST_1 = "asia-southeast1-a", GCP_SINGAPORE = GCP_ASIA_SOUTHEAST_1, 53 | GCP_AUSTRALIA_SOUTHEAST_1 = "australia-southeast1-a", GCP_SYDNEY = GCP_AUSTRALIA_SOUTHEAST_1, 54 | GCP_EUROPE_WEST_1 = "europe-west1-b", GCP_BELGIUM = GCP_EUROPE_WEST_1, 55 | GCP_EUROPE_WEST_2 = "europe-west2-a", GCP_LONDON = GCP_EUROPE_WEST_2, 56 | GCP_EUROPE_WEST_3 = "europe-west3-a", GCP_FRANKFURT = GCP_EUROPE_WEST_3, 57 | GCP_EUROPE_WEST_4 = "europe-west4-b", GCP_NETHERLANDS = GCP_EUROPE_WEST_4, 58 | GCP_NORTH_AMERICA_NORTHEAST_1 = "northamerica-northeast1-a", 59 | GCP_MONTREAL = GCP_NORTH_AMERICA_NORTHEAST_1, 60 | GCP_SOUTH_AMERICA_EAST_1 = "southamerica-east1-a", GCP_SAO_PAULO = GCP_SOUTH_AMERICA_EAST_1, 61 | GCP_US_CENTRAL_1 = "us-central1-a", GCP_IOWA = GCP_US_CENTRAL_1, 62 | GCP_US_EAST_1 = "us-east1-b", GCP_SOUTH_CAROLINA = GCP_US_EAST_1, 63 | GCP_US_EAST_4 = "us-east4-a", GCP_NORTHERN_VIRGINIA = GCP_US_EAST_4, 64 | GCP_US_WEST_1 = "us-west1-a", GCP_OREGON = GCP_US_WEST_1, 65 | GCP_US_WEST_2 = "us-west2-a", GCP_LOS_ANGELES = GCP_US_WEST_2; 66 | 67 | } 68 | } -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Bridge/BridgedObjectConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Reflection; 6 | using YamlDotNet.Core; 7 | using YamlDotNet.Core.Events; 8 | using YamlDotNet.Serialization; 9 | 10 | namespace Abstracta.JmeterDsl.Core.Bridge 11 | { 12 | public class BridgedObjectConverter : IYamlTypeConverter 13 | { 14 | private const string DslClassPrefix = "Dsl"; 15 | private const string NamespacePrefix = "Abstracta.JmeterDsl."; 16 | 17 | public IValueSerializer ValueSerializer { get; set; } 18 | 19 | public bool Accepts(Type type) => 20 | typeof(IDslTestElement).IsAssignableFrom(type) 21 | || typeof(IDslProperty).IsAssignableFrom(type) 22 | || typeof(IDslJmeterEngine).IsAssignableFrom(type) 23 | || typeof(TestPlanExecution).IsAssignableFrom(type); 24 | 25 | public object ReadYaml(IParser parser, Type type) => 26 | throw new NotImplementedException(); 27 | 28 | public void WriteYaml(IEmitter emitter, object value, Type type) 29 | { 30 | var valueType = value.GetType(); 31 | var tagName = BuildTagName(valueType); 32 | emitter.Emit(new MappingStart(null, tagName, false, MappingStyle.Any)); 33 | var fields = valueType.GetFields(BindingFlags.NonPublic | BindingFlags.Instance); 34 | fields = fields.Where(f => !(f.Name == "_name" && f.GetValue(value) == null)).ToArray(); 35 | if (fields.Length == 1 && typeof(IDictionary).IsAssignableFrom(fields[0].FieldType)) 36 | { 37 | WriteDictionaryYaml((IDictionary)fields[0].GetValue(value), emitter); 38 | } 39 | else 40 | { 41 | // this set is used to avoid processing fields that have been hidden by subclass members (using new keyword) 42 | ISet processedFields = new HashSet(); 43 | foreach (FieldInfo field in fields) 44 | { 45 | if (!processedFields.Contains(field.Name) && !IsIgnoredField(field)) 46 | { 47 | WriteFieldYaml(field, field.GetValue(value), emitter); 48 | } 49 | processedFields.Add(field.Name); 50 | } 51 | } 52 | emitter.Emit(new MappingEnd()); 53 | } 54 | 55 | private string BuildTagName(Type valueType) 56 | { 57 | var ret = valueType.GetCustomAttribute()?.TagName; 58 | if (ret == null) 59 | { 60 | ret = IsCoreElement(valueType) || typeof(IDslProperty).IsAssignableFrom(valueType) ? BuildSimpleTagName(valueType) : BuildCompleteTagName(valueType); 61 | } 62 | return "!" + ret; 63 | } 64 | 65 | private bool IsCoreElement(Type valueType) 66 | { 67 | var typeFullName = valueType.FullName; 68 | return typeFullName.StartsWith(NamespacePrefix + "Core.") 69 | || typeFullName.StartsWith(NamespacePrefix + "Http.") 70 | || typeFullName.StartsWith(NamespacePrefix + "Java."); 71 | } 72 | 73 | private string BuildSimpleTagName(Type valueType) 74 | { 75 | var ret = valueType.Name; 76 | if (ret.StartsWith(DslClassPrefix)) 77 | { 78 | ret = ret.Substring(DslClassPrefix.Length); 79 | } 80 | ret = LowerFirstChar(ret); 81 | return ret; 82 | } 83 | 84 | private string LowerFirstChar(string val) => 85 | val.Substring(0, 1).ToLower() + val.Substring(1); 86 | 87 | private string BuildCompleteTagName(Type valueType) 88 | { 89 | var ret = valueType.FullName; 90 | if (ret.StartsWith(NamespacePrefix)) 91 | { 92 | ret = ret.Substring(NamespacePrefix.Length); 93 | } 94 | return LowerNamespaces(ret); 95 | } 96 | 97 | private string LowerNamespaces(string fullName) 98 | { 99 | var ret = string.Empty; 100 | int start = 0; 101 | int end = fullName.IndexOf('.'); 102 | while (end != -1) 103 | { 104 | ret += fullName.Substring(start, end - start + 1).ToLower(); 105 | start = end + 1; 106 | end = fullName.IndexOf('.', start); 107 | } 108 | ret += fullName.Substring(start); 109 | return ret; 110 | } 111 | 112 | private void WriteDictionaryYaml(IDictionary value, IEmitter emitter) 113 | { 114 | foreach (KeyValuePair entry in value) 115 | { 116 | emitter.Emit(new Scalar(entry.Key)); 117 | emitter.Emit(new Scalar(entry.Value)); 118 | } 119 | } 120 | 121 | private bool IsIgnoredField(FieldInfo field) => 122 | field.GetCustomAttribute(typeof(YamlIgnoreAttribute)) != null; 123 | 124 | private void WriteFieldYaml(FieldInfo field, object value, IEmitter emitter) 125 | { 126 | if (value == null || (value is ICollection collection && collection.Count == 0)) 127 | { 128 | return; 129 | } 130 | 131 | // removing first character since fields are prefixed with underscore 132 | emitter.Emit(new Scalar(field.Name.Substring(1))); 133 | ValueSerializer.SerializeValue(emitter, value, field.FieldType == typeof(object) ? value.GetType() : field.FieldType); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/Samplers/DslDummySampler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Abstracta.JmeterDsl.Core.Samplers 4 | { 5 | /// 6 | /// Allows using JMeter Dummy Sampler plugin to emulate other samples and ease testing post 7 | /// processors and other parts of a test plan. 8 | ///
9 | /// By default, this element is set with no request, url, response code=200, response message = OK, 10 | /// and response time with random value between 50 and 500 milliseconds. Additionally, emulation of 11 | /// response times (through sleeps) is disabled to speed up testing. 12 | ///
13 | public class DslDummySampler : BaseSampler 14 | { 15 | private readonly string _responseBody; 16 | private bool? _successful; 17 | private string _responseCode; 18 | private string _responseMessage; 19 | private string _responseTime; 20 | private bool? _simulateResponseTime; 21 | private string _url; 22 | private string _requestBody; 23 | 24 | public DslDummySampler(string name, string responseBody) 25 | : base(name) 26 | { 27 | _responseBody = responseBody; 28 | } 29 | 30 | /// 31 | /// Allows generating successful or unsuccessful sample results for this sampler. 32 | /// 33 | /// when true, generated sample result will be successful, otherwise it will be 34 | /// marked as failure. When not specified, successful sample results are 35 | /// generated. 36 | /// the sampler for further configuration or usage. 37 | public DslDummySampler Successful(bool successful) 38 | { 39 | _successful = successful; 40 | return this; 41 | } 42 | 43 | /// 44 | /// Specifies the response code included in generated sample results. 45 | /// 46 | /// defines the response code included in sample results. When not set, 200 is used. 47 | /// the sampler for further configuration or usage. 48 | public DslDummySampler ResponseCode(string code) 49 | { 50 | _responseCode = code; 51 | return this; 52 | } 53 | 54 | /// 55 | /// Specifies the response message included in generated sample results. 56 | /// 57 | /// defines the response message included in sample results. When not set, OK is 58 | /// used. 59 | /// the sampler for further configuration or usage. 60 | public DslDummySampler ResponseMessage(string message) 61 | { 62 | _responseMessage = message; 63 | return this; 64 | } 65 | 66 | /// 67 | /// Specifies the response time used in generated sample results. 68 | /// 69 | /// defines the response time associated to the sample results. When not set, a 70 | /// randomly calculated value between 50 and 500 milliseconds is used. 71 | /// the sampler for further configuration or usage. 72 | public DslDummySampler ResponseTime(TimeSpan responseTime) 73 | { 74 | _responseTime = ((long)responseTime.TotalMilliseconds).ToString(); 75 | return this; 76 | } 77 | 78 | /// 79 | /// Same as but allowing to specify a JMeter expression for 80 | /// evaluation. 81 | ///
82 | /// This is useful when you want response time to be calculated dynamically. For example, 83 | /// ${__Random(50, 500)}} 84 | ///
85 | /// specifies the JMeter expression to be used to calculate response times, 86 | /// in milliseconds, for the sampler. 87 | /// the sampler for further configuration or usage. 88 | /// 89 | public DslDummySampler ResponseTime(string responseTime) 90 | { 91 | _responseTime = responseTime; 92 | return this; 93 | } 94 | 95 | /// 96 | /// Specifies if used response time should be simulated (the sample will sleep for the given 97 | /// duration) or not. 98 | ///
99 | /// Having simulation disabled allows for really fast emulation and trial of test plan, which is 100 | /// very handy when debugging. If you need a more accurate emulation in more advanced cases, like 101 | /// you don't want to generate too many requests per second, and you want a behavior closer to the 102 | /// real thing, then consider enabling response time simulation. 103 | ///
104 | /// when true enables simulation of response times, when false no wait is done 105 | /// speeding up test plan execution. By default, simulation is disabled. 106 | /// the sampler for further configuration or usage. 107 | public DslDummySampler SimulateResponseTime(bool simulate) 108 | { 109 | _simulateResponseTime = simulate; 110 | return this; 111 | } 112 | 113 | /// 114 | /// Specifies the URL used in generated sample results. 115 | ///
116 | /// This might be helpful in scenarios where extractors, pre-processors or other test plan elements 117 | /// depend on the URL. 118 | ///
119 | /// defines the URL associated to generated sample results. When not set, an empty URL 120 | /// is used. 121 | /// the sampler for further configuration or usage. 122 | public DslDummySampler Url(string url) 123 | { 124 | _url = url; 125 | return this; 126 | } 127 | 128 | /// 129 | /// Specifies the request body used in generated sample results. 130 | ///
131 | /// This might be helpful in scenarios where extractors, pre-processors or other test plan elements 132 | /// depend on the request body. 133 | ///
134 | /// defines the request body associated to generated sample results. When not 135 | /// set, an empty body is used. 136 | /// the sampler for further configuration or usage. 137 | public DslDummySampler RequestBody(string requestBody) 138 | { 139 | _requestBody = requestBody; 140 | return this; 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /Abstracta.JmeterDsl/Core/PostProcessors/DslRegexExtractor.cs: -------------------------------------------------------------------------------- 1 | namespace Abstracta.JmeterDsl.Core.PostProcessors 2 | { 3 | /// 4 | /// Allows extracting part of a request or response using regular expressions to store into a 5 | /// variable. 6 | ///
7 | /// By default, the regular extractor is configured to extract from the main sample (does not include 8 | /// sub samples) response body the first capturing group (part of regular expression that is inside 9 | /// of parenthesis) of the first match of the regex. If no match is found, then the variable will not 10 | /// be created or modified. 11 | ///
12 | public class DslRegexExtractor : DslVariableExtractor 13 | { 14 | private readonly string _regex; 15 | private string _template; 16 | private DslTargetField? _fieldToCheck; 17 | 18 | public DslRegexExtractor(string varName, string regex) 19 | : base(null, varName) 20 | { 21 | _regex = regex; 22 | } 23 | 24 | public enum DslTargetField 25 | { 26 | /// 27 | /// Applies the regular extractor to the plain string of the response body. 28 | /// 29 | ResponseBody, 30 | 31 | /// 32 | /// Applies the regular extractor to the response body replacing all HTML escape codes. 33 | /// 34 | ResponseBodyUnescaped, 35 | 36 | /// 37 | /// Applies the regular extractor to the string representation obtained from parsing the response 38 | /// body with Apache Tika. 39 | /// 40 | ResponseBodyAsDocument, 41 | 42 | /// 43 | /// Applies the regular extractor to response headers. Response headers is a string with headers 44 | /// separated by new lines and names and values separated by colons. 45 | /// 46 | ResponseHeaders, 47 | 48 | /// 49 | /// Applies the regular extractor to request headers. Request headers is a string with headers 50 | /// separated by new lines and names and values separated by colons. 51 | /// 52 | RequestHeaders, 53 | 54 | /// 55 | /// Applies the regular extractor to the request URL. 56 | /// 57 | RequestUrl, 58 | 59 | /// 60 | /// Applies the regular extractor to response code. 61 | /// 62 | ResponseCode, 63 | 64 | /// 65 | /// Applies the regular extractor to response message. 66 | /// 67 | ResponseMessage, 68 | } 69 | 70 | /// 71 | /// Sets the match number to be extracted. 72 | ///
73 | /// For example, if a response looks like this: 74 | /// user=test&user=tester 75 | /// and you use user=([^&]+) as regular expression, first match (1) would extract 76 | /// test and second match (2) would extract tester. 77 | ///
78 | /// When not specified, the first match will be used. When 0 is specified, a random match will be 79 | /// used. When negative, all the matches are extracted to variables with name 80 | /// <variableName>_<matchNumber>, the number of matches is stored in 81 | /// <variableName>_matchNr, and default value is assigned to <variableName>. 82 | ///
83 | /// specifies the match number to use. 84 | /// the extractor for further configuration or usage. 85 | public DslRegexExtractor MatchNumber(int matchNumber) 86 | { 87 | _matchNumber = matchNumber; 88 | return this; 89 | } 90 | 91 | /// 92 | /// Specifies the final string to store in the JMeter Variable. 93 | ///
94 | /// The string may contain capturing groups (regular expression segments between parenthesis) 95 | /// references by using $<groupId>$ expressions (eg: $1$ for first group). Check 96 | /// JMeter 97 | /// Regular Expression Extractor documentation for more details. 98 | ///
99 | /// For example, if a response looks like this: 100 | /// email=tester@abstracta.us 101 | /// And you use user=([^&]+) as regular expression. Then $1$-$2$ will result in 102 | /// storing in the specified JMeter variable the value tester-abstracta. 103 | ///
104 | /// When not specified $1$ will be used. 105 | ///
106 | /// specifies template to use for storing in the JMeter variable. 107 | /// the extractor for further configuration or usage. 108 | public DslRegexExtractor Template(string template) 109 | { 110 | _template = template; 111 | return this; 112 | } 113 | 114 | /// 115 | /// Sets the default value to be stored in the JMeter variable when the regex does not match. 116 | ///
117 | /// When match number is negative then the value is always assigned to the variable name. 118 | ///
119 | /// A common pattern is to specify this value to a known value (e.g.: 120 | /// <VAR>_EXTRACTION_FAILURE) and then add some assertion on the variable to mark request as 121 | /// failure when the match doesn't work. 122 | ///
123 | /// When not specified then the variable will not be set if no match is found. 124 | /// specifies the default value to be used. 125 | /// the extractor for further configuration or usage. 126 | public DslRegexExtractor DefaultValue(string defaultValue) 127 | { 128 | _defaultValue = defaultValue; 129 | return this; 130 | } 131 | 132 | /// 133 | /// Allows specifying what part of request or response to apply the regular extractor to. 134 | ///
135 | /// When not specified then the regular extractor will be applied to the response body. 136 | ///
137 | /// field to apply the regular extractor to. 138 | /// the extractor for further configuration or usage. 139 | /// 140 | public DslRegexExtractor FieldToCheck(DslTargetField fieldToCheck) 141 | { 142 | _fieldToCheck = fieldToCheck; 143 | return this; 144 | } 145 | } 146 | } --------------------------------------------------------------------------------