You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在C#单元测试中手动Stub函数:自定义Mock拦截Product#Price调用以测试User#Foobar

How to Build a Custom Mock/Stub for Testing User#Foobar in C#

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 decouple User from the concrete Product class, 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 IProduct where we control the return value directly.

内容的提问来源于stack exchange,提问作者CaptainDucko

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.28 22:37:33