如何使用 Mock 避免重复逻辑

发布于 2024-07-15 05:05:40 字数 1486 浏览 9 评论 0原文

我面临以下挑战,但我还没有找到好的答案。 我正在使用模拟框架(本例中为 JMock)来允许单元测试与数据库代码隔离。 我正在模拟对涉及数据库逻辑的类的访问,并使用 DBUnit 单独测试数据库类。

我遇到的问题是我注意到一种模式,其中逻辑在概念上在多个地方重复。 例如,我需要检测数据库中的值不存在,因此在这种情况下我可能会从方法返回 null。 所以我有一个数据库访问类,它执行数据库交互,并适当地返回 null。 然后,我有一个业务逻辑类,它从模拟接收 null,然后进行测试,以便在值为 null 时采取适当的操作。

现在,如果将来该行为需要改变并且返回 null 不再合适,比如因为状态变得更加复杂,那么我需要返回一个报告该值不存在的对象以及来自数据库。

现在,如果我将数据库类的行为更改为在这种情况下不再返回 null,则业务逻辑类似乎仍然可以运行,并且该错误只会在 QA 中被发现,除非有人记住了耦合,或者正确遵循了该方法的用法。

我觉得我错过了一些东西,必须有一种更好的方法来避免这种概念上的重复,或者至少对其进行测试,以便如果它发生变化,则更改未传播的事实会使单元测试失败。

有什么建议么?

更新:

让我尝试澄清我的问题。 我正在考虑当代码随着时间的推移而演变时,如何确保通过模拟测试的类和模拟所代表的类的实际实现之间的集成不会中断。

例如,我刚刚有一个案例,我有一个最初创建的方法,并且不期望空值,所以这不是对真实对象的测试。 然后,该类的用户(通过模拟测试)得到增强,可以在某些情况下传入 null 作为参数。 集成失败了,因为真正的类没有被测试为空。 现在,在最初构建这些类时,这并不是什么大问题,因为您在构建时正在测试两端,但是如果设计需要在两个月后发展,而您往往会忘记细节,那么您将如何测试之间的交互这两组对象(通过模拟测试的对象与实际实现测试的对象)?

潜在的问题似乎是重复之一(即违反了 DRY 原则),期望实际上保留在两个地方,尽管这种关系是概念性的,但没有实际的重复代码。

[在 Aaron Digulla 对他的答案进行第二次编辑后编辑]:

是的,这正是我正在做的事情(除了在通过 DBUnit 测试的类中与数据库进行一些进一步的交互,并在其期间与数据库进行交互)测试,但这是相同的想法)。 现在,假设我们需要修改数据库行为以使结果不同。 使用模拟的测试将继续通过,除非 1)有人记得或 2)它在集成中中断。 因此,数据库的存储过程返回值(例如)在模拟的测试数据中本质上是重复的。 现在,重复让我烦恼的是逻辑是重复的,这是对 DRY 的微妙违反。 可能事情就是这样(毕竟有集成测试的原因),但我觉得我错过了一些东西。

[编辑开始赏金]

阅读与亚伦的互动就说到了问题的关键,但我真正想要的是一些关于如何避免或管理明显重复的见解,以便改变真实的类将在与模拟交互的单元测试中显示为损坏的东西。 显然这不会自动发生,但可能有一种方法可以正确设计场景。

[关于授予赏金的编辑]

感谢所有花时间回答问题的人。 获胜者教会了我一些关于如何考虑在两层之间传递数据的新知识,并首先找到了答案。

I have the following challenge, and I haven't found a good answer. I am using a Mocking framework (JMock in this case) to allow unit tests to be isolated from database code. I'm mocking the access to the classes that involve the database logic, and seperately testing the database classes using DBUnit.

The problem I'm having is that I'm noticing a pattern where the logic is conceptually duplicated in multiple places. For example I need to detect that a value in the database doesn't exist, so I might return null from a method in that case. So I have a database access class which does the database interaction, and returns null appropriately. Then I have the business logic class which receives null from the mock and then is tested to act appropriately if the value is null.

Now what if in the future that behavior needs to change and returning null is no longer appropriate, say because the state has grown more complicated, so I'll need to return an object that reports the value doesn't exist and some additional fact from the database.

Now, if I change the behavior of the database class to no longer return null in that case, the business logic class would still appear to function, and the bug would only be caught in QA, unless someone remembered the coupling, or properly followed the usages of the method.

I fell like I'm missing something, and there has to be a better way to avoid this conceptual duplication, or at least have it under test so that if it changes, the fact that the change is not propagated fails a unit test.

Any suggestions?

UPDATE:

Let me try to clarify my question. I'm thinking of when code evolves over time, how to ensure that the integration doesn't break between the classes tested via the mock and actual implementation of the classed that the mock represents.

For example, I just had a case where I had a method that was originally created and didn't expect null values, so this was not a test on the real object. Then the user of the class (tested via a mock) was enhanced to pass in a null as a parameter under certain circumstances. On integration that broke, because the real class wasn't tested for null. Now when building these classes at first this is not a big deal, because you are testing both ends as you build, but if the design needs to evolve two months later when you tend to forget about the details, how would you test the interaction between these two sets of objects (the one tested via a mock vs the actual implementation)?

The underlying problem seems to be one of duplication (that is violating the DRY principle), the expectations are really kept in two places, although the relationship is conceptual, there is no actual duplicate code.

[Edit after Aaron Digulla's second edit on his answer]:

Right, that is exactly the kind of thing I am doing (except that there is some further interaction with the DB in a class that is tested via DBUnit and interacts with the database during its tests, but it is the same idea). So now, say we need to modify the database behavior so that the results are different. The test using the mock will continue to pass unless 1) someone remembers or 2) it breaks in integration. So the stored procedure return values (say) of the database are essentially duplicated in the test data of the mock. Now what bothers me about the duplication is that the logic is duplicated, and it is a subtle violation of DRY. It could be that that is just the way it is (there is a reason for integration tests after all), but I was feeling that instead I'm missing something.

[Edit on starting the bounty]

Reading the interact with Aaron gets to the point of the question, but what I'm really looking for is some insight into how to avoid or manage the apparent duplication, so that a change in the behavior of the real class will show up in the unit tests that interact with the mock as something that broke. Obviously that doesn't happen automatically, but there may be a way to design the scenario correctly.

[Edit on awarding the bounty]

Thanks to everyone who spent the time answering the question. The winner taught me something new about how to think about passing the data between the two layers, and got to the answer first.

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

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

发布评论

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

评论(11

我一直都在从未离去 2024-07-22 05:05:40

从根本上来说,你要求的是不可能的事情。 您要求单元测试在您更改外部资源的行为时预测并通知您。 如果不编写测试来产生新行为,他们怎么能知道呢?

您所描述的是添加一个必须测试的全新状态 - 现在有一些对象从数据库中出来,而不是空结果。 您的测试套件如何可能知道被测对象对于某些新的随机对象的预期行为应该是什么? 您需要编写一个新测试。

正如您评论的那样,模拟并不“行为不端”。 模拟正在完全按照您设置的方式进行。 规范更改这一事实对于模拟来说没有任何影响。 这种情况下的唯一问题是实施更改的人忘记更新单元测试。 事实上,我不太清楚为什么你认为存在重复的担忧。

向系统添加一些新返回结果的编码人员负责添加单元测试来处理这种情况。 如果该代码也 100% 确定现在不可能返回 null 结果,那么他也可以删除旧的单元测试。 但你为什么要这么做呢? 单元测试正确描述了被测对象在收到空结果时的行为。 如果您将系统的后端更改为某个返回空值的新数据库,会发生什么情况? 如果规范改回返回 null 会怎样? 您不妨保留测试,因为就您的对象而言,它确实可以从外部资源返回任何内容,并且它应该优雅地处理每种可能的情况。

模拟的全部目的是将测试与真实资源分离。 它不会自动避免您将错误引入系统。 如果您的单元测试准确地描述了收到 null 时的行为,那就太棒了! 但是此测试不应该了解任何其他状态,当然也不应该以某种方式告知外部资源将不再发送空值。

如果您进行正确的松散耦合设计,您的系统可以拥有您可以想象的任何后端。 您不应该在编写测试时只考虑一种外部资源。 听起来如果您添加一些使用真实数据库的集成测试,从而消除模拟层,您可能会更高兴。 对于进行构建或健全性/冒烟测试来说,这始终是一个好主意,但通常会阻碍日常开发。

You are fundamentally asking for the impossible. You are asking for your unit tests to predict and notify you when you change the external resource's behaviour. Without writing a test to produce the new behaviour, how can they know?

What you are describing is adding a brand new state that must be tested for - instead of a null result, now there is some object coming out of the database. How could your test suite possibly know what the intended behaviour of the object under test should be for some new, random object? You need to write a new test.

The mock is not "misbehaving", as you commented. The mock is doing exactly what you set it up to do. The fact that the specification changed is of no consequence to the mock. The only problem in this scenario is that the person who implemented the change forgot to update the unit tests. I'm actually not too sure why you think there is any duplication of concerns going on.

The coder that is adding some new return result to the system is responsible for adding a unit test to handle this case. If that code is also 100% sure that there is no way that the null result could possibly be returned now, then he could also delete the old unit test. But why would you? The unit test correctly describes the behavior of the object under test when it receives a null result. What happens if you change the backend of your system to some new database that does return a null? What if the specification changed back to returning null? You might as well keep the test, since as far as your object is concerned, it could really get anything back from the external resource, and it should gracefully handle every possible case.

The whole purpose of mocking is to decouple your tests from real resources. It's not going to automatically save you from introducing bugs into the system. If your unit test accurately describes the behavior when it receives a null, great! But this test should not have any knowledge of any other state, and certainly should not be somehow informed that the external resource will no longer be sending nulls.

If you're doing proper, loosely coupled design, your system could have any backend you could imagine. You shouldn't be writing tests with one single external resource in mind. It sounds like you might be happier if you added some integration tests that use your real database, thereby eliminating the mocking layer. This is always a great idea for use with doing a build or sanity/smoke tests, but is usually obstructive for day to day development.

红尘作伴 2024-07-22 05:05:40

你在这里没有错过任何东西。 这是使用模拟对象进行单元测试的弱点。 听起来您正在正确地将单元测试分解为大小合理的单元。 这是一件好事; 人们在“单元”测试中测试过多的情况更为常见。

不幸的是,当您在这种粒度级别进行测试时,您的单元测试不会涵盖协作对象之间的交互。 您需要进行一些集成测试或功能测试来涵盖这一点。 我真的不知道比这更好的答案了。

有时,在单元测试中使用真正的协作者而不是模拟是很实用的。 例如,如果您正在对数据访问对象进行单元测试,则在单元测试中使用真实域对象而不是模拟通常很容易设置并且执行效果也很好。 反之则不然——数据访问对象通常需要数据库连接、文件或网络连接,并且设置起来相当复杂且耗时; 在对域对象进行单元测试时使用真实的数据对象会将需要几微秒的单元测试变成需要数百或数千毫秒的单元测试。

总结一下:

  1. 编写一些集成/功能测试来捕获协作对象的问题
  2. 并不总是需要模拟协作者 - 使用您的最佳判断

You're not missing something here. This is a weakness in unit testing with mock objects. It sounds like you are properly breaking your unit tests down into reasonably sized units. This is a good thing; it's far more common to find people testing too much in a "unit" test.

Unfortunately, when you test at this level of granularity, your unit tests don't cover the interaction between collaborating objects. You need to have some integration tests or functional tests to cover this. I don't really know a better answer than that.

Sometimes it's practical to use the real collaborator instead of a mock in your unit test. For example, if you're unit testing a data access object, using the real domain object in the unit test instead of a mock is often easy enough to set up and performs just as well. The reverse is often not true -- data access objects typically need a database connection, file or network connection and are pretty complicated and time consuming to set up; using a real data object when unit testing your domain object will turn a unit test that takes microseconds into one that takes hundreds or thousands of milliseconds.

So to summarize:

  1. Write some integration/functional testing to catch problems with collaborating objects
  2. It's not always necessary to mock out collaborators -- use your best judgement
傾旎 2024-07-22 05:05:40

您的数据库抽象使用 null 表示“未找到结果”。 忽略在对象之间传递 null 是一个坏主意的事实,当您的测试想要测试未找到任何内容时会发生什么时,不应使用该 null 文字。 相反,请使用常量或测试数据生成器,以便您的测试仅引用信息在对象之间传递,而不是如何表示该信息。 然后,如果您需要更改数据库层表示“未找到结果”(或您的测试依赖的任何信息)的方式,您在测试中只有一个地方可以更改它。

Your database abstraction uses null to mean "no results found". Ignoring the fact that it's a bad idea to pass null between objects, your tests should not use that null literal when they want to test what happens when nothing is found. Instead, use a constant or a test data builder so that your tests only refer to what information is passed between objects, not to how that information is represented. Then if you need to change the way that the database layer represents "no results found" (or whatever information your test relies upon) you only have one place in your tests to change that.

我要还你自由 2024-07-22 05:05:40

单元测试无法告诉您某个方法何时突然具有较小的可能结果集。 这就是代码覆盖率的用途:它会告诉您代码不再执行。 这反过来会导致应用层死代码的发现。

[编辑]基于评论:模拟除了允许实例化被测试的类并允许收集附加信息之外不能做任何事情。 特别是,它绝不能影响您想要测试的结果。

[EDIT2] 模拟数据库意味着您不关心数据库驱动程序是否工作。 您想知道的是您的代码是否可以正确解释数据库返回的数据。 另外,这是测试错误处理是否正常工作的唯一方法,因为您无法告诉真正的数据库驱动程序“当您看到此 SQL 时,抛出此错误”。 这只能通过模拟来实现。

我同意,需要一些时间来适应。 这是我所做的:

  • 我进行了测试来检查 SQL 是否有效。 每个 SQL 都会针对静态测试数据库执行一次,并验证返回的数据是否符合我的预期。

  • 所有其他测试都使用返回预定义结果的模拟数据库连接器运行。 我喜欢通过对数据库运行代码并在某处记录主键来获得这些结果。 然后,我编写一个工具,它采用这些主键并将带有模拟的 Java 代码转储到 System.out。 这样,我可以非常快速地创建新的测试用例,并且测试用例将反映“真相”。

    更好的是,我可以通过再次运行旧 ID 和我的工具来重新创建旧测试(当数据库更改时)

Unit tests can't tell you when a method suddenly has a smaller set of possible results. That's what code coverage is for: It will tell you that code isn't executed anymore. This in turn will lead to the discovery of the dead code in the application layer.

[EDIT] Based on a comment: A mock must not do anything but allowing to instantiate the class under test and allow to collect additional information. Especially, it must never influences the result of what you want to test.

[EDIT2] Mocking a database means that you don't care whether the DB driver works. What you want to know is whether your code can interpret the data returned by the DB correctly. Also, this is the only way to test whether your error handling works correctly because you can't tell the real DB driver "when you see this SQL, throw this error." This is only possible with a mock.

I agree, it takes some time to get used to. Here is what I do:

  • I have tests which check whether the SQL works. Each SQL gets executed once against a static test DB and I verify that the data returned is what I expect.

  • All other tests run with a mock DB connector that returns predefined results. I like to get these results by running the code against the database, logging the primary keys somewhere. I then write a tool which takes these primary keys and dumps Java code with the mock to System.out. This way, I can create new test cases very quickly and the test cases will reflect the "truth".

    Even better, I can recreate old tests (when the DB changes) by running the old IDs and my tool again

晚风撩人 2024-07-22 05:05:40

我想将问题缩小到核心。

问题

当然,您的大部分更改都会被测试捕获。
但是,在某些场景中,您的测试不会失败 - 尽管它应该失败:

在编写代码时,您会多次使用您的方法。 方法定义和使用之间存在 1:n 关系。 使用该方法的每个类都将在相应的测试中使用它的模拟。 所以mock也被使用了n次。

您的方法结果曾经被期望永远不会为 null。 更改此设置后,您可能会记得修复相应的测试。 到目前为止,一切都很好。

您运行测试 - 全部通过

但是随着时间的推移,你会忘记一些东西......模拟永远不会返回null。 因此,对使用模拟的 n 个类进行测试时,不会测试 null

您的质量检查将会失败 - 尽管您的测试没有失败。

显然,您将必须修改其他测试。 但没有失败的合作。 因此,您需要一个比记住所有引用测试更有效的解决方案。

解决方案

为了避免此类问题,您必须从一开始就编写更好的测试。 如果您错过了被测试的类应该处理错误或 null 值的情况,那么您的测试就只是不完整。 这就像没有测试类的所有功能一样。

以后再添加这个就很难了。 - 所以尽早开始并进行广泛的测试。

正如其他用户所提到的 - 代码覆盖率揭示了一些未经测试的情况。 但是缺少错误处理代码缺少的相应测试不会出现在代码覆盖率中。 (100% 的代码覆盖率并不意味着您没有遗漏某些内容。)

因此,请编写良好的测试:假设外部世界是恶意的。 这不仅包括传递错误参数(例如null值)。 您的模拟也是外部世界的一部分。传递null和异常 - 并观察您的类按预期处理它们。

如果您确定 null 为有效值 - 这些测试稍后将失败(因为缺少异常)。
这样你就会得到一份无法合作的清单。

因为每个调用类处理错误或 null 的方式不同 - 这不是可以避免的重复代码。 不同的处理需要不同的测试。


提示:保持你的模拟简单干净。 将预期返回值移至测试方法。 (您的模拟可以简单地将它们传递回来。)避免在模拟中测试决策。

I would like to narrow the problem down to it's core.

The Problem

Of course, most of your changes will be caught by the test.
But there is subset of Scenarios where your test won't fail - although it should:

As you write code, you use your methods multiple times. You get a 1:n relation between method definition and use. Each class that uses that method will use it's mock in the according test. So the mock is also used n times.

Your methods result was once expected to never be null. After you change this, you probably will remember to fix the according test. So far so good.

You run your Tests - all pass.

But over time you forgot something ... the mock never returns a null. So n test for n classes that use the mock do not test for null.

Your QA will fail - although your tests did not fail.

Obviously you will have to modify your other tests. But there are no fails to work along. So you need a solution, that works better than remembering all referencing tests.

A Solution

To avoid problems like this, you will have to write better tests from the beginning. If you miss out the cases, where the tested class should handle errors or null values, you simply have incomplete tests. It's like not testing all functions of your class.

It's hard to add this later. - So start early and be extensive with your tests.

As mentioned by other users - the code coverage reveals some untested cases. But missing error-handling code and the missing according test won't appear in code coverage. (Code coverage of 100% doesn't mean, that you are not missing something.)

So write good test: Assume the outside world to be malicious. That does not only include to pass bad parameters (like null values). Your mocks are a part of the outside world too. Pass nulls and exceptions - and watch your class handling them as expected.

If you decide null to be a valid value - these test will later fail (because of missing exceptions).
So you get a list of fails to work along.

Because each calling class handles the errors or null different - it is not duplicate code that could be avoided. Different treatment needs different tests.


Hint: Keep your mock simple and clean. Move the expected return values to the testing method. (Your mock can pass them simply back.) Avoid testing decisions in mocks.

书间行客 2024-07-22 05:05:40

我是这样理解您的问题的:

您正在使用实体的模拟对象来使用 JMock 测试应用程序的业务层。 您还可以使用 DBUnit 测试 DAO 层(应用程序和数据库之间的接口),并传递填充有一组已知值的实体对象的真实副本。 因为您使用两种不同的方法来准备测试对象,所以您的代码违反了 DRY,并且随着代码的更改,您的测试可能会与现实不同步。

Folwer 说...

它并不完全相同,但它确实让我想起了 Martin Fowler 的 Mocks Aren't Stubs 文章。 我认为 JMock 路线是mockist 方式,而“真实对象”路线是经典 执行测试的方式。

在解决这个问题时,尽可能保持 DRY 的一种方法是成为一个古典主义者,而不是一个模仿主义者。 也许您可以妥协并在测试中使用 bean 对象的真实副本。

用户创客以避免重复

我们在一个项目中所做的就是为每个业务对象创建创客。 制造商包含静态方法,这些方法将构造给定实体对象的副本,并填充已知值。 然后,无论您需要哪种类型的对象,您都可以调用该对象的制造商并获取其已知值的副本以用于您的测试。 如果该对象有子对象,您的创建者将调用子对象的创建者,以便从上到下构造它,并且您将根据需要返回完整的对象图。 您可以将这些制造商对象用于所有测试——在测试 DAO 层时将它们传递到数据库,以及在测试业务服务时将它们传递到服务调用。 因为制作者是可重复使用的,所以这是一种相当 DRY 的方法。

然而,您仍然需要使用 JMock 的一件事是在测试服务层时模拟您的 DAO 层。 如果您的服务调用 DAO,您应该确保它被注入模拟。 但您仍然可以同样使用您的 Maker——当设置您的期望时,只需确保您的模拟 DAO 使用相关实体对象的 Maker 传回预期结果。 这样我们仍然没有违反 DRY。

编写良好的测试将在代码更改时通知您

为了避免代码随时间变化而出现问题,我的最后建议是始终有一个解决空输入的测试。 假设当您第一次创建方法时,空值是不可接受的。 您应该有一个测试来验证如果使用 null 是否会引发异常。 如果稍后,空值变得可以接受,您的应用程序代码可能会更改,以便以新的方式处理空值,并且不再引发异常。 当这种情况发生时,您的测试将开始失败,并且您将“注意”事情不同步。

Here's how I understand your question:

You are using mock objects of your entities to test the business layer of your application using JMock. You are also testing your DAO layer (the interface between your app and your database) using DBUnit, and passing real copies of your entity objects populated with a known set of values. Because you are using 2 different methods of preparing your test objects, your code is violating DRY, and you risk your tests getting out of sync with reality as code changes.

Folwer says...

Its not exactly the same, but it certainly reminds me of Martin Fowler's Mocks Aren't Stubs article. I see the JMock route as being the mockist way, and the 'real objects' route as being the classicist way to perform testing.

One way to be as DRY as possible when tackling this problem is to be more of a classicist then a mockist. Maybe you can compromise and use real copies of your bean objects in your tests.

User Makers to avoid duplication

What we have done on one project is to create Makers for each of our business objects. The maker contains static methods which will construct a copy of a given entity object, populated with known values. Then, whichever kind of object you need, you can call the maker for that object and get a copy of it with known values to use for your testing. If that object has child objects, your maker will call makers for the children in order to construct it from top to bottom, and you will get back as much of the complete object graph as you need. You can use these maker objects for all of your tests -- passing them to the DB when testing your DAO layer, as well as passing them to your service calls when testing your business services. Because the makers are reusable, its a fairly DRY approach.

One thing you will still need to use JMock for, however, is to mock your DAO layer when testing your service layer. If your service makes a call to the DAO, you should make sure it is injected with a mock instead. But you can still use your Makers just the same -- when set up your expectations, just make sure your mocked DAO passes back the expected result using the Maker for the relevant entity object. That way we still aren't violating DRY.

Well written tests will notify you when code changes

My final advice to avoid your problem with code changing over time is to always have a test that addresses null inputs. Suppose when you first create your method nulls are not acceptable. You should have a test that verifies that an exception is thrown if null is used. If at a later time, nulls become acceptable, your app code might change so that null values are handled in a new way, and the exception is no longer thrown. When that happens, your test will begin to fail, and you will have a "heads up" that things are out of sync.

尽揽少女心 2024-07-22 05:05:40

您只需确定返回 null 是外部 API 的预期部分还是实现细节。

单元测试不应该关心实现细节。

如果它是您预期的外部 API 的一部分,那么由于您的更改可能会破坏客户端,因此这自然也会破坏单元测试。

从外部 POV 来看,这个东西返回 NULL 是否有意义,或者这是一个方便的结果,因为可以在客户端中对该 NULL 的含义做出直接假设?
NULL 应该意味着 void/nix/nada/unavailable,没有任何其他含义。

如果您打算稍后细化此条件,那么您应该将 NULL 检查包装到返回信息性异常、枚举或显式命名的布尔值的内容中。

编写单元测试的挑战之一是,即使是编写的第一个单元测试也应该反映最终产品中的完整 API。 您需要可视化完整的 API,然后针对它进行编程。

此外,您需要在单元测试代码中保持与生产代码中相同的规则,避免出现重复和功能嫉妒等问题。

You simply need to make up your mind of wether the returning of null is an intended part of the external API or if it is an implementation detail.

Unit tests should not care about implementation details.

If it is part of your intended external API, then as your change would potentially break clients, this naturally also should break the unit test.

Does it make sense from an external POV that this thing returns NULL or is this a convenient consequence because direct assumptions can be made in the client as to the meaning of this NULL?
A NULL should mean void/nix/nada/unavailable without any other meaning.

If you plan on granulating this condition later, then you should wrap the NULL check into something that returns either an informative exception, enum or an explicitly named bool.

One of the challenges with writing unit tests is that even the first unit tests written should reflect the complete API in the end product. You need to visualize the complete API and then program against THAT.

Also, you need to maintain the same discipline in your unit test code as you do in the production code avoiding smells like duplication and feature envy.

单身情人 2024-07-22 05:05:40

对于特定场景,您正在更改方法的返回类型,这将在编译时捕获。 如果没有,它会出现在代码覆盖率中(正如亚伦提到的)。 即使如此,您也应该进行自动化功能测试,该测试将在签入后立即运行。 也就是说,我做了自动冒烟测试,所以就我而言,那些测试会发现这一点:)。

如果不考虑上述因素,在初始场景中仍然有两个重要因素在起作用。 您希望对单元测试代码给予与其余代码相同的关注,这意味着保持它们干燥是合理的。 如果你正在做 TDD,那甚至会首先将这个问题推到你的设计中。 如果你不喜欢这个,涉及的另一个相反因素是 YAGNI,你不想在你的代码中得到每一个(不)可能的场景。 所以,对我来说,这将是:如果我的测试告诉我我遗漏了一些东西,我会仔细检查测试是否正常并继续进行更改。 我确保不要在测试中执行假设场景,因为这是一个陷阱。

For the specific scenario, you are changing the return type of the method, that will be caught at compile time. If it didn't it would appear on code coverage (as mentioned by Aaron). Even then, you should have automated functional tests, which would be run soon after the check-in. That said, I do automated smoke tests, so in my case those would caught that :).

Without thinking on the above, you still have 2 important factors playing in the initial scenario. You want to give your unit testing code the same attention as the rest of the code, which means it is reasonable to want to keep them DRY. If you were doing TDD, that would even push this concern to your design in the first place. If you are not into that, the other contrary factor involved is YAGNI, you don't want to get every (un)likely scenario in your code. So, for me it would be: if my tests are telling me I am missing something, I double check the test is ok and proceed with the change. I make sure not to do what if scenarios with my tests, as it is a trap.

拧巴小姐 2024-07-22 05:05:40

如果我正确理解这个问题,那么您有一个使用模型的业务对象。 有一个测试是针对 BO 和模型之间的交互(测试 A),还有另一个测试是测试模型和数据库之间的交互(测试 B)。 测试 B 更改为返回一个对象,但该更改不会影响测试 A,因为测试 A 的模型已被模拟。

我认为当测试 B 更改时使测试 A 失败的唯一方法是不模拟测试 A 中的模型并将两者合并到一个测试中,这不好,因为您将测试太多(并且您使用不同的框架)。

如果您在编写测试时了解这种依赖性,我认为一个可接受的解决方案是在每个测试中留下注释,描述依赖性以及如果一个更改,您需要如何更改另一个。 无论如何,重构时您都必须更改测试 B,一旦您进行更改,当前测试就会失败。

If I understand the question correctly, you have a Business Object that uses a Model. There is a test for the interaction between the BO and Model (Test A), and there is another test that tests the interaction between the model and the database (Test B). Test B changes to return an object, but that change doesn't effect test A because test A's model is mocked.

The only way I see to make test A fail when test B changes is to not mock the model in test A and combine the two into a single test, which isn't good because you'll be testing too much (and you're using different frameworks).

If you know about this dependency when you write the tests, I think an acceptable solution would be to leave a comment in each test describing the dependency and how if one changes, you need to change the other. You'll have to change Test B when you refactor anyway, the current test will fail as soon as you make your change.

瞎闹 2024-07-22 05:05:40

你的问题很混乱,而且文字数量并没有多大帮助。

但是我可以通过快速阅读提取的含义对我来说没有什么意义,因为您希望非合同更改会影响模拟的工作方式。

模拟可以让您集中精力测试系统的特定部分。 模拟的部分将始终以指定的方式工作,测试可以集中于测试它应该的特定逻辑。 因此,您不会受到不相关的逻辑、延迟问题、意外数据等的影响。

您可能会有单独数量的测试来检查另一个上下文中的模拟功能。

关键是,模拟接口和其实际实现之间根本不应该存在任何连接。 它只是没有任何意义,因为你在嘲笑合同并给它一个你自己的实现。

Your question is quite confusing, and the amount of text doesn't exactly help.

But the meaning I could extract through a quick read makes little sense to me, in that you want a non-contract changing change affect how the mock works.

Mocking is an enabler for you to concentrate on testing a specific part of the system. The mocked part will always work in a specified way, and the test can concentrate on testing the specific logic it should. Thus you won't be affected by unrelated logic, latency issues, unexpected data, etc.

You'll probably have a separate number of tests checking the mocked functionality in another context.

The point is, no connection should exist between the mocked interface and the real implementation of that at all. It just doesn't make any sense, since you're mocking the contract and are giving it an implementation of your own.

少女情怀诗 2024-07-22 05:05:40

我认为你的问题违反了里氏替换原则:

子类型必须可以替换其基本类型

理想情况下,你会有一个类,它依赖于抽象。 一个抽象说“为了能够工作,我需要这个方法的实现,它接受这个参数,返回这个结果,如果我做了这个错误的事情,就会抛出这个异常”。 这些都将通过编译时间限制或注释在您所依赖的接口上定义。

从技术上讲,您可能看起来依赖于抽象,但在您所讲述的场景中,您并不真正依赖于抽象,您实际上依赖于实现。 你说“如果这个方法改变了它的行为,它的用户就会崩溃,而我的测试永远不会知道”。 在单元测试级别上,你是对的。 但在契约层面上,以这种方式改变行为是错误的。 因为通过更改方法,您显然违反了方法与其调用者之间的约定。

为什么要改变方法? 很明显,该方法的调用者现在需要不同的行为。 因此,您要做的第一件事不是更改方法本身,而是更改客户所依赖的抽象或契约。 他们必须首先改变并开始使用新的契约:“好吧,我的需求改变了,我不再希望这个方法返回,在这个特定的场景中,这个接口的实现者必须返回这个”。 因此,您要更改界面,根据需要更改界面的用户,这包括更新他们的测试,而您要做的最后一件事是更改传递给客户的实际实现。 这样,您就不会遇到您所说的错误。

因此,

class NeedsWork(IWorker b) { DoSth() { b.Work() }; }
...
AppBuilder() { INeedWork GetA() { return new NeedsWork(new Worker()); } }
  1. 修改 IWorker 使其反映 NeedsWork 的新需求。
  2. 修改 DoSth,使其能够与满足其新需求的新抽象一起工作。
  3. 测试 NeedsWork 并确保它适用于新行为。
  4. 更改您为 IWorker 提供的所有实现(本场景中为 Worker)(您现在首先尝试执行此操作)。
  5. 测试 Worker 使其满足新的期望。

看起来很可怕,但在现实生活中,这对于微小的变化来说是微不足道的,而对于巨大的变化来说却是痛苦的,事实上,事实必须如此。

I think your problem is violating the Liskov Substitution Principle:

Subtypes must be substitutable for their base types

Ideally, you would have a class, which depends on an abstraction. An abstraction that says "for being able to work, I need an implementation of this method which takes this parameter, returns this result and if I do this wrong stuff, throws me this exception". These would all be defined on your interface which you depend, either by compile time constraints or by comments.

Technically you may seem to depend on an abstraction but in the scenario you tell, you do not really depend on an abstraction, you actually depend on an implementation. You say that "if this method changes its behavior, its users will break and my tests will never know". On the unit test level, you are right. But on the contract level, changing the behavior in this way is wrong. Because by changing the method, you clearly violate the contract between your method and callers of it.

Why do you change a method? It is clear that callers of that method needs a different behavior now. So, the first thing you want to do is not changing the method itself, but changing the abstraction, or the contract, that your clients depend on. They must change first and begin working with the new contract: "OK, my needs changed, I no longer want this method to return that in this particular scenario, implementors of this interface must return this instead". So, you go change your interface, you go change the users of the interface as necessary, and this includes updating their tests and the last thing you do is changing the actual implementation that you pass to your clients. This way, you will not encounter the error you talk about.

So,

class NeedsWork(IWorker b) { DoSth() { b.Work() }; }
...
AppBuilder() { INeedWork GetA() { return new NeedsWork(new Worker()); } }
  1. Modify IWorker so that it reflects new needs of NeedsWork.
  2. Modify DoSth so that it works with the new abstraction that satisfies its new needs.
  3. Test NeedsWork and make sure it works with the new behavior.
  4. Change all the implementations (Worker in this scenario) you provide for IWorker (which you are now trying to do first).
  5. Test Worker so that it meets new expectations.

Seems scary but in real life this would be trivial for small changes and painful for huge changes as it, in fact, must be.

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