在不调用基础服务的情况下设置Mock返回值

本文关键字:情况下 设置 Mock 返回值 服务 调用 | 更新日期: 2023-09-27 18:04:28

让我们假设我们有我想测试的PaymentService

public interface IPaymentService
{
    int Pay(int clientId);
}    
public class PaymentService : IPaymentService
{
    // Insert payment and return PaymentID
    public int Pay(int clientId)
    {
        int storeId = StaticContext.Store.CurrentStoreId; // throws NullReferenceException
        // ... other related tasks
    }
}
public class Payment_Tests
{
    [Test]
    public void When_Paying_Should_Return_PaymentId
    {
        // Arrange
        var paymentServiceMock = new Mock<IPaymentService>();
        paymentService.Setup(x=>x.Pay(Moq.It.IsAny<int>).Returns(999); // fails because of NullReferenceException inside Pay method.
        // Act
        var result = paymentService.Object.Pay(123);
        // Asserts and rest of the test goes here
    }
}

但我无法模拟StaticContext类我无法重构它,也无法通过构造函数将此类注入IPaymentService-这是旧代码,必须保持原样:(

有没有可能简单地返回预期的结果,在我的情况下是999,而不调用底层StaticContent.Store.CurrentStoreId?

编辑:我知道目前这个测试没有意义,但我想知道是否有办法以我要求的方式做到这一点。这只是我问题的简化版本。

在不调用基础服务的情况下设置Mock返回值

不,您不能使用它来测试服务。看看在MSTests或Fakes中使用Moles(如果可以的话(。

你必须创建一个假的程序集:

using (ShimsContext.Create())
{
    var paymentServiceMock = new Mock<IPaymentService>();
    paymentService.Setup(x=>x.Pay(Moq.It.IsAny<int>).Returns(999);
    // Shim DateTime.Now to return a fixed date:
    System.Fakes.ShimDateTime.StaticContext.Store.CurrentStoreIdGet = () =>  { 1 };
    // Act
    var result = paymentService.Object.Pay(123);
}

Mock对象是Moq为您正在模拟的接口生成的代理类。所以,当你练习模拟对象时

var result = paymentService.Object.Pay(123);

实际上,您正在验证Moq框架的实现——它是否返回您为mock设置的结果。我认为您不想对Moq框架进行单元测试。若您正在为PaymentService类编写测试,那个么您应该练习这个类的实例。但它内部有静态依赖关系。所以,第一步将使PaymentService可测试——即用抽象替换静态依赖关系,并将这些抽象注入PaymentService实例。

public interface IStore
{
    int CurrentStoreId { get; }
}

然后使PaymentService依赖于这个抽象:

public class PaymentService : IPaymentService
{
    private IStore _store;
    public PaymentService(IStore store)
    {
        _store = store;
    }
    public int Pay(int clientId)
    {
        int storeId = _store.CurrentStoreId;
        // ... other related tasks
    }    
}

因此,现在不存在静态依赖关系。下一步是编写PaymentService的测试,它将使用模拟依赖项:

[Test]
public void When_Paying_Should_Return_PaymentId
{
    // Arrange
    var storeMock = new Mock<IStore>();
    storeMock.Setup(s => s.CurrentStoreId).Returns(999);
    var paymentService = new PaymentService(storeMock.Object);
    // Act
    var result = paymentService.Pay(123);
    storeMock.Verify();
    // Asserts and rest of the test goes here        
}

最后是IStore抽象的实际实现。您可以创建类,将调用委托给静态StaticContext.Store:

public class StoreWrapper : IStore
{
    public int CurrentStoreId 
    {
       get { return StaticContext.Store.CurrentStoreId; }
    }
}

在实际应用程序中设置依赖关系时使用此包装。

public interface IPaymentService
{
    int Pay(int clientId);
}    
public interface IStore
{
    int ID { get; }
    // Returns the payment ID of the payment you just created
    // You would expand this method to include more parameters as
    // necessary
    int CreatePayment();
}
public class PaymentService : IPaymentService
{
    private readonly IStore _store;
    public PaymentService(IStore store)
    {
        _store = store;
    }
    // Insert payment and return PaymentID
    public int Pay(int clientId)
    {
        //int storeId = StaticContext.Store.CurrentStoreId;
        // Static is bad for testing and this also means you're hiding
        // Payment Service's dependencies. Inject a store into the constructor
        var storeId = _store.ID;
        // stuff
        ....
        return _store.CreatePayment();
    }
}
public class Payment_Tests
{
    [Test]
    public void When_Paying_Should_Return_PaymentId
    {
        // Arrange
        var store = new Mock<IStore>();
        var expectedId = 42;
        store.Setup(x => x.CreatePayment()).Returns(expectedId);
        var service = new PaymentService(store);
        // Act
        var result = paymentService.Pay(123);
        // Asserts and rest of the test goes here
        Assert.Equal(expectedId, result);
    }
}

IStore对象注入PaymentService-使用StaticContext是关于PaymentService的依赖关系的,违反了最小惊喜原则(开发人员试图使用PaymentService,然后意识到他们必须在抛出异常和一些挖掘(例如,通过注入依赖关系在构造函数中没有记录(后做其他事情(,这使得测试更加困难(正如您所注意到的,StaticContext.Store为空,因为它还没有设置(,并且灵活性降低。

之后,您将告诉Store从CreatePayment返回某个值,并测试该服务是否返回相同的值(即支付ID(

编辑:

我无法重构它,也无法通过构造函数将此类注入IPaymentService-这是旧代码,必须保持原样:(

关于这个注释,在这种情况下,你能做的最好的事情就是将StaticContext.Store值设置为一个伪造的Store对象,该对象返回一个硬编码的数字,并测试。。。但实际上,您应该重构这段代码,因为从长远来看,这会让它变得容易得多。

// inside test code
// obviously change the type as necessary
// as C# doesn't have ducktyping
class FakedStore
{
   public int CurrentStoreId { get { return 42; } }
}
var store = new FakedStore();
StaticContext.Store = store;
// rest your test to test the payment service
var result = ..
Assert.Equals(result, store.CurrentStoreId)