Skip to content

Commit 18191e2

Browse files
committed
* Use custom exception on protected method mock errors and additional unit tests to verify error conditions
* Rename unit tests with better description * Fix unit test failure due to orphaned argument spec when previous setup throws
1 parent 88998b2 commit 18191e2

File tree

6 files changed

+151
-11
lines changed

6 files changed

+151
-11
lines changed

src/NSubstitute/Core/ThreadLocalContext.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -112,9 +112,8 @@ public IList<IArgumentSpecification> DequeueAllArgumentSpecifications()
112112
public IList<IArgumentSpecification> PeekAllArgumentSpecifications()
113113
{
114114
var queue = _argumentSpecifications.Value;
115-
if (queue == null) { throw new SubstituteInternalException("Argument specification queue is null."); }
116115

117-
if (queue.Count > 0)
116+
if (queue?.Count > 0)
118117
{
119118
var items = new IArgumentSpecification[queue.Count];
120119

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace NSubstitute.Exceptions;
2+
3+
public class ProtectedMethodNotFoundException(string message, Exception? innerException) : SubstituteException(message, innerException)
4+
{
5+
public ProtectedMethodNotFoundException() : this("", null)
6+
{ }
7+
8+
public ProtectedMethodNotFoundException(string message) : this(message, null)
9+
{ }
10+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace NSubstitute.Exceptions;
2+
3+
public class ProtectedMethodNotVirtualException(string message, Exception? innerException) : SubstituteException(message, innerException)
4+
{
5+
public ProtectedMethodNotVirtualException() : this("", null)
6+
{ }
7+
8+
public ProtectedMethodNotVirtualException(string message) : this(message, null)
9+
{ }
10+
}

src/NSubstitute/Extensions/ProtectedExtensions.cs

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public static class ProtectedExtensions
1919
/// <param name="args">The method arguments.</param>
2020
/// <returns>Result object from the method invocation.</returns>
2121
/// <exception cref="NSubstitute.Exceptions.NullSubstituteReferenceException">Substitute - Cannot mock null object</exception>
22+
/// <exception cref="NSubstitute.Exceptions.ProtectedMethodNotFoundException">Error mocking method. Method must be protected virtual and with correct matching arguments and type</exception>
2223
/// <exception cref="System.ArgumentException">Must provide valid protected method name to mock - methodName</exception>
2324
public static object Protected<T>(this T obj, string methodName, params object[] args) where T : class
2425
{
@@ -28,8 +29,17 @@ public static object Protected<T>(this T obj, string methodName, params object[]
2829
IList<IArgumentSpecification> argTypes = SubstitutionContext.Current.ThreadContext.PeekAllArgumentSpecifications();
2930
MethodInfo mthdInfo = obj.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance, Type.DefaultBinder, argTypes.Select(x => x.ForType).ToArray(), null);
3031

31-
if (mthdInfo == null) { throw new Exception($"Method {methodName} not found"); }
32-
if (!mthdInfo.IsVirtual) { throw new Exception($"Method {methodName} is not virtual"); }
32+
if (mthdInfo == null)
33+
{
34+
_ = SubstitutionContext.Current.ThreadContext.DequeueAllArgumentSpecifications();
35+
throw new ProtectedMethodNotFoundException($"No protected virtual method found with signature {methodName}({string.Join(", ", argTypes.Select(x => x.ForType))}) in {obj.GetType().BaseType!.Name}. " +
36+
"Check that the method name and arguments are correct. Public virtual methods must use standard NSubstitute mocking. See the documentation for additional info.");
37+
}
38+
if (!mthdInfo.IsVirtual)
39+
{
40+
_ = SubstitutionContext.Current.ThreadContext.DequeueAllArgumentSpecifications();
41+
throw new ProtectedMethodNotVirtualException($"{mthdInfo} is not virtual. NSubstitute can only work with virtual members of the class that are overridable in the test assembly");
42+
}
3343

3444
return mthdInfo.Invoke(obj, args);
3545
}
@@ -43,6 +53,7 @@ public static object Protected<T>(this T obj, string methodName, params object[]
4353
/// <param name="args">The method arguments.</param>
4454
/// <returns>WhenCalled&lt;T&gt;.</returns>
4555
/// <exception cref="NSubstitute.Exceptions.NullSubstituteReferenceException">Substitute - Cannot mock null object</exception>
56+
/// <exception cref="NSubstitute.Exceptions.ProtectedMethodNotFoundException">Error mocking method. Method must be protected virtual and with correct matching arguments and type</exception>
4657
/// <exception cref="System.ArgumentException">Must provide valid protected method name to mock - methodName</exception>
4758
public static WhenCalled<T> When<T>(this T obj, string methodName, params object[] args) where T : class
4859
{
@@ -52,8 +63,17 @@ public static WhenCalled<T> When<T>(this T obj, string methodName, params object
5263
IList<IArgumentSpecification> argTypes = SubstitutionContext.Current.ThreadContext.PeekAllArgumentSpecifications();
5364
MethodInfo mthdInfo = obj.GetType().GetMethod(methodName, BindingFlags.NonPublic | BindingFlags.Instance, Type.DefaultBinder, argTypes.Select(y => y.ForType).ToArray(), null);
5465

55-
if (mthdInfo == null) { throw new Exception($"Method {methodName} not found"); }
56-
if (!mthdInfo.IsVirtual) { throw new Exception($"Method {methodName} is not virtual"); }
66+
if (mthdInfo == null)
67+
{
68+
_ = SubstitutionContext.Current.ThreadContext.DequeueAllArgumentSpecifications();
69+
throw new ProtectedMethodNotFoundException($"No protected virtual method found with signature {methodName}({string.Join(", ", argTypes.Select(x => x.ForType))}) in {obj.GetType().BaseType!.Name}. " +
70+
"Check that the method name and arguments are correct. Public virtual methods must use standard NSubstitute mocking. See the documentation for additional info.");
71+
}
72+
if (!mthdInfo.IsVirtual)
73+
{
74+
_ = SubstitutionContext.Current.ThreadContext.DequeueAllArgumentSpecifications();
75+
throw new ProtectedMethodNotVirtualException($"{mthdInfo} is not virtual. NSubstitute can only work with virtual members of the class that are overridable in the test assembly");
76+
}
5777

5878
return new WhenCalled<T>(SubstitutionContext.Current, obj, x => mthdInfo.Invoke(x, args), MatchArgs.AsSpecifiedInCall);
5979
}

tests/NSubstitute.Acceptance.Specs/Infrastructure/AnotherClass.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,11 @@ public abstract class AnotherClass
1414

1515
protected abstract void ProtectedMethodWithNoReturn(string msg, int i, char j);
1616

17+
public abstract void PublicVirtualMethod();
18+
19+
protected void ProtectedNonVirtualMethod()
20+
{ }
21+
1722
public string DoWork()
1823
{
1924
return ProtectedMethod();

tests/NSubstitute.Acceptance.Specs/ProtectedExtensionsTests.cs

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using NSubstitute.Acceptance.Specs.Infrastructure;
2+
using NSubstitute.Exceptions;
23
using NSubstitute.Extensions;
34
using NUnit.Framework;
45

@@ -29,8 +30,7 @@ public void Should_mock_and_verify_protected_method_with_arg()
2930
sub.Protected("ProtectedMethod", Arg.Any<int>()).Returns(expectedMsg);
3031

3132
Assert.That(worker.DoMoreWork(sub, 5), Is.EqualTo(expectedMsg));
32-
var a = sub.Received(1);
33-
a.Protected("ProtectedMethod", Arg.Any<int>());
33+
sub.Received(1).Protected("ProtectedMethod", Arg.Any<int>());
3434
}
3535

3636
[Test]
@@ -47,7 +47,55 @@ public void Should_mock_and_verify_protected_method_with_multiple_args()
4747
}
4848

4949
[Test]
50-
public void Should_mock_and_verify_method_with_no_return_and_no_args()
50+
public void Should_throw_on_mock_null_substitute()
51+
{
52+
Assert.Throws<NullSubstituteReferenceException>(() => (null as AnotherClass).Protected("ProtectedMethod"));
53+
}
54+
55+
[TestCase("")]
56+
[TestCase(" ")]
57+
[TestCase(null)]
58+
public void Should_throw_on_mock_invalid_method_name(string methodName)
59+
{
60+
var sub = Substitute.For<AnotherClass>();
61+
62+
Assert.Throws<ArgumentException>(() => sub.Protected(methodName));
63+
}
64+
65+
[Test]
66+
public void Should_throw_on_mock_method_not_found()
67+
{
68+
var sub = Substitute.For<AnotherClass>();
69+
70+
Assert.Throws<ProtectedMethodNotFoundException>(() => sub.Protected("MethodDoesNotExist"));
71+
}
72+
73+
[Test]
74+
public void Should_throw_on_mock_method_arg_mismatch()
75+
{
76+
var sub = Substitute.For<AnotherClass>();
77+
78+
Assert.Throws<ProtectedMethodNotFoundException>(() => sub.Protected("ProtectedMethod", Arg.Any<IEnumerable<char>>()));
79+
}
80+
81+
[Test]
82+
public void Should_throw_on_mock_public_virtual_method()
83+
{
84+
var sub = Substitute.For<AnotherClass>();
85+
86+
Assert.Throws<ProtectedMethodNotFoundException>(() => sub.Protected("PublicVirtualMethod"));
87+
}
88+
89+
[Test]
90+
public void Should_throw_on_mock_non_virtual()
91+
{
92+
var sub = Substitute.For<AnotherClass>();
93+
94+
Assert.Throws<ProtectedMethodNotVirtualException>(() => sub.Protected("ProtectedNonVirtualMethod"));
95+
}
96+
97+
[Test]
98+
public void Should_mock_and_verify_void_method_and_no_args()
5199
{
52100
var count = 0;
53101
var sub = Substitute.For<AnotherClass>();
@@ -61,7 +109,7 @@ public void Should_mock_and_verify_method_with_no_return_and_no_args()
61109
}
62110

63111
[Test]
64-
public void Should_mock_and_verify_method_with_no_return_with_arg()
112+
public void Should_mock_and_verify_void_method_with_arg()
65113
{
66114
var count = 0;
67115
var sub = Substitute.For<AnotherClass>();
@@ -75,7 +123,7 @@ public void Should_mock_and_verify_method_with_no_return_with_arg()
75123
}
76124

77125
[Test]
78-
public void Should_mock_and_verify_method_with_no_return_with_multiple_args()
126+
public void Should_mock_and_verify_void_method_with_multiple_args()
79127
{
80128
var count = 0;
81129
var sub = Substitute.For<AnotherClass>();
@@ -88,6 +136,54 @@ public void Should_mock_and_verify_method_with_no_return_with_multiple_args()
88136
sub.Received(1).Protected("ProtectedMethodWithNoReturn", Arg.Any<string>(), Arg.Any<int>(), Arg.Any<char>());
89137
}
90138

139+
[Test]
140+
public void Should_throw_on_void_method_null_substitute()
141+
{
142+
Assert.Throws<NullSubstituteReferenceException>(() => (null as AnotherClass).When("ProtectedMethod"));
143+
}
144+
145+
[TestCase("")]
146+
[TestCase(" ")]
147+
[TestCase(null)]
148+
public void Should_throw_on_mock_void_method_invalid_method_name(string methodName)
149+
{
150+
var sub = Substitute.For<AnotherClass>();
151+
152+
Assert.Throws<ArgumentException>(() => sub.When(methodName));
153+
}
154+
155+
[Test]
156+
public void Should_throw_on_mock_void_method_not_found()
157+
{
158+
var sub = Substitute.For<AnotherClass>();
159+
160+
Assert.Throws<ProtectedMethodNotFoundException>(() => sub.When("MethodDoesNotExist"));
161+
}
162+
163+
[Test]
164+
public void Should_throw_on_mock_void_method_arg_mismatch()
165+
{
166+
var sub = Substitute.For<AnotherClass>();
167+
168+
Assert.Throws<ProtectedMethodNotFoundException>(() => sub.When("ProtectedMethod", Arg.Any<IEnumerable<char>>()));
169+
}
170+
171+
[Test]
172+
public void Should_throw_on_mock_public_virtual_void_method()
173+
{
174+
var sub = Substitute.For<AnotherClass>();
175+
176+
Assert.Throws<ProtectedMethodNotFoundException>(() => sub.When("PublicVirtualMethod"));
177+
}
178+
179+
[Test]
180+
public void Should_throw_on_mock_non_virtual_void_method()
181+
{
182+
var sub = Substitute.For<AnotherClass>();
183+
184+
Assert.Throws<ProtectedMethodNotVirtualException>(() => sub.When("ProtectedNonVirtualMethod"));
185+
}
186+
91187
private class Worker
92188
{
93189
internal string DoWork(AnotherClass worker)

0 commit comments

Comments
 (0)