Repository: devlooped/moq

Issue:
`IInvocation` should expose `Exception` along with `ReturnValue`
#921 which just got merged added a `ReturnValue` property to `IInvocation`, which allows user code to find out what got returned from a mocked invocation. The author of that PR (@MaStr11) suggested that an `Exception` property also be added for similar reasons; if an invocation throws, user code might want to find out what it threw.

I think we ought to add `Exception Exception { get; }` to `IInvocation` for completeness. Incidentally, we'd then reach [parity with Moq 5](https://github.com/moq/moq/blob/a76c3cea64a4d9be4934d048c19d9c041ef487d5/src/Stunts/Stunts/IMethodReturn.cs#L16-L20) in this particular regard.

#### Some implementation notes

* I [mentioned in that PR](https://github.com/moq/moq4/pull/921#issuecomment-706759795) that the addition of an `Exception` property wouldn't make `Invocation` heavier (by adding a corresponding field). The existing field `Invocation.returnValue` could serve double duty (holding either a return value, or an exception). We'd need to ensure that methods returning an exception as a regular return value should not report that via the new `Exception` property.

* Also [mentioned in the PR] is the likely Moq [code location](https://github.com/moq/moq4/blob/947c6a0aa1bbf265b5555ad0a84beb5bfc53b4e2/src/Moq/ProxyFactories/CastleProxyFactory.cs#L99-L107) where `Exception` would be set.


Hints:
> P.S. @MaStr11: In case you were wondering where we stand regarding thrown exceptions, I didn't forget about it, but thought it would be cleaner to do that in a separate PR, to keep this one as straightforward as it was.

> If you would like to submit a PR that adds an Exception Exception { get; } property to IInvocation (as you described in (1) above), please feel free to do so, and I'll merge that, too.

> It shouldn't be too hard to do, setting the Invocation.Exception property would likely happen in a catch block being added there. (Ideally, we would make this addition without adding a new field to Invocation—to keep it as lightweight as possible–but that's not essential, it'd be more of an optimization. The existing returnValue field could serve double duty since an invocation cannot both return a value and throw. We'd only have to make sure that returned Exception objects won't leak out through the Exception property; this could be done by wrapping thrown exceptions in an internal type that only we can check for.)

From https://github.com/moq/moq4/pull/921#issuecomment-706759795

@stakx  I will give it a try.

Patch:
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<T>` (@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; }
 
 		/// <summary>
-		/// Gets the return value of the invocation.
+		/// The value being returned for a non-void method if no exception was thrown.
 		/// </summary>
 		object ReturnValue { get; }
+
+		/// <summary>
+		/// Optional exception if the method invocation results in an exception being thrown.
+		/// </summary>
+		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;
 
 		/// <summary>
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
+{
+	/// <summary>
+	/// 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 <see cref="System.Exception"/>
+	/// return by the invocation.
+	/// </summary>
+	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();


Test patch:
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<IComparable>();
+
+			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<IComparable>();
+			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<Random>(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<ICloneable>();
+			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<IComparable>();
+			var exception = new Exception("Message");
+			mock.Setup(c => c.CompareTo(It.IsAny<object>())).Throws(exception);
+
+			var thrown = Assert.Throws<Exception>(() => 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<Test>()
+			{
+				CallBase = true,
+			};
+
+			var thrown = Assert.Throws<InvalidOperationException>(() => 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<ICloneable>(MockBehavior.Strict);
+
+			var thrown = Assert.Throws<MockException>(() => 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");
+		}
 	}
 }