如何对互联网协议实现进行单元测试?
我决定向我的项目添加单元测试,并以测试驱动的方式继续开发。我目前正在为我的 ManageSieve 客户端对象实施单元测试,但我不确定测试该野兽的最佳方法是什么。
我的 SieveClient
对象依赖于另外两个对象进行网络通信:CocoaAsyncSocket
和我自己的 SaslConn
对象,它是我对 Cyrus SASL 库的包装器处理身份验证方法。为了测试,我需要用模拟对象替换它们。我将为此使用 OCMock 框架。我不太确定如何执行此操作,因为 SieveClient 对象需要自己创建这些对象。现在,我覆盖该对象的(私有)设置器,以便始终使用 OCMocks partialMockForObject:
方法安装我的模拟对象。但这对我来说感觉不对。有什么想法可以更好地解决这个问题吗?
我遇到麻烦的另一部分是套接字本身。为了能够测试协议细节,我需要一种从套接字返回预定义测试数据的方法。我想我可以使用 OCMock 机制来伪造来自套接字的返回值。但由于 CocoaAsyncSocket 提供了许多不同的方法来从套接字读取数据,我必须准确地知道协议对象按什么顺序使用哪些方法。我不希望我的单元测试依赖于我的协议对象的实现细节。那么我应该在这里做什么呢?手动实现套接字类的模拟对象?这看起来并不简单,所以我可能也需要对此进行单元测试。这是个好主意吗?
我读过,如果某件事很难测试,那么它可能也设计得不是很好。但我不知道如何才能做得更好,因为困难的部分在于与我必须做的套接字交互。
如果您想查看代码,可以在 Bitbucket 中找到:SieveClient.m 和 SieveClient.h
编辑:依赖注入
所以我读到了依赖注入,我想我将使用它来获取 AsyncSocket
和 SaslConn
对象进入我的 SieveClient
对象。我将更改我的构造函数以接受这些对象并使用它们。由于此类的用户通常不关心套接字和 SASL 对象,因此我将添加一个工厂方法(以便捷构造函数的形式),该方法仅创建这些对象并将它们传递给构造函数。
但这只解决了我的测试问题的第一部分(也是更简单的部分)。
I decided to add unit tests to my project and continue development in a test-driven kind of way. I’m currently working on implementing unit tests for my ManageSieve client object and I’m not sure what’s the best way to test that beast.
My SieveClient
object relies on two other objects for the network communication: CocoaAsyncSocket
and my own SaslConn
object, which is my wrapper around the Cyrus SASL library to handle the authentication methods. For testing I need to replace those with mock objects. I’m going to use the OCMock framework for this. I’m not quite sure how to do this, since the SieveClient object needs to create those objects itself. Right now I overwrite the (private) setters for that object to always install my mock objects using OCMocks partialMockForObject:
method. But this feels not right to me. Any ideas how this could be solved better?
The other part I have trouble with is the socket itself. To be able to test the protocol details I’d need a way to return predefined test data from the socket. I suppose I could just use OCMock mechanisms to fake the return values from the socket. But since CocoaAsyncSocket
provides many different methods to read data from the socket I have to know exactly which are being used by the protocol object in which order. I don’t want my unit test to be that dependent on implementation details of my protocol object. So what should I do here? Implement a mock object for the socket class by hand? This seems non-trivial, so I’d probably need unit tests for that too. Is that a good idea?
I’ve read that if something is hard to test it’s probably not very well designed either. But I don’t see how I could do better, since the hard part lies in interacting with the socket which I have to do.
If you’d like to see code you can find it at Bitbucket: SieveClient.m and SieveClient.h
Edit: Dependency Injection
So I read about Dependency Injection, and I think I’m going to use this to get the AsyncSocket
and SaslConn
objects into my SieveClient
object. I’ll change my constructor to accept those objects and use them. Since the user of this class usually doesn’t care about the socket and the SASL object I’ll add a factory method (in the form of a convenience constructor) that just creates those objects and passes them to the constructor.
But this solves only the first (and easier) part of my testing problem.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(3)
就不会出现同样的问题了。
我假设您需要 SieveClient 在内部控制其他对象的实例化,因为它是您不想公开的 API 的一部分。如果这就是原因,通过将它们分开,您将不再有相同的需求,因为您可以让 SieveClient 控制绑定,而执行协议的另一部分接收实例与 合作。
通过执行上述操作,您可以将模拟对象移交给您的协议实现。 这些模拟将会有您可能需要的任何期望。如果您发现它,您最终会发现它过于复杂,那么您可能需要重新集中职责,这通常会导致更清晰/更简单的协议实现(如果您发现需要进行这些单元测试)。
如上所述,您还需要考虑您尝试测试的代码是否尽可能专注于协议并且没有任何额外的元素。如果是这样的话,那么它就不适合对其进行单元测试,因为它的唯一责任是与外部系统的交互。我会决定这个系统中的协议规范有多重要,如果它全部都是关于与外部系统的集成,我会将其视为集中的集成测试,而不是命中真正的外部系统并与单元测试分开< /strong> (因此它不会影响运行系统其余部分的单元测试所需的速度)。
由于编辑而重新阅读问题后,我必须强调我上面所说的关于重点集成测试的内容。你问:
如果您正在处理一个非常复杂的对象,并且该对象完全是关于超出边界的集成,那么您通常最好避免将其作为单元测试的一部分。在这种情况下,您需要进行集中的集成测试/以影响真正的外部系统。 这并不意味着其余代码的所有单元测试都会影响外部系统,而只是使用该对象的非常简单的代码/类单元。
在您的场景中,此类对象很可能是 SieveClient,在这种情况下,请忘记该代码段的单元测试。相反,您想要做的是在测试使用 SieveClient 的代码时模拟 SieveClient。另一方面,如果您发现 SieveClient 的功能远不止于此,您需要添加一个类来简化这些通信方面,这将是您在测试 SieveClient 时模拟的内容,也是您进行集中集成测试的对象。
这种类型的测试是确保与外部交互的代码按预期工作的非常有效的方法,因为这是类和所涉及的测试的重点。 如果外部系统上的某些内容开始以不同的方式工作,您会清楚地注意到它 - 而不是将其与您的应用程序逻辑混合在一起,或者最糟糕的是根本没有经过测试。
It wouldn't be the same problem.
I'll assume you need SieveClient to control the instantiation of the other objects internally because its part of an API you don't want to expose. If that's the reason, by separating them, you no longer have the same need, since you can have the SieveClient control the bindings and the other part that does the protocol receives the instances it'll work with.
By doing the above, you can hand over the mocked objects to your protocol implementation. Those mocks would then have any expectations you may need. If you find it you end up with it being too involved, then you probably need to refocus responsibilities, which usually results in a cleaner/simpler protocol implementation anyway (if you find that you need to from doing those unit tests).
Above said, you also need to consider if the code you are trying to test is as focused as possible on the protocol and doesn't have any extra elements. If that's the case, it wouldn't be a good candidate to unit test it, as its sole responsibility is the interaction with an external system. I'd decide how important is the protocol spec in this system, and if its all about integration with an external system I'd treat it like a focused integration test instead that hits the real external system and is kept separated from the unit tests (so it doesn't affects the speed needed to run unit tests of the rest of the system).
After re-reading the question because of the edit, I have to stress out what I said about focused integration tests above. You ask:
If you are dealing with a very complex object and that object is all about integration beyond a boundary, you usually are best avoiding it as part of unit tests. In that scenario you want a focused integration test / to hit the real external system. This doesn't mean all the unit tests of the rest of your code, hit the external system, just the very simple unit of code/class that uses that object.
It may very well be the case that such object is SieveClient in your scenario, in that case forget about unit tests of that piece of the code. What you want to do instead is mock the SieveClient when testing code that uses it. On the other hand, if you find that SieveClient is much more than that, you want to add a class that simplifies these communication aspects, and that'd be what you mock when testing SieveClient and also what you do a focused integration test against.
This type of tests are a very effective way to make sure that the code that interacts with the external is working as expected, since that's the focus of both the class and the tests involved. In case something on the external system starts working differently, you notice it clearly - as opposed to having it mixed with your application logic or worst not tested at all.
您能否将您正在做的事情分成两部分,一部分是抽象协议,另一部分是与套接字的绑定?然后,您可以更轻松地测试抽象协议,并将绑定测试的重点放在它是否正确调用任何连接的抽象协议的方法/操作。
抽象地讲,您将减少代码各部分之间的耦合。这提高了可测试性,但代价是总体复杂性有所增加(尽管还不错,因为您可以通过关注点分离获得更好的工具来管理它),并且可能会降低性能(尽管对大多数系统来说这并不是什么大问题;你的计算机比它的 I/O 子系统快得多)。
Can you split what you're doing into two parts, one of which is the abstract protocol and the other of which is the binding to sockets? Then you can test the abstract protocol more easily, and focus the testing of the binding on whether it invokes methods/operations of any connected abstract protocol correctly.
Abstractly, you'd be decreasing the coupling between the parts of your code. That increases testability, at a cost of some increase in overall complexity (though not too bad because you're getting better tools to manage it through Separation of Concerns) and some potential decrease in performance (not much of an issue with most systems though; your computer is far faster than its I/O subsystems).
不要过多听信教条。寻找可能有效的最简单的方法,也用于测试。 (免责声明:我确实了解 TDD,但我不了解 Objective C)。
要让
SieveClient
在生产代码中创建其SaslConn
,但在测试中使用模拟代码,您可以使用 依赖注入。向SieveClient
添加 setter 方法以传入 工厂(作为对象或函数,具体取决于 Objective C 允许的内容),SieveClient
将使用它来生成其SaslConn
,而不是自己生成它们。测试代码提供了一个测试工厂来进行模拟。用于制作SaslConn
的生产案例代码要么移动到另一个工厂进行独立单元测试,要么如果它太简单而无法破坏,则保留为SieveClient
内的默认行为> 当工厂设置者没有被调用时。测试网络客户端代码的最简单方法是实现或重用模拟服务器。不要在 SaslConn 中模拟血淋淋的套接字详细信息;相反,在测试中编写 SASL 服务器。事实上,您的
SaslConn
可以与它对话,这对于为该模拟服务器提供测试大有帮助;换句话说,SaslConn 和模拟服务器是彼此的单元测试。 (是的,不是纯粹意义上的“单元”,但没有人在乎。)最后,我对“难以测试的代码设计得很糟糕”这一格言有着复杂的感觉。这取决于。您应该设计代码,使其易于使用(在调用者代码中)且易于修改。单元测试只是达到这些目的的一种手段:它们是您将编写的第一个调用者代码,并且它们让您确信在进行更改时不会搞砸。不要让特定的框架或方法扭曲并损害您的设计这点超过了 TDD 的好处。特别是,基于期望的模拟框架(例如 OCMock)使得 编写脆弱的测试,就像“我希望方法
foo
被调用 3 次,然后才调用方法bar
”如此这般的论点”。与其使用错误的工具来完成工作,不如编写自己的工具!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 itsSaslConn
's in production code, but use mock ones in tests, you can use dependency injection. Add a setter method toSieveClient
to pass in a factory (as an object or a function, depending on what Objective C permits), whichSieveClient
will use to make itsSaslConn
's, instead of making them by itself. The test code provides a test factory that dishes out mocks. The production-case code for makingSaslConn
's either moves to another factory to be unit-tested independently, or if it's too simple to break, remains as the default behavior insideSieveClient
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 yourSaslConn
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 methodbar
to be called with exactly such and such arguments". Rather than using the wrong tools for the job, write your own!