6

我决定将单元测试添加到我的项目中,并以测试驱动的方式继续开发。我目前正在为我的 ManageSieve 客户端对象实施单元测试,但我不确定测试该野兽的最佳方法是什么。

我的SieveClient对象依赖于其他两个对象进行网络通信:CocoaAsyncSocket以及我自己的SaslConn对象,它是我对 Cyrus SASL 库的包装,用于处理身份验证方法。为了测试,我需要用模拟对象替换那些。我将为此使用 OCMock 框架。我不太确定如何执行此操作,因为 SieveClient 对象需要自己创建这些对象。现在我覆盖了该对象的(私有)设置器,以始终使用 OCMockspartialMockForObject:方法安装我的模拟对象。但这对我来说感觉不对。有什么想法可以更好地解决这个问题吗?

我遇到的另一部分是套接字本身。为了能够测试协议细节,我需要一种从套接字返回预定义测试数据的方法。我想我可以使用 OCMock 机制来伪造来自套接字的返回值。但是由于CocoaAsyncSocket提供了许多不同的方法来从套接字读取数据,所以我必须确切地知道协议对象以何种顺序使用了哪些数据。我不希望我的单元测试依赖于我的协议对象的实现细节。那么我应该在这里做什么?手动为套接字类实现一个模拟对象?这似乎并不简单,所以我可能也需要单元测试。这是一个好主意吗?

我读过,如果某些东西很难测试,那么它的设计也可能不是很好。但我看不出我能做得更好,因为困难的部分在于与我必须做的套接字交互。

如果您想查看代码,可以在 Bitbucket 中找到:SieveClient.mSieveClient.h

编辑:依赖注入

所以我读到了依赖注入,我想我将使用它来将AsyncSocketandSaslConn对象放入我的SieveClient对象中。我将更改我的构造函数以接受这些对象并使用它们。由于此类的用户通常不关心套接字和 SASL 对象,因此我将添加一个工厂方法(以便利构造函数的形式),它只创建这些对象并将它们传递给构造函数。

但这仅解决了我的测试问题的第一部分(也是更简单的部分)。

4

3 回答 3

7

但后来我拒绝了这个想法,因为它没有多大帮助。我可以更轻松地测试 SieveClient 对象,真的。但是我在测试新对象时也会遇到同样的问题。在我看来,这只是给以后添麻烦。特别是因为我没有任何东西可以重新使用新课程。

这不会是同样的问题。

我假设您需要 SieveClient 来控制内部其他对象的实例化,因为它是您不想公开的 API 的一部分。如果这是原因,通过将它们分开,您不再有相同的需求,因为您可以让SieveClient 控制绑定,而执行协议的另一部分接收它将使用的实例

通过执行上述操作,您可以将模拟对象移交给您的协议实现那些模拟然后会有你可能需要的任何期望。如果您发现它最终过于参与,那么您可能需要重新关注职责,这通常会导致更清洁/更简单的协议实现(如果您发现需要进行这些单元测试)。

如上所述,您还需要考虑您尝试测试的代码是否尽可能关注协议并且没有任何额外的元素。如果是这种情况,那么对它进行单元测试就不是一个好的选择,因为它的唯一责任是与外部系统的交互。我会决定该系统中协议规范的重要性,如果它与外部系统集成有关,我会将其视为重点集成测试,而不是触及真正的外部系统并与单元测试分开(所以它不会影响运行系统其余部分的单元测试所需的速度)。


在由于编辑而重新阅读问题后,我必须强调我所说的关于上面重点集成测试的内容。你问:

但是由于 CocoaAsyncSocket 提供了许多不同的方法来从套接字读取数据,我必须确切地知道协议对象以何种顺序使用了哪些数据。我不希望我的单元测试依赖于我的协议对象的实现细节。那么我应该在这里做什么?手动为套接字类实现一个模拟对象?这似乎并不简单,所以我可能也需要单元测试。这是一个好主意吗?

如果您正在处理一个非常复杂的对象并且该对象完全是关于超越边界的集成,那么您通常最好避免将它作为单元测试的一部分。在那种情况下,您需要一个集中的集成测试/来击中真正的外部系统。这并不意味着其余代码的所有单元测试都命中外部系统,只是使用该对象的非常简单的代码/类单元

在您的场景中,这样的对象很可能是 SieveClient,在这种情况下,忘记那段代码的单元测试。相反,您想要做的是在测试使用 SieveClient 的代码时模拟它。另一方面,如果您发现 SieveClient 远不止这些,您希望添加一个简化这些通信方面的类,这将是您在测试 SieveClient 时模拟的内容,也是您针对其进行集中集成测试的内容。

这种类型的测试是确保与外部交互的代码按预期工作的非常有效的方法,因为这是类和所涉及的测试的重点。如果外部系统上的某些东西开始以不同的方式工作,您会清楚地注意到它 - 而不是将它与您的应用程序逻辑混合在一起,或者最糟糕的是根本没有测试。

于 2010-09-06T23:00:30.477 回答
2

Don't listen to dogmas too much. Go for the simplest thing that could possibly work, also for tests. (Disclaimer: I do know TDD, but I don't know Objective C).

To let SieveClient create its SaslConn's in production code, but use mock ones in tests, you can use dependency injection. Add a setter method to SieveClient to pass in a factory (as an object or a function, depending on what Objective C permits), which SieveClient will use to make its SaslConn's, instead of making them by itself. The test code provides a test factory that dishes out mocks. The production-case code for making SaslConn's either moves to another factory to be unit-tested independently, or if it's too simple to break, remains as the default behavior inside SieveClient when the factory setter is not called.

The simplest way to test network client code is by far to implement or re-use a mock server. Don't mock out the gory socket details in SaslConn; instead, write an SASL server in your tests. The fact that your SaslConn can talk to it goes a long way towards providing testing for that mock server; in other words, SaslConn and the mock server are each other's unit tests. (Yeah, not "unit" in the purist sense, but nobody cares.)

Finally, I have mixed feelings about the precept that hard to test code is badly designed. It depends. You should design your code so that it's easy to use (in caller code) and easy to modify. Unit tests are but a means to these ends: they are the first caller code that you will write, and they give you confidence that you don't screw up when making changes. Don't let a particular framework or methodology twist and maim your design to the point of outweighing the benefits of TDD. In particular, expectation-based mocking frameworks such as OCMock make it way too easy to write brittle tests that go like "I expect method foo to be called 3 times, and only then method bar to be called with exactly such and such arguments". Rather than using the wrong tools for the job, write your own!

于 2010-09-09T21:28:14.360 回答
2

你能把你正在做的事情分成两部分,一部分是抽象协议,另一部分是绑定到套接字?然后您可以更轻松地测试抽象协议,并将绑定的测试重点放在它是否正确调用任何连接的抽象协议的方法/操作上。

抽象地说,您将减少代码各部分之间的耦合。这增加了可测试性,但代价是整体复杂性有所增加(尽管还不错,因为您通过关注点分离获得了更好的工具来管理它)和一些潜在的性能下降(尽管对于大多数系统来说这不是什么大问题;您的计算机比它的 I/O 子系统快得多)。

于 2010-09-04T11:31:46.080 回答