diff --git a/src/Samples/TestStack.BDDfy.Samples/TestStack.BDDfy.Samples.csproj b/src/Samples/TestStack.BDDfy.Samples/TestStack.BDDfy.Samples.csproj index 803424c9..eb227dc8 100644 --- a/src/Samples/TestStack.BDDfy.Samples/TestStack.BDDfy.Samples.csproj +++ b/src/Samples/TestStack.BDDfy.Samples/TestStack.BDDfy.Samples.csproj @@ -14,7 +14,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + all diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.StepNameWillBeBuiltFromStepTitle.approved.txt b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.StepNameWillBeBuiltFromStepTitle.approved.txt new file mode 100644 index 00000000..4d533684 --- /dev/null +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.StepNameWillBeBuiltFromStepTitle.approved.txt @@ -0,0 +1,6 @@ + +### Step name will be built from step title + i have a red car with a crazy horn + When i honk the horn at volume high + Then i verify diagnostics a red car honked crazy horn at high volume + diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.StepNamesAreBuiltFromAllChanedMethods.approved.txt b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.StepNamesAreBuiltFromAllChanedMethods.approved.txt new file mode 100644 index 00000000..26b83725 --- /dev/null +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.StepNamesAreBuiltFromAllChanedMethods.approved.txt @@ -0,0 +1,6 @@ + +### Step names are built from all chaned methods + Given i have a car with color red with horn crazy + When i honk the horn + Then i verify diagnostics a red car honked crazy horn + diff --git a/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.cs b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.cs new file mode 100644 index 00000000..4410e187 --- /dev/null +++ b/src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepsWithChainedMethods.cs @@ -0,0 +1,57 @@ +using Shouldly; +using TestStack.BDDfy.Reporters; +using TestStack.BDDfy.Reporters.MarkDown; +using TestStack.BDDfy.Tests.Reporters; +using Xunit; + +namespace TestStack.BDDfy.Tests.Scanner.FluentScanner +{ + public class StepsWithChainedMethods + { + private SutStepBuilder _sutStepBuilder = new(); + private class SutStepBuilder + { + public string state; + private string color; + private string horn; + + public SutStepBuilder with_color(string color) { this.color = color; return this; } + public SutStepBuilder with_horn(string horn) { this.horn = horn; return this; } + internal void at_volume(string volume) { state+= $" at {volume} volume"; } + internal SutStepBuilder have_a_car() { state = "a"; return this; } + internal SutStepBuilder honk_the_horn() { state+= $" {color} car honked {horn} horn"; return this; } + + internal void verify_diagnostics(string v) => state.ShouldBe(v); + } + + [Fact] + public void StepNamesAreBuiltFromAllChanedMethods() + { + var story = this.Given(_ => i().have_a_car().with_color("red").with_horn("crazy")) + .When(_ => i().honk_the_horn()) + .Then(_ => i().verify_diagnostics("a red car honked crazy horn")) + .LazyBDDfy(); + + var result = story.Run(); + var model = new[] {result}.ToReportModel(); + var fileReport = new FileReportModel(model); + ReportApprover.Approve(fileReport, new MarkDownReportBuilder()); + } + + [Fact] + public void StepNameWillBeBuiltFromStepTitle() + { + var story = this.Given(_ => i().have_a_car().with_color("red").with_horn("crazy"), "i have a {0} car with a {1} horn") + .When(_ => i().honk_the_horn().at_volume("high")) + .Then(_ => i().verify_diagnostics("a red car honked crazy horn at high volume")) + .LazyBDDfy(); + + var result = story.Run(); + var model = new[] { result }.ToReportModel(); + var fileReport = new FileReportModel(model); + ReportApprover.Approve(fileReport, new MarkDownReportBuilder()); + } + + private SutStepBuilder i() => _sutStepBuilder; + } +} diff --git a/src/TestStack.BDDfy.Tests/Startup.cs b/src/TestStack.BDDfy.Tests/Startup.cs new file mode 100644 index 00000000..e4c67152 --- /dev/null +++ b/src/TestStack.BDDfy.Tests/Startup.cs @@ -0,0 +1,16 @@ +using System.Runtime.CompilerServices; +using TestStack.BDDfy.Configuration; +using TestStack.BDDfy.Tests; + +namespace TestStack.BDDfy.Samples +{ + public class Startup + { + [ModuleInitializer] + public static void Initialize() + { + Configurator.Processors.Add(() => new XUnitOutputReporter()); + Configurator.Processors.ConsoleReport.Enable(); + } + } +} \ No newline at end of file diff --git a/src/TestStack.BDDfy.Tests/TestStack.BDDfy.Tests.csproj b/src/TestStack.BDDfy.Tests/TestStack.BDDfy.Tests.csproj index f9fd8232..e4bb5a53 100644 --- a/src/TestStack.BDDfy.Tests/TestStack.BDDfy.Tests.csproj +++ b/src/TestStack.BDDfy.Tests/TestStack.BDDfy.Tests.csproj @@ -13,7 +13,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs b/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs index 3aacce02..374ffde8 100644 --- a/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs +++ b/src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs @@ -10,7 +10,7 @@ internal class DefaultStepTitleFactory : IStepTitleFactory public bool IncludeInputsInStepTitle { get; set; } = true; public StepTitle Create( - string stepTextTemplate, + string? stepTextTemplate, bool? includeInputsInStepTitle, MethodInfo methodInfo, StepArgument[] inputArguments, @@ -19,7 +19,7 @@ public StepTitle Create( { string createTitle() { - var flatInputArray = inputArguments.Select(o => o.Value).FlattenArrays(); + var flatInputArray = inputArguments.Select(o => o.Value!).FlattenArrays(); var name = methodInfo.Name; var titleAttribute = methodInfo.GetCustomAttribute(true); var executableAttribute = methodInfo.GetCustomAttribute(true); diff --git a/src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs b/src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs index 14c16bd5..3a264cbe 100644 --- a/src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs +++ b/src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs @@ -7,7 +7,7 @@ public interface IStepTitleFactory bool IncludeInputsInStepTitle { get; set; } public StepTitle Create( - string stepTextTemplate, + string? stepTextTemplate, bool? includeInputsInStepTitle, MethodInfo methodInfo, StepArgument[] inputArguments, diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/ExpressionExtensions.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/ExpressionExtensions.cs index c5eabc5e..c65331c8 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/ExpressionExtensions.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/ExpressionExtensions.cs @@ -21,10 +21,10 @@ public static IEnumerable ExtractArguments(this Expression _arguments; - private object _value; + private List _arguments = []; + private object? _value; - public IEnumerable ExtractArguments(LambdaExpression methodCallExpression, object value) + public IEnumerable ExtractArguments(LambdaExpression methodCallExpression, object? value) { _arguments = []; _value = value; @@ -34,6 +34,8 @@ public IEnumerable ExtractArguments(LambdaExpression methodCallExp protected override Expression VisitMethodCall(MethodCallExpression node) { + if (node.Object is MethodCallExpression methodCallExpression) Visit(methodCallExpression); + var arguments = node.Arguments.Select(ExtractStepArgument); _arguments.AddRange(arguments); return node; @@ -75,32 +77,32 @@ private StepArgument ExtractStepArgument(Expression a) } } - private Func GetValue(Expression a) + private Func GetValue(Expression expression) { // If the expression is a member access on the lambda parameter (e.g. _ => _.Prop) // replace the parameter with the supplied _value so the compiled delegate can be invoked - if (a is MemberExpression memberExpression && memberExpression.Expression is ParameterExpression) + if (expression is MemberExpression memberExpression && memberExpression.Expression is ParameterExpression) { var replaced = Expression.Convert(Expression.MakeMemberAccess(Expression.Constant(_value), memberExpression.Member), typeof(object)); return Expression.Lambda>(replaced).Compile(); } - return Expression.Lambda>(Expression.Convert(a, typeof(object))).Compile(); + return Expression.Lambda>(Expression.Convert(expression, typeof(object))).Compile(); } - private Action SetValue(Expression a, Type parameterType) + private Action SetValue(Expression expression, Type parameterType) { var parameter = Expression.Parameter(typeof(object)); var unaryExpression = Expression.Convert(parameter, parameterType); - if (a is MemberExpression memberExpression && memberExpression.Expression is ParameterExpression) + if (expression is MemberExpression memberExpression && memberExpression.Expression is ParameterExpression) { var memberAccess = Expression.MakeMemberAccess(Expression.Constant(_value), memberExpression.Member); var assign = Expression.Assign(memberAccess, unaryExpression); return Expression.Lambda>(assign, parameter).Compile(); } - var assignDefault = Expression.Assign(a, unaryExpression); + var assignDefault = Expression.Assign(expression, unaryExpression); return Expression.Lambda>(assignDefault, parameter).Compile(); } } diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentApi.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentApi.cs index c5e3f0fb..dbd3fbcc 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentApi.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentApi.cs @@ -404,7 +404,7 @@ public FluentStepBuilder(TScenario testObject) public IFluentStepBuilder Given(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.SetupState, false, "Given"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.SetupState, false, "Given"); return this; } @@ -422,7 +422,7 @@ public IFluentStepBuilder Given(Expression> step) public IFluentStepBuilder Given(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.SetupState, false, "Given"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.SetupState, false, "Given"); return this; } @@ -463,7 +463,7 @@ public IFluentStepBuilder Given(string title) } public IFluentStepBuilder When(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.Transition, false, "When"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.Transition, false, "When"); return this; } @@ -481,7 +481,7 @@ public IFluentStepBuilder When(Expression> step) public IFluentStepBuilder When(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.Transition, false, "When"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.Transition, false, "When"); return this; } @@ -522,7 +522,7 @@ public IFluentStepBuilder When(string title) } public IFluentStepBuilder Then(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.Assertion, true, "Then"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.Assertion, true, "Then"); return this; } @@ -540,7 +540,7 @@ public IFluentStepBuilder Then(Expression> step) public IFluentStepBuilder Then(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.Assertion, true, "Then"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.Assertion, true, "Then"); return this; } @@ -581,7 +581,7 @@ public IFluentStepBuilder Then(string title) } public IFluentStepBuilder And(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.ConsecutiveStep, false, "And"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.ConsecutiveStep, false, "And"); return this; } @@ -599,7 +599,7 @@ public IFluentStepBuilder And(Expression> step) public IFluentStepBuilder And(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.ConsecutiveStep, false, "And"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.ConsecutiveStep, false, "And"); return this; } @@ -640,13 +640,13 @@ public IFluentStepBuilder And(string title) } public IFluentStepBuilder But(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.ConsecutiveStep, false, "But"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.ConsecutiveStep, false, "But"); return this; } public IFluentStepBuilder But(Expression> step, bool includeInputsInStepTitle) { - scanner.AddStep(step, null, includeInputsInStepTitle, true, ExecutionOrder.ConsecutiveStep, false, "But"); + scanner.AddStep(step, null, includeInputsInStepTitle, false, ExecutionOrder.ConsecutiveStep, false, "But"); return this; } @@ -658,7 +658,7 @@ public IFluentStepBuilder But(Expression> step) public IFluentStepBuilder But(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, true, ExecutionOrder.ConsecutiveStep, false, "But"); + scanner.AddStep(step, stepTextTemplate, false, true, ExecutionOrder.ConsecutiveStep, false, "But"); return this; } @@ -699,7 +699,7 @@ public IFluentStepBuilder But(string title) } public IFluentStepBuilder TearDownWith(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, false, ExecutionOrder.TearDown, false, ""); + scanner.AddStep(step, stepTextTemplate, false, false, ExecutionOrder.TearDown, false, ""); return this; } @@ -717,7 +717,7 @@ public IFluentStepBuilder TearDownWith(Expression> public IFluentStepBuilder TearDownWith(Expression> step, string stepTextTemplate) { - scanner.AddStep(step, stepTextTemplate, null, false, ExecutionOrder.TearDown, false, ""); + scanner.AddStep(step, stepTextTemplate, false, false, ExecutionOrder.TearDown, false, ""); return this; } diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentScanner.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentScanner.cs index 15fd6c27..e18159c2 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentScanner.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/FluentScanner.cs @@ -21,7 +21,9 @@ internal FluentScanner(TScenario testObject) { _testObject = testObject; _testContext = TestContext.GetContext(_testObject); - _fakeExecuteActionMethod = typeof(FluentScanner).GetMethod(nameof(ExecuteAction), BindingFlags.Instance | BindingFlags.NonPublic); + _fakeExecuteActionMethod = typeof(FluentScanner) + .GetMethod(nameof(ExecuteAction), BindingFlags.Instance | BindingFlags.NonPublic) + ?? throw new InvalidOperationException("Failed to retrieve method info for ExecuteAction."); } IScanner IFluentScanner.GetScanner(string scenarioTitle, Type explicitStoryType) @@ -63,7 +65,7 @@ private void ExecuteAction(ExampleAction _) public void AddStep( Expression> stepAction, - string stepTextTemplate, + string? stepTextTemplate, bool? includeInputsInStepTitle, bool reports, ExecutionOrder executionOrder, @@ -71,16 +73,22 @@ public void AddStep( string stepPrefix) { var action = stepAction.Compile(); - var inputArguments = stepAction.ExtractArguments(_testObject).ToArray(); - var title = CreateTitle( - stepTextTemplate, - includeInputsInStepTitle, - GetMethodInfo(stepAction), - inputArguments, - stepPrefix); + StepTitle title; + List args; + + if (string.IsNullOrWhiteSpace(stepTextTemplate) && IsChainedMethodCall(stepAction.Body)) + { + title = FluentScanner.BuildChainedTitle(stepAction, stepPrefix); + args = []; + } + else + { + var inputArguments = stepAction.ExtractArguments(_testObject).ToArray(); + title = CreateTitle(stepTextTemplate, includeInputsInStepTitle, GetMethodInfo(stepAction), inputArguments, stepPrefix); + args = [.. inputArguments.Where(s => !string.IsNullOrWhiteSpace(s.Name))]; + } - var args = inputArguments.Where(s => !string.IsNullOrWhiteSpace(s.Name)).ToList(); var stepDelegate = StepActionFactory.GetStepAction(action); var shouldFixAsserts = FixAsserts(asserts, executionOrder); var shouldFixConsecutiveStep = FixConsecutiveStep(executionOrder); @@ -90,7 +98,7 @@ public void AddStep( public void AddStep( Expression> stepAction, - string stepTextTemplate, + string? stepTextTemplate, bool? includeInputsInStepTitle, bool reports, ExecutionOrder executionOrder, @@ -102,21 +110,37 @@ public void AddStep( AddStep(action, stepAction, stepTextTemplate, includeInputsInStepTitle, reports, executionOrder, asserts, stepPrefix); } - private void AddStep(Action action, LambdaExpression stepAction, string stepTextTemplate, bool? includeInputsInStepTitle, - bool reports, ExecutionOrder executionOrder, bool asserts, string stepPrefix) + private void AddStep( + Action action, + LambdaExpression stepAction, + string? stepTextTemplate, + bool? includeInputsInStepTitle, + bool reports, + ExecutionOrder executionOrder, + bool asserts, + string stepPrefix) { - var inputArguments = stepAction.ExtractArguments(_testObject).ToArray(); + StepTitle title; + List args; - var title = CreateTitle(stepTextTemplate, includeInputsInStepTitle, GetMethodInfo(stepAction), inputArguments, stepPrefix); - - var args = inputArguments.Where(s => !string.IsNullOrEmpty(s.Name)).ToList(); + if (string.IsNullOrWhiteSpace(stepTextTemplate) && IsChainedMethodCall(stepAction.Body)) + { + title = FluentScanner.BuildChainedTitle(stepAction, stepPrefix); + args = []; + } + else + { + var inputArguments = stepAction.ExtractArguments(_testObject).ToArray(); + title = CreateTitle(stepTextTemplate, includeInputsInStepTitle, GetMethodInfo(stepAction), inputArguments, stepPrefix); + args = [.. inputArguments.Where(s => !string.IsNullOrEmpty(s.Name))]; + } _steps.Add(new Step(StepActionFactory.GetStepAction(action), title, FixAsserts(asserts, executionOrder), FixConsecutiveStep(executionOrder), reports, args)); } private StepTitle CreateTitle( - string stepTextTemplate, + string? stepTextTemplate, bool? includeInputsInStepTitle, MethodInfo methodInfo, StepArgument[] inputArguments, @@ -170,10 +194,57 @@ private ExecutionOrder FixConsecutiveStep(ExecutionOrder executionOrder) return executionOrder; } - private static MethodInfo GetMethodInfo(LambdaExpression stepAction) + private static MethodInfo GetMethodInfo(LambdaExpression stepAction) => ((MethodCallExpression)stepAction.Body).Method; + + private static bool IsChainedMethodCall(Expression body) => body is MethodCallExpression { Object: MethodCallExpression }; + + private static StepTitle BuildChainedTitle(LambdaExpression stepAction, string stepPrefix) + { + var chain = new List<(MethodInfo Method, StepArgument[] Args)>(); + var node = (MethodCallExpression)stepAction.Body; + + while (node is not null) + { + var args = FluentScanner.ExtractArgumentsFromCall(node); + chain.Add((node.Method, args)); + node = node.Object as MethodCallExpression; + } + + chain.Reverse(); + + return new StepTitle(() => + { + var parts = chain.Select(entry => + { + var humanized = Configurator.Humanizer.Humanize(entry.Method.Name); + if (entry.Args.Length > 0) + { + var argValues = entry.Args.Select(a => a.Value?.FlattenArray()).ToArray(); + humanized = humanized + " " + string.Join(", ", argValues); + } + return humanized; + }); + + var title = string.Join(" ", parts).Trim(); + if (!string.IsNullOrEmpty(stepPrefix) && !title.StartsWith(stepPrefix, StringComparison.CurrentCultureIgnoreCase)) + { + title = $"{stepPrefix} {title[..1].ToLower()}{title[1..]}"; + } + return title; + }); + } + + private static StepArgument[] ExtractArgumentsFromCall(MethodCallExpression node) { - var methodCall = (MethodCallExpression)stepAction.Body; - return methodCall.Method; + var parameters = node.Method.GetParameters(); + var args = new StepArgument[node.Arguments.Count]; + for (int i = 0; i < node.Arguments.Count; i++) + { + var arg = node.Arguments[i]; + var value = Expression.Lambda>(Expression.Convert(arg, typeof(object))).Compile(); + args[i] = new StepArgument(parameters[i].Name, parameters[i].ParameterType, value, null); + } + return args; } } } diff --git a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/StepArgument.cs b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/StepArgument.cs index 2f09b6c8..f5ebf703 100644 --- a/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/StepArgument.cs +++ b/src/TestStack.BDDfy/Scanners/StepScanners/Fluent/StepArgument.cs @@ -7,12 +7,13 @@ public class StepArgument private readonly Action _set = o => { }; private readonly Func _get; - public StepArgument(string name, Type argumentType, Func getValue, Action setValue) + public StepArgument(string? name, Type argumentType, Func getValue, Action? setValue) { Name = name; _get = getValue; - if (setValue != null) - _set = setValue; + + if (setValue != null) _set = setValue; + ArgumentType = argumentType; } @@ -22,9 +23,9 @@ public StepArgument(Func value) ArgumentType = typeof(object); } - public string Name { get; private set; } + public string? Name { get; private set; } - public object Value + public object? Value { get {