diff --git a/CHANGELOG.md b/CHANGELOG.md index d50a10327..f6cbe09c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ The format is loosely based on [Keep a Changelog](http://keepachangelog.com/en/1 #### Added * New method overloads for `It.Is`, `It.IsIn`, and `It.IsNotIn` that compare values using a custom `IEqualityComparer` (@weitzhandler, #1064) -* New property `IInvocation.ReturnValue` to query recorded invocations return values (@MaStr11, #921) +* New properties `ReturnValue` and `Exception` on `IInvocation` to query recorded invocations return values or exceptions (@MaStr11, #921, #1077) ## 4.14.7 (2020-10-14) diff --git a/src/Moq/IInvocation.cs b/src/Moq/IInvocation.cs index 8324365b6..cd9e6a21b 100644 --- a/src/Moq/IInvocation.cs +++ b/src/Moq/IInvocation.cs @@ -1,6 +1,7 @@ // 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; using System.Collections.Generic; using System.Reflection; @@ -32,8 +33,13 @@ public interface IInvocation bool IsVerified { get; } /// - /// Gets the return value of the invocation. + /// The value being returned for a non-void method if no exception was thrown. /// object ReturnValue { get; } + + /// + /// Optional exception if the method invocation results in an exception being thrown. + /// + Exception Exception { get; } } } \ No newline at end of file diff --git a/src/Moq/Invocation.cs b/src/Moq/Invocation.cs index db641e315..cbb08e0ec 100644 --- a/src/Moq/Invocation.cs +++ b/src/Moq/Invocation.cs @@ -71,7 +71,9 @@ public MethodInfo MethodImplementation public object ReturnValue { - get => this.returnValue; + get => this.returnValue is InvocationExceptionWrapper + ? null + : this.returnValue; set { Debug.Assert(this.returnValue == null); @@ -79,6 +81,11 @@ public object ReturnValue } } + public Exception Exception + => this.returnValue is InvocationExceptionWrapper wrapper + ? wrapper.Exception + : null; + public bool IsVerified => this.verified; /// diff --git a/src/Moq/InvocationExceptionWrapper.cs b/src/Moq/InvocationExceptionWrapper.cs new file mode 100644 index 000000000..213c44e13 --- /dev/null +++ b/src/Moq/InvocationExceptionWrapper.cs @@ -0,0 +1,22 @@ +// 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 +{ + /// + /// Internal type to mark invocation results as "exception occurred during execution". The type just + /// wraps the Exception so a thrown exception can be distinguished from an + /// return by the invocation. + /// + internal readonly struct InvocationExceptionWrapper + { + public InvocationExceptionWrapper(Exception exception) + { + Exception = exception; + } + + public Exception Exception { get; } + } +} diff --git a/src/Moq/ProxyFactories/CastleProxyFactory.cs b/src/Moq/ProxyFactories/CastleProxyFactory.cs index e99e9e57d..26d9775ec 100644 --- a/src/Moq/ProxyFactories/CastleProxyFactory.cs +++ b/src/Moq/ProxyFactories/CastleProxyFactory.cs @@ -101,6 +101,11 @@ public void Intercept(Castle.DynamicProxy.IInvocation underlying) this.interceptor.Intercept(invocation); underlying.ReturnValue = invocation.ReturnValue; } + catch (Exception ex) + { + invocation.ReturnValue = new InvocationExceptionWrapper(ex); + throw; + } finally { invocation.DetachFromUnderlying(); diff --git a/tests/Moq.Tests/InvocationsFixture.cs b/tests/Moq.Tests/InvocationsFixture.cs index d67258d30..8e4558a4f 100644 --- a/tests/Moq.Tests/InvocationsFixture.cs +++ b/tests/Moq.Tests/InvocationsFixture.cs @@ -2,6 +2,8 @@ // All rights reserved. Licensed under the BSD 3-Clause License; see License.txt. using System; +using System.Collections; +using System.Collections.Generic; using System.Linq; using Xunit; @@ -45,11 +47,116 @@ public void MockInvocationsIncludeArguments() var invocation = mock.Invocations[0]; - var expectedArguments = new[] {obj}; + var expectedArguments = new[] { obj }; Assert.Equal(expectedArguments, invocation.Arguments); } + [Fact] + public void MockInvocationsIncludeReturnValue_NoSetup() + { + var mock = new Mock(); + + var obj = new object(); + + mock.Object.CompareTo(obj); + + var invocation = mock.Invocations[0]; + + Assert.Equal(0, invocation.ReturnValue); + Assert.Null(invocation.Exception); + } + + [Fact] + public void MockInvocationsIncludeReturnValue_Setup() + { + var mock = new Mock(); + var obj = new object(); + mock.Setup(c => c.CompareTo(obj)).Returns(42); + + mock.Object.CompareTo(obj); + + var invocation = mock.Invocations[0]; + + Assert.Equal(42, invocation.ReturnValue); + Assert.Null(invocation.Exception); + } + + [Fact] + public void MockInvocationsIncludeReturnValue_BaseCall() + { + var mock = new Mock(1) // seed: 1 + { + CallBase = true, + }; + + mock.Object.Next(); + + var invocation = mock.Invocations[0]; + + Assert.Equal(new Random(Seed: 1).Next(), invocation.ReturnValue); + Assert.Null(invocation.Exception); + } + + [Fact] + public void MockInvocationsIncludeReturnValue_ReturnsException() + { + var mock = new Mock(); + var returnValue = new Exception(); + mock.Setup(c => c.Clone()).Returns(returnValue); + + mock.Object.Clone(); + + var invocation = mock.Invocations[0]; + + Assert.Equal(returnValue, invocation.ReturnValue); + Assert.Null(invocation.Exception); + } + + [Fact] + public void MockInvocationsIncludeException_Setup() + { + var mock = new Mock(); + var exception = new Exception("Message"); + mock.Setup(c => c.CompareTo(It.IsAny())).Throws(exception); + + var thrown = Assert.Throws(() => mock.Object.CompareTo(null)); + + Assert.Equal(exception.Message, thrown.Message); + + var invocation = mock.Invocations[0]; + Assert.Same(thrown, invocation.Exception); + } + + [Fact] + public void MockInvocationsIncludeException_BaseCall_Virtual() + { + var mock = new Mock() + { + CallBase = true, + }; + + var thrown = Assert.Throws(() => mock.Object.ThrowingVirtualMethod()); + + Assert.Equal("Message", thrown.Message); + + var invocation = mock.Invocations[0]; + Assert.Same(thrown, invocation.Exception); + } + + [Fact] + public void MockInvocationsIncludeException_MockException() + { + var mock = new Mock(MockBehavior.Strict); + + var thrown = Assert.Throws(() => mock.Object.Clone()); + + Assert.Equal(MockExceptionReasons.NoSetup, thrown.Reasons); + + var invocation = mock.Invocations[0]; + Assert.Same(thrown, invocation.Exception); + } + [Fact] public void MockInvocationsCanBeEnumerated() { @@ -269,5 +376,10 @@ public FlagInitiallySetToTrue() public virtual bool Flag { get; set; } } + + public class Test + { + public virtual int ThrowingVirtualMethod() => throw new InvalidOperationException("Message"); + } } }