Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix async transformations #2230 #2660

Merged
merged 5 commits into from
Oct 24, 2022
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
55 changes: 55 additions & 0 deletions TechTalk.SpecFlow/Bindings/AsyncMethodHelper.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System;
using System.Threading.Tasks;
using TechTalk.SpecFlow.Bindings.Reflection;

namespace TechTalk.SpecFlow.Bindings
{
internal static class AsyncMethodHelper
{
private static readonly IBindingType TaskOfT = new RuntimeBindingType(typeof(Task<>));

private static bool IsTaskOfT(Type type, out Type typeArg)
{
typeArg = null;
var isTaskOfT = type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Task<>);
if (isTaskOfT)
{
typeArg = type.GetGenericArguments()[0];
}
return isTaskOfT;
}

private static bool IsValueTaskOfT(Type type, out Type typeArg)
{
typeArg = null;
#if NETFRAMEWORK
return false;
#else
var isTaskOfT = type.IsGenericType && type.GetGenericTypeDefinition() == typeof(ValueTask<>);
if (isTaskOfT)
{
typeArg = type.GetGenericArguments()[0];
}
return isTaskOfT;
#endif
}

private static bool IsAwaitableOfT(Type type, out Type typeArg)
{
return IsTaskOfT(type, out typeArg) || IsValueTaskOfT(type, out typeArg);
}

public static IBindingType GetAwaitableReturnType(this IBindingMethod bindingMethod)
{
var returnType = bindingMethod.ReturnType;

if (returnType is RuntimeBindingType runtimeReturnType &&
IsAwaitableOfT(runtimeReturnType.Type, out var typeArg))
{
return new RuntimeBindingType(typeArg);
}

return returnType;
}
}
}
3 changes: 2 additions & 1 deletion TechTalk.SpecFlow/Bindings/StepArgumentTypeConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,8 @@ public bool CanConvert(object value, IBindingType typeToConvertTo, CultureInfo c

private bool CanConvert(IStepArgumentTransformationBinding stepTransformationBinding, object valueToConvert, IBindingType typeToConvertTo)
{
if (!stepTransformationBinding.Method.ReturnType.TypeEquals(typeToConvertTo))
var awaitableReturnType = stepTransformationBinding.Method.GetAwaitableReturnType();
if (!awaitableReturnType.TypeEquals(typeToConvertTo))
return false;

if (stepTransformationBinding.Regex != null && valueToConvert is string stringValue)
Expand Down
56 changes: 54 additions & 2 deletions Tests/TechTalk.SpecFlow.RuntimeTests/StepTransformationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,20 @@ public User Create(string name)
return new User {Name = name};
}

[StepArgumentTransformation("user (w+)")]
public async Task<User> AsyncCreate(string name)
{
await Task.Yield();
return new User {Name = name};
}

[StepArgumentTransformation("user (w+)")]
public async ValueTask<User> AsyncCreateValueTask(string name)
{
await Task.Yield();
return new User {Name = name};
}

[StepArgumentTransformation]
public IEnumerable<User> CreateUsers(Table table)
{
Expand Down Expand Up @@ -156,7 +170,44 @@ public async Task TypeToTypeConverterShouldConvertTableToTable()
public async Task StepArgumentTypeConverterShouldUseUserConverterForConversion()
{
UserCreator stepTransformationInstance = new UserCreator();
var transformMethod = new RuntimeBindingMethod(stepTransformationInstance.GetType().GetMethod("Create"));
var transformMethod = new RuntimeBindingMethod(stepTransformationInstance.GetType().GetMethod(nameof(UserCreator.Create)));
var stepTransformationBinding = CreateStepTransformationBinding(@"user (\w+)", transformMethod);
stepTransformations.Add(stepTransformationBinding);
var resultUser = new User();
methodBindingInvokerStub
.Setup(i => i.InvokeBindingAsync(stepTransformationBinding, It.IsAny<IContextManager>(), It.IsAny<object[]>(), It.IsAny<ITestTracer>(), It.IsAny<DurationHolder>()))
.ReturnsAsync(resultUser);

var stepArgumentTypeConverter = CreateStepArgumentTypeConverter();

var result = await stepArgumentTypeConverter.ConvertAsync("user xyz", typeof(User), new CultureInfo("en-US", false));
result.Should().Be(resultUser);
}

[Fact]
public async Task StepArgumentTypeConverterShouldUseAsyncUserConverterForConversion()
{
UserCreator stepTransformationInstance = new UserCreator();
var transformMethod = new RuntimeBindingMethod(stepTransformationInstance.GetType().GetMethod(nameof(UserCreator.AsyncCreate)));
var stepTransformationBinding = CreateStepTransformationBinding(@"user (\w+)", transformMethod);
stepTransformations.Add(stepTransformationBinding);
var resultUser = new User();
methodBindingInvokerStub
.Setup(i => i.InvokeBindingAsync(stepTransformationBinding, It.IsAny<IContextManager>(), It.IsAny<object[]>(), It.IsAny<ITestTracer>(), It.IsAny<DurationHolder>()))
.ReturnsAsync(resultUser);

var stepArgumentTypeConverter = CreateStepArgumentTypeConverter();

var result = await stepArgumentTypeConverter.ConvertAsync("user xyz", typeof(User), new CultureInfo("en-US", false));
result.Should().Be(resultUser);
}

#if !NETFRAMEWORK
[Fact]
public async Task StepArgumentTypeConverterShouldUseAsyncValueTaskUserConverterForConversion()
{
UserCreator stepTransformationInstance = new UserCreator();
var transformMethod = new RuntimeBindingMethod(stepTransformationInstance.GetType().GetMethod(nameof(UserCreator.AsyncCreateValueTask)));
var stepTransformationBinding = CreateStepTransformationBinding(@"user (\w+)", transformMethod);
stepTransformations.Add(stepTransformationBinding);
var resultUser = new User();
Expand All @@ -169,6 +220,7 @@ public async Task StepArgumentTypeConverterShouldUseUserConverterForConversion()
var result = await stepArgumentTypeConverter.ConvertAsync("user xyz", typeof(User), new CultureInfo("en-US", false));
result.Should().Be(resultUser);
}
#endif

private StepArgumentTypeConverter CreateStepArgumentTypeConverter()
{
Expand All @@ -181,7 +233,7 @@ public async Task ShouldUseStepArgumentTransformationToConvertTable()
var table = new Table("Name");

UserCreator stepTransformationInstance = new UserCreator();
var transformMethod = new RuntimeBindingMethod(stepTransformationInstance.GetType().GetMethod("CreateUsers"));
var transformMethod = new RuntimeBindingMethod(stepTransformationInstance.GetType().GetMethod(nameof(UserCreator.CreateUsers)));
var stepTransformationBinding = CreateStepTransformationBinding(@"", transformMethod);
stepTransformations.Add(stepTransformationBinding);
var resultUsers = new User[3];
Expand Down
1 change: 1 addition & 0 deletions changelog.txt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ Features:
+ Add an option to colorize test result output
+ Support for using Cucumber Expressions for step definitions.
+ Support Rule tags (can be used for hook filters, scoping and access through 'ScenarioInfo.CombinedTags')
+ Support for async step argument transformations. Fixes #2230

Changes:
+ Existing step definition expressions detected to be either regular or cucumber expression. Check https://docs.specflow.org/projects/specflow/en/latest/Guides/UpgradeSpecFlow3To4.html for potential upgrade issues.
Expand Down
2 changes: 2 additions & 0 deletions docs/Bindings/Asynchronous-Bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ public async Task HttpClientGet(string url)
_httpResponseMessage = await _httpClient.GetAsync(url);
}
```

From SpecFlow v4 you can also use asynchronous [step argument transformations](Step-Argument-Conversions.md).