【问题标题】:Writing additional code to perform unit-tests involving ivars OK or should be avoided at all costs?编写额外的代码来执行涉及 ivars 的单元测试可以还是应该不惜一切代价避免?
【发布时间】:2013-01-21 08:57:43
【问题描述】:

在编写单元测试时我还是个新手,我经常遇到这样的情况,我不知道什么是正确的做事方式。为计划中的设计编写测试我遇到了其中一个引起头皮屑的实例。我的设计:

一个 ViewController 根据用户的输入将消息发送到 dataFetcherClass。 (以下代码已更改以保护无辜者)。

-(void) userPushedLocalBusinessButtons{
    [_businessDataFetcher fetchLocalData];
}

-(void) userPushedWorldwideBusinessButtons{
    [_businessDataFetcher fetchWorldwideData];
}

这些操作的数据格式相同,它是 dataFetcher 应从更改中收集数据的位置。所以,在 BusinessDataFetcherClass 我有这些方法:

-(void) fetchLocalData{
    _dataAddress = @"localData.json";
    [self fetchData]; 
}

-(void) fetchWorldwideData{
    _dataAddress = @"worldwideData.json";
    [self fetchData]; 
}

fetchData 方法异步获取数据,并在完成后发送包含收集到的数据的通知。现在,我想编写单元测试,检查在执行 fetchLocalData 或 fetchWorldwideData 时 ivar _dataAddress 是否已更改。

如果不更改代码,这显然是不可能的。有人会说,这可以通过将 _dataAddress 设置为公共属性来轻松解决,这是一种解决方案。另一种方法是创建一个返回 _dataAddress ivar 值的方法。我对这两种选择都不完全满意,因为它们在两种情况下都迫使我只为测试更改代码,而不是提高实际代码库本身的整体质量。

我选择了第二种选择并包含了一个方法 -(NSString *) dataAddress;我的问题(如标题中所述)是这样是否可以?我的设计有问题吗?显然,TDD 的首要目标是避免回归,但我相信提高整体代码质量也是一个重要目标。是否会偶尔添加绒毛?

【问题讨论】:

  • 查看模拟对象进行测试。
  • 如果我理解你们都是正确的,我基本上只需要为返回数据的“公共”方法编写测试?现在,我必须承认,这在很多方面听起来都是明智的,作为一个懒惰的草皮,这对我很有吸引力。不过,让我有点困惑的是我经常看到的对测试覆盖的关注。如果我掌握了您回复的要点,就百分比而言,这将是微不足道的。

标签: objective-c unit-testing tdd code-design


【解决方案1】:

我想编写单元测试来检查 ivar _dataAddress 当 fetchLocalData 或 fetchWorldwideData 已更改时已更改 执行。

当你编写单元测试时,它应该测试一个类的外部行为。这是该类的实现细节。如果您想更改获取数据的工作方式,则该类可能在单元测试失败时仍然有效。这会让你的单元测试很烦人,没有帮助。

异步获取数据并发送通知 完成后收集数据。

听起来这是该类中这些方法的外部行为。这是您应该编写测试来检查的内容。我不知道objective-c,所以这里是一个伪代码示例:

setup expected local data (preferably with a mock)
call fetchLocalData on BusinessDataFetcherClass 
wait a little bit
check that local data is populated on ViewController

我的设计有问题吗?

您的设计确实使编写测试变得有点困难,尽管这不是一个大问题。特别是需要在测试中发生的“等待”。您的测试指出的设计问题是您的类至少有两个职责:获取数据和管理异步。如果您将这些职责分开,它们将更容易测试。

显然,TDD 的首要目标是 避免回归,但我相信提高整体代码质量是 也是一个重要的目标。是否会偶尔添加绒毛?

我认为在这种情况下您可能不需要更多绒毛,但有时在单元测试中确实会发生这种情况。当您编写带有测试的代码时,您最终会得到两个代码客户端:测试代码和生产代码。在不同的上下文中满足两个客户的需要迫使一些“绒毛”进入,或者迫使一些设计改变。好消息是,当您的设计可以轻松满足两个客户时,如果需要,您可能会相当容易地满足第三个和第四个客户。对我来说,这种效果是 TDD 最重要的好处之一。

【讨论】:

    【解决方案2】:

    你不想测试你的类的内部状态——这是没有意义的。你唯一关心的是你的班级在与外部世界的互动中做了什么(信息是向内还是向外或双向)。

    换句话说:一旦你为你的类编写了一个测试,重写你的类的实现(内部)同时保持它的可见行为不应该破坏你的测试。如果是这样,那么您的测试在 IMO 中被破坏了。

    测试类行为的一个好方法是使用 Mock 对象——例如,请参阅OCMock for iOS。

    模拟对象允许您测试目标类的行为。为此,您需要以某种方式编写目标类:在您的示例中,您需要能够传入网络提供程序类,而不是让您的类关闭并使用某个硬编码的提供程序(可重用的组件永远不应该配置自己,而应该被配置)。以这种方式进行设置后,您的单元测试类可以传入一个模拟网络服务提供程序,该服务提供程序检查是否访问了正确的 URL。

    Mock 对象乍一看似乎令人费解,但您正在测试正确的东西 - 目标类的行为 - 没有使用任何特殊的测试方法等污染它。

    另请注意,使您的代码易于测试也使其更易于重复使用:您的测试用例成为代码的第二个“用户”。

    【讨论】:

    • 我实际上在我的其他一些课程(OCMockito)中使用了模拟。我已经看到过你不应该测试一个类的内部的论点,但我也看到人们提出完全相反的观点。虽然我同意外部行为是应用程序的成败(字面意思)。我个人觉得(至少目前)测试方法的范围越广,回归的机会就越小……
    • 测试范围越广越好,我同意。不要将宽与深混为一谈;而你在这里谈论的很深。除非有非常好的特殊原因,否则测试类的内部是错误的。人们说这是个好主意是错误的。
    • 内部状态测试何时有意义的示例:假设您有一个组件进行大量计算,并且您需要检查其内部存储使用量是否不超过某个理论最大值。
    • 谢谢,这是有道理的。另一方面:你会说需要测试内部使用(就像你的例子)表明它应该被分离到它自己的类中?
    • “也许”。在某些情况下,将您的设计分解为子对象/实体可能是有意义的,但这完全取决于具体情况。在某些情况下,它会过度设计。
    【解决方案3】:

    我也不是 ObjectiveC 开发人员,但我认为您发布此内容的原因是因为您正在听您的代码,而您的代码告诉您有些地方不太对劲。

    我想问一下你对fetchData 调用的结果做了什么?我怀疑你在某处渲染数据。如果 iOS 正在渲染它,那么您可能在某处可以断言而不是断言实例变量的回调。如果您从类中更新 UI,如果您引入 Observer 以将您的 UI 和获取数据的代码解耦,它将更容易测试。然后,您可以将您的测试寄存器作为接收器,并在那里断言您的状态发生变化。

    希望有帮助!

    布兰登

    【讨论】:

      猜你喜欢
      • 2010-09-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2012-01-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多