如何在C#单元测试中手动Stub函数:自定义Mock拦截Product#Price调用以测试User#Foobar
Great question—building your own mocking logic is a fantastic way to understand how frameworks like Moq work under the hood. Let's walk through this step by step, since your current code has a key issue that's blocking you first.
First: Fix the Tight Coupling
The problem right now is that User.Foobar() directly creates a new Product instance inside the method. This means you can't swap out the real Product with a mocked one—they're tightly coupled. The fix here is to use dependency injection with an interface.
Let's refactor your classes to depend on an abstraction instead of the concrete Product:
// Define an interface for Product so we can swap implementations public interface IProduct { string Price(); } // The real Product class implements the interface public class Product : IProduct { public string Price() { return "I WANT TO STUB THIS OUT, DONT WANT TO SEE THIS STRING"; } } // User now accepts an IProduct via constructor injection instead of creating it public class User { private readonly IProduct _product; public User(IProduct product) { _product = product; } public string Foobar() { return $"Foobar, Product#price returns: {_product.Price()}"; } }
Now User doesn't care what kind of IProduct it gets—it could be the real one or our mocked version.
Option 1: Build a Generic Mock with Dynamic Proxies
If you want a reusable mock system (like Moq), you'll need to use dynamic proxies to intercept method calls. The easiest way to do this without writing tons of reflection code is to use the Castle.Core NuGet package (this is what Moq uses internally).
Here's a simple mock implementation:
using Castle.DynamicProxy; using System.Collections.Generic; public class Mock<T> where T : class { private readonly Dictionary<string, object> _stubbedMethods = new(); // Tell the mock what value to return for a specific method public void Stub(string methodName, object returnValue) { _stubbedMethods[methodName] = returnValue; } // Create the mocked instance public T Create() { var generator = new ProxyGenerator(); return generator.CreateInterfaceProxyWithoutTarget<T>(new MockInterceptor(_stubbedMethods)); } } // This interceptor handles method calls to the mock public class MockInterceptor : IInterceptor { private readonly Dictionary<string, object> _stubbedMethods; public MockInterceptor(Dictionary<string, object> stubbedMethods) { _stubbedMethods = stubbedMethods; } public void Intercept(IInvocation invocation) { // Check if we have a stubbed value for this method if (_stubbedMethods.TryGetValue(invocation.Method.Name, out var returnValue)) { invocation.ReturnValue = returnValue; return; } // If no stub is set, you could throw an error or proceed (your call) throw new System.NotImplementedException($"No stub set for method {invocation.Method.Name}"); } }
Then update your test code to use this mock:
public class MyUnitTest { public void RunTests() { // Create a mock for IProduct var productMock = new Mock<IProduct>(); productMock.Stub("Price", "MOCKED DATA"); // Inject the mocked product into User var user = new User(productMock.Create()); string actual = user.Foobar(); string expected = "Foobar, Product#price returns: MOCKED DATA"; Assert(actual, expected); } public void Assert(string actual, string expected) { Console.WriteLine(actual == expected ? "✅ Test Passed!" : $"❌ Test Failed: Expected '{expected}', Got '{actual}'"); } } public class Program { public static void Main() { new MyUnitTest().RunTests(); } }
Option 2: Manual Stub (No External Dependencies)
If you don't want to use a NuGet package, you can create a manual stub class for IProduct. This is simpler for one-off tests:
// A manual stub that lets us set the Price return value public class StubbedProduct : IProduct { public string PriceReturnValue { get; set; } public string Price() { return PriceReturnValue; } } // Update your test to use the stub public void RunTests() { var stubbedProduct = new StubbedProduct { PriceReturnValue = "MOCKED DATA" }; var user = new User(stubbedProduct); string actual = user.Foobar(); string expected = "Foobar, Product#price returns: MOCKED DATA"; Assert(actual, expected); }
This is lightweight and doesn't require any external libraries—perfect for simple scenarios.
Why This Works
- Abstraction: By using
IProduct, we decoupleUserfrom the concreteProductclass, making it testable. - Interception (Option 1): The dynamic proxy intercepts calls to
Price()and returns our stubbed value instead of executing the real method. - Manual Stub (Option 2): We create a fake implementation of
IProductwhere we control the return value directly.
内容的提问来源于stack exchange,提问作者CaptainDucko

