使用最小起订量测试底层类 - 请求输入

发布于 2024-12-21 06:52:26 字数 3453 浏览 1 评论 0原文

我正在开发同步引擎。我有一个主引擎类,它从两个提供者(作为构造函数传入的对象)获取两个 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());
}

I'm working on a sync engine. I have a main engine class that gets two lists of PickLists from two providers (objects passed in as constructors). The method SyncPickLists() calls methods on the underlying objects (the two providers + a logger), an internal method to get a list of everything to do, and then does it as such:

  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) {
      ...
    }
  }

I'm trying to use Moq to mock and test this starting with the sync engine, but running into issues checking ot see methods on the logger are called and evaluating the results that are generated from the BuildPickListUpdatePlan() method. I think this is because I'm trying to prime the providers and just call SyncPickLists()... which could be the wrong way to do this. Maybe I should make the BuildPickListUpdatePlan() method public/internal (and decoate the assembly appropriately) and just test that? Looking for input here as I'm new to mocking and not sure i"m going about this the right way.

Here's what my test looks like so far, but it isn't complete, correct or doing what I need it to do...

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

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

柠檬 2024-12-28 06:52:26

首先,你永远不会嘲笑被测试的类。在本例中,您正在测试 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,所以我无法检查语法,但是它应该给你一些想法。我已用解释替换了您的评论。

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

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

First of all you never mock the class under test. In this case you are testing SyncEngine, so you should not mock it.

If everything is mocked, you aren't testing anything. Mocks are there to abstract away dependencies in your tests and to facilitate writing tests.

You abstract away dependencies to:

  • Concentrate your tests on the code you want to test
  • Abstract away dependencies on things like databases, file systems,
    other processes, etc.
  • Simplify tests by reducing the code needed to test a particular feature or by easily getting a desired value from an object.

Secondly, always mock things for a reason, always consider if each class should or should not be mocked.

Third, it is considered bad practice by some (including myself) to test private methods. BuildPickListUpdatePlan is private, you should rather test the public methods of your class that use BuildPickListUpdatePlan.

Testing private methods will often require "tricks" to test correctly and result in tests that are very brittle. From my experience, these tests are more prone to needing to be changed rather than tests on the public interface or API of your class. Unless you have private methods that aren't used (in which case they are dead code and be deleted rather than tested), all your private methods will get called by your public methods. Concentrate on testing these.

I do not know the details of your code, so my proposition might be off, but here is what I would propose your test code could look like:

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

I don't have VS at hand, so I couldn't check the syntax, but it should give you some ideas. I have replaced your comments with explanations.

Here is some simple code that show a basic example of mocking a dependency:

[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());
}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文