Skip to content

Commit

Permalink
Merge pull request #732 from stakx/matcherattribute
Browse files Browse the repository at this point in the history
De-deprecate `[Matcher]` attribute
  • Loading branch information
stakx authored Dec 8, 2018
2 parents 8acf1c8 + ae57a2e commit 233063e
Show file tree
Hide file tree
Showing 13 changed files with 124 additions and 66 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,17 @@ All notable changes to this project will be documented in this file.
The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).


## Unreleased

#### Changed

* **Breaking change:** All custom argument matcher methods (including those using `Match.Create<T>`) must now be marked with the `[Matcher]` attribute. For this reason, `MatcherAttribute` is no longer marked obsolete (@stakx, #732)

#### Fixed

* `InvalidOperationException` when specifiying setup on mock with mock containing property of type `Nullable<T>` (@dav1dev, #725)


## 4.10.1 (2018-12-03)

#### Fixed
Expand Down
3 changes: 3 additions & 0 deletions src/Moq/Capture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public static class Capture
/// Assert.Equal("Hello!", parameters.Single());
/// </code>
/// </example>
[Matcher]
public static T In<T>(ICollection<T> collection)
{
var match = new CaptureMatch<T>(collection.Add);
Expand All @@ -51,6 +52,7 @@ public static T In<T>(ICollection<T> collection)
/// Assert.Equal("Hello!", parameters.Single());
/// </code>
/// </example>
[Matcher]
public static T In<T>(IList<T> collection, Expression<Func<T, bool>> predicate)
{
var match = new CaptureMatch<T>(collection.Add, predicate);
Expand All @@ -73,6 +75,7 @@ public static T In<T>(IList<T> collection, Expression<Func<T, bool>> predicate)
/// Assert.Equal("Hello!", capturedValue);
/// </code>
/// </example>
[Matcher]
public static T With<T>(CaptureMatch<T> match)
{
return Match.Create(match);
Expand Down
4 changes: 3 additions & 1 deletion src/Moq/ExpressionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,10 @@ private static bool PartialMatcherAwareEval_ShouldEvaluate(Expression expression
return false;

case ExpressionType.Call:
return !((MethodCallExpression)expression).Method.IsDefined(typeof(MatcherAttribute), true);

case ExpressionType.MemberAccess:
return !expression.IsMatch(out _);
return !((MemberExpression)expression).Member.IsDefined(typeof(MatcherAttribute), true);

default:
return true;
Expand Down
10 changes: 10 additions & 0 deletions src/Moq/It.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ public static class Ref<TValue>
}

/// <include file='It.xdoc' path='docs/doc[@for="It.IsAny"]/*'/>
[Matcher]
public static TValue IsAny<TValue>()
{
return Match<TValue>.Create(
Expand All @@ -43,6 +44,7 @@ public static TValue IsAny<TValue>()
}

/// <include file='It.xdoc' path='docs/doc[@for="It.IsNotNull"]/*'/>
[Matcher]
public static TValue IsNotNull<TValue>()
{
return Match<TValue>.Create(
Expand All @@ -57,6 +59,7 @@ public static TValue IsNotNull<TValue>()


/// <include file='It.xdoc' path='docs/doc[@for="It.Is"]/*'/>
[Matcher]
[SuppressMessage("Microsoft.Design", "CA1006:DoNotNestGenericTypesInMemberSignatures")]
public static TValue Is<TValue>(Expression<Func<TValue, bool>> match)
{
Expand All @@ -66,6 +69,7 @@ public static TValue Is<TValue>(Expression<Func<TValue, bool>> match)
}

/// <include file='It.xdoc' path='docs/doc[@for="It.IsInRange"]/*'/>
[Matcher]
public static TValue IsInRange<TValue>(TValue from, TValue to, Range rangeKind)
where TValue : IComparable
{
Expand All @@ -87,30 +91,35 @@ public static TValue IsInRange<TValue>(TValue from, TValue to, Range rangeKind)
}

/// <include file='It.xdoc' path='docs/doc[@for="It.IsIn(enumerable)"]/*'/>
[Matcher]
public static TValue IsIn<TValue>(IEnumerable<TValue> items)
{
return Match<TValue>.Create(value => items.Contains(value), () => It.IsIn(items));
}

/// <include file='It.xdoc' path='docs/doc[@for="It.IsIn(params)"]/*'/>
[Matcher]
public static TValue IsIn<TValue>(params TValue[] items)
{
return Match<TValue>.Create(value => items.Contains(value), () => It.IsIn(items));
}

/// <include file='It.xdoc' path='docs/doc[@for="It.IsNotIn(enumerable)"]/*'/>
[Matcher]
public static TValue IsNotIn<TValue>(IEnumerable<TValue> items)
{
return Match<TValue>.Create(value => !items.Contains(value), () => It.IsNotIn(items));
}

/// <include file='It.xdoc' path='docs/doc[@for="It.IsNotIn(params)"]/*'/>
[Matcher]
public static TValue IsNotIn<TValue>(params TValue[] items)
{
return Match<TValue>.Create(value => !items.Contains(value), () => It.IsNotIn(items));
}

/// <include file='It.xdoc' path='docs/doc[@for="It.IsRegex(regex)"]/*'/>
[Matcher]
public static string IsRegex(string regex)
{
Guard.NotNull(regex, nameof(regex));
Expand All @@ -123,6 +132,7 @@ public static string IsRegex(string regex)
}

/// <include file='It.xdoc' path='docs/doc[@for="It.IsRegex(regex,options)"]/*'/>
[Matcher]
public static string IsRegex(string regex, RegexOptions options)
{
Guard.NotNull(regex, nameof(regex));
Expand Down
1 change: 1 addition & 0 deletions src/Moq/Match.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public abstract class Match : IMatcher
/// Provided for the sole purpose of rendering the delegate passed to the
/// matcher constructor if no friendly render lambda is provided.
/// </devdoc>
[Matcher]
internal static TValue Matcher<TValue>()
{
return default(TValue);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
// All rights reserved. Licensed under the BSD 3-Clause License; see License.txt.

using System;
using System.ComponentModel;

namespace Moq
{
Expand All @@ -12,9 +11,10 @@ namespace Moq
/// matching rules.
/// </summary>
/// <remarks>
/// <b>This feature has been deprecated in favor of the new
/// and simpler <see cref="Match{T}"/>.
/// </b>
/// <para>
/// This attribute may be used in conjunction with <see cref="Match.Create{T}"/>,
/// or with the older mechanism described below.
/// </para>
/// <para>
/// The argument matching is used to determine whether a concrete
/// invocation in the mock matches a given setup. This
Expand Down Expand Up @@ -119,9 +119,7 @@ namespace Moq
/// // use mock, invoke Save, and have the matcher filter.
/// </code>
/// </example>
[AttributeUsage(AttributeTargets.Method, Inherited = true)]
[EditorBrowsable(EditorBrowsableState.Never)]
[Obsolete("This feature has been deprecated in favor of `Match.Create`.")]
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Property, Inherited = true)]
public sealed class MatcherAttribute : Attribute
{
}
Expand Down
30 changes: 14 additions & 16 deletions src/Moq/MatcherFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -85,30 +85,28 @@ public static IMatcher CreateMatcher(Expression expression)
return matchExpression.Match;
}

if (expression.NodeType == ExpressionType.Call)
if (expression is MethodCallExpression call)
{
if (expression.IsMatch(out var match))
{
return match;
}

#pragma warning disable 618
var call = (MethodCallExpression)expression;
if (call.Method.IsDefined(typeof(MatcherAttribute), true))
{
if (expression.IsMatch(out var match))
{
return match;
}

return new MatcherAttributeMatcher(call);
}
#pragma warning restore 618
else
{
return new LazyEvalMatcher(originalExpression);
}

return new LazyEvalMatcher(originalExpression);
}
else if (expression.NodeType == ExpressionType.MemberAccess)
else if (expression is MemberExpression memberAccess)
{
if (expression.IsMatch(out var match))
if (memberAccess.Member.IsDefined(typeof(MatcherAttribute), true))
{
return match;
if (expression.IsMatch(out var match))
{
return match;
}
}
}

Expand Down
4 changes: 4 additions & 0 deletions tests/Moq.Tests/CustomMatcherFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ public void UsesCustomMatcherWithArgument()
Assert.True(mock.Object.Do(5));
}

[Matcher]
public TValue Any<TValue>()
{
return Match.Create<TValue>(v => true);
}

[Matcher]
public TValue Between<TValue>(TValue from, TValue to, Range rangeKind)
where TValue : IComparable
{
Expand Down Expand Up @@ -89,8 +91,10 @@ public void Custom_matcher_method_appears_by_name_in_verification_error_message(

public class Toy
{
[Matcher]
public static Toy IsRed => Match.Create((Toy toy) => toy.Color == "red");

[Matcher]
public static Toy IsGreen() => Match.Create((Toy toy) => toy.Color == "green");

public string Color { get; set; }
Expand Down
74 changes: 37 additions & 37 deletions tests/Moq.Tests/ExtensibilityFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;

using Xunit;

namespace Moq.Tests
Expand All @@ -14,7 +15,7 @@ public class ExtensibilityFixture
public void ShouldExtendMatching()
{
var mock = new Mock<IOrderRepository>();
mock.Setup(repo => repo.Save(OrderIs.Big()))
mock.Setup(repo => repo.Save(Order.IsBig()))
.Throws(new InvalidOperationException());

try
Expand Down Expand Up @@ -46,7 +47,7 @@ public void ShouldExtendWithSimpleMatchers()
public void ShouldExtendWithPropertyMatchers()
{
var mock = new Mock<IOrderRepository>();
mock.Setup(repo => repo.Save(Orders.IsSmall))
mock.Setup(repo => repo.Save(Order.IsSmall))
.Throws(new InvalidOperationException());

try
Expand All @@ -60,64 +61,63 @@ public void ShouldExtendWithPropertyMatchers()
}
}

//[Fact]
//public void SetterMatcherRendersNicely()
//{
// var mock = new Mock<IOrderRepository>();
[Fact]
public void Built_in_matcher_renders_nicely_when_using_delegate_based_setup_expression()
{
var mock = new Mock<IOrderRepository>();

// try
// {
// mock.VerifySet(repo => repo.Value = It.IsAny<int>());
// }
// catch (MockException me)
// {
// Console.WriteLine(me.Message);
// }
var ex = Record.Exception(() => mock.VerifySet(repo => repo.Value = It.IsAny<int>()));
Assert.Contains("repo => repo.Value = It.IsAny<int>()", ex.Message);
}

// mock.Object.Value = 25;
[Fact]
public void Custom_matcher_with_render_expression_renders_nicely_when_using_delegate_based_setup_expression()
{
var mock = new Mock<IOrderRepository>();

// mock.VerifySet(repo => repo.Value = It.IsInRange(10, 25, Range.Inclusive));
//}
var ex = Record.Exception(() => mock.VerifySet(repo => repo.OrderSavedLast = Order.IsBig()));
Assert.Contains("repo => repo.OrderSavedLast = Order.IsBig()", ex.Message);
}

[Fact]
public void Custom_matcher_without_render_expression_renders_semi_nicely_when_using_delegate_based_setup_expression()
{
var mock = new Mock<IOrderRepository>();

var ex = Record.Exception(() => mock.VerifySet(repo => repo.OrderSavedLast = Order.IsSmall));
Assert.Contains("repo => repo.OrderSavedLast = Match.Matcher<Order>()", ex.Message);
}
}

public static class Orders
{
[Matcher]
public static IEnumerable<Order> Contains(Order order)
{
return Match.Create<IEnumerable<Order>>(orders => orders.Contains(order));
}

public static Order IsBig()
{
return Match.Create<Order>(o => o.Amount >= 1000);
}

public static Order IsSmall
{
get
{
return Match.Create<Order>(o => o.Amount <= 1000);
}
}
}

public interface IOrderRepository
{
void Save(Order order);
void Save(IEnumerable<Order> orders);
Order OrderSavedLast { get; set; }
int Value { get; set; }
}

public static class OrderIs
public class Order
{
public static Order Big()
public int Amount { get; set; }

[Matcher]
public static Order IsBig()
{
return Match.Create<Order>(o => o.Amount >= 1000);
return Match.Create<Order>(o => o.Amount >= 1000, () => Order.IsBig());
}
}

public class Order
{
public int Amount { get; set; }

[Matcher]
public static Order IsSmall => Match.Create<Order>(o => o.Amount <= 1000);
}
}
1 change: 1 addition & 0 deletions tests/Moq.Tests/Linq/SupportedQuerying.cs
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ public void WhenUsingCustomMatcherForArgument_ThenSetsReturnValue()
Assert.Equal("foo", foo.Do(5));
}

[Matcher]
public TValue Any<TValue>()
{
return Match.Create<TValue>(v => true);
Expand Down
1 change: 0 additions & 1 deletion tests/Moq.Tests/MatcherAttributeFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@

namespace Moq.Tests
{
[Obsolete("This fixture contains tests related to `" + nameof(MatcherAttribute) + "`, which is obsolete.")]
public class MatcherAttributeFixture
{
public interface IFoo
Expand Down
1 change: 1 addition & 0 deletions tests/Moq.Tests/MockFixture.cs
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,7 @@ public void ShouldAllowMultipleCustomMatcherWithArguments()
Assert.Equal(3, mock.Object.Echo(3));
}

[Matcher]
private int IsMultipleOf(int value)
{
return Match.Create<int>(i => i % value == 0);
Expand Down
Loading

0 comments on commit 233063e

Please sign in to comment.