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
86 changes: 71 additions & 15 deletions src/TestStack.BDDfy.Tests/Scanner/FluentScanner/StepTitleTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Linq;
using Shouldly;
using TestStack.BDDfy.Configuration;
using TestStack.BDDfy.Tests.Concurrency;
using Xunit;

Expand All @@ -8,7 +9,45 @@ namespace TestStack.BDDfy.Tests.Scanner.FluentScanner
[Collection(TestCollectionName.ModifiesConfigurator)]
public class StepTitleTests
{
private string _mutatedState;
private string _state;

[Fact]
public void UseConfiguration_IncludeInputsInStepTitle()
{
try
{
Configurator.StepTitleFactory.IncludeInputsInStepTitle = false;
FooClass something = new();
var story = this
.Given(_=>something.Sub.GivenWithStepTitleAndArgument(1))
.When(_ => something.Sub.ActionWithArgument("foo"))
.And(_ => something.Sub.ActionWithArgumentsDisabledInTitle("foo"))
.And(_ => something.Sub.ActionWithTemplateTitleAndArguments("foo"))
.And(_ => something.Sub.ActionWithArgumentsEnabledInTitle("foo"))
.BDDfy();

var actualTitles = story.Scenarios.Single().Steps.Select(s => s.Title).ToArray();
var expectedTitles = new[]
{
"Given step title with 1 args",
"When with arg",
"And with arg",
"And with foo arg",
"And with arg foo"
};

actualTitles.ShouldBeEquivalentTo(expectedTitles); ;
}
catch
{
throw;
}
finally
{
Configurator.StepTitleFactory.IncludeInputsInStepTitle = true;
}

}

[Fact]
public void MethodCallInStepTitle()
Expand All @@ -19,16 +58,24 @@ public void MethodCallInStepTitle()
.When(_ => something.Sub.SomethingHappens())
.And(_ => something.Sub.SomethingWithDifferentTitle())
.Then(_ => ThenTitleHas(AMethodCall()))
.And(_ => something.Sub.SomethingWithArg("foo"))
.And(_ => something.Sub.SomethingWithArg2("foo"))
.And(_ => something.Sub.SomethingWithArg3("foo"))
.And(_ => something.Sub.ActionWithArgument("foo"))
.And(_ => something.Sub.ActionWithArgumentsDisabledInTitle("foo"))
.And(_ => something.Sub.ActionWithTemplateTitleAndArguments("foo"))
.BDDfy();

story.Scenarios.Single().Steps.ElementAt(2).Title.ShouldBe("And different title");
story.Scenarios.Single().Steps.ElementAt(3).Title.ShouldBe("Then title has Mutated state");
story.Scenarios.Single().Steps.ElementAt(4).Title.ShouldBe("And with arg foo");
story.Scenarios.Single().Steps.ElementAt(5).Title.ShouldBe("And with arg");
story.Scenarios.Single().Steps.ElementAt(6).Title.ShouldBe("And with foo arg");
var actualTitles = story.Scenarios.Single().Steps.Select(s => s.Title).ToArray();
var expectedTitles = new[]
{
"Given we mutate some state",
"When something happens",
"And different title",
"Then title has Mutated state",
"And with arg foo",
"And with arg",
"And with foo arg"
};

actualTitles.ShouldBeEquivalentTo(expectedTitles);
}

public class FooClass
Expand All @@ -54,34 +101,43 @@ public void SomethingWithDifferentTitle()
}

[StepTitle("With arg")]
public void SomethingWithArg(string arg)
public void ActionWithArgument(string arg)
{
}

[StepTitle("With arg", false)]
public void SomethingWithArg2(string arg)
public void ActionWithArgumentsDisabledInTitle(string arg)
{
}

[StepTitle("With arg", true)]
public void ActionWithArgumentsEnabledInTitle(string arg)
{
}

[StepTitle("With {0} arg", false)]
public void SomethingWithArg3(string arg)
public void ActionWithTemplateTitleAndArguments(string arg)
{
}

[Given("step title with {0} args")]
public void GivenWithStepTitleAndArgument(int count)
{ }
}

private string AMethodCall()
{
return _mutatedState;
return _state;
}

private void GivenWeMutateSomeState()
{
_mutatedState = "Mutated state";
_state = "Mutated state";
}

private void ThenTitleHas(string result)
{
result.ShouldBe(_mutatedState);
result.ShouldBe(_state);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,17 @@ public class UsingCustomStepTitleFactory
{
private class CustomStepTitleFactory : IStepTitleFactory
{
public bool IncludeInputsInStepTitle { get; set; } = true;

public StepTitle Create(
string stepTextTemplate,
bool includeInputsInStepTitle,
bool? includeInputsInStepTitle,
MethodInfo methodInfo,
StepArgument[] inputArguments,
ITestContext testContext,
string stepPrefix) => new StepTitle("Custom Step Title");
string stepPrefix) => new("Custom Step Title");

public StepTitle Create(string title, string stepPrefix, ITestContext testContext) => new StepTitle(title);
public StepTitle Create(string title, string stepPrefix, ITestContext testContext) => new(title);
}

[Fact]
Expand Down
30 changes: 18 additions & 12 deletions src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,36 @@ namespace TestStack.BDDfy.Abstractions;

internal class DefaultStepTitleFactory : IStepTitleFactory
{
public bool IncludeInputsInStepTitle { get; set; } = true;

public StepTitle Create(
string stepTextTemplate,
bool includeInputsInStepTitle,
bool? includeInputsInStepTitle,
MethodInfo methodInfo,
StepArgument[] inputArguments,
ITestContext testContext,
string stepPrefix)
{
Func<string> createTitle = () =>
string createTitle()
{
var flatInputArray = inputArguments.Select(o => o.Value).FlattenArrays();
var name = methodInfo.Name;
var stepTitleAttribute = methodInfo.GetCustomAttributes(typeof(StepTitleAttribute), true).SingleOrDefault();
if (stepTitleAttribute != null)
var titleAttribute = methodInfo.GetCustomAttribute<StepTitleAttribute>(true);
var executableAttribute = methodInfo.GetCustomAttribute<ExecutableAttribute>(true);

includeInputsInStepTitle ??= titleAttribute?.IncludeInputsInStepTitle ?? IncludeInputsInStepTitle;

var titleTemplate = titleAttribute?.StepTitle ?? executableAttribute?.StepTitle;

if (titleTemplate is not null)
{
var titleAttribute = ((StepTitleAttribute)stepTitleAttribute);
name = string.Format(titleAttribute.StepTitle, flatInputArray);
if (titleAttribute.IncludeInputsInStepTitle != null)
includeInputsInStepTitle = titleAttribute.IncludeInputsInStepTitle.Value;
name = string.Format(titleTemplate, flatInputArray);
}

var stepTitle = AppendPrefix(Configurator.Humanizer.Humanize(name), stepPrefix);

if (!string.IsNullOrEmpty(stepTextTemplate)) stepTitle = string.Format(stepTextTemplate, flatInputArray);
else if (includeInputsInStepTitle)
else if (includeInputsInStepTitle.Value)
{
var parameters = methodInfo.GetParameters();
var stringFlatInputs =
Expand All @@ -56,23 +61,24 @@ public StepTitle Create(
return i.Value.Value.FlattenArray();
})
.ToArray();

stepTitle = stepTitle + " " + string.Join(", ", stringFlatInputs);
}

return stepTitle.Trim();
};
}

return new StepTitle(createTitle);
}

public StepTitle Create(string title, string stepPrefix, ITestContext testContext) => new StepTitle(AppendPrefix(title, stepPrefix));
public StepTitle Create(string title, string stepPrefix, ITestContext testContext) => new(AppendPrefix(title, stepPrefix));

private static string AppendPrefix(string title, string stepPrefix)
{
if (!title.StartsWith(stepPrefix, StringComparison.CurrentCultureIgnoreCase))
{
if (title.Length == 0) return string.Format("{0} ", stepPrefix);
return string.Format("{0} {1}{2}", stepPrefix, title.Substring(0, 1).ToLower(), title.Substring(1));
return string.Format("{0} {1}{2}", stepPrefix, title[..1].ToLower(), title[1..]);
}

return title;
Expand Down
4 changes: 3 additions & 1 deletion src/TestStack.BDDfy/Abstractions/IStepTitleFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ namespace TestStack.BDDfy.Abstractions;

public interface IStepTitleFactory
{
bool IncludeInputsInStepTitle { get; set; }

public StepTitle Create(
string stepTextTemplate,
bool includeInputsInStepTitle,
bool? includeInputsInStepTitle,
MethodInfo methodInfo,
StepArgument[] inputArguments,
ITestContext testContext,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,20 @@ internal static class ArgumentCleaningExtensions
{
internal static object[] FlattenArrays(this IEnumerable<object> inputs)
{
return inputs.Select(FlattenArray).ToArray();
return [.. inputs.Select(FlattenArray)];
}

public static object FlattenArray(this object input)
{
var inputArray = input as Array;
if (inputArray != null)
if (input is Array inputArray)
{
var temp = (from object arrElement in inputArray select GetSafeString(arrElement)).ToArray();
var temp = from object arrElement in inputArray select GetSafeValue(arrElement);
return string.Join(", ", temp);
}

if (input == null) return "'null'";

return input;
return GetSafeValue(input);
}

static string GetSafeString(object input)
{
if (input == null)
return "'null'";

return input.ToString();
}
static object GetSafeValue(object input) => input ?? "'null'";
}
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
using System;
using System.Diagnostics.CodeAnalysis;
using TestStack.BDDfy.Annotations;

namespace TestStack.BDDfy
{
/// <summary>
/// This attribute is marked with <see cref="TestStack.BDDfy.Annotations.MeansImplicitUseAttribute"/>
/// This attribute is marked with <see cref="MeansImplicitUseAttribute"/>
/// so that any method decorated with <c>[Executable]</c> (or derived GWT attributes)
/// is treated as "used implicitly" by code-analysis tools (ReSharper/Rider/etc).
///
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,12 @@ public static IEnumerable<StepArgument> ExtractArguments<T>(this Expression<Func
private class ArgumentExtractorVisitor : ExpressionVisitor
{
private List<StepArgument> _arguments;
private object _value;

public IEnumerable<StepArgument> ExtractArguments(LambdaExpression methodCallExpression, object value)
{
_arguments = new List<StepArgument>();
_arguments = [];
_value = value;
Visit(methodCallExpression);
return _arguments;
}
Expand All @@ -37,7 +39,7 @@ protected override Expression VisitMethodCall(MethodCallExpression node)
return node;
}

private static StepArgument ExtractStepArgument(Expression a)
private StepArgument ExtractStepArgument(Expression a)
{
switch (a.NodeType)
{
Expand Down Expand Up @@ -73,17 +75,33 @@ private static StepArgument ExtractStepArgument(Expression a)
}
}

private static Func<object> GetValue(Expression a)
private Func<object> GetValue(Expression a)
{
// 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)
{
var replaced = Expression.Convert(Expression.MakeMemberAccess(Expression.Constant(_value), memberExpression.Member), typeof(object));
return Expression.Lambda<Func<object>>(replaced).Compile();
}

return Expression.Lambda<Func<object>>(Expression.Convert(a, typeof(object))).Compile();
}

private static Action<object> SetValue(Expression a, Type parameterType)
private Action<object> SetValue(Expression a, Type parameterType)
{
var parameter = Expression.Parameter(typeof(object));
var unaryExpression = Expression.Convert(parameter, parameterType);
var assign = Expression.Assign(a, unaryExpression);
return Expression.Lambda<Action<object>>(assign, parameter).Compile();

if (a 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<Action<object>>(assign, parameter).Compile();
}

var assignDefault = Expression.Assign(a, unaryExpression);
return Expression.Lambda<Action<object>>(assignDefault, parameter).Compile();
}
}
}
Expand Down
Loading
Loading