【问题标题】:Setup Mock return value without calling underlying service在不调用底层服务的情况下设置 Mock 返回值
【发布时间】:2014-05-21 09:42:00
【问题描述】:

假设我们有我想测试的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?

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

【问题讨论】:

  • 那时,您认为您实际上会测试什么?如果你模拟了IPaymentService,你就不是在测试PaymentService...
  • 看起来你在模拟运动。你想在这里测试什么?
  • 是的,但这只是我的问题的简化版本。我真正的 PaymentService 用于我的代码的其他部分。我只想跳过支付方式。
  • 如果你试图做一个部分模拟(这就是它的样子),这表明你需要将你的班级分成另一个班级
  • StaticContext 是静态类吗?如果是,Moq 不能模拟静态类或静态方法。

标签: c# unit-testing tdd moq


【解决方案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);
}

【讨论】:

  • 看起来Fakes 只在VS Ultimate 中?我有专业的。
  • 我认为他们在 VS 2012 的 Update4 中将其包含在 Professional 中
  • 我没有,我也在使用 VS 2012 Professional,今天早上更新。似乎它仍然只存在于 13 年。
【解决方案2】:

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

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

您实际上是在验证 Moq 框架的实现——它是否返回您为模拟设置的结果。我认为您不想对 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; }
    }
}

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

【讨论】:

  • 我同意,但正如我所说:我不能重构这个。在我的情况下这是禁止的。悲伤但真实。
  • @Dariusz 那么事实是 - 你不能用 Moq 对它进行单元测试。您唯一的选择是使用允许模拟静态依赖项的框架(但其中大多数不是免费的)。但更好的选择是尝试重构此代码,使其松散耦合和可测试
【解决方案3】:
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)

【讨论】:

    猜你喜欢
    • 2022-10-01
    • 2014-09-11
    • 1970-01-01
    • 2021-11-04
    • 1970-01-01
    • 1970-01-01
    • 2011-08-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多