Skip to content

Commit 103da45

Browse files
Implement #136
When using reflective API, you can now mix examples with [RunStepWithArgs] attribute It should generate something like this. Scenario: Example values and run step args both get passed to method Given step with <first example> passed as parameter value 1 And step with <first example> passed as parameter value 2 Then example value is set And run step arg is set Examples: | FirstExample | | 1 | | 2 |
1 parent 7291631 commit 103da45

6 files changed

Lines changed: 284 additions & 152 deletions

File tree

.github/copilot-instructions.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Copilot Instructions
22

33
## Project Guidelines
4-
- Repository preference: format FluentStepScannerExtensions extension methods with multiline parameters and expression-bodied single-line where clauses; add [Pure] attribute to IFluentStepBuilder<TScenario> members.
4+
- Repository preference: format FluentStepScannerExtensions extension methods with multiline parameters and expression-bodied single-line where clauses; add [Pure] attribute to IFluentStepBuilder<TScenario> members.
5+
- Prefer "is null" and "is not null" pattern matching over "== null" and "!= null" comparisons.

src/TestStack.BDDfy.Tests/Scanner/Examples/ExampleActionTests.CanUseActionsInExamples.approved.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Scenario: Can use actions in examples
33
Given some setup
4-
When <Action to perform>
4+
When <Action to perform>
55
Then should be <Value should be>
66

77
Examples:

src/TestStack.BDDfy.Tests/Scanner/FluentScanner/ExampleColumnNameMatchingMethodParameterName.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace TestStack.BDDfy.Tests.Scanner.FluentScanner
1111
/// </summary>
1212
public class ExampleColumnNameMatchingMethodParameterName
1313
{
14-
private string _existing = "";
14+
private readonly string _existing = "";
1515
private string _resource = "";
1616

1717
private void GivenThereIsOnly__existing__ResourceInDatabase(string resource)
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
using Shouldly;
2+
using Xunit;
3+
4+
namespace TestStack.BDDfy.Tests.Scanner;
5+
6+
public class WhenCombiningRunStepWithArgsAndExamples
7+
{
8+
[Fact]
9+
public void ExampleValuesAndRunStepArgsBothGetPassedToMethod()
10+
{
11+
var story = new Scenario()
12+
.WithExamples(new ExampleTable("FirstExample") { 1, 2 })
13+
.BDDfy();
14+
15+
var actualReport = story.Scenarios.Select(x => new
16+
{
17+
x.Title,
18+
x.Result,
19+
Steps = x.Steps.Select(x => x.Title).ToArray()
20+
}).ToArray();
21+
22+
var expectedReport = new[]
23+
{
24+
new
25+
{
26+
Title = "Example values and run step args both get passed to method",
27+
Result = Result.Passed,
28+
Steps = new[] {
29+
"Given step with <first example> passed as parameter value 1",
30+
"And step with <first example> passed as parameter value 2",
31+
"Then example value is set",
32+
"And run step arg is set" }
33+
},
34+
new
35+
{
36+
Title = "Example values and run step args both get passed to method",
37+
Result = Result.Passed,
38+
Steps = new[] {
39+
"Given step with <first example> passed as parameter value 1",
40+
"And step with <first example> passed as parameter value 2",
41+
"Then example value is set",
42+
"And run step arg is set" }
43+
}
44+
};
45+
46+
actualReport.ShouldBeEquivalentTo(expectedReport);
47+
}
48+
49+
private class Scenario
50+
{
51+
private int _firstExample;
52+
private string? _input;
53+
54+
[RunStepWithArgs("value 1")]
55+
[RunStepWithArgs("value 2")]
56+
public void GivenStepWith__FirstExample__PassedAsParameter(int firstExample, string input)
57+
{
58+
_firstExample = firstExample;
59+
_input = input;
60+
}
61+
62+
public void ThenExampleValueIsSet()
63+
{
64+
_firstExample.ShouldBeGreaterThan(0);
65+
}
66+
67+
public void AndRunStepArgIsSet()
68+
{
69+
_input.ShouldBe("value 2");
70+
}
71+
}
72+
}

src/TestStack.BDDfy/Abstractions/DefaultStepTitleFactory.cs

Lines changed: 100 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -22,83 +22,131 @@ string createTitle()
2222
var titleAttribute = methodInfo.GetCustomAttribute<StepTitleAttribute>(true);
2323
var executableAttribute = methodInfo.GetCustomAttribute<ExecutableAttribute>(true);
2424

25-
var callerSuppliedTemplate = stepTextTemplate != null;
25+
var callerSuppliedTemplate = stepTextTemplate is not null;
2626
includeInputsInStepTitle ??= titleAttribute?.IncludeInputsInStepTitle;
27-
stepTextTemplate ??= titleAttribute != null
27+
28+
stepTextTemplate ??= titleAttribute is not null
2829
? (NullIfEmpty(titleAttribute.StepTitle) ?? "")
2930
: NullIfEmpty(executableAttribute?.StepTitle);
30-
var stepTextTemplateWasNotSupplied = string.IsNullOrWhiteSpace(stepTextTemplate);
3131

32+
var hasTemplate = !string.IsNullOrWhiteSpace(stepTextTemplate);
3233
stepTextTemplate ??= methodInfo.Name;
3334

34-
var formattedStepTitle = string.Format(Configurator.CultureInfo, stepTextTemplate, flatInputArray);
35-
var stepTitle = stepTextTemplateWasNotSupplied ? Configurator.Humanizer.Humanize(formattedStepTitle) : formattedStepTitle;
36-
37-
var shouldAddPrefix = stepTextTemplateWasNotSupplied
38-
|| IsPrimaryPrefix(stepPrefix)
39-
|| (!callerSuppliedTemplate && AddGherkinPrefixToSecondarySteps);
40-
41-
if (shouldAddPrefix)
42-
stepTitle = AppendPrefix(stepTitle, stepPrefix);
35+
var formattedTitle = string.Format(Configurator.CultureInfo, stepTextTemplate, flatInputArray);
36+
var stepTitle = (hasTemplate ? formattedTitle : Configurator.Humanizer.Humanize(formattedTitle))
37+
?? throw new InvalidOperationException($"Failed to create a step title for method '{methodInfo.Name}'.");
4338

44-
if (stepTextTemplate != formattedStepTitle && titleAttribute?.IncludeInputsInStepTitle is null)
45-
includeInputsInStepTitle??= false;
39+
if (ShouldAddPrefix(hasTemplate, callerSuppliedTemplate, stepPrefix))
40+
stepTitle = PrependPrefix(stepTitle, stepPrefix);
4641

47-
if (stepTitle!.Contains('<') && stepTitle.Contains('>') && titleAttribute?.IncludeInputsInStepTitle is null)
48-
includeInputsInStepTitle??=false;
42+
includeInputsInStepTitle ??= ShouldIncludeInputs(stepTitle, stepTextTemplate, formattedTitle, titleAttribute, methodInfo);
4943

5044
if (includeInputsInStepTitle ?? IncludeInputsInStepTitle)
51-
{
52-
var parameters = methodInfo.GetParameters();
53-
var stringFlatInputs =
54-
inputArguments
55-
.Select((a, i) => new { ParameterName = parameters[i].Name, Value = a })
56-
.Select(i =>
57-
{
58-
if (testContext.Examples != null)
59-
{
60-
var matchingHeaders = testContext.Examples.Headers
61-
.Where(header => ExampleTable.HeaderMatches(header, i.ParameterName) || ExampleTable.HeaderMatches(header, i.Value.Name))
62-
.ToList();
63-
64-
if (matchingHeaders.Count > 1)
65-
throw new AmbiguousMatchException($"More than one headers for examples, match the parameter '{i.ParameterName}' provided for '{methodInfo.Name}'");
66-
67-
var matchingHeader = matchingHeaders.SingleOrDefault();
68-
if (matchingHeader != null)
69-
return string.Format("<{0}>", matchingHeader);
70-
}
71-
return i.Value.Value?.FlattenArray() ?? Array.Empty<string>();
72-
})
73-
.ToArray();
74-
75-
stepTitle = stepTitle + " " + string.Join(", ", stringFlatInputs.Select(o => o.ToTextRepresentation()));
76-
}
45+
stepTitle = AppendInputSuffix(stepTitle, methodInfo, inputArguments, testContext);
7746

7847
return stepTitle.Trim();
7948
}
8049

8150
return new StepTitle(createTitle);
8251
}
8352

84-
public StepTitle Create(string title, string stepPrefix, ITestContext testContext) => new(AppendPrefix(title, stepPrefix));
53+
public StepTitle Create(string title, string stepPrefix, ITestContext testContext) =>
54+
new(PrependPrefix(title, stepPrefix));
55+
56+
private bool ShouldAddPrefix(bool hasTemplate, bool callerSuppliedTemplate, string stepPrefix) =>
57+
!hasTemplate
58+
|| IsPrimaryPrefix(stepPrefix)
59+
|| (!callerSuppliedTemplate && AddGherkinPrefixToSecondarySteps);
60+
61+
private static bool? ShouldIncludeInputs(
62+
string stepTitle,
63+
string stepTextTemplate,
64+
string formattedTitle,
65+
StepTitleAttribute? titleAttribute,
66+
MethodInfo methodInfo)
67+
{
68+
if (titleAttribute?.IncludeInputsInStepTitle is not null)
69+
return null;
70+
71+
if (stepTextTemplate != formattedTitle)
72+
return false;
73+
74+
if (stepTitle.Contains('<') && stepTitle.Contains('>'))
75+
return methodInfo.GetCustomAttributes<RunStepWithArgsAttribute>(true).Any();
76+
77+
return null;
78+
}
79+
80+
private static string AppendInputSuffix(
81+
string stepTitle,
82+
MethodInfo methodInfo,
83+
StepArgument[] inputArguments,
84+
ITestContext testContext)
85+
{
86+
var parameters = methodInfo.GetParameters();
87+
88+
var inputRepresentations = inputArguments
89+
.Select((arg, i) => FormatInput(arg, parameters[i].Name, methodInfo.Name, testContext))
90+
.Where(x => IsNotAlreadyInTitle(stepTitle, x))
91+
.ToArray();
92+
93+
if (inputRepresentations.Length == 0)
94+
return stepTitle;
95+
96+
return stepTitle.Trim() + " " + string.Join(", ", inputRepresentations.Select(o => o.ToTextRepresentation()));
97+
}
98+
99+
private static object FormatInput(StepArgument arg, string? parameterName, string methodName, ITestContext testContext)
100+
{
101+
if (testContext.Examples is null)
102+
return arg.Value?.FlattenArray() ?? Array.Empty<string>();
103+
104+
var matchingHeader = FindMatchingExampleHeader(testContext.Examples, parameterName, arg.Name, methodName);
105+
return matchingHeader is not null
106+
? $"<{matchingHeader}>"
107+
: arg.Value?.FlattenArray() ?? Array.Empty<string>();
108+
}
85109

86-
private static string AppendPrefix(string? title, string stepPrefix)
110+
private static string? FindMatchingExampleHeader(ExampleTable examples, string? parameterName, string? argName, string methodName)
111+
{
112+
var matchingHeaders = examples.Headers
113+
.Where(header => ExampleTable.HeaderMatches(header, parameterName) || ExampleTable.HeaderMatches(header, argName))
114+
.ToList();
115+
116+
if (matchingHeaders.Count > 1)
117+
throw new AmbiguousMatchException(
118+
$"More than one headers for examples, match the parameter '{parameterName}' provided for '{methodName}'");
119+
120+
return matchingHeaders.SingleOrDefault();
121+
}
122+
123+
private static bool IsNotAlreadyInTitle(string stepTitle, object arg)
124+
{
125+
if (arg is not string token)
126+
return true;
127+
128+
if (!token.StartsWith('<') || !token.EndsWith('>'))
129+
return true;
130+
131+
return !stepTitle.Replace(" ", "").Contains(token, StringComparison.OrdinalIgnoreCase);
132+
}
133+
134+
private static string PrependPrefix(string? title, string stepPrefix)
87135
{
88136
var stepTitle = (title ?? string.Empty).Trim();
89137

90-
if (!stepTitle.StartsWith(stepPrefix, ignoreCase: true, Configurator.CultureInfo))
91-
{
92-
if (stepTitle.Length == 0) return string.Format(Configurator.CultureInfo, "{0} ", stepPrefix);
138+
if (stepTitle.StartsWith(stepPrefix, ignoreCase: true, Configurator.CultureInfo))
139+
return stepTitle;
93140

94-
return string.Format(Configurator.CultureInfo, "{0} {1}{2}", stepPrefix, stepTitle[..1].ToLower(Configurator.CultureInfo), stepTitle[1..]);
95-
}
141+
if (stepTitle.Length == 0)
142+
return $"{stepPrefix} ";
96143

97-
return stepTitle;
144+
return $"{stepPrefix} {stepTitle[..1].ToLower(Configurator.CultureInfo)}{stepTitle[1..]}";
98145
}
99146

100-
private static string? NullIfEmpty(string? value) => string.IsNullOrWhiteSpace(value) ? null : value;
147+
private static string? NullIfEmpty(string? value) =>
148+
string.IsNullOrWhiteSpace(value) ? null : value;
101149

102150
private static bool IsPrimaryPrefix(string prefix) =>
103151
prefix is "Given" or "When" or "Then";
104-
}
152+
}

0 commit comments

Comments
 (0)