Skip to content

Commit

Permalink
Add support for CsvDataSet
Browse files Browse the repository at this point in the history
  • Loading branch information
rabelenda committed Feb 5, 2024
1 parent 6a24ff9 commit d4334a9
Show file tree
Hide file tree
Showing 9 changed files with 344 additions and 14 deletions.
15 changes: 3 additions & 12 deletions Abstracta.JmeterDsl.Tests/Abstracta.JmeterDsl.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -28,21 +28,12 @@
<ItemGroup>
<ProjectReference Include="..\Abstracta.JmeterDsl\Abstracta.JmeterDsl.csproj" />
</ItemGroup>
<ItemGroup>
<None Remove="Http\" />
<None Remove="Core\" />
<None Remove="Core\Listeners\" />
<None Remove="Core\ThreadGroups\" />
</ItemGroup>
<ItemGroup>
<Folder Include="Http\" />
<Folder Include="Core\" />
<Folder Include="Core\Listeners\" />
<Folder Include="Core\ThreadGroups\" />
</ItemGroup>
<ItemGroup>
<None Update="log4j2.xml">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Update="Core\Configs\data.csv">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
22 changes: 22 additions & 0 deletions Abstracta.JmeterDsl.Tests/Core/Configs/DslCsvDataSetTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
namespace Abstracta.JmeterDsl.Core.Configs
{
using static JmeterDsl;

public class DslCsvDataSetTest
{
[Test]
public void ShouldGetExpectedSamplesWhenTestPlanWithSampleNamesFromCsvDataSet()
{
var stats = TestPlan(
CsvDataSet("Core/Configs/data.csv"),
ThreadGroup(1, 2,
DummySampler("${VAR1}-${VAR2}", "ok")
)).Run();
Assert.Multiple(() =>
{
Assert.That(stats.Labels["val1-val2"].SamplesCount, Is.EqualTo(1));
Assert.That(stats.Labels["val,3-val4"].SamplesCount, Is.EqualTo(1));
});
}
}
}
3 changes: 3 additions & 0 deletions Abstracta.JmeterDsl.Tests/Core/Configs/data.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
VAR1,VAR2
val1,"val2"
"val,3",val4
218 changes: 218 additions & 0 deletions Abstracta.JmeterDsl/Core/Configs/DslCsvDataSet.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
using System.Text;

namespace Abstracta.JmeterDsl.Core.Configs
{
/// <summary>
/// Allows using a CSV file as input data for JMeter variables to use in test plan.
/// <br/>
/// This element reads a CSV file and uses each line to generate JMeter variables to be used in each
/// iteration and thread of the test plan.
/// <br/>
/// Is ideal to be able to easily create test plans that test with a lot of different of potential
/// requests or flows.
/// <br/>
/// By default, it consumes comma separated variables, which names are included in first line of CSV,
/// automatically resets to the beginning of the file when the end is reached and the consumption of
/// the file is shared by all threads and thread groups in the test plan (ie: any iteration on a
/// thread will consume a line from the file, and advance to following line).
/// <br/>
/// Additionally, this element sets by default the "quoted data" flag on JMeter CSV Data Set
/// element.
/// </summary>
public class DslCsvDataSet : BaseConfigElement
{
private readonly string _csvFile;
private string _delimiter;
private string _encoding;
private string[] _variableNames;
private bool? _ignoreFirstLine;
private bool? _stopThreadOnEOF;
private Sharing? _sharedIn;
private bool? _randomOrder;

public DslCsvDataSet(string csvFile)
: base(null)
{
_csvFile = csvFile;
}

/// <summary>
/// Specifies the way the threads in a test plan consume the CSV.
/// </summary>
public enum Sharing
{
/// <summary>
/// All threads in the test plan will share the CSV file, meaning that any thread iteration will
/// consume an entry from it. You can think as having only one pointer to the current line of the
/// CSV, being advanced by any thread iteration. The file is only opened once.
/// </summary>
AllThreads,

/// <summary>
/// CSV file consumption is only shared within thread groups. This means that threads in separate
/// thread groups will use separate indexes to consume the data. The file is open once per thread
/// group.
/// </summary>
ThreadGroup,

/// <summary>
/// CSV file consumption is isolated per thread. This means that each thread will start consuming
/// the CSV from the beginning and not share any information with other threads. The file is open
/// once per thread.
/// </summary>
Thread,
}

/// <summary>
/// Specifies the delimiter used by the file to separate variable values.
/// </summary>
/// <param name="delimiter">specifies the delimiter. By default, it uses commas (,) as delimiters. If you need to use tabs, then specify "\\t".</param>
/// <returns>the dataset for further configuration or usage.</returns>
public DslCsvDataSet Delimiter(string delimiter)
{
_delimiter = delimiter;
return this;
}

/// <summary>
/// Specifies the file encoding used by the file.
/// <br/>
/// This method is useful when specifying a dynamic encoding (through JMeter variable or function
/// reference). Otherwise prefer using <see cref="Encoding(Encoding)"/>.
/// </summary>
/// <param name="encoding">the file encoding of the file. By default, it will use UTF-8 (which differs
/// from JMeter default, to have more consistent test plan execution). This might
/// require to be changed but in general is good to have all files in same encoding
/// (eg: UTF-8).</param>
/// <returns>the dataset for further configuration or usage.</returns>
public DslCsvDataSet Encoding(string encoding)
{
_encoding = encoding;
return this;
}

/// <summary>
/// Specifies the file encoding used by the file.
/// <br/>
/// If you need to specify a dynamic encoding (through JMeter variable or function reference), then
/// use <see cref="Encoding(string)"/> instead.
/// </summary>
/// <param name="encoding">the file encoding of the file. By default, it will use UTF-8 (which differs
/// from JMeter default, to have more consistent test plan execution). This might
/// require to be changed but in general is good to have all files in same encoding
/// (eg: UTF-8).</param>
/// <returns>the dataset for further configuration or usage.</returns>
public DslCsvDataSet Encoding(Encoding encoding)
{
_encoding = encoding.EncodingName;
return this;
}

/// <summary>
/// Specifies variable names to be assigned to the parsed values.
/// <br/>
/// If you have a CSV file with existing headers and want to overwrite the name of generated
/// variables, then use <see cref="IgnoreFirstLine()"/> in conjunction with this method to specify the
/// new variable names. If you have a CSV file without a headers line, then you will need to use
/// this method to set proper names for the variables (otherwise first line of data will be used as
/// headers, which will not be good).
/// </summary>
/// <param name="variableNames">names of variables to be extracted from the CSV file.</param>
/// <returns>the dataset for further configuration or usage.</returns>
public DslCsvDataSet VariableNames(params string[] variableNames)
{
_variableNames = variableNames;
return this;
}

/// <summary>
/// Specifies to ignore first line of the CSV.
/// <br/>
/// This should only be used in conjunction with <see cref="VariableNames(string[])"/> to overwrite
/// existing CSV headers names.
/// </summary>
/// <returns>the dataset for further configuration or usage.</returns>
public DslCsvDataSet IgnoreFirstLine()
=> IgnoreFirstLine(true);

/// <summary>
/// Same as <see cref="IgnoreFirstLine()"/> but allowing to enable or disable it.
/// <br/>
/// This is helpful when the resolution is taken at runtime.
/// </summary>
/// <param name="enable">specifies to enable or disable the setting. By default, it is set to false.</param>
/// <returns>the dataset for further configuration or usage.</returns>
/// <seealso cref="IgnoreFirstLine()"/>
public DslCsvDataSet IgnoreFirstLine(bool enable)
{
_ignoreFirstLine = enable;
return this;
}

/// <summary>
/// Specifies to stop threads when end of given CSV file is reached.
/// <br/>
/// This method will automatically internally set JMeter test element property "recycle on EOF", so
/// you don't need to worry about such property.
/// </summary>
/// <returns>the dataset for further configuration or usage.</returns>
public DslCsvDataSet StopThreadOnEOF()
=> StopThreadOnEOF(true);

/// <summary>
/// Same as <see cref="StopThreadOnEOF()"/> but allowing to enable or disable it.
/// <br/>
/// This is helpful when the resolution is taken at runtime.
/// </summary>
/// <param name="enable">specifies to enable or disable the setting. By default, it is set to false.</param>
/// <returns>the dataset for further configuration or usage.</returns>
/// <seealso cref="StopThreadOnEOF()"/>
public DslCsvDataSet StopThreadOnEOF(bool enable)
{
_stopThreadOnEOF = enable;
return this;
}

/// <summary>
/// Allows changing the way CSV file is consumed (shared) by threads.
/// </summary>
/// <param name="shareMode">specifies the way threads consume information from the CSV file. By default,
/// all threads share the CSV information, meaning that any thread iteration will
/// advance the consumption of the file (the file is a singleton). When
/// <see cref="RandomOrder()"/> is used, THREAD_GROUP shared mode is not supported.</param>
/// <returns>the dataset for further configuration or usage.</returns>
/// <seealso cref="Sharing"/>
public DslCsvDataSet SharedIn(Sharing shareMode)
{
_sharedIn = shareMode;
return this;
}

/// <summary>
/// Specifies to get file lines in random order instead of sequentially iterating over them.
/// <br/>
/// When this method is invoked <a href="https://github.com/Blazemeter/jmeter-bzm-plugins/blob/master/random-csv-data-set/RandomCSVDataSetConfig.md">Random CSV Data Set plugin</a> is used.
/// <br/>
/// <b>Warning:</b> Getting lines in random order has a performance penalty.
/// <br/>
/// <b>Warning:</b> When random order is enabled, share mode THREAD_GROUP is not supported.
/// </summary>
/// <returns>the dataset for further configuration or usage.</returns>
public DslCsvDataSet RandomOrder()
=> RandomOrder(true);

/// <summary>
/// Same as <see cref="RandomOrder()"/> but allowing to enable or disable it.
/// <br/>
/// This is helpful when the resolution is taken at runtime.
/// </summary>
/// <param name="enable">specifies to enable or disable the setting. By default, it is set to false.</param>
/// <returns>the dataset for further configuration or usage.</returns>
/// <seealso cref="RandomOrder()"/>
public DslCsvDataSet RandomOrder(bool enable)
{
_randomOrder = enable;
return this;
}
}
}
28 changes: 28 additions & 0 deletions Abstracta.JmeterDsl/JmeterDsl.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Abstracta.JmeterDsl.Core;
using Abstracta.JmeterDsl.Core.Configs;
using Abstracta.JmeterDsl.Core.Controllers;
using Abstracta.JmeterDsl.Core.Listeners;
using Abstracta.JmeterDsl.Core.PostProcessors;
Expand Down Expand Up @@ -322,5 +323,32 @@ public static ResponseFileSaver ResponseFileSaver(string fileNamePrefix) =>
/// <seealso cref="Core.Listeners.ResultsTreeVisualizer"/>
public static ResultsTreeVisualizer ResultsTreeVisualizer() =>
new ResultsTreeVisualizer();

/// <summary>
/// Builds a CSV Data Set which allows loading from a CSV file variables to be used in test plan.
/// <br/>
/// This allows to store for example in a CSV file one line for each user credentials, and then in
/// the test plan be able to use all the credentials to test with different users.
/// <br/>
/// By default, the CSV data set will read comma separated values, use first row as name of the
/// generated variables, restart from beginning when csv entries are exhausted and will read a new
/// line of CSV for each thread and iteration.
/// <br/>
/// E.g: If you have a csv with 2 entries and a test plan with two threads, iterating 2 times each,
/// you might get (since threads run in parallel, the assignment is not deterministic) following
/// assignment of rows:
/// <br/>
/// <pre>
/// thread 1, row 1
/// thread 2, row 2
/// thread 2, row 1
/// thread 1, row 2
/// </pre>
/// </summary>
/// <param name="csvFile">path to the CSV file to read the data from.</param>
/// <returns>the CSV Data Set instance for further configuration and usage.</returns>
/// <seealso cref="DslCsvDataSet"/>
public static DslCsvDataSet CsvDataSet(string csvFile) =>
new DslCsvDataSet(csvFile);
}
}
67 changes: 67 additions & 0 deletions docs/guide/request-generation/csv-dataset.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
### CSV as input data for requests

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.

This can be easily achieved using the provided `CsvDataSet` element. For example, having a file like this one:

```csv
USER,PASS
user1,pass1
user2,pass2
```

You can implement a test plan that tests recurrent login with the two users with something like this:

```cs
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Net.Mime;
using static Abstracta.JmeterDsl.JmeterDsl;

public class PerformanceTest
{
[Test]
public void LoadTest()
{
var stats = TestPlan(
CsvDataSet("users.csv"),
ThreadGroup(5, 10,
HttpSampler("http://my.service/login")
.Post("{\"${USER}\": \"${PASS}\"", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)),
HttpSampler("http://my.service/logout")
.Method(HttpMethod.Post.Method)
)
).Run();
Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5)));
}
}
```

::: tip
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.

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:

```java
using static Abstracta.JmeterDsl.Core.Configs.DslCsvDataSet;
...
var stats = TestPlan(
CsvDataSet("users.csv")
.SharedIn(Sharing.Thread),
ThreadGroup(5, 10,
HttpSampler("http://my.service/login")
.Post("{\"${USER}\": \"${PASS}\"", new MediaTypeHeaderValue(MediaTypeNames.Application.Json)),
HttpSampler("http://my.service/logout")
.Method(HttpMethod.Post.Method)
)
).Run();
Assert.That(stats.Overall.SampleTimePercentile99, Is.LessThan(TimeSpan.FromSeconds(5)));
```
:::

::: warning
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.
:::

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.).
1 change: 1 addition & 0 deletions docs/guide/request-generation/index.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
## Requests generation

<!-- @include: loops/index.md -->
<!-- @include: csv-dataset.md -->
2 changes: 1 addition & 1 deletion docs/guide/request-generation/loops/forloop-controller.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#### Iterating a fixed number of times

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:
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:

```cs
using static Abstracta.JmeterDsl.JmeterDsl;
Expand Down
Loading

0 comments on commit d4334a9

Please sign in to comment.