设置Mock返回值而不调用底层服务

时间:2014-05-21 09:42:00

标签: c# unit-testing tdd moq

让我们想象一下,我想测试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而不调用底层的StaticContext.Store.CurrentStoreId?

编辑:我知道此时此测试没有任何问题,但我想知道是否有办法以我要问的方式执行此操作。这只是我问题的简单版本。

3 个答案:

答案 0 :(得分:1)

不,您无法使用该服务测试服务。请查看使用Moles in MSTestsFakes(如果这是一个选项)。

你必须创建一个假装配:

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);
}

答案 1 :(得分:0)

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; }
    }
}

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

答案 2 :(得分:0)

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是null,因为它尚未设置),并且不太灵活。

之后你会告诉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)