使用模拟对象时,如何防止我的单元测试需要有关实现内部结构的知识?

发布于 2024-09-14 01:19:01 字数 623 浏览 4 评论 0原文

我仍处于关于单元测试的学习阶段,特别是关于模拟(我正在使用 PascalMockDUnit 框架)。我现在偶然发现的一件事是,我找不到一种方法来将被测试的类/接口的实现细节硬编码到我的单元测试中,这感觉是错误的......

例如:我想测试一个实现了一个非常简单的界面,用于读取和写入应用程序设置(基本上是名称/值对)。呈现给消费者的界面完全不知道值的实际存储位置和方式(例如注册表、INI 文件、XML、数据库等)。当然,访问层是由另一个不同的类实现的,该类在构造时注入到测试的类中。我为此访问层创建了一个模拟对象,现在我可以完全测试接口实现类,而无需实际读取或写入任何注册表/INI 文件/任何内容。

但是,为了确保模拟在被测试类访问时的行为与真实事物完全相同,我的单元测试必须通过非常明确地定义预期的方法调用和被测试类预期的返回值来设置模拟对象。这意味着,如果我必须更改访问层的接口或测试类使用该层的方式,我还必须更改内部使用该接口的类的单元测试,即使 <我实际测试的类的界面根本没有改变。这是我在使用模拟时必须忍受的事情,还是有更好的方法来设计类依赖关系来避免这种情况?

I'm still in the learning stages regarding unit-testing and in particular regarding mocking (I'm using the PascalMock and DUnit frameworks). One thing I now stumbled over was that I couldn't find a way around hard-coding implementation details of the tested class/interface into my unit test and that just feels wrong...

For example: I want to test a class that implements a very simple interface for reading and writing application settings (basically name/value pairs). The interface that is presented to the consumer is completely agnostic to where and how the values are actually stored (e.g. registry, INI-file, XML, database, etc.). Naturally, the access layer is implemented by yet a different class that gets injected into the tested class on construction. I created a mock object for this access layer and I am now able to fully test the interface-implementing class without actually reading or writing anything to any registry/INI-file/whatever.

However, in order to ensure the mock behaves exactly like the real thing when accessed by the tested class, my unit tests have to set up the mock object by very explicitly defining expected method calls and the return values expected by the tested class. This means that if I should ever have to make changes to the interface of the access layer or to the way that the tested class uses that layer I will also have to change the unit tests for the class that internally uses that interface even though the interface of the class I'm actually testing hasn't changed at all. Is this something I will just have to live with when using mocks or is there a better way to design the class-dependencies that would avoid this?

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

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

发布评论

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

评论(3

半步萧音过轻尘 2024-09-21 01:19:01

为了确保模拟在被测试类访问时的行为与真实事物完全相同,我的单元测试必须通过非常明确地定义预期的方法调用和被测试类预期的返回值来设置模拟对象。

正确的。

更改访问层的接口或测试类使用该层的方式我还必须更改单元测试

正确。

即使我实际测试的类的接口根本没有改变。

“实际测试”?你的意思是暴露的接口类?没关系。

“测试”(接口)类使用访问层的方式意味着您已将内部接口更改为访问层。接口更改(甚至是内部更改)需要进行测试更改,如果您做错了什么,可能会导致损坏。

这没有什么问题。事实上,重点是对访问层的任何更改必须需要对模拟进行更改,以确保更改“有效”。

测试不应该是“稳健的”。应该是脆的。如果您做出的更改会改变内部行为,那么事情可能会崩溃。如果你的测试太稳健,他们就不会测试任何东西——它们只会工作。那是错误的。

测试应该只在正确的原因下有效。

to ensure the mock behaves exactly like the real thing when accessed by the tested class, my unit tests have to set up the mock object by very explicitly defining expected method calls and the return values expected by the tested class.

Correct.

changes to the interface of the access layer or to the way that the tested class uses that layer I will also have to change the unit tests

Correct.

even though the interface of the class I'm actually testing hasn't changed at all.

"Actually testing"? You mean the exposed interface class? That's fine.

The way the "tested" (interface) class uses the access layer means you've changed the internal interface to the access layer. Interface changes (even internal ones) require test changes and may lead to breakage if you've done something wrong.

Nothing wrong with this. Indeed, the whole point is that any change to the access layer must require changes to the mocks to assure that the change "works".

Testing is not supposed to be "robust". It's supposed to be brittle. If you make a change that alters internal behavior, then things can break. If your tests were too robust they wouldn't test anything -- they'd just work. And that's wrong.

Tests should only work for the exact right reason.

如何视而不见 2024-09-21 01:19:01

这是我必须要做的事情吗?
使用模拟时一起生活或存在
更好的设计方法
可以避免的类依赖性
这个?

很多时候,模拟(尤其是像 JMock 这样的敏感框架)会迫使您考虑与您尝试测试的行为不直接相关的细节,有时这甚至可以通过暴露做得太多的可疑代码来提供帮助。并且有太多的调用/依赖。

但是,就您的情况而言,如果我正确阅读您的描述,听起来您确实没有问题。如果您正确设计了读/写层并具有适当的抽象级别,则不必更改它。

这意味着如果我应该有
更改界面
访问层或以这样的方式
被测试的类使用该层 I
还必须更改单位
测试内部的类
使用该接口,即使
我实际上是班级的界面
测试根本没有改变。

编写抽象访问层的目的不是为了避免这种情况吗?一般来说,遵循开放/封闭原则,这种接口<​​em >不应该改变,也不应该破坏与使用它的类的合同,并且通过扩展,它也不会破坏你的单元测试。现在,如果您更改方法调用的顺序,或者必须对抽象层进行新的调用,那么,是的,特别是对于某些框架,您的模拟期望将会被打破。这只是使用模拟成本的一部分,而且是完全可以接受的。但一般来说,界面本身应该保持稳定。

Is this something I will just have to
live with when using mocks or is there
a better way to design the
class-dependencies that would avoid
this?

A lot of times mocks (particularly sensitive frameworks like JMock) force you to account for details that don't relate directly to the behavior you're trying to test, and sometimes this can even be helpful by exposing suspect code that is doing too much and has too many calls/dependencies.

However in your case, if I read your description right, it sounds like you really don't have a problem. If you design the read/write layer correctly and with an appropriate level of abstraction, you shouldn't have to change it.

This means that if I should ever have
to make changes to the interface of
the access layer or to the way that
the tested class uses that layer I
will also have to change the unit
tests for the class that internally
uses that interface even though the
interface of the class I'm actually
testing hasn't changed at all.

Isn't the point of writing the abstracted access layer to avoid this? In general, following the Open/Closed principle, an interface of this sort shouldn't change and shouldn't break the contract with the class that consumes it, and by extension it won't break your unit tests either. Now if you change the order of the method calls, or have to make new calls to the abstracted layer, then, yes, particularly with some frameworks, your mock expectations will break. This is just part of the cost of using mocks, and it's perfectly acceptable. But the interface itself should, in general, remain stable.

请远离我 2024-09-21 01:19:01

只是为了给您的示例添加一些名称,

  • RegistryBasedDictionary 实现了角色(接口)字典。
  • RegistryBasedDictionary 依赖于由RegistryWinAPIWrapper 实现的角色RegistryAccessor。

您当前有兴趣测试RegistryBasedDictionary。单元测试将为RegistryAccessor Role注入模拟依赖项,并测试与依赖项的预期交互。

  • 这里避免不必要的测试维护的技巧是“准确地指定应该发生什么......而不是更多。”(来自 GOOS 书(模拟风格的 TDD 必读),因此,如果依赖方法调用的顺序并不重要,则不要在测试中指定它。这样您就可以自由地更改实现中的调用顺序。)
  • 设计角色,使其不包含实际实现中的任何泄漏 - 保持角色与实现无关

更改RegistryBasedDictionary 测试的唯一原因是更改RegistryBasedDictionary 的行为,而不是更改其任何依赖项。因此,如果它与其依赖项的交互或角色/合同发生变化,则需要更新测试。这是为了获得独立测试的灵活性而需要付出的基于交互的测试的代价。但实际上,这种情况并不经常发生。

Just to put some names to your example,

  • RegistryBasedDictionary implements the Role (interface) Dictionary.
  • RegistryBasedDictionary has a dependency on the Role RegistryAccessor, implemented by RegistryWinAPIWrapper.

You are currently interested in testing RegistryBasedDictionary. The unit tests would inject a mock dependency for the RegistryAccessor Role and would test the expected interaction with the dependencies.

  • The trick here to avoid unnecessary test-maintenance is to "Specify precisely what should happen.. and no more." (From the GOOS book (must-read for mock flavored TDD), so if order of dependency method calls doesn't matter, don't specify it in the test. That leaves you free to change the order of calls in the implementation.)
  • Design the Roles such that the they do not contain any leaks from the actual implementations - keep the Roles implementation-agnostic.

The only reason to change RegistryBasedDictionary tests, would be a change in the behavior of RegistryBasedDictionary and not in any of its dependencies. So if its interaction with its dependencies or the roles/contracts change, the tests would need to be updated. That is the price of interaction-based tests you need to pay, for the flexibility to test in isolation. However in practice, it doesn't that happen that often.

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