【问题标题】:TDD: How to write unit tests when method depends on network URL'sTDD:当方法依赖于网络 URL 时如何编写单元测试
【发布时间】:2015-11-29 17:26:11
【问题描述】:

我尝试编写消耗一些 url 地址并返回资源(例如 RSS 提要)的方法。

public class NewsSourcesService {

    public List<News> getNewsSource(String url) {

    }

}

我计划在内部使用 ROME,但它不应该被依赖项看到。 很难为该方法编写单元测试,因为准备假状态是不可能的,而且没有什么可模拟的。

在这种情况下编写测试的最佳方法是什么?

【问题讨论】:

  • 问题是:你想测试什么?是否在端点发出请求?
  • 为什么准备假状态是不可能的?
  • @Tunaki RSS/ATOMS 资源的不同变体被正确使用。
  • @erip 好的,这并不容易。对我来说唯一的想法是将假 RSS 资源准备为文件。但是测试状态而不是行为并不好。

标签: java unit-testing mocking tdd


【解决方案1】:

我更喜欢从新功能的组件测试开始。组件测试是指服务的测试(在 SOA 术语中)连同它的适配器(shell 脚本、http 资源、gui 等),被视为一个黑盒子(你不知道里面是什么,你只知道入口接口和外部服务这取决于)。

在您的服务签名中,我们看到您希望从给定的 RSS 网址获取 RSS 新闻列表。假设该服务的适配器是一个简单的 http 控制器。

我将在这里使用伪代码,因为我不是 Java 本地人;)

function testThatListOfRSSNewsCanBeRetrieved() {
    expectedNews = [
         new News.with(new Title('Test title.')).with(new Content('Test content.')).build(),
         new News.with(new Title('Test title 2.')).with(new Content('Test content 2.')).build()
    ];
    rssFeed.addNews(expectedNews);

    retrievedNews = applicationRunner.getRSSNews();
    assertThat(retrievedNews, areSameAs(expectedNews))
}

好的,这很容易。如您所见,测试清晰易懂(我希望如此)。

首先我们使用构建器模式创建预期的新闻列表。

请注意,您不必这样做。您可以直接在此处直接使用域值对象。我喜欢使用构建器,因为当给定值对象的构造器发生更改时,我不必在所有测试中更改它,只需在构建器中更改即可。

在第二部分中,我们使用rssFeed 实例添加新闻。

rssFeed 是一个控制模拟外部 rss 服务器的对象。我们必须在测试之前启动该服务器。它只是服务于我们告诉它服务的东西。通过调用addNews,我们告诉它在请求时提供给定的新闻列表。有此类假服务器的库。使用这种方法,我们不会将服务实现与测试耦合,因为即使实现发生变化,测试仍然可以工作。

然后我们运行我们的应用程序并检查它是否按预期工作。

为了运行应用程序,我们使用包装器对象applicationRunner,它应该在单独的进程中运行应用程序(在这个例子中启动http服务器),向它发出请求,从响应中创建新闻并返回这些。 最后,我们断言检索到的新闻与我们之前准备的相同。

这就是组件测试的全部内容。请记住,您不应该创建太多这样的测试,因为它们很慢。通常一个,最多两个,足以覆盖一个功能。

这不是结束!

现在从您的组件测试开始,您可以计划服务实施并为其进行单元/集成测试。

我们希望我们的服务做的是从给定的 url 返回一个 RSS 新闻列表,这是我们的首要职责,将在服务层实现。让我们为此编写一个测试:

function testThatNewsListCanBeRetrieved() {
    rssURL = 'http://megarss.com';
    expectedNews = [new News.build()];
    mockedRSSDataRetrieve = mock(RSSDataRetrieve());
    mockedRSSDataRetrieve.on('get').withParameters(rssURL).returnValue(expectedNews);
    service = new RSSNewsRetrieve(mockedRSSDataRetrieve);

    newsList = service.newsList();
    assertThat(newsList, areSameAs(expectedNews));
}

通过编写此测试的实现,我们可以提炼出两个新职责(请注意,我们可以在实现过程中发现新职责,这种情况经常发生并且没有错):

  • RSS 数据检索(外部 RSS 服务的网关):

    向给定 url 发出请求并返回原始 RSS 数据的网关对象。

  • RSS 响应解析:

    获取原始 RSS 数据并返回我们的 News 对象。

为了测试服务RSSNewsRetrieve,我们不得不模拟出RSSDataRetrieve,因为我们不想在单元测试中调用外部服务(单元测试应该独立且快速地运行)。 RSSResponseParse 不必被嘲笑甚至注入,因为它没有使我们无法孤立运行的依赖项。

现在我们可以为RSSResponseParser 编写单元测试,因为它很简单,所以我不会在这里介绍。

剩下要测试的是RSSDataRetrieve。我们将使用集成测试对其进行测试。

集成测试不需要单独运行,并且可能比单元测试慢一些。因为我们不想将测试与实现耦合并保持其确定性,所以我们可以再次使用为组件测试创建的假 RSS 服务器。

function testThatRSSDataCanBeRetrieved() {
    expectedNews = [new News.build()];
    rssFeed.addNews(expectedNews);

    rssDataRetrieve = new RSSDataRetrieve();
    rssData = rssDataRetrieve.get(this.url); // this url is passed to rssFeed on initialization
    assertThat(rssData, isTheSameAs(newsToRawRSSData(expectedNews));
}

我没有为服务 HTTP 适配器编写单元测试,因为它包含在组件测试中。

通过这组测试,我们涵盖了保持高水平可读性和可维护性的功能基础。

当然,现在您可以针对不同情况添加更多测试,例如,如果 RSS 服务器不可用或 RSS 服务器以错误格式返回数据,应用程序应该如何运行。

我希望这会有所帮助。

【讨论】:

    【解决方案2】:

    常用的技术是“依赖注入”,您的类依赖于您可以模拟以进行测试的抽象。

    【讨论】:

      【解决方案3】:

      如果您将“从 url 检索 RSS 提要”和“使用 RSS 的变体”视为两个独立的responsibilities,您可以通过创建单独的组件轻松地在代码中反映这一点。 这样两个组件应该更容易单独测试(即只接受输入/输出,“消费”组件没有网络流量)。

      如果您担心这会破坏封装,您可以按照 Mike 的建议查看 dependency injection。如果一个组件 A 需要某些行为 B 才能工作,那么至少对我来说应该非常明显(例如,通过将其作为构造函数参数询问)它需要这样的依赖而不是隐藏它在其实施中。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2020-08-28
        • 2015-01-16
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多