admin管理员组文章数量:1355528
Here is my code, as you can see there are many It.IsAny<>
which look ugly.
_mockTimeSpanModel
.Setup(
x => x.CaptureTargetScreen(
It.IsAny<string>(),
It.IsAny<int>(),
It.IsAny<int>(),
It.IsAny<Action<Bitmap>>()
)
)
.Callback(
(string windowTitle, int offsetFromLeft, int offsetFromBottom, Action<Bitmap> onImageCaptured) =>
{
if( windowTitle == "my" ) ...
}
);
The code I want is as below. Instead of It.IsAny<>
, default values are used. It looks clean. But it does not work as above.
_mockTimeSpanModel
.Setup(
x => x.CaptureTargetScreen(
"",
0,
0,
null
)
)
.Callback(
(string windowTitle, int offsetFromLeft, int offsetFromBottom, Action<Bitmap> onImageCaptured) =>
{
if( windowTitle == "my" ) ...
}
);
Can I make it work as the first code without using It.IsAny<T>
?
Here is my code, as you can see there are many It.IsAny<>
which look ugly.
_mockTimeSpanModel
.Setup(
x => x.CaptureTargetScreen(
It.IsAny<string>(),
It.IsAny<int>(),
It.IsAny<int>(),
It.IsAny<Action<Bitmap>>()
)
)
.Callback(
(string windowTitle, int offsetFromLeft, int offsetFromBottom, Action<Bitmap> onImageCaptured) =>
{
if( windowTitle == "my" ) ...
}
);
The code I want is as below. Instead of It.IsAny<>
, default values are used. It looks clean. But it does not work as above.
_mockTimeSpanModel
.Setup(
x => x.CaptureTargetScreen(
"",
0,
0,
null
)
)
.Callback(
(string windowTitle, int offsetFromLeft, int offsetFromBottom, Action<Bitmap> onImageCaptured) =>
{
if( windowTitle == "my" ) ...
}
);
Can I make it work as the first code without using It.IsAny<T>
?
3 Answers
Reset to default 4Treating the default
values as any
would be problematic because sometimes you would want to use the default
values for their values, not as token for any
, i.e. 0
as zero.
If you don't like the verbosity there are several "workarounds".
One is defining a static helper class for common types like int
, string
, etc.:
public static class Any {
public static int Int => It.IsAny<int>();
}
This would save you a bit of typing Any.Int
instead of It.IsAny<int>()
.
or alternatively if you'd prefer not typing the paranthesis but still have it generic and somewhat fluent Any<int>.Value
over It.IsAny<int>()
:
public static class Any<T> {
public static T Value => It.IsAny<T>();
}
Door number 3 involves a bit of work with Expressions
, but will give you exactly what you want. When you put default
for an argument it will be replaced with It.IsAny<T>()
: dotnet fiddle
public static class Extensions {
public static ISetup<T> SetupWithAny<T>(this Mock<T> mock, Expression<Action<T>> action)
where T : class {
var expVisitor = new ItIsAnyExpressionVisitor();
var itIsAnyExpression = (Expression<Action<T>>)expVisitor.Visit(action);
return mock.Setup(itIsAnyExpression);
}
public static ISetup<T, TReturn> SetupWithAny<T, TReturn>(this Mock<T> mock,
Expression<Func<T, TReturn>> func)
where T : class {
var expVisitor = new ItIsAnyExpressionVisitor();
var itIsAnyExpression = (Expression<Func<T, TReturn>>)expVisitor.Visit(func);
return mock.Setup(itIsAnyExpression);
}
private class ItIsAnyExpressionVisitor : ExpressionVisitor {
static MethodInfo s_IsAnyMethodInfo = typeof(It)
.GetMethods()
.Where(x => x.Name.Contains("IsAny") && x.IsGenericMethod)
.FirstOrDefault();
static ConcurrentDictionary<Type, MethodInfo>
s_genericMethodInfos = new ConcurrentDictionary<Type, MethodInfo>();
#region DefaultValue
private static MethodInfo s_getDefaultTypeGeneric =
typeof(ItIsAnyExpressionVisitor).GetMethod(nameof(ItIsAnyExpressionVisitor.GetDefaultTypeValueCore),
BindingFlags.Static | BindingFlags.NonPublic);
private static ConcurrentDictionary<Type, object> s_defaultValues = new();
private static T GetDefaultTypeValueCore<T>() => default(T);
private static object GetDefaultTypeValue(Type type) {
ArgumentNullException.ThrowIfNull(type);
if (!type.IsValueType) return null;
var defaultValue = s_defaultValues.GetOrAdd(type,
t => s_getDefaultTypeGeneric.MakeGenericMethod(t).Invoke(null, null));
return defaultValue;
}
#endregion
protected override Expression VisitMethodCall(MethodCallExpression node) {
var itIsAnyArguments = node.Arguments
.Select(a => {
if (a is not ConstantExpression constExpr)
return a;
var defaultValueForType = GetDefaultTypeValue(a.Type);
// default(T) is null and Value is not null -> we take it
if (defaultValueForType == null
&& constExpr.Value != null)
return a;
// default(T) is not null, i.e. 0
// so if it's not Equal, i.e. 10 then return
if (defaultValueForType != null
&& defaultValueForType.Equals(constExpr.Value) == false)
return a;
return Expression.Call(null,
s_genericMethodInfos.GetOrAdd(a.Type,
t => s_IsAnyMethodInfo.MakeGenericMethod(t)));
})
.ToArray();
var newMethodCall = Expression.Call(
node.Object, node.Method, itIsAnyArguments);
return newMethodCall;
}
}
}
Test code:
public interface Foo {
void Bar(int a, string b);
int Baz(string a);
}
var mock = new Mock<Foo>();
//mock.Setup(x => x.Bar(42, default))
//.Callback(() => Console.WriteLine("CALLED from Regular Setup"));
mock.SetupWithAny(x => x.Bar(42, default)) // change to 43 and won't "work"
.Callback(() => Console.WriteLine("CALLED from Extension"));
//mock.Setup(x => x.Baz(default)).Returns(4);
mock.SetupWithAny(x => x.Baz(default)).Returns(4444);
mock.Object.Bar(42, "baz");
// CALLED from Extension
Console.WriteLine(mock.Object.Baz("foo")); // 4444
According to general logic, it's really hard to give an authoritative negative answer, but with a decade or two of experience with .NET and Moq, I'm fairly convinced that the answer is no.
The suggested alternative ought to compile, but of course, this will only match if the first argument is the empty string, the two next ones 0
, and the last null
.
Consider that this combination may be a a valid combination that you'd like to model in a test case. The Moq library can't tell that that's not what you mean.
If you don't like repeating those It.IsAny<T>
calls, consider writing a Test Helper that encapsulates this setup. Something like
internal static ?? SetupCaptureTargetScreenAny(this Mock<TimeSpanModel> mockTimeSpanModel)
{
mockTimeSpanModel.Setup(
x => x.CaptureTargetScreen(
It.IsAny<string>(),
It.IsAny<int>(),
It.IsAny<int>(),
It.IsAny<Action<Bitmap>>()
)
)
}
I can't quite remember what type Setup
returns, so I've just indicated ??
, but you should be able to look that up.
All that said, I'd like to warn against relying too much on It.IsAny
. It can be fine when testing certain error paths, but in general, the test should specify the arguments that the dependency expects. Otherwise, you'll risk that your tests behave in all sorts of unexpected ways. Ask my how I know.
Only thing that comes to my mind to avoid repeating It.IsAny
over and over is to use little bit of reflection. Eventually what I aim is to pass minimal information, i.e. mock, method name and return value for mocked method:
public static class MockHelper
{
private static MethodInfo GetMethod<TMock>(string methodName) where TMock : class
{
var method = typeof(TMock)
.GetMethods(BindingFlags.Public | BindingFlags.Instance)
.FirstOrDefault(m => m.Name == methodName);
if (method == null)
{
throw new ArgumentException($"Method {methodName} not found in {typeof(TMock).Name}.");
}
return method;
}
private static Expression[] GetParameterExpressions(MethodInfo method)
{
return method.GetParameters()
.Select(p => Expression.Call(typeof(It), nameof(It.IsAny), new Type[] { p.ParameterType }))
.ToArray();
}
public static void SetupMethodWithAny<TMock, TReturn>(
this Mock<TMock> mock,
string methodName,
TReturn returnValue) where TMock : class
{
var method = GetMethod<TMock>(methodName);
var mockParameter = Expression.Parameter(typeof(TMock), "m");
var methodCall = Expression.Call(mockParameter, method, GetParameterExpressions(method));
var lambda = Expression.Lambda<Func<TMock, TReturn>>(methodCall, mockParameter);
var setupMethod = typeof(Mock<TMock>)
.GetMethods()
.FirstOrDefault(m => m.Name == nameof(Mock<TMock>.Setup) && m.IsGenericMethod)
?.MakeGenericMethod(typeof(TReturn));
var setup = setupMethod?.Invoke(mock, new object[] { lambda });
(setup as Moq.Language.Flow.ISetup<TMock, TReturn>)?.Returns(returnValue);
}
}
Code basically is using reflection to find all relevant method such as It.IsAny
and Setup
. Then uses passed mock to wire everything up.
Possible vectors for improvments:
- making overload that would mock
void
methods, - extend to accept factory method instead of
returnValue
EDIT
Here's sample implementation for void
methods, that would throw an exception:
public static void SetupMethodWithException<TMock, TException>(
this Mock<TMock> mock,
string methodName,
TException exception)
where TException : Exception
where TMock : class
{
var method = GetMethod<TMock>(methodName);
var mockParameter = Expression.Parameter(typeof(TMock), "m");
var methodCall = Expression.Call(mockParameter, method, GetParameterExpressions(method));
var lambda = Expression.Lambda<Action<TMock>>(methodCall, mockParameter);
var setupMethod = typeof(Mock<TMock>)
.GetMethods()
.FirstOrDefault(m => m.Name == nameof(Mock<TMock>.Setup) && !m.IsGenericMethod);
var setup = setupMethod?.Invoke(mock, new object[] { lambda });
(setup as Moq.Language.Flow.ISetup<TMock>)?.Throws(exception);
}
Examples
Having such setup:
public interface ISystemUnderTest
{
void Test();
string ReturnTest(string a, int b);
}
Here are example unit tests with the methods:
public class UnitTest1
{
[Fact]
public void Test()
{
// Arrange
var mock = new Mock<ISystemUnderTest>();
mock.SetupMethodWithException(nameof(ISystemUnderTest.Test), new InvalidOperationException("My Exception"));
// Act & Assert
Assert.Throws<InvalidOperationException>(mock.Object.Test);
}
[Fact]
public void ReturnTest()
{
// Arrange
var mock = new Mock<ISystemUnderTest>();
var guid = Guid.NewGuid().ToString();
mock.SetupMethodWithAny(nameof(ISystemUnderTest.ReturnTest), guid);
// Act
var result = mock.Object.ReturnTest();
// Assert
Assert.Equal(guid, result);
}
}
本文标签: cHow to match any parameters in Setup() without using ItIsAnyltTgtStack Overflow
版权声明:本文标题:c# - How to match any parameters in Setup() without using It.IsAny<T>? - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1743942048a2565738.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
""
or0
are very specific values. They can't be used as wildcards. You need a syntax that says "use any value" – Panagiotis Kanavos Commented Mar 31 at 14:32Setup2
function to treat those default values asIt.IsAny<>
. I just checked the source code, but it is too complicated for me to add a newSetup2
function. @PanagiotisKanavos – Zach Commented Mar 31 at 15:220
to aMultiply
mock I ALWAYS expect the result to be 0. What would make sense would be anIt.IsAny()
that doesn't require a type. This can't work right now because C# can't infer type arguments from return values. You can probably writeusing static It;
and useIsAny<string>()
. – Panagiotis Kanavos Commented Apr 1 at 7:06It.Is<int>(t=>t==0)
– Zach Commented Apr 1 at 8:47It.IsAny<int>()
. If special syntax is needed, it should be used for special cases. Default values aren't special values. They're normal values and everyone expects them to be the same in every .NET application or library.Point p=default
is expected to be the same asnew Point()
ornew Point(0,0)
. And astring s=default;
is expected to benull
, not""
. – Panagiotis Kanavos Commented Apr 1 at 9:04