减少单元测试的责任和合作者

发布于 2024-11-24 19:04:56 字数 1269 浏览 0 评论 0原文

我有一个具有明确定义的职责的类 - 使用对象所需的信息“丰富”对象。该信息是从各种来源(服务)收集的。例如:

public class Enricher
{
      private CounterpartyService counterPartyService;
      private BookingEntityService bookingEntityService;
      private ExchangeRateService exchangeRateService;
      private BrokerService brokerService;
      ... 6 more services

    public EnrichedTradeRequest enrichTrade(TradeRequest request)
    {
        EnrichedTradeRequest enrichedRequest = new EnrichedRequest(request);

        // Enrich with counterparty info
        enrichedRequest = enrichCounterParty(enrichedRequest)

        // Enrich with booking entity info
        enrichedRequest = enrichBookingEntity(enrichedRequest)

        // Enrich with exchange rate info
        ...

        // Enrich with broker info
        ...

        // ....etc
        return enrichedRequest;
    }

    private EnrichedTradeRequest enrichCounterparty(EnrichedRequest enrichedRequest)
    {
        // Get info from CounterpartyService
        // ...

        return enrichedRequest;
    }

这里包含“如何”丰富请求的逻辑。例如,该类可以扩展到不同类型的贸易。

我们一步丰富了贸易,因为我们不希望任何部分丰富的物体漂浮在周围,这没有多大意义。

该类确实很难进行单元测试,因为它有很多协作者(它调用了多达 12 个其他服务)。我需要模拟 12 个服务,每个服务有 3 或 4 种不同的方法。

在这种情况下,如何减少协作者的数量,并使该代码可测试?

I have a class which has a well-defined responsibility - to "Enrich" an object with the information it needs. This information is gathered from a variety of sources (Services). Eg:

public class Enricher
{
      private CounterpartyService counterPartyService;
      private BookingEntityService bookingEntityService;
      private ExchangeRateService exchangeRateService;
      private BrokerService brokerService;
      ... 6 more services

    public EnrichedTradeRequest enrichTrade(TradeRequest request)
    {
        EnrichedTradeRequest enrichedRequest = new EnrichedRequest(request);

        // Enrich with counterparty info
        enrichedRequest = enrichCounterParty(enrichedRequest)

        // Enrich with booking entity info
        enrichedRequest = enrichBookingEntity(enrichedRequest)

        // Enrich with exchange rate info
        ...

        // Enrich with broker info
        ...

        // ....etc
        return enrichedRequest;
    }

    private EnrichedTradeRequest enrichCounterparty(EnrichedRequest enrichedRequest)
    {
        // Get info from CounterpartyService
        // ...

        return enrichedRequest;
    }

The logic for "how" to enrich a request is contained here. The class could be extended for different types of trade for example.

We enrich the trade in one step because we don't want any partially-enriched objects floating around, which wouldn't make a lot of sense.

The class is really difficult to unit test, because it has so many collaborators (up to 12 other services it calls upon). I would need to mock up 12 services, each with 3 or 4 different methods.

How do I reduce the number of collaborators in this case, and make this code testable?

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

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

发布评论

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

评论(5

江湖彼岸 2024-12-01 19:04:56

根本问题是你有太多的合作者。如果难以测试,那么设计可能可以改进。

一种选择是创建一个外观服务来管理协作者的交互。在您的情况下,您可能有一个外观层次结构,因为只有一个外观只会将大量模拟的需求转移到另一个区域,这并不能解决问题。如果可能,尝试将更有可能一起使用的服务分组到外观中。或者,如果您总是跨服务一起调用相同的 X 方法,请将该功能放在某个地方的单个方法中。如果您有一个外观,而该外观又调用 3-4 个其他外观,则每个测试只需要 3-4 个模拟,这更易于管理。

最终结果将是您的“丰富器”将仅调用一个外观服务,因此测试将很容易。权衡是需要测试你的外墙,这将是可管理的。

The fundamental problem is that you have too many collaborators. If its difficult to test, then design can probably be improved.

One option is to create a facade service that manages the interaction of the collaborators. In your case, you might have a hierarchy of facades, since having only one will just move the need to have a lot of mocks into another area, which doesn't solve the problem. If possible, try to group services that are more likely to be used together into facades. Or alternatively, if you always call the same X methods across services together, put that functionality in a single method somewhere. If you have a single facade which in turn calls 3-4 other facades, each test will only need 3-4 mocks which is more manageable.

The end result of this will be that your 'enricher', will only call one facade service, so testing will be easy. The trade off being the need to test your facades, which will be manageable.

栩栩如生 2024-12-01 19:04:56

我认为编写可测试代码的最佳方法是练习 TDD。这可以帮助您编写可测试的代码,因为您首先需要编写测试,然后才能编写任何生产代码。我建议您阅读Bob叔叔的TDD三定律。但下面我将向您总结第一部分:

多年来,我开始用以下方式描述测试驱动开发:
三个简单规则的条款。它们是: 你不可以写
任何生产代码,除非它要通过失败的单元测试。你
不允许编写超过足够的单元测试
失败;编译失败就是失败。你不被允许
编写足以通过测试的生产代码
单元测试失败。

您必须首先为您想要的功能编写单元测试
打算写。但根据规则 2,你不能写太多该单元的内容
测试。一旦单元测试代码无法编译,或者失败
断言后,您必须停止并编写生产代码。但根据规则 3 你
只能编写使测试编译的生产代码或
通过,仅此而已。

如果你想一想,你就会意识到你根本无法写作
根本没有编译和执行任何东西的大量代码。
确实,这确实是重点。我们所做的每一件事,无论是写作
测试、编写生产代码或重构,我们保留系统
时刻执行。运行测试之间的时间是按顺序排列的
秒或分钟。即使 10 分钟也太长了。

这本指南:编写可测试代码也非常有趣,可以为您提供很多知识编写可测试代码的技巧。

更新

重构

当您进行了测试时,您只需要进行重构,但请记住,在测试失败之前,您不允许编写任何生产代码。我认为你的(可能)班级有很多责任 当它有最多 12 个协作者时。

提取一个要改变现有行为的类。当你工作时
在现有功能上,(即添加另一个条件)提取一个
班级肩负着这一责任。这将开始采取
遗留类中的块,您将能够测试每个块
隔离块(使用依赖注入)。

有效地使用旧代码

我想指出有效地使用旧代码

旧代码更改算法

当您必须对遗留代码库进行更改时,这里有一个
您可以使用的算法。

1. 识别变化点。
2.寻找测试点。
3. 打破依赖关系。
4. 编写测试。
5. 进行更改和重构。

模拟框架

另外我想指出的是,您可以通过使用模拟框架来消除模拟对象的许多痛苦,例如 Mockito。我玩过这个,我最喜欢这个。

I think the best way to write testable code is by practicing TDD. This helps you write testable code, because you first need to write your test before you can write any production code. I recommend you to read Uncle Bob's three laws of TDD. But below I will give you a summarize of the first part:

Over the years I have come to describe Test Driven Development in
terms of three simple rules. They are: You are not allowed to write
any production code unless it is to make a failing unit test pass. You
are not allowed to write any more of a unit test than is sufficient to
fail; and compilation failures are failures. You are not allowed to
write any more production code than is sufficient to pass the one
failing unit test.

You must begin by writing a unit test for the functionality that you
intend to write. But by rule 2, you can't write very much of that unit
test. As soon as the unit test code fails to compile, or fails an
assertion, you must stop and write production code. But by rule 3 you
can only write the production code that makes the test compile or
pass, and no more.

If you think about this you will realize that you simply cannot write
very much code at all without compiling and executing something.
Indeed, this is really the point. In everything we do, whether writing
tests, writing production code, or refactoring, we keep the system
executing at all times. The time between running tests is on the order
of seconds, or minutes. Even 10 minutes is too long.

This Guide: Writing Testable Code is also very interesting read and gives you a lot of tips to write testable code.

UPDATE

Refactoring

When you have test in place you just need to do refactoring, but keep in mind you are not allowed to write any production code, before you have a failing test. I think your (might)class has to much responsibilities when it has up to 12 collaborators.

Extract a class where you are altering existing behavior. As you work
on existing functionality, (i.e. adding another conditional) extract a
class pulling along that responsibility. This will start to take
chunks out of the legacy class, and you will be able to test each
chunk in isolation (using Dependency Injection).

Working Effectively with Legacy Code

I would like to point out Working Effectively with Legacy Code

The Legacy Code Change Algorithm

When you have to make a change in a legacy code base, here is an
algorithm you can use.

1. Identify change points.
2. Find test points.
3. Break dependencies.
4. Write tests.
5. Make changes and refactor.

Mocking frameworks

Also I would like to point that you can eliminate much of the pain mocking objects by using mocking frameworks like for example Mockito. I played with this one and I like this one the most.

不必在意 2024-12-01 19:04:56

无需回到复杂的设计模式:为什么不将“丰富”方法转移到相应的服务中?这样,您可以在 Enricher 类中保留处理丰富步骤的列表,但将实际的 enrich 调用委托给具有实际丰富知识的服务。贸易。然后可以单独测试这些服务。

应用于您的代码:

public class CounterpartyService {
    private EnrichedTradeRequest enrichCounterparty(EnrichedRequest enrichedRequest)
    {
        // Enrich trade with counterparty details..
        // ..
        return enrichedRequest;
    }
}

public class Enricher
{
    private CounterpartyService counterPartyService;
    // ... 6 more services

    public EnrichedTradeRequest enrichTrade(TradeRequest request)
    {
        EnrichedTradeRequest enrichedRequest = new EnrichedRequest(request);

        // Enrich with counterparty info
        enrichedRequest = counterPartyService.enrichCounterParty(enrichedRequest);

        // Enrich with other info
        //  ...

    }
}

Without going back to complicated design patterns: Why don't you move your "enrich" methods into the respective services? This way, you could keep the list of which steps of enrichments are processed inside your Enricher class but delegate the actual enrich calls into the services which have the knowledge to actually enrich the trade. These services can then be tested individually.

Applied to your code:

public class CounterpartyService {
    private EnrichedTradeRequest enrichCounterparty(EnrichedRequest enrichedRequest)
    {
        // Enrich trade with counterparty details..
        // ..
        return enrichedRequest;
    }
}

public class Enricher
{
    private CounterpartyService counterPartyService;
    // ... 6 more services

    public EnrichedTradeRequest enrichTrade(TradeRequest request)
    {
        EnrichedTradeRequest enrichedRequest = new EnrichedRequest(request);

        // Enrich with counterparty info
        enrichedRequest = counterPartyService.enrichCounterParty(enrichedRequest);

        // Enrich with other info
        //  ...

    }
}
瑾夏年华 2024-12-01 19:04:56

您的根本问题是最近的语言漂移,通过这种漂移,单元测试已经意味着隔离测试

单元测试的一个稍微传统的定义来自短语“测试单元是发布单元”。因此,如果像您的丰富器这样的东西除了它所包装的服务之外无法进行有效的讨论、指定或交付,那么同样的情况也适用于它的测试。

如果这为您提供了“以这种方式戳它,并且它按照应有的方式运行”形式的测试,那么单元测试警察不会来逮捕您......

当有理由时代理:异步、缓慢、外部、多重实施或不稳定的服务。否则,使用真实的东西进行测试会给你更好、更快的结果。

Your undelying problem is the recent linguistic drift by which unit testing has come to mean isolation testing.

A slightly more traditional definition of unit testing comes from the phrase 'the unit of testing is the unit of release'. So if a something like your enricher can't be usefully discussed, specified or delivered except in terms of the services it wraps, the the same applies to its tests too.

If that gives you tests of of the form 'poke it this way, and it behaves according to the way it should', then the unit test police won't come and arrest you...

Proxy when there is a reason to: asynchronous, slow, external, multiply-implemented, or unstable services. Otherwise, a test that uses the real thing will give you better results, faster.

指尖凝香 2024-12-01 19:04:56
  1. 建立原子浓缩链。
  2. 使每项丰富都是 Guava 框架中的一个功能。
  3. 单独测试和开发每个功能
  4. 将测试的功能合并到生产代码中。

对于开始 - 一个空的丰富

final Function<EnrichedTradeRequest,EnrichedTradeRequest> ID = 
  new Function<EnrichedTradeRequest,EnrichedTradeRequest>() {
     public EnrichedTradeRequest apply(EnrichedTradeRequest arg) {
       return arg;
  }});

要调用它 - 调用 Y = ID.apply(X)。

然后对每一项丰富功能进行一一开发、测试并引入生产。

import com.google.common.base.*;

private FuEnrichCounterParty = ...;
void setFuEnrichCounterParty(...) 
private FuEnrichBookingEntity = ...
void setFuEnrichBookingEntity(...) 
...

public EnrichedTradeRequest enrichTrade(TradeRequest request)
{
    EnrichedTradeRequest enrichedRequest = new EnrichedRequest(request);
    // Enrich with counterparty info
    enrichedRequest = FuEnrichCounterParty.apply(enrichedRequest)
    // Enrich with booking entity info
    enrichedRequest = FuEnrichBookingEntity(enrichedRequest)
    // Enrich with exchange rate info
    ...
    return enrichedRequest;
  1. Make a chain of atomic enrichments.
  2. Make each enrichment is a function in term of Guava framework.
  3. Test and develop each function in isolation
  4. Combine tested functions into the production code.

For start - an empty enrichment

final Function<EnrichedTradeRequest,EnrichedTradeRequest> ID = 
  new Function<EnrichedTradeRequest,EnrichedTradeRequest>() {
     public EnrichedTradeRequest apply(EnrichedTradeRequest arg) {
       return arg;
  }});

To invoke it - call Y = ID.apply(X).

Then one by one develop, test and introduce into the production each of the enrichment functions.

import com.google.common.base.*;

private FuEnrichCounterParty = ...;
void setFuEnrichCounterParty(...) 
private FuEnrichBookingEntity = ...
void setFuEnrichBookingEntity(...) 
...

public EnrichedTradeRequest enrichTrade(TradeRequest request)
{
    EnrichedTradeRequest enrichedRequest = new EnrichedRequest(request);
    // Enrich with counterparty info
    enrichedRequest = FuEnrichCounterParty.apply(enrichedRequest)
    // Enrich with booking entity info
    enrichedRequest = FuEnrichBookingEntity(enrichedRequest)
    // Enrich with exchange rate info
    ...
    return enrichedRequest;
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文