From 0b1d0abe5be4131e2866a5e4850328b5b9015b01 Mon Sep 17 00:00:00 2001 From: Marcin Celej Date: Mon, 27 Feb 2023 20:31:42 +0100 Subject: [PATCH] #23: Added first version of BDD framework --- .../Synergy.Behaviours.Testing/Feature.cs | 13 ++ .../FeatureGenerator.cs | 118 ++++++++++++++++++ .../Synergy.Behaviours.Testing/IFeature.cs | 5 + .../Synergy.Behaviours.Testing.csproj | 23 ++++ .../Synergy.Behaviours.Testing/synergy.png | Bin 0 -> 2624 bytes .../Calculator.Behaviours.cs | 49 ++++++++ .../Calculator.Feature.cs | 45 +++++++ .../Synergy.Behaviours.Tests/Calculator.cs | 35 ++++++ .../Calculator.feature | 41 ++++++ .../Synergy.Behaviours.Tests.csproj | 26 ++++ Synergy.Framework.sln | 16 +++ 11 files changed, 371 insertions(+) create mode 100644 Behaviours/Synergy.Behaviours.Testing/Feature.cs create mode 100644 Behaviours/Synergy.Behaviours.Testing/FeatureGenerator.cs create mode 100644 Behaviours/Synergy.Behaviours.Testing/IFeature.cs create mode 100644 Behaviours/Synergy.Behaviours.Testing/Synergy.Behaviours.Testing.csproj create mode 100644 Behaviours/Synergy.Behaviours.Testing/synergy.png create mode 100644 Behaviours/Synergy.Behaviours.Tests/Calculator.Behaviours.cs create mode 100644 Behaviours/Synergy.Behaviours.Tests/Calculator.Feature.cs create mode 100644 Behaviours/Synergy.Behaviours.Tests/Calculator.cs create mode 100644 Behaviours/Synergy.Behaviours.Tests/Calculator.feature create mode 100644 Behaviours/Synergy.Behaviours.Tests/Synergy.Behaviours.Tests.csproj diff --git a/Behaviours/Synergy.Behaviours.Testing/Feature.cs b/Behaviours/Synergy.Behaviours.Testing/Feature.cs new file mode 100644 index 0000000..3de18df --- /dev/null +++ b/Behaviours/Synergy.Behaviours.Testing/Feature.cs @@ -0,0 +1,13 @@ +namespace Synergy.Behaviours.Testing; + +public class Feature : IFeature + where TFeature : new() +{ + public static TFeature Given() => new(); + public TFeature When() => Self; + public TFeature Then() => Self; + public TFeature And() => Self; + public TFeature But() => Self; + public TFeature Moreover() => Self; + protected TFeature Self => (TFeature)(object)this; +} \ No newline at end of file diff --git a/Behaviours/Synergy.Behaviours.Testing/FeatureGenerator.cs b/Behaviours/Synergy.Behaviours.Testing/FeatureGenerator.cs new file mode 100644 index 0000000..2b7ee4a --- /dev/null +++ b/Behaviours/Synergy.Behaviours.Testing/FeatureGenerator.cs @@ -0,0 +1,118 @@ +using System.Runtime.CompilerServices; +using System.Text; +using System.Text.RegularExpressions; + +namespace Synergy.Behaviours.Testing; + +public static class FeatureGenerator +{ + public static void Generate( + this TBehaviour feature, + string from, + string to, + bool withMoreover = false, + [CallerFilePath] string callerFilePath = "" + ) + where TBehaviour : IFeature + { + StringBuilder code = new StringBuilder(); + string className = feature.GetType().Name; + var gherkinPath = Path.Combine(Path.GetDirectoryName(callerFilePath), from); + var gherkins = File.ReadAllLines(gherkinPath); + + code.AppendLine("using System.CodeDom.Compiler;"); + code.AppendLine(); + code.AppendLine($"namespace {feature.GetType().Namespace};"); + code.AppendLine(); + code.AppendLine( + $"[GeneratedCode(\"{typeof(FeatureGenerator).Assembly.FullName}\", \"{typeof(FeatureGenerator).Assembly.GetName().Version.ToString()}\")]"); + code.AppendLine($"public partial class {className}"); + code.AppendLine("{"); + // code.AppendLine(" [Fact]"); + // code.AppendLine(" public void Generate()"); + // code.AppendLine($" => Behaviours<{featureClass}>.Generate({nameof(from)}: \"{from}\", this);"); + + string? scenarioMethod = null; + foreach (var line in gherkins) + { + if (line.Contains("#")) + { + //scenarioMethod = Moreover(); + code.AppendLine(line.Replace("#", "//")); + continue; + } + + if (string.IsNullOrEmpty(line.Trim())) + { + scenarioMethod = CloseScenario(); + code.AppendLine(); + continue; + } + + var scenario = Regex.Match(line, "\\s*Scenario\\: (.*)"); + if (scenario.Success) + { + scenarioMethod = FeatureGenerator.ToMethod(scenario.Groups[1] + .Value); + code.AppendLine(" [Xunit.Fact]"); + code.AppendLine($" public void {scenarioMethod}() // {line.Trim()}"); + code.AppendLine($" => "); + } + + var given = Regex.Match(line, "\\s*Given (.*)"); + if (given.Success) + code.AppendLine($" Given().{FeatureGenerator.ToMethod(given.Groups[1].Value)}() // {line.Trim()}"); + + var and = Regex.Match(line, "\\s*And (.*)"); + if (and.Success) + code.AppendLine($" .And().{FeatureGenerator.ToMethod(and.Groups[1].Value)}() // {line.Trim()}"); + + var but = Regex.Match(line, "\\s*But (.*)"); + if (but.Success) + code.AppendLine($" .But().{FeatureGenerator.ToMethod(but.Groups[1].Value)}() // {line.Trim()}"); + + var when = Regex.Match(line, "\\s*When (.*)"); + if (when.Success) + code.AppendLine($" .When().{FeatureGenerator.ToMethod(when.Groups[1].Value)}() // {line.Trim()}"); + + var then = Regex.Match(line, "\\s*Then (.*)"); + if (then.Success) + code.AppendLine($" .Then().{FeatureGenerator.ToMethod(then.Groups[1].Value)}() // {line.Trim()}"); + } + + CloseScenario(); + + // code.AppendLine(); + // code.AppendLine($" partial class {featureClass}"); + // code.AppendLine(" {"); + // code.AppendLine(" }"); + code.AppendLine("}"); + + var destinationFilePath = Path.Combine(Path.GetDirectoryName(callerFilePath), to); + File.WriteAllText(destinationFilePath, code.ToString()); + + string? CloseScenario() + { + if (scenarioMethod != null) + { + if (withMoreover) + code.AppendLine($" .Moreover().Verify{scenarioMethod}();"); + else + code.AppendLine($" ;"); + } + + scenarioMethod = null; + return scenarioMethod; + } + } + + private static string ToMethod(string sentence) + { + var parts = sentence.Split(" "); + var m = string.Concat(parts.Where(p => !string.IsNullOrEmpty(p)) + .Select(p => p.Substring(0, 1) + .ToUpperInvariant() + p.Substring(1))); + m = Regex.Replace(m, "[^A-Za-z0-9_]", ""); + return m; + } +} \ No newline at end of file diff --git a/Behaviours/Synergy.Behaviours.Testing/IFeature.cs b/Behaviours/Synergy.Behaviours.Testing/IFeature.cs new file mode 100644 index 0000000..f27eb7e --- /dev/null +++ b/Behaviours/Synergy.Behaviours.Testing/IFeature.cs @@ -0,0 +1,5 @@ +namespace Synergy.Behaviours.Testing; + +public interface IFeature +{ +} \ No newline at end of file diff --git a/Behaviours/Synergy.Behaviours.Testing/Synergy.Behaviours.Testing.csproj b/Behaviours/Synergy.Behaviours.Testing/Synergy.Behaviours.Testing.csproj new file mode 100644 index 0000000..589ade0 --- /dev/null +++ b/Behaviours/Synergy.Behaviours.Testing/Synergy.Behaviours.Testing.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + enable + Synergy Behaviour Driven Development + Synergy Marcin Celej + Behaviour Driven Development support + Copyright © Synergy Marcin Celej 2023 + https://github.com/synergy-software/synergy.framework + synergy.png + BDD Behaviour Driven Development + + + + + true + + + + + diff --git a/Behaviours/Synergy.Behaviours.Testing/synergy.png b/Behaviours/Synergy.Behaviours.Testing/synergy.png new file mode 100644 index 0000000000000000000000000000000000000000..76e1da4259d359fc08d5352dabea2e5d91b78a30 GIT binary patch literal 2624 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 +{ + [Fact] + public void GenerateFeature() + => this.Generate( + from: "Calculator.feature", + to: "Calculator.Feature.cs" + ); + + private int _firstNumber; + + private CalculatorFeature TheFirstNumberIs50() + { + this._firstNumber = 50; + return this; + } + + private int _secondNumber; + private int _result; + + private CalculatorFeature TheSecondNumberIs70() + { + this._secondNumber = 70; + return this; + } + + private CalculatorFeature TheTwoNumbersAreAdded() + { + var calculator = new Calculator { FirstNumber = this._firstNumber, SecondNumber = this._secondNumber }; + _result = calculator.Add(); + return this; + } + + private CalculatorFeature TheResultShouldBe120() + { + Assert.Equal(120, this._result); + return this; + } + + private CalculatorFeature VerifyAddTwoNumbers() + { + return this; + } +} \ No newline at end of file diff --git a/Behaviours/Synergy.Behaviours.Tests/Calculator.Feature.cs b/Behaviours/Synergy.Behaviours.Tests/Calculator.Feature.cs new file mode 100644 index 0000000..30fd8d5 --- /dev/null +++ b/Behaviours/Synergy.Behaviours.Tests/Calculator.Feature.cs @@ -0,0 +1,45 @@ +using System.CodeDom.Compiler; + +namespace Synergy.Behaviours.Tests; + +[GeneratedCode("Synergy.Behaviours.Testing, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null", "1.0.0.0")] +public partial class CalculatorFeature +{ + + + [Xunit.Fact] + 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 + ; + +// @Subtract +// Scenario: Subtract two numbers +// Given the first number is 50 +// And the second number is 25 +// When the two numbers are subtracted +// Then the result should be 25 +// +// @Divide +// Scenario: Divide two numbers +// Given the first number is 100 +// And the second number is 2 +// When the two numbers are divided +// Then the result should be 50 +// +// @Divide +// Scenario: Divide by 0 returns 0 +// Given the first number is 0 +// And the second number is 70 +// When the two numbers are divided +// Then the result should be 0 +// +// @Multiply +// Scenario: Multiply two numbers +// Given the first number is 5 +// And the second number is 50 +// When the two numbers are multiplied +// Then the result should be 250 +} diff --git a/Behaviours/Synergy.Behaviours.Tests/Calculator.cs b/Behaviours/Synergy.Behaviours.Tests/Calculator.cs new file mode 100644 index 0000000..a86b439 --- /dev/null +++ b/Behaviours/Synergy.Behaviours.Tests/Calculator.cs @@ -0,0 +1,35 @@ +namespace Synergy.Behaviours.Tests; + +public class Calculator +{ + public int FirstNumber { get; set; } + + public int SecondNumber { get; set; } + + public int Add() + { + return FirstNumber + SecondNumber; + } + + public int Subtract() + { + return FirstNumber - SecondNumber; + } + + public int Divide() + { + if (FirstNumber == 0 || SecondNumber == 0) + { + return 0; + } + else + { + return FirstNumber / SecondNumber; + } + } + + public int Multiply() + { + return FirstNumber * SecondNumber; + } +} \ No newline at end of file diff --git a/Behaviours/Synergy.Behaviours.Tests/Calculator.feature b/Behaviours/Synergy.Behaviours.Tests/Calculator.feature new file mode 100644 index 0000000..442cf7e --- /dev/null +++ b/Behaviours/Synergy.Behaviours.Tests/Calculator.feature @@ -0,0 +1,41 @@ +Feature: Calculator + ![Calculator](https://specflow.org/wp-content/uploads/2020/09/calculator.png) + Simple calculator for performing calculations on **two** numbers + + 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)** + + @Add + Scenario: Add two numbers + Given the first number is 50 + And the second number is 70 + When the two numbers are added + Then the result should be 120 + +# @Subtract +# Scenario: Subtract two numbers +# Given the first number is 50 +# And the second number is 25 +# When the two numbers are subtracted +# Then the result should be 25 +# +# @Divide +# Scenario: Divide two numbers +# Given the first number is 100 +# And the second number is 2 +# When the two numbers are divided +# Then the result should be 50 +# +# @Divide +# Scenario: Divide by 0 returns 0 +# Given the first number is 0 +# And the second number is 70 +# When the two numbers are divided +# Then the result should be 0 +# +# @Multiply +# Scenario: Multiply two numbers +# Given the first number is 5 +# And the second number is 50 +# When the two numbers are multiplied +# Then the result should be 250 \ No newline at end of file diff --git a/Behaviours/Synergy.Behaviours.Tests/Synergy.Behaviours.Tests.csproj b/Behaviours/Synergy.Behaviours.Tests/Synergy.Behaviours.Tests.csproj new file mode 100644 index 0000000..ca4df66 --- /dev/null +++ b/Behaviours/Synergy.Behaviours.Tests/Synergy.Behaviours.Tests.csproj @@ -0,0 +1,26 @@ + + + + net6.0 + enable + enable + enable + Library + + + + + + + + + + + + + + Calculator.Feature.cs + + + + diff --git a/Synergy.Framework.sln b/Synergy.Framework.sln index e51ef8a..cd52474 100644 --- a/Synergy.Framework.sln +++ b/Synergy.Framework.sln @@ -69,6 +69,12 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Synergy.Markdowns.Test", "M EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Synergy.Documentation", "Contracts\Synergy.Documentation\Synergy.Documentation.csproj", "{C2D8A794-86EC-48F5-A7ED-B7B6746757DA}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Behaviours", "Behaviours", "{06C9B7C0-23DF-4716-B632-DFB7DE7447A4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Synergy.Behaviours.Testing", "Behaviours\Synergy.Behaviours.Testing\Synergy.Behaviours.Testing.csproj", "{266B20E2-5F87-48F3-8DF6-40FCCB584070}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Synergy.Behaviours.Tests", "Behaviours\Synergy.Behaviours.Tests\Synergy.Behaviours.Tests.csproj", "{705DC578-EE0C-4BF3-8C7E-21EF4FCB85FF}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -175,6 +181,14 @@ Global {C2D8A794-86EC-48F5-A7ED-B7B6746757DA}.Debug|Any CPU.Build.0 = Debug|Any CPU {C2D8A794-86EC-48F5-A7ED-B7B6746757DA}.Release|Any CPU.ActiveCfg = Release|Any CPU {C2D8A794-86EC-48F5-A7ED-B7B6746757DA}.Release|Any CPU.Build.0 = Release|Any CPU + {266B20E2-5F87-48F3-8DF6-40FCCB584070}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {266B20E2-5F87-48F3-8DF6-40FCCB584070}.Debug|Any CPU.Build.0 = Debug|Any CPU + {266B20E2-5F87-48F3-8DF6-40FCCB584070}.Release|Any CPU.ActiveCfg = Release|Any CPU + {266B20E2-5F87-48F3-8DF6-40FCCB584070}.Release|Any CPU.Build.0 = Release|Any CPU + {705DC578-EE0C-4BF3-8C7E-21EF4FCB85FF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {705DC578-EE0C-4BF3-8C7E-21EF4FCB85FF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {705DC578-EE0C-4BF3-8C7E-21EF4FCB85FF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {705DC578-EE0C-4BF3-8C7E-21EF4FCB85FF}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -205,6 +219,8 @@ Global {CDA7013B-5E75-4A1F-8549-F8B9057118B3} = {A72ACF23-1F2D-425F-A349-A39DA75C373D} {2464CDA4-AA2E-4932-A17A-7283647689B6} = {A72ACF23-1F2D-425F-A349-A39DA75C373D} {C2D8A794-86EC-48F5-A7ED-B7B6746757DA} = {146E5D84-D923-4731-BAB4-8D46E30A1433} + {266B20E2-5F87-48F3-8DF6-40FCCB584070} = {06C9B7C0-23DF-4716-B632-DFB7DE7447A4} + {705DC578-EE0C-4BF3-8C7E-21EF4FCB85FF} = {06C9B7C0-23DF-4716-B632-DFB7DE7447A4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {2DD5F49D-3F67-4D88-A29C-EFDBE7B76098}