Skip to content

Commit

Permalink
Add AnyValueAttribute and usage tests devlooped#1199
Browse files Browse the repository at this point in the history
  • Loading branch information
adamfk committed Aug 18, 2021
1 parent 2a70b29 commit bcf18aa
Show file tree
Hide file tree
Showing 4 changed files with 297 additions and 0 deletions.
17 changes: 17 additions & 0 deletions src/Moq/AnyValueAttribute.cs
@@ -0,0 +1,17 @@
// Copyright (c) 2007, Clarius Consulting, Manas Technology Solutions, InSTEDD, and Contributors.
// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt.

using System;

namespace Moq
{
/// <summary>
/// A class member with this attribute will be treated automatically like <see cref="It.IsAny{TValue}"/>.
/// Supports https://github.com/moq/moq4/issues/1199
/// </summary>
[AttributeUsage(AttributeTargets.Property, Inherited = true)] //TODO consider and test `| AttributeTargets.Method | AttributeTargets.Field`
public class AnyValueAttribute : Attribute
{

}
}
21 changes: 21 additions & 0 deletions src/Moq/MatcherFactory.cs
Expand Up @@ -137,6 +137,15 @@ internal static class MatcherFactory
return new Pair<IMatcher, Expression>(matchExpression.Match, matchExpression);
}

// add support for https://github.com/moq/moq4/issues/1199
if (expression is MemberExpression memberExpression)
{
if (memberExpression.Member.IsDefined(typeof(AnyValueAttribute), inherit: true))
{
return BuildMatchPair(originalExpression);
}
}

if (expression is MethodCallExpression call)
{
if (expression.IsMatch(out var match))
Expand Down Expand Up @@ -180,5 +189,17 @@ internal static class MatcherFactory
throw new NotSupportedException(
string.Format(CultureInfo.CurrentCulture, Resources.UnsupportedExpression, originalExpression));
}

// There is probably a better way to do this, but I'm not super familiar with all the Moq internals
private static Pair<IMatcher, Expression> BuildMatchPair(Expression expression)
{
using (var observer = MatcherObserver.Activate())
{
var methodCallExpr = It.IsAny(expression.Type);
Expression.Lambda<Action>(methodCallExpr).CompileUsingExpressionCompiler().Invoke();
observer.TryGetLastMatch(out Match match);
return new Pair<IMatcher, Expression>(match, expression);
}
}
}
}
135 changes: 135 additions & 0 deletions tests/Moq.Tests/Matchers/AnyValueAttributeTests1.cs
@@ -0,0 +1,135 @@
using Xunit;
using System;
using Moq.Tests.Matchers.AnyAttribute1.SomeOtherLib;

/// <summary>
/// This example allows using `_` for any type including interfaces, but it has the downside of requiring some
/// extra user maintennance when refactoring the interfaces. This is overcome in <see cref="Moq.Tests.Matchers.AnyAttribute2.AnyValueAttributeTests2"/>.
/// Tests for https://github.com/moq/moq4/issues/1199
/// </summary>

namespace Moq.Tests.Matchers.AnyAttribute1.SomeOtherLib
{
/// <summary>
/// Helper class probably provided by user/3rd party lib and not Moq
/// </summary>
public class AnyValueHelper<T> where T : AnyValueBase, new()
{
[AnyValue] //new Attribute that allows this to work
public static T _ => default;
[AnyValue]
public static T any => default;
}

/// <summary>
/// Helper class probably provided by user/3rd party lib and not Moq
/// </summary>
public class AnyValueBase
{
public static implicit operator int(AnyValueBase _) => default;
public static implicit operator byte(AnyValueBase _) => default;
//TODO all the built-in value types
}
}


namespace Moq.Tests.Matchers.AnyAttribute1
{
using static AnyValueHelper<AdamsAnyHelper>; // note using static to simplify syntax

/// <summary>
/// Helper class probably provided by user (Adam) for any types not handled by <see cref="AnyValueBase"/>.
/// </summary>
public class AdamsAnyHelper : AnyValueBase, ICar
{
#region boilerplate

int ICar.Calc(int a, int b, int c, int d)
{
throw new NotImplementedException();
}

int ICar.DoSomething(ICar a, GearId b, int c)
{
throw new NotImplementedException();
}

int ICar.Echo(int a)
{
throw new NotImplementedException();
}

int ICar.Race(ICar a)
{
throw new NotImplementedException();
}

public static implicit operator GearId(AdamsAnyHelper _) => default;

#endregion
}

/// <summary>
/// Example enum
/// </summary>
public enum GearId
{
Reverse,
Neutral,
Gear1,
}

/// <summary>
/// Example interface
/// </summary>
public interface ICar
{
int Echo(int a);
int Calc(int a, int b, int c, int d);
int Race(ICar a);
int DoSomething(ICar a, GearId b, int c);
}

public class AnyValueAttributeTests2
{
[Fact]
public void Echo_1Primitive()
{
var mockCar = new Mock<ICar>();
var car = mockCar.Object;

mockCar.Setup(car => car.Echo(_)).Returns(0x68);
Assert.Equal(0x68, car.Echo(default));
}

[Fact]
public void Calc_4Primitives()
{
var mockCar = new Mock<ICar>();
var car = mockCar.Object;

mockCar.Setup(car => car.Calc(_, _, _, _)).Returns(123456);
Assert.Equal(123456, car.Calc(default, default, default, default));
}

[Fact]
public void Race_1Interface()
{
var mockCar = new Mock<ICar>();
var car = mockCar.Object;

mockCar.Setup(car => car.Race(_)).Returns(0x68);
Assert.Equal(0x68, car.Race(default));
}

[Fact]
public void DoSomething_MixedTypes()
{
var mockCar = new Mock<ICar>();
var car = mockCar.Object;

mockCar.Setup(car => car.DoSomething(_, _, _)).Returns(0x68);
Assert.Equal(0x68, car.DoSomething(default, default, default));
}
}
}
124 changes: 124 additions & 0 deletions tests/Moq.Tests/Matchers/AnyValueAttributeTests2.cs
@@ -0,0 +1,124 @@
using Xunit;
using Moq.Tests.Matchers.AnyAttribute2.SomeOtherLib;

/// <summary>
/// This example allows using `Any` for any non-interface type and `AnyI` otherwise.
/// This avoids extra user maintennance when the interfaces are refactored.
/// Tests for https://github.com/moq/moq4/issues/1199
/// </summary>

namespace Moq.Tests.Matchers.AnyAttribute2.SomeOtherLib
{
/// <summary>
/// Helper class probably provided by user/3rd party lib and not Moq
/// </summary>
public class AnyValueHelper<T, IType> where T : AnyValueBase
{
/// <summary>
/// Any non-interface type. Class or primitive type.
/// </summary>
[AnyValue] //new Attribute that allows this to work
public static T Any => default;

/// <summary>
/// Any Interface
/// </summary>
[AnyValue]
public static IType AnyI => default;
}

/// <summary>
/// Helper class probably provided by user/3rd party lib and not Moq
/// </summary>
public class AnyValueBase
{
public static implicit operator int(AnyValueBase _) => default;
public static implicit operator byte(AnyValueBase _) => default;
//TODO all the built-in value types
}
}


namespace Moq.Tests.Matchers.AnyAttribute2
{
using static AnyValueHelper<AdamsAnyHelper, IAdamsAnyInterfaceHelper>; // note using static to simplify syntax

/// <summary>
/// Helper class provided by user (Adam) for any types not handled by <see cref="AnyValueBase"/>.
/// </summary>
public class AdamsAnyHelper : AnyValueBase
{
public static implicit operator GearId(AdamsAnyHelper _) => default;
}

/// <summary>
/// Helper interface provided by user (Adam) for any interfaces
/// </summary>
public interface IAdamsAnyInterfaceHelper : ICar
{
}

/// <summary>
/// Example enum
/// </summary>
public enum GearId
{
Reverse,
Neutral,
Gear1,
}

/// <summary>
/// Example interface
/// </summary>
public interface ICar
{
int Echo(int a);
int Calc(int a, int b, int c, int d);
int Race(ICar a);
int DoSomething(ICar a, GearId b, int c);
}

public class AnyValueAttributeTests2
{
[Fact]
public void Echo_1Primitive()
{
var mockCar = new Mock<ICar>();
var car = mockCar.Object;

mockCar.Setup(car => car.Echo(Any)).Returns(0x68);
Assert.Equal(0x68, car.Echo(default));
}

[Fact]
public void Calc_4Primitives()
{
var mockCar = new Mock<ICar>();
var car = mockCar.Object;

mockCar.Setup(car => car.Calc(Any, Any, Any, Any)).Returns(123456);
Assert.Equal(123456, car.Calc(default, default, default, default));
}

[Fact]
public void Race_1Interface()
{
var mockCar = new Mock<ICar>();
var car = mockCar.Object;

mockCar.Setup(car => car.Race(AnyI)).Returns(0x68);
Assert.Equal(0x68, car.Race(default));
}

[Fact]
public void DoSomething_MixedTypes()
{
var mockCar = new Mock<ICar>();
var car = mockCar.Object;

mockCar.Setup(car => car.DoSomething(AnyI, Any, Any)).Returns(0x68);
Assert.Equal(0x68, car.DoSomething(default, default, default));
}
}
}

0 comments on commit bcf18aa

Please sign in to comment.