【问题标题】:Testing underlying classes with MOQ - input requested使用最小起订量测试底层类 - 请求输入
【发布时间】:2011-12-14 21:49:46
【问题描述】:

我正在开发一个同步引擎。我有一个主引擎类,它从两个提供者(作为构造函数传入的对象)获取两个 PickList 列表。方法 SyncPickLists() 调用底层对象(两个提供者 + 一个记录器)上的方法,一个内部方法来获取所有要做的事情的列表,然后这样做:

  public class SyncEngine {
    public virtual ILoggingProvider Logger { get; private set; }
    public virtual ICrmProvider CrmProvider { get; private set; }
    public virtual ICacheProvider CacheProvider { get; private set; }

    public SyncEngine(ILoggingProvider loggingProvider, ICrmProvider crmProvider, ICacheProvider cacheProvider) {
      Logger = loggingProvider;
      CrmProvider = crmProvider;
      CacheProvider = cacheProvider;
    }

    public virtual void SyncPickLists() {
      Logger.LogBeginPicklistSync();

      // get all the pick lists from the local cache
      var localCachePickLists = CacheProvider.GetPickLists().ToList();
      // get all the pick lists from the remote system
      var crmPickLists = CrmProvider.GetPickLists().ToList();

      // build a sync plan
      var changes = BuildPickListUpdatePlan(localCachePickLists, crmPickLists).ToList();

      // run the sync
      RunPickListSync(changes);

      Logger.LogEndPicklistSync();
    }

    private IEnumerable<PickListAction> BuildPickListUpdatePlan(List<PickList> localCachePickLists, List<PickList> crmPickLists) {
      ...
    }
  }

我正在尝试使用 Moq 从同步引擎开始模拟和测试它,但遇到问题检查记录器上的方法被调用并评估从 BuildPickListUpdatePlan() 方法生成的结果。我认为这是因为我试图启动提供程序并调用 SyncPickLists()... 这可能是错误的方法。也许我应该使 BuildPickListUpdatePlan() 方法公开/内部(并适当地装饰程序集)并进行测试?在这里寻找输入,因为我是新手,不确定我是否以正确的方式进行。

这是我的测试到目前为止的样子,但它不完整、不正确或没有做我需要做的事情......

[TestMethod]
public void TestPickListSync() {
  // assign
  var _fakeCrmProvider = new Mock<ICrmProvider>().Object;
  var _fakeCacheProvider = new Mock<ICacheProvider>().Object;
  var _fakeLoggingProvider = new Mock<ILoggingProvider>().Object;
  var _fakeSyncEngine = new Mock<CustomerSyncEngine>(_fakeLoggingProvider, _fakeCrmProvider, _fakeCacheProvider).Object;

  // set picklists for CRM provider
  var crmList1 = new List<string>() { "AAA", "CCC", "DDD" };
  var crmList2 = new List<string>() { "WWW", "XXX", "YYY" };
  var crmPickLists = new List<PickList>() {
    new PickList(){ InternalName = "PickList1", DisplayName = "PickList1", Values = crmList1 },
    new PickList(){ InternalName = "PickList2", DisplayName = "PickList2", Values = crmList2 }
  };
  Mock.Get(_fakeCrmProvider).Setup(x => x.GetPickLists()).Returns(crmPickLists);
  Mock.Get(_fakeSyncEngine).SetupGet(x => x.CrmProvider).Returns(_fakeCrmProvider);

  // set picklists for cache provider
  var cacheList1 = new List<string>() { "AAA", "BBB", "CCC" };
  var cacheList2 = new List<string>() { "WWW", "XXX", "ZZZ" };
  var cachePickLists = new List<PickList>() {
    new PickList(){ InternalName = "PickList1", DisplayName = "PickList1", Values = cacheList1 },
    new PickList(){ InternalName = "PickList2", DisplayName = "PickList2", Values = cacheList2 }
  };
  Mock.Get(_fakeCacheProvider).Setup(x => x.GetPickLists()).Returns(cachePickLists);
  Mock.Get(_fakeSyncEngine).SetupGet(x => x.CacheProvider).Returns(_fakeCacheProvider);

  // act
  _fakeSyncEngine.SyncPickLists();

  // assert
  Mock.Get(_fakeLoggingProvider).Verify(x => x.LogBeginPicklistSync(), Times.Once());
  Mock.Get(_fakeLoggingProvider).Verify(x => x.LogEndPicklistSync(), Times.Once());
}

【问题讨论】:

  • 问题,SyncEngine 和 public virtual 上的三个属性是用于测试的吗?

标签: moq


【解决方案1】:

首先,您永远不要模拟被测类。在这种情况下,您正在测试 SyncEngine,因此您不应模拟它。

如果一切都是模拟的,那么你就没有测试任何东西。模拟用于抽象出测试中的依赖关系并方便编写测试。

您将依赖项抽象为:

  • 将测试集中在要测试的代码上
  • 抽象出对数据库、文件系统等事物的依赖, 其他流程等。
  • 通过减少测试特定功能所需的代码或通过轻松从对象获取所需值来简化测试。

其次,总是有原因地模拟事物,总是考虑是否应该或不应该模拟每个类。

第三,一些人(包括我自己)认为测试私有方法是不好的做法。 BuildPickListUpdatePlan 是私有的,您应该测试使用 BuildPickListUpdatePlan 的类的公共方法。

测试私有方法通常需要“技巧”来正确测试并导致测试非常脆弱。根据我的经验,这些测试更容易需要更改,而不是在您的类的公共接口或 API 上进行测试。除非您有未使用的私有方法(在这种情况下它们是死代码并被删除而不是测试),否则您的所有私有方法都将被您的公共方法调用。集中精力测试这些。

我不知道你的代码的细节,所以我的提议可能是错误的,但我建议你的测试代码如下所示:

[TestMethod]
public void TestPickListSync() {
  // Consider if each of these should be mocked, in this case the answer seems to be yes
  var _fakeCrmProvider = new Mock<ICrmProvider>();
  var _fakeCacheProvider = new Mock<ICacheProvider>();
  var _fakeLoggingProvider = new Mock<ILoggingProvider>();

  // Don't mock the class under test
  var syncEngine = SyncEngine(_fakeLoggingProvider.Object, _fakeCrmProvider.Object, _fakeCacheProvider.Object);

  // 
  var crmList1 = new List<string>() { "AAA", "CCC", "DDD" };
  var crmList2 = new List<string>() { "WWW", "XXX", "YYY" };
  var crmPickLists = new List<PickList>() {
    new PickList(){ InternalName = "PickList1", DisplayName = "PickList1", Values = crmList1 },
    new PickList(){ InternalName = "PickList2", DisplayName = "PickList2", Values = crmList2 }
  };

  // Since your class under test is already using the mocks, there 
  // is no need to "mock.get", just mock the methods that will be called on the mocked objects
  _fakeCrmProvider.Setup(x => x.GetPickLists()).Returns(crmPickLists);

  // set picklists for cache provider
  var cacheList1 = new List<string>() { "AAA", "BBB", "CCC" };
  var cacheList2 = new List<string>() { "WWW", "XXX", "ZZZ" };
  var cachePickLists = new List<PickList>() {
    new PickList(){ InternalName = "PickList1", DisplayName = "PickList1", Values = cacheList1 },
    new PickList(){ InternalName = "PickList2", DisplayName = "PickList2", Values = cacheList2 }
  };

  // Since your class under test is already using the mocks, there 
  // is no need to "mock.get", just mock the methods that will be called on the mocked objects
  _fakeCacheProvider.Setup(x => x.GetPickLists()).Returns(cachePickLists);


  // act
  _syncEngine.SyncPickLists();

  // You can do your asserts like this
  _fakeLoggingProvider.Verify(x => x.LogBeginPicklistSync(), Times.Once());
  _fakeLoggingProvider.Verify(x => x.LogEndPicklistSync(), Times.Once());
}

我手头没有VS,所以无法检查语法,但它应该能给你一些想法。我已经用解释替换了你的 cmets。

下面是一些简单的代码,展示了一个模拟依赖项的基本示例:

[TestMethod]
public void SampleTestMethod() {
    // Arrange
    var mockMyLoggingClass = new Mock<IMyLogging>();

    var classUnderTest = new ClassUnderTest(mockMyLoggingClass.Object);

    mockMyLoggingClass.Setup(mock = > mock.MethodThatWillGetCalled()).Returns(someValue)

    // Act
    var result = classUnderTest.MethodThatWillCallLoggingClass();

    // Assert
    Assert.AreEqual(expectedValue, result); // we can still do normal asserts apart from the verify
    mockMyLoggingClass.Verify(mock => mock.MethodThatWillGetCalled(), Times.Once());
}

【讨论】:

  • 谢谢@Gilles ...这有帮助。将我带到下一个问题,这是我真正需要测试的。如果您查看我在 SyncPickLists() 方法中的原始代码,您会看到我调用的是私有的 BuildSyncListPlan(),然后是 RunPickListSync(),也是私有的。我需要测试的是确保 BuildPickListSync() 得到正确的操作来执行......这就是我询问测试该私有方法的原因。理想情况下,我也想测试 RunPickListSync() 方法,以确保它也使用 Verify() 结构执行正确的操作。这可行吗?
  • 发送到 BuildPickListUpdatePlan 的操作由 CacheProvider.GetPickLists() 和 CrmProvider.GetPickLists() 提供。您最好的选择是对这两种方法进行其他测试。如果您有测试可以确保这两种方法都返回正确的参数并且 SyncPickLists() 也是正确的,那么您是非常安全的。另一种选择是不模拟这两个提供程序并在 SyncPickLists() 测试中测试最终结果。
猜你喜欢
  • 2020-07-25
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2016-11-07
相关资源
最近更新 更多