Skip to content

Commit

Permalink
#23: BDD: Added ability to parse and convert Scenario Outline to [XUn…
Browse files Browse the repository at this point in the history
…it.Theory] test method.
  • Loading branch information
MarcinCelej committed Jan 18, 2024
1 parent 961cf95 commit d2125fd
Show file tree
Hide file tree
Showing 10 changed files with 238 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Text;
using System.Text.RegularExpressions;
using Synergy.Behaviours.Testing.Gherkin;
using Feature = Synergy.Behaviours.Testing.Gherkin.Feature;

Expand All @@ -22,7 +23,7 @@ public XUnitFeatureGenerator(
public StringBuilder Generate(
Feature feature,
object featureClass
)
)
{
StringBuilder code = new StringBuilder();
code.AppendLine("// <auto-generated />");
Expand Down Expand Up @@ -59,7 +60,7 @@ object featureClass

if (this.currentScenario)
code.AppendLine(" partial void CurrentScenario(params string[] scenario);");

code.AppendLine("}");

return code;
Expand Down Expand Up @@ -103,11 +104,31 @@ private void GenerateBackgroundMethod(StringBuilder code, string backgroundMetho

private void Generate(StringBuilder code, Scenario scenario, string? backgroundMethod)
{
// if (scenario is ScenarioOutline)
// throw new NotImplementedException("Scenario Outline is not supported yet.");

this.GenerateTraits(code, scenario.Tags, " ");
string scenarioOriginalTitle = scenario.Line.Text.Trim();
code.AppendLine($" [Xunit.Fact(DisplayName = \"{scenarioOriginalTitle.Replace("\"", "\\\"")}\")]");
string methodName = Sentence.ToMethod(scenario.Title);
code.AppendLine($" public void {methodName}() // {scenarioOriginalTitle}");
var arguments = "";
string displayName = scenarioOriginalTitle.Replace("\"", "\\\"");
if (scenario is ScenarioOutline)
{
// TODO: Marcin Celej [from: Marcin Celej on: 18-01-2024]: convert the arguments to parameters by making the name camelCase
Examples examples = ((ScenarioOutline) scenario).Examples;
arguments = "string " + string.Join(", string ", examples.Header);
code.AppendLine($" [Xunit.Theory(DisplayName = \"{displayName}\")]");

foreach (var row in examples.Rows)
{
code.AppendLine($" [Xunit.InlineData(\"{string.Join("\", \"", row)}\")]");
}
}
else
{
code.AppendLine($" [Xunit.Fact(DisplayName = \"{displayName}\")]");
}
code.AppendLine($" public void {methodName}({arguments}) // {scenarioOriginalTitle}");
code.AppendLine(" {");

if (this.currentScenario)
Expand Down Expand Up @@ -138,15 +159,28 @@ private void Generate(StringBuilder code, Scenario scenario, string? backgroundM

private void GenerateSteps(StringBuilder code, List<Step> steps)
{
var max = steps.Max(step => GetStepType(step).Length + Sentence.ToMethod(step.Text).Length) + 2;
var argumentsRegex = new Regex("<(.*?)>");

var max = steps.Max(step => MethodCall(step).Length) + 2;
foreach (var step in steps)
{
string stepType = GetStepType(step);
string method = Sentence.ToMethod(step.Text);
var spaces = new string(' ', max - stepType.Length - method.Length);
code.AppendLine($" {stepType}().{method}();{spaces}// {step.Line.Text.Trim()}");
// TODO: Marcin Celej [from: Marcin Celej on: 18-01-2024]: Consider Introducing inherited Step wit arguments (in outline only)

string methodCall = MethodCall(step);
var spaces = new string(' ', max - methodCall.Length);
code.AppendLine($" {methodCall};{spaces}// {step.Line.Text.Trim()}");
}

string MethodCall(Step theStep)
{
string stepType = GetStepType(theStep);
string stepText = theStep.Text;
string methodName = Sentence.ToMethod(argumentsRegex.Replace(stepText, ""));
// TODO: Marcin Celej [from: Marcin Celej on: 18-01-2024]: convert the arguments to parameters by making the name camelCase
var arguments = argumentsRegex.Matches(stepText).Select(match => match.Groups[1].Value).ToArray();
return $"{stepType}().{methodName}({string.Join(", ", arguments)})";
}

string GetStepType(Step step)
{
var type = step.Type;
Expand Down
8 changes: 6 additions & 2 deletions Behaviours/Synergy.Behaviours.Testing/Gherkin/Examples.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,9 @@ namespace Synergy.Behaviours.Testing.Gherkin;

public record Examples(
List<string> Header,
List<List<string>> Rows
);
List<List<string>> Rows,
Line Line
)
{
public const string Keyword = "Examples";
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,15 @@ public static Feature Parse(string[] lines)
continue;
}

// TODO: Marcin Celej [from: Marcin Celej on: 10-05-2023]: Support Scenario Outline/Template along with Examples
if (token.Type.In(ScenarioOutline.Keywords))
{
var scenario = GherkinParser.ParseScenarioOutline(token, tags, stack, currentRule);
feature.Scenarios.Add(scenario);
tags = new List<string>();
continue;
}

throw new Exception("Unsupported token at line " + token.Line.Number + ": " + token.Line.Text.Trim());
throw new Exception($"Unsupported token at line {token.Line.Number}: {token.Line.Text.Trim()}");
}

return feature;
Expand All @@ -81,6 +87,8 @@ private static Feature ParseFeature(Stack<GherkinToken> stack)
feature = feature with { Title = token.Value, Line = token.Line };
break;
}

// TODO: Marcin Celej [from: Marcin Celej on: 18-01-2024]: Parse description lines after Feature: line
}

return feature;
Expand Down Expand Up @@ -139,7 +147,7 @@ private static Scenario ParseScenario(GherkinToken token, List<string> tags, Sta

private static Scenario ParseScenarioOutline(GherkinToken token, List<string> tags, Stack<GherkinToken> stack, Rule? rule)
{
var scenario = new Scenario(token.Value, tags, new List<Step>(), rule, token.Line);
var scenario = new ScenarioOutline(token.Value, tags, new List<Step>(), rule, null!, token.Line);
while (stack.Any())
{
var stepToken = stack.Pop();
Expand All @@ -153,10 +161,52 @@ private static Scenario ParseScenarioOutline(GherkinToken token, List<string> ta
continue;
}

if (stepToken.Type == Examples.Keyword)
{
var examples = ParseExamples(stepToken, stack);
scenario = scenario with { Examples = examples };
continue;
}

stack.Push(stepToken);
break;
}

if (scenario.Examples == null)
throw new Exception($"Scenario Outline must have Examples section. Line: {token.Line.Number}: {token.Line.Text.Trim()}");

return scenario;
}

private static Examples ParseExamples(GherkinToken token, Stack<GherkinToken> stack)
{
// TODO: Marcin Celej [from: Marcin Celej on: 18-01-2024]: Introduce class for header and row
var headerToken = stack.Pop();
var header = ParseRow(headerToken);
var examples = new Examples(header, new List<List<string>>(), token.Line);

while (stack.Any())
{
var stepToken = stack.Pop();
if (stepToken.Type == Comment.Keyword)
continue;

if (stepToken.Type == "|")
{
var row = ParseRow(stepToken);
examples.Rows.Add(row);
continue;
}

stack.Push(stepToken);
break;
}

return examples;

List<string> ParseRow(GherkinToken theToken)
{
return theToken.Value.Trim('|').Split('|').Select(x => x.Trim()).ToList();
}
}
}
2 changes: 1 addition & 1 deletion Behaviours/Synergy.Behaviours.Testing/Gherkin/Scenario.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public bool IsTagged(string tag, params string[] tags)
private bool IsTagged(string tag)
=> this.Tags.Any(t => t.TrimStart('@').Equals(tag.TrimStart('@'), StringComparison.InvariantCultureIgnoreCase));

public string[] Lines
public virtual string[] Lines
{
get
{
Expand Down
30 changes: 30 additions & 0 deletions Behaviours/Synergy.Behaviours.Testing/Gherkin/ScenarioOutline.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
namespace Synergy.Behaviours.Testing.Gherkin;

public record ScenarioOutline(
string Title,
List<string> Tags,
List<Step> Steps,
Rule? Rule,
Examples Examples,
Line Line
) : Scenario(
Title,
Tags,
Steps,
Rule,
Line
)
{
public new static string[] Keywords { get; } = { "Scenario Outline", "Scenario Template" };

/// <inheritdoc />
public override string[] Lines
{
get
{
var lines = new List<string>(base.Lines);
// TODO: Marcin Celej [from: Marcin Celej on: 18-01-2024]: add here Examples section when there is deicated class for header and row
return lines.ToArray();
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
# Technical Debt for Synergy.Contracts

Total: 1
Total: 6

## [XUnitFeatureGenerator.cs](../../../Synergy.Behaviours.Testing/Generator/XUnitFeatureGenerator.cs)
- TODO: Marcin Celej [from: Marcin Celej on: 18-01-2024]: convert the arguments to parameters by making the name camelCase
- TODO: Marcin Celej [from: Marcin Celej on: 18-01-2024]: Consider Introducing inherited Step wit arguments (in outline only)
- TODO: Marcin Celej [from: Marcin Celej on: 18-01-2024]: convert the arguments to parameters by making the name camelCase

## [GherkinParser.cs](../../../Synergy.Behaviours.Testing/Gherkin/Parser/GherkinParser.cs)
- TODO: Marcin Celej [from: Marcin Celej on: 10-05-2023]: Support Scenario Outline/Template along with Examples
- TODO: Marcin Celej [from: Marcin Celej on: 18-01-2024]: Parse description lines after Feature: line
- TODO: Marcin Celej [from: Marcin Celej on: 18-01-2024]: Introduce class for header and row

## [ScenarioOutline.cs](../../../Synergy.Behaviours.Testing/Gherkin/ScenarioOutline.cs)
- TODO: Marcin Celej [from: Marcin Celej on: 18-01-2024]: add here Examples section when there is deicated class for header and row
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,34 @@
## Gherkin.Background (record) : IEquatable<Background>
- Line: Line { get; set; }
- Steps: List<Step> { get; set; }
- Background.Keyword: string (field)
- ctor(
Steps: List<Step>,
Line: Line
)

## Gherkin.Comment (record) : IEquatable<Comment>
- Comment.Keyword: string (field)
- ctor()

## Gherkin.Examples (record) : IEquatable<Examples>
- Header: List<string> { get; set; }
- Line: Line { get; set; }
- Rows: List<List<string>> { get; set; }
- Examples.Keyword: string (field)
- ctor(
Header: List<string>,
Rows: List<List<string>>,
Line: Line
)

## Gherkin.Feature (record) : IEquatable<Feature>
- Background: Background? [Nullable] { get; set; }
- Line: Line { get; set; }
- Scenarios: List<Scenario> { get; set; }
- Tags: List<string> { get; set; }
- Title: string { get; set; }
- Feature.Keyword: string (field)
- ctor(
Title: string,
Tags: List<string>,
Expand All @@ -60,13 +77,15 @@
- Background: Background? [Nullable] { get; set; }
- Line: Line { get; set; }
- Title: string { get; set; }
- Rule.Keyword: string (field)
- ctor(
Title: string,
Background: Background? [Nullable],
Line: Line
)

## Gherkin.Scenario (record) : IEquatable<Scenario>
- Scenario.Keywords: String[] { get; }
- Line: Line { get; set; }
- Lines: String[] { get; }
- Rule: Rule? [Nullable] { get; set; }
Expand All @@ -85,7 +104,30 @@
tags: params String[] [ParamArray]
) : bool

## Gherkin.ScenarioOutline (record) : Scenario, IEquatable<Scenario>, IEquatable<ScenarioOutline>
- Examples: Examples { get; set; }
- ScenarioOutline.Keywords: String[] { get; }
- Line: Line { get; set; }
- Lines: String[] { get; }
- Rule: Rule? [Nullable] { get; set; }
- Steps: List<Step> { get; set; }
- Tags: List<string> { get; set; }
- Title: string { get; set; }
- ctor(
Title: string,
Tags: List<string>,
Steps: List<Step>,
Rule: Rule? [Nullable],
Examples: Examples,
Line: Line
)
- IsTagged(
tag: string,
tags: params String[] [ParamArray]
) : bool

## Gherkin.Step (record) : IEquatable<Step>
- Step.Keywords: String[] { get; }
- Line: Line { get; set; }
- Text: string { get; set; }
- Type: string { get; set; }
Expand All @@ -95,5 +137,9 @@
Line: Line
)

## Gherkin.Tag (record) : IEquatable<Tag>
- Tag.Keyword: string (field)
- ctor()

## IFeature (interface)

Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,27 @@ public void AddTwoNumbersInDifferentWay() // Example: Add two numbers in "differ
Then().TheResultShouldBe120(); // Then the result should be 120
}

[Xunit.Trait("Category", "Add")] // @Add
[Xunit.Theory(DisplayName = "Scenario Outline: Add many numbers")]
[Xunit.InlineData("0", "70", "70")]
[Xunit.InlineData("50", "70", "120")]
public void AddManyNumbers(string first, string second, string result) // Scenario Outline: Add many numbers
{
CurrentScenario(
" Scenario Outline: Add many numbers",
" Given the first number is <first>",
" And the second number is <second>",
" When the two numbers are added",
" Then the result should be <result>"
);

Background().CalculatorBackground();

Given().TheFirstNumberIs(first); // Given the first number is <first>
And().TheSecondNumberIs(second); // And the second number is <second>
When().TheTwoNumbersAreAdded(); // When the two numbers are added
Then().TheResultShouldBe(result); // Then the result should be <result>
}

partial void CurrentScenario(params string[] scenario);
}
15 changes: 15 additions & 0 deletions Behaviours/Synergy.Behaviours.Tests/Samples/Calculator.Steps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,4 +65,19 @@ partial void CurrentScenario(params string[] scenario)
{
_scenario = scenario;
}

private void TheFirstNumberIs(string first)
{
this._calculator.FirstNumber = Convert.ToInt32(first);
}

private void TheSecondNumberIs(string second)
{
this._calculator.SecondNumber = Convert.ToInt32(second);
}

private void TheResultShouldBe(string result)
{
Assert.Equal(Convert.ToInt32(result), this._result);
}
}
Loading

0 comments on commit d2125fd

Please sign in to comment.