Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/dotnet.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
runs-on: ubuntu-latest

env:
Version: 1.1.${{ github.run_number }}
Version: 1.2.${{ github.run_number }}
Solution_Path: ./src/GherXunit.sln
Project_Path: ./src/lib/GherXunit/GherXunit.csproj
Package_Path: ./src/lib/GherXunit/**/*.
Expand Down
185 changes: 185 additions & 0 deletions QuickStart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
# Refactoring tests and extracting more readable scenarios
It's easy to mess up your test code when you're trying to write test scenarios. I know how it feels to have a test code
that is hard to read and maintain.**GherXunit** is a library that helps you write test scenarios in a structure familiar
to those already using xUnit.

In this section, you will learn how to define test scenarios and implement step methods using **GherXunit**.
For that, we will show you how to define test scenarios using Gherkin syntax and implement step methods using **GherXunit**.

## Example of Scenario Definition
The following test class `DataProcessingServiceTests` represents a common scenario where we have a complex test that needs to be more readable and maintainable.
In this case, the behavior is hidden into the test methods, becoming hard to understand and maintain.
In fact, the test methods are mixed with the setup of the mocks and the configuration of the test environment.

```csharp
The test class contains the method: `ProcessData_ShouldProcessSuccessfully`. Also, the test class contains the setup of
the mocks and the configuration of the test environment. The following code snippet shows the test class `DataProcessingServiceTests`:

```csharp
public class DataProcessingServiceTests
{
private readonly Mock<IApiClient> _apiClientMock;
private readonly Mock<IRepository> _repositoryMock;
private readonly Mock<IQueueService> _queueServiceMock;
private readonly Mock<ILogger<DataProcessingService>> _loggerMock;
private readonly DataProcessingService _dataProcessingService;

public DataProcessingServiceTests()
{
_apiClientMock = new Mock<IApiClient>();
_repositoryMock = new Mock<IRepository>();
_queueServiceMock = new Mock<IQueueService>();
_loggerMock = new Mock<ILogger<DataProcessingService>>();

_dataProcessingService = new DataProcessingService(
_apiClientMock.Object,
_repositoryMock.Object,
_queueServiceMock.Object,
_loggerMock.Object
);
}

[Fact]
public async Task ProcessData_ShouldProcessSuccessfully()
{
// Arrange
var data = new Data { Value = "Test" };

_apiClientMock.Setup(api => api.FetchDataAsync())
.ReturnsAsync(data);

_repositoryMock.Setup(repo => repo.SaveDataAsync(data))
.Returns(Task.CompletedTask);

_queueServiceMock.Setup(queue => queue.PublishMessage(data.Value))
.Returns(Task.CompletedTask);

// Act
await _dataProcessingService.ProcessData();

// Assert
_apiClientMock.Verify(api => api.FetchDataAsync(), Times.Once);
_repositoryMock.Verify(repo => repo.SaveDataAsync(data), Times.Once);
_queueServiceMock.Verify(queue => queue.PublishMessage(data.Value), Times.Once);

_loggerMock.Verify(
log => log.Log(
LogLevel.Information,
It.IsAny<EventId>(),
It.Is<It.IsAnyType>((v, t) => v.ToString().Contains("Processing completed")),
null,
It.IsAny<Func<It.IsAnyType, Exception, string>>()
), Times.Once);
}

...
}
```
We will refactor this test class using **GherXunit** to separate the test methods from the Feature definition.
The first step is to define the Feature and Scenarios using the Gherkin syntax.

### Removing Test Methods to Separate Classes
For that, we will use `partial` classes to separate the test methods from the Feature definition.
The `DataProcessingServiceTests` class will contain the test methods, and the `DataProcessingServiceTests.Features` class will contain the Feature definition.
In the `DataProcessingServiceTests` we will use the `IGherXunit` interface.
It's a marker interface that allows the `DataProcessingServiceTests` class to be recognized as a GherXunit test class.

> [!IMPORTANT]
> The Output property is used to write messages to the test output.

```csharp
// DataProcessingServiceTests.cs
public partial class DataProcessingServiceTests: IGherXunit
{
public ITestOutputHelper Output { get; }
...

public DataProcessingServiceTests(..., ITestOutputHelper output)
{
...
Output = output;
}
}

// DataProcessingServiceTests.Features.cs
public partial class DataProcessingServiceTests
{
}
```

### Writing the Feature Definition
Now, we will define the Feature and Scenarios using the Gherkin syntax. For that, we will use the `Feature` and `Scenario` attributes.

```csharp
// DataProcessingServiceTests.Features.cs
[Feature("Data Processing")]
public partial class DataProcessingServiceTests : IAsyncLifetime
{
[Background]
public async Task InitializeAsync() => await this.ExecuteAscync(
refer: Setup,
steps: "Given the processing service is configured");

[Scenario("Successful data processing")]
public async Task ProcessData_ShouldProcessSuccessfully_Scenario() => await this.ExecuteAscync(
refer: ProcessData_ShouldProcessSuccessfully,
steps: """
Given the external API returns valid data
And the repository saves the data successfully
And the queue service publishes the message successfully
When the processing service is executed
Then the processing is completed successfully
And a confirmation message is logged
""");
}

// DataProcessingServiceTests.cs
public partial class DataProcessingServiceTests : IGherXunit
{
...

private Task Setup()
{
//TODO: Instantiate the mocks and configure the setup here.
return Task.CompletedTask;
}

private async Task ProcessData_ShouldProcessSuccessfully()
{
...
}

...
}
```
In the file `DataProcessingServiceTests.Features.cs`, we define the Feature and Scenarios using the Gherkin syntax. Please note
the method `InitializeAsync` is used to set up the test environment before executing the scenarios. this method is decorated with the `Background` attribute and
calls the `Setup` method to configure the mocks.

The `ProcessData_ShouldProcessSuccessfully_Scenario` method represents the test scenario and calls the `ProcessData_ShouldProcessSuccessfully`
method to execute the steps. Notice that the `ProcessData_ShouldProcessSuccessfully` does not have the Fact attribute anymore.
It is now a private method that represents the steps of the test scenario. The result of running the test scenarios defined
in the `DataProcessingServiceTests` class would be similar to the following output:

```gherkindotnet
.
TEST RESULT: 🟢 SUCCESS
⤷ FEATURE Data Processing
⤷ BACKGROUND Successful data processing
| GIVEN ↘ the processing service is configured
.
TEST RESULT: 🟢 SUCCESS
⤷ FEATURE Data Processing
⤷ SCENARIO Successful data processing
| GIVEN ↘ the external API returns valid data
| AND ↘ the repository saves the data successfully
| AND ↘ the queue service publishes the message successfully
| WHEN ↘ the processing service is executed
| THEN ↘ the processing is completed successfully
| AND ↘ a confirmation message is logged
```
The steps shown before are the simplified version of the test class `DataProcessingServiceTests` using **GherXunit**.
It's the first step to make your test code more readable and maintainable. We could go further and refactor the test methods
to be more descriptive and easier to understand, like removing repetitive code and separating concerns.

We hope this guide helps you get started with **GherXunit**. If you have any questions or need further assistance, please let us know.
37 changes: 15 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,7 @@ The following code snippet shows a test scenario defined using Gherkin syntax in

```csharp
using GherXunit.Annotations;

namespace BddTests.Samples.Features;
...

[Feature("Subscribers see different articles based on their subscription level")]
public partial class SubscriptionTest
Expand Down Expand Up @@ -69,11 +68,6 @@ public partial class SubscriptionTest
#### 📌 Example of Step Implementation:
The following code snippet shows the implementation of the step methods for the test scenario defined in the `SubscriptionTest` class:
```csharp
using GherXunit.Annotations;
using Xunit.Abstractions;

namespace BddTests.Samples.Features;

public partial class SubscriptionTest(ITestOutputHelper output): IGherXunit
{
public ITestOutputHelper Output { get; } = output;
Expand All @@ -87,21 +81,20 @@ public partial class SubscriptionTest(ITestOutputHelper output): IGherXunit

#### 📌 Example of output highlighting the test results:
The result of running the test scenarios defined in the `SubscriptionTest` class would be similar to the following output:
```shell
11:11:49.683 |V| Run: 0821bae4-1a1a-447b-807e-2eb5042f1fe5 - Discovery result processing started
11:11:49.688 |V| Run: 0821bae4-1a1a-447b-807e-2eb5042f1fe5 - Discovery result processing finished: (+0 ~2 -0), interrupted: 0
11:11:49.879 |I| Process /usr/local/share/dotnet/x64/dotnet:21042 has exited with code (0)
11:11:49.879 |I| Output stream:

SCENARIO ⇲ [🟢]Free subscribers see only the free articles
GIVEN ⇲ Free Frieda has a free subscription
WHEN ⇲ Free Frieda logs in with her valid credentials
THEN ⇲ she sees a Free article

SCENARIO ⇲ [🔴]Subscriber with a paid subscription can access both free and paid articles
GIVEN ⇲ Paid Patty has a basic-level paid subscription
WHEN ⇲ Paid Patty logs in with her valid credentials
THEN ⇲ she sees a Free article and a Paid article
```gherkindotnet
TEST RESULT: 🟢 SUCCESS
⤷ FEATURE Subscribers see different articles based on their subscription level
⤷ SCENARIO Free subscribers see only the free articles
| GIVEN ↘ Free Frieda has a free subscription
| WHEN ↘ Free Frieda logs in with her valid credentials
| THEN ↘ she sees a Free article

TEST RESULT: 🟢 SUCCESS
⤷ FEATURE Subscribers see different articles based on their subscription level
⤷ SCENARIO Subscriber with a paid subscription can access both free and paid articles
| GIVEN ↘ Paid Patty has a basic-level paid subscription
| WHEN ↘ Paid Patty logs in with her valid credentials
| THEN ↘ she sees a Free article and a Paid article
```

### 🔎 Is GherXunit for You?
Expand Down
39 changes: 16 additions & 23 deletions README_PTBR.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,8 @@ O trecho de código a seguir mostra um cenário de teste definido usando a sinta

```csharp
using GherXunit.Annotations;

namespace BddTests.Samples.Features;

...

[Feature("Subscribers see different articles based on their subscription level")]
public partial class SubscriptionTest
{
Expand Down Expand Up @@ -70,15 +69,10 @@ public partial class SubscriptionTest
O trecho de código a seguir mostra a implementação dos métodos de passos para o cenário de teste definido na classe `SubscriptionTest`:

```csharp
using GherXunit.Annotations;
using Xunit.Abstractions;

namespace BddTests.Samples.Features;

public partial class SubscriptionTest(ITestOutputHelper output): IGherXunit
{
public ITestOutputHelper Output { get; } = output;
private void WhenPattyLogsSteps() { }
private void WhenPattyLogsSteps() => Assert.True(true);
private async Task WhenFriedaLogsSteps() => await Task.CompletedTask;
}
```
Expand All @@ -90,20 +84,19 @@ public partial class SubscriptionTest(ITestOutputHelper output): IGherXunit
O resultado da execução dos cenários de teste definidos na classe `SubscriptionTest` seria semelhante à saída a seguir:

```shell
11:11:49.683 |V| Run: 0821bae4-1a1a-447b-807e-2eb5042f1fe5 - Discovery result processing started
11:11:49.688 |V| Run: 0821bae4-1a1a-447b-807e-2eb5042f1fe5 - Discovery result processing finished: (+0 ~2 -0), interrupted: 0
11:11:49.879 |I| Process /usr/local/share/dotnet/x64/dotnet:21042 has exited with code (0)
11:11:49.879 |I| Output stream:

SCENARIO ⇲ [🟢]Free subscribers see only the free articles
GIVEN ⇲ Free Frieda has a free subscription
WHEN ⇲ Free Frieda logs in with her valid credentials
THEN ⇲ she sees a Free article

SCENARIO ⇲ [🔴]Subscriber with a paid subscription can access both free and paid articles
GIVEN ⇲ Paid Patty has a basic-level paid subscription
WHEN ⇲ Paid Patty logs in with her valid credentials
THEN ⇲ she sees a Free article and a Paid article
TEST RESULT: 🟢 SUCCESS
⤷ FEATURE Subscribers see different articles based on their subscription level
⤷ SCENARIO Free subscribers see only the free articles
| GIVEN ↘ Free Frieda has a free subscription
| WHEN ↘ Free Frieda logs in with her valid credentials
| THEN ↘ she sees a Free article

TEST RESULT: 🟢 SUCCESS
⤷ FEATURE Subscribers see different articles based on their subscription level
⤷ SCENARIO Subscriber with a paid subscription can access both free and paid articles
| GIVEN ↘ Paid Patty has a basic-level paid subscription
| WHEN ↘ Paid Patty logs in with her valid credentials
| THEN ↘ she sees a Free article and a Paid article
```

### 🔎 O GherXunit é para você?
Expand Down
4 changes: 2 additions & 2 deletions src/base/GherXunit.Core/Attributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
namespace GherXunit.Annotations;

// Description attributes
public sealed class FeatureAttribute(string description) : DescriptionAttribute($"Feature: {description}");
public sealed class RuleAttribute(string description) : DescriptionAttribute($"Rule: {description}");
public sealed class FeatureAttribute(string description) : DescriptionAttribute(description);
public sealed class RuleAttribute(string description) : DescriptionAttribute(description);
public sealed class BackgroundAttribute() : DescriptionAttribute("Background");

// Xunit attributes
Expand Down
13 changes: 13 additions & 0 deletions src/base/GherXunit.Core/Backgrounds/BackgroundTest.Context.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace BddTests.Samples.Backgrounds;

public class BackgroundContext
{
public string ContextId { get; } = Guid.NewGuid().ToString();
public Dictionary<string, string> OwnersAndBlogs { get; } = new();

public BackgroundContext()
{
OwnersAndBlogs.Add("Greg", "Greg's anti-tax rants");
OwnersAndBlogs.Add("Dr. Bill", "Expensive Therapy");
}
}
49 changes: 49 additions & 0 deletions src/base/GherXunit.Core/Backgrounds/BackgroundTest.Steps.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
using System.Runtime.CompilerServices;
using GherXunit.Annotations;
using Xunit.Abstractions;

namespace BddTests.Samples.Backgrounds;

public partial class BackgroundTest(ITestOutputHelper output) : IGherXunit, IAsyncLifetime
{
public ITestOutputHelper Output { get; } = output;
private BackgroundContext? Context { get; set; }

private Task Setup()
{
Context = new BackgroundContext();
return Task.CompletedTask;
}

private async Task DrBillPostToBlogStep()
{
var result = await PostToBlog("Dr. Bill", "Expensive Therapy");
Assert.Equal("Your article was published.", result);
}

private async Task DrBillPostToBlogFailStep()
{
var result = await PostToBlog("Dr. Bill", "Greg's anti-tax rants");
Assert.Equal("Hey! That's not your blog!", result);
}

private async Task GregPostToBlogStep()
{
var result = await PostToBlog("Greg", "Greg's anti-tax rants");
Assert.Equal("Your article was published.", result);
}

private async Task<string> PostToBlog(string owner, string blog)
{
if (!Context!.OwnersAndBlogs.TryGetValue(owner, out string? value) || value != blog)
return "Hey! That's not your blog!";

await Task.Yield();
return "Your article was published.";
}

public Task DisposeAsync()
{
return Task.CompletedTask;
}
}
Loading