From 26e58aa3afeb836ffe2297a49997f2946bd9baf4 Mon Sep 17 00:00:00 2001 From: Marcin Celej Date: Sun, 30 Apr 2023 17:18:00 +0200 Subject: [PATCH] #23: Added support of Gherkin Background in BDD framework --- .../Synergy.Behaviours.Testing/Feature.cs | 1 + .../FeatureGenerator.cs | 136 +++++++++++++----- .../Samples/Calculator.Feature.verified.cs | 32 +++-- .../Samples/Calculator.Steps.cs | 9 +- .../Samples/Calculator.feature | 5 +- .../Synergy.Behaviours.Tests.csproj | 9 ++ .../Todos/Todos.Technical.Debt.verified.md | 4 +- 7 files changed, 143 insertions(+), 53 deletions(-) diff --git a/Behaviours/Synergy.Behaviours.Testing/Feature.cs b/Behaviours/Synergy.Behaviours.Testing/Feature.cs index 18dbd1e..e36ba51 100644 --- a/Behaviours/Synergy.Behaviours.Testing/Feature.cs +++ b/Behaviours/Synergy.Behaviours.Testing/Feature.cs @@ -2,6 +2,7 @@ public abstract class Feature : IFeature { + public virtual TFeature Background() => Self; public virtual TFeature Given() => Self; public virtual TFeature When() => Self; public virtual TFeature Then() => Self; diff --git a/Behaviours/Synergy.Behaviours.Testing/FeatureGenerator.cs b/Behaviours/Synergy.Behaviours.Testing/FeatureGenerator.cs index acafe09..87ab342 100644 --- a/Behaviours/Synergy.Behaviours.Testing/FeatureGenerator.cs +++ b/Behaviours/Synergy.Behaviours.Testing/FeatureGenerator.cs @@ -22,7 +22,7 @@ public static void Generate( } public static string Generate( - this TBehaviour feature, + this TBehaviour featureClass, string from, bool withMoreover = false, string[]? include = null, @@ -31,14 +31,14 @@ public static string Generate( ) { StringBuilder code = new StringBuilder(); - string className = feature.GetType().Name; + string className = featureClass.GetType().Name; var gherkinPath = Path.Combine(Path.GetDirectoryName(callerFilePath), from); var gherkins = File.ReadAllLines(gherkinPath); code.AppendLine("// "); code.AppendLine("using System.CodeDom.Compiler;"); code.AppendLine(); - code.AppendLine($"namespace {feature.GetType().Namespace};"); + code.AppendLine($"namespace {featureClass.GetType().Namespace};"); code.AppendLine(); code.AppendLine($"[GeneratedCode(\"{typeof(FeatureGenerator).Assembly.FullName}\", \"{typeof(FeatureGenerator).Assembly.GetName().Version.ToString()}\")]"); code.AppendLine($"public partial class {className}"); @@ -53,6 +53,11 @@ public static string Generate( bool includeScenario = ResetInclude(); List? tags = null; int lineNo = 0; + string? backgroundMethod = null; + string? backgroundStarted = null; + string? featureName = null; + string? ruleName = null; + foreach (var line in gherkins) { lineNo++; @@ -60,7 +65,6 @@ public static string Generate( { tags = null; includeScenario = ResetInclude(); - //scenarioMethod = Moreover(); code.AppendLine(line.Replace("#", "//")); continue; } @@ -69,15 +73,43 @@ public static string Generate( { tags = null; includeScenario = ResetInclude(); - scenarioMethod = CloseScenario(); + + continue; + } + + var feature = Regex.Match(line, "\\s*Feature\\: (.*)"); + if (feature.Success) + { + featureName = feature.Groups[1] + .Value; + continue; + } + + var rule = Regex.Match(line, "\\s*Rule\\: (.*)"); + if (rule.Success) + { + CloseBackground(); + CloseScenario(); + + ruleName = rule.Groups[1] + .Value; + + code.AppendLine($" // {line.Trim()}"); code.AppendLine(); + continue; } - var background = Regex.Match(line, "\\s*Background\\: (.*)"); + var background = Regex.Match(line, "\\s*Background\\:"); if (background.Success) { - throw new NotSupportedException($"Background keyword is not supported\nLine {lineNo}: {line.Trim()}"); + CloseScenario(); + + backgroundMethod = FeatureGenerator.ToMethod(ruleName ?? featureName ?? "Feature") + "Background"; + backgroundStarted = backgroundMethod; + code.AppendLine($" private {className} {backgroundMethod}() // {line.Trim()}"); + code.Append($" => "); + continue; } var outline = Regex.Match(line, "\\s*Scenario (Outline|Template)\\: (.*)"); @@ -92,17 +124,10 @@ public static string Generate( throw new NotSupportedException($"Examples keyword is not supported\nLine {lineNo}: {line.Trim()}"); } - // if (line.TrimStart().StartsWithAny("scenario", "rule", "background", "example", "@") && - // scenarioMethod != null - // ) - // { - // tags = null; - // includeScenario = ResetInclude(); - // scenarioMethod = CloseScenario(); - // } - if (line.Trim().StartsWith("@")) { + CloseScenario(); + tags = Regex.Matches(line, "\\@\\w+") .Select(m=>m.Value).ToList(); @@ -116,63 +141,94 @@ public static string Generate( // if (include) // code.AppendLine($" [Xunit.Trait({string.Join(", ", tags.Select(t => "\"" + t.TrimStart('@') + "\""))})]"); + continue; } - if (includeScenario == false) + if (includeScenario == false && backgroundStarted == null) continue; - // TODO: add Rule handling - - // TODO: Marcin Celej [from: Marcin Celej on: 08-04-2023]: Add 'Background' handling - generate method with background steps and call it in each following scenario - var scenario = Regex.Match(line, "\\s*Scenario\\: (.*)"); if (scenario.Success) { + CloseBackground(); + CloseScenario(); + scenarioMethod = FeatureGenerator.ToMethod(scenario.Groups[1] .Value); code.AppendLine(" [Xunit.Fact]"); if (tags != null) code.AppendLine($" // {String.Join(" ", tags)}"); code.AppendLine($" public void {scenarioMethod}() // {line.Trim()}"); - code.Append($" => "); + code.Append($" => {nameof(Feature.Background)}()"); + if (backgroundMethod == null) + code.AppendLine(); + else + { + code.AppendLine($".{backgroundMethod}()"); + } + continue; } var given = Regex.Match(line, "\\s*Given (.*)"); if (given.Success) - code.AppendLine($" Given().{FeatureGenerator.ToMethod(given.Groups[1].Value)}() // {line.Trim()}"); + { + if (backgroundStarted == null) + { + code.Append($" ."); + } + else + { + code.Append($" "); + } + + code.AppendLine($"{nameof(Feature.Given)}().{FeatureGenerator.ToMethod(given.Groups[1].Value)}() // {line.Trim()}"); + continue; + } var and = Regex.Match(line, "\\s*And (.*)"); if (and.Success) - code.AppendLine($" .And().{FeatureGenerator.ToMethod(and.Groups[1].Value)}() // {line.Trim()}"); + { + code.AppendLine($" .{nameof(Feature.And)}().{FeatureGenerator.ToMethod(and.Groups[1].Value)}() // {line.Trim()}"); + continue; + } var asterisk = Regex.Match(line, "\\s*\\* (.*)"); if (asterisk.Success) - code.AppendLine($" .And().{FeatureGenerator.ToMethod(asterisk.Groups[1].Value)}() // {line.Trim()}"); + { + code.AppendLine($" .{nameof(Feature.And)}().{FeatureGenerator.ToMethod(asterisk.Groups[1].Value)}() // {line.Trim()}"); + continue; + } var but = Regex.Match(line, "\\s*But (.*)"); if (but.Success) - code.AppendLine($" .But().{FeatureGenerator.ToMethod(but.Groups[1].Value)}() // {line.Trim()}"); + { + code.AppendLine($" .{nameof(Feature.But)}().{FeatureGenerator.ToMethod(but.Groups[1].Value)}() // {line.Trim()}"); + continue; + } var when = Regex.Match(line, "\\s*When (.*)"); if (when.Success) - code.AppendLine($" .When().{FeatureGenerator.ToMethod(when.Groups[1].Value)}() // {line.Trim()}"); + { + code.AppendLine($" .{nameof(Feature.When)}().{FeatureGenerator.ToMethod(when.Groups[1].Value)}() // {line.Trim()}"); + continue; + } var then = Regex.Match(line, "\\s*Then (.*)"); if (then.Success) - code.AppendLine($" .Then().{FeatureGenerator.ToMethod(then.Groups[1].Value)}() // {line.Trim()}"); + { + code.AppendLine($" .{nameof(Feature.Then)}().{FeatureGenerator.ToMethod(then.Groups[1].Value)}() // {line.Trim()}"); + continue; + } } + CloseBackground(); CloseScenario(); - // code.AppendLine(); - // code.AppendLine($" partial class {featureClass}"); - // code.AppendLine(" {"); - // code.AppendLine(" }"); code.AppendLine("}"); return code.ToString(); - string? CloseScenario() + void CloseScenario() { if (scenarioMethod != null) { @@ -180,10 +236,22 @@ public static string Generate( code.AppendLine($" .Moreover().Verify{scenarioMethod}();"); else code.AppendLine($" ;"); + + code.AppendLine(); } scenarioMethod = null; - return scenarioMethod; + } + + void CloseBackground() + { + if (backgroundStarted != null) + { + code.AppendLine($" ;"); + code.AppendLine(); + } + + backgroundStarted = null; } bool ResetInclude() diff --git a/Behaviours/Synergy.Behaviours.Tests/Samples/Calculator.Feature.verified.cs b/Behaviours/Synergy.Behaviours.Tests/Samples/Calculator.Feature.verified.cs index cd0146f..2beb0ce 100644 --- a/Behaviours/Synergy.Behaviours.Tests/Samples/Calculator.Feature.verified.cs +++ b/Behaviours/Synergy.Behaviours.Tests/Samples/Calculator.Feature.verified.cs @@ -6,33 +6,37 @@ namespace Synergy.Behaviours.Tests.Samples; [GeneratedCode("Synergy.Behaviours.Testing, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "1.0.0.0")] public partial class CalculatorFeature { + private CalculatorFeature CalculatorBackground() // Background: + => Given().UserOpenedCalculator() // Given User opened calculator + ; - - + // Rule: Adding numbers [Xunit.Fact] // @Add public void AddTwoNumbers() // Scenario: Add two numbers - => Given().TheFirstNumberIs50() // Given the first number is 50 - .And().TheSecondNumberIs70() // And the second number is 70 - .When().TheTwoNumbersAreAdded() // When the two numbers are added - .Then().TheResultShouldBe120() // Then the result should be 120 + => Background().CalculatorBackground() + .Given().TheFirstNumberIs50() // Given the first number is 50 + .And().TheSecondNumberIs70() // And the second number is 70 + .When().TheTwoNumbersAreAdded() // When the two numbers are added + .Then().TheResultShouldBe120() // Then the result should be 120 ; [Xunit.Fact] // @Add public void AddTwoNumbersInDifferentWay() // Scenario: Add two numbers in different way - => Given().TwoNumbers() // Given Two numbers: - .And().TheFirstNumberIs50() // * the first number is 50 - .And().TheSecondNumberIs70() // * the second number is 70 - .When().TheTwoNumbersAreAdded() // When the two numbers are added - .Then().TheResultShouldBe120() // Then the result should be 120 + => Background().CalculatorBackground() + .Given().TwoNumbers() // Given Two numbers: + .And().TheFirstNumberIs50() // * the first number is 50 + .And().TheSecondNumberIs70() // * the second number is 70 + .When().TheTwoNumbersAreAdded() // When the two numbers are added + .Then().TheResultShouldBe120() // Then the result should be 120 ; + // Rule: Subtracting numbers + // Rule: Dividing numbers - - - + // Rule: Multiplying numbers } diff --git a/Behaviours/Synergy.Behaviours.Tests/Samples/Calculator.Steps.cs b/Behaviours/Synergy.Behaviours.Tests/Samples/Calculator.Steps.cs index cd571e3..0707108 100644 --- a/Behaviours/Synergy.Behaviours.Tests/Samples/Calculator.Steps.cs +++ b/Behaviours/Synergy.Behaviours.Tests/Samples/Calculator.Steps.cs @@ -14,9 +14,16 @@ public async Task GenerateFeature() //exclude: new[] { "@Divide" } ); - await Verifier.Verify(code, "cs").UseFileName("Calculator.Feature"); + await Verifier + .Verify(code, "cs") + .UseFileName("Calculator.Feature"); } + private CalculatorFeature UserOpenedCalculator() + { + return this; + } + private int _firstNumber; private CalculatorFeature TheFirstNumberIs50() diff --git a/Behaviours/Synergy.Behaviours.Tests/Samples/Calculator.feature b/Behaviours/Synergy.Behaviours.Tests/Samples/Calculator.feature index 8f6bd70..af3eb65 100644 --- a/Behaviours/Synergy.Behaviours.Tests/Samples/Calculator.feature +++ b/Behaviours/Synergy.Behaviours.Tests/Samples/Calculator.feature @@ -7,8 +7,11 @@ Feature: Calculator Link to a feature: [Calculator](SpecFlowCalculator.Specs/Features/Calculator.feature) ***Further read***: **[Learn more about how to generate Living Documentation](https://docs.specflow.org/projects/specflow-livingdoc/en/latest/LivingDocGenerator/Generating-Documentation.html)** + Background: + Given User opened calculator + Rule: Adding numbers - + @Add Scenario: Add two numbers Given the first number is 50 diff --git a/Behaviours/Synergy.Behaviours.Tests/Synergy.Behaviours.Tests.csproj b/Behaviours/Synergy.Behaviours.Tests/Synergy.Behaviours.Tests.csproj index 4888faa..da4fc80 100644 --- a/Behaviours/Synergy.Behaviours.Tests/Synergy.Behaviours.Tests.csproj +++ b/Behaviours/Synergy.Behaviours.Tests/Synergy.Behaviours.Tests.csproj @@ -30,4 +30,13 @@ + + + Calculator.feature + + + Calculator.feature + + + diff --git a/Behaviours/Synergy.Behaviours.Tests/Todos/Todos.Technical.Debt.verified.md b/Behaviours/Synergy.Behaviours.Tests/Todos/Todos.Technical.Debt.verified.md index 93008de..5196541 100644 --- a/Behaviours/Synergy.Behaviours.Tests/Todos/Todos.Technical.Debt.verified.md +++ b/Behaviours/Synergy.Behaviours.Tests/Todos/Todos.Technical.Debt.verified.md @@ -1,8 +1,6 @@ # Technical Debt for Synergy.Contracts -Total: 3 +Total: 1 ## [FeatureGenerator.cs](../../Synergy.Behaviours.Testing/FeatureGenerator.cs) - TODO: Support tags above the feature -- TODO: add Rule handling -- TODO: Marcin Celej [from: Marcin Celej on: 08-04-2023]: Add 'Background' handling - generate method with background steps and call it in each following scenario