实战单元测试之外部数据的准备

上篇我们讲解了数据库中的数据准备,这次我们来讲下写测试时碰到外部数据的情况改如何应对。

首先来讲一下外部数据和测试数据库的区别,测试数据库是可控的外部依赖,因为测试数据库是我们完全可控的。

而我们现在要讲的是外部数据是不可控的数据,如:天气,汇率等需要从外部服务获取的数据。

刚才说到了外部依赖这个词,一定有同学想到了要用 Test double 了,这里简单说明一下:

系统专用的数据库(前提是系统的数据库是专用的,不包括多个系统共用同一数据库的情况),虽然很明显是一个外部依赖,但是由于数据库只和系统交互,从外部来看,是感觉不到数据库的存在的。因此我们可以把它视为系统的一部分。

回到正题,本文提到的外部数据那是不归系统管的,比如天气问题,股票汇率等,系统只能接收这些数据,无法进行控制。

那么对于这样的数据,只能使用 Stub 了,关于 Stub 和 Mock 之类的话题之后会专门讲解的,这里只要知道这种情况要使用 Stub 就行了。

现代的测试工具功能都很强大,只要你想,都可以 Stub 出来,但是如果可能,还是应该尽量使用优雅的方法。

比如现在有一段代码,提供货物到达的时间:

class Shipment
  attr_reader :arrival_time

  def initialize(arrival_time)
    @arrival_time = arrival_time
  end

  def get_arrival_time()
    weather = WeatherSDK.new('Sydney').get_today_weather
    extra_time = case weather
                 when 'sunny'
                   5.minutes
                 when 'cloudy'
                   10.minutes
                 when 'rainy'
                   15.minutes
                 else
                   0
                 end
    self.arrival_time + extra_time
  end
end

那么测试的时候,需要验证不同天气下到达时间的计算是否正确,那当然不能真的去外部服务器获取真实数据。 我们可以,用这样的方法来强制获取 sunny 的天气,这样就保证了测试时的稳定性。

WeatherSDK.any_instance.stub(:get_today_weiather).and_return('sunny')

测试倒是没问题,但是生产代码是否可以改进一下呢?

假设这个提供天气信息的服务停止了,我们需要另一个 SDK 来维持这个功能不受影响,那就需要同时修改测试和生产代码。

所以对于第三方的 SDK, 应该无脑操作自己再封装一次,这样测试代码中的

MyWeatherSDK.any_instance.stub(:get_today_weiather).and_return('sunny')

就不需要修改了。

还没完,假设我们目前所有的物流都限于悉尼一座城市,如果这个系统上海分部的公司也需要使用呢? 当然我们可以给 get_arrival_time 加一个地理位置的参数,不过要说这样的代码的根本问题是,在方法中存在一个不可控的变量。

如果可以把这个外部依赖使用依赖注入的手法来消除这个隐患的话那再好不过了。

class Shipment
  attr_reader :arrival_time, :weather_sdk

  def initialize(arrival_time, weather_sdk)
    @arrival_time = arrival_time
    @weather_sdk = weather_sdk
  end

  def get_arrival_time()
    weather = weather_sdk.get_today_weather
    extra_time = case weather
                 when 'sunny'
                   5.minutes
                 when 'cloudy'
                   10.minutes
                 when 'rainy'
                   15.minutes
                 else
                   0
                 end
    self.arrival_time + extra_time
  end
end

这样一来,测试的方式也有了变化,由于现在 weather sdk 是显式注入的,我们可以不需要使用测试工具的 stub 方法,而采取另一种 Stub: dummy

dummy_weiather_sdk = OpenStruct.new(get_today_weather: "sunny")

对比一下之前的 stub 方式,在阅读代码时,需要知道要 stub 一些东西,但是 stub 的对象从表面根本发现不了,需要阅读实现细节才能知道。

相比之下,通过显式注入和 dummy 对象的测试起来无论可读性还是运行效率都要更高。

当然在这个例子中,你可以说直接注入 today_weather 更加简单,不过这不是本文要讨论的了。

之后我们会进入下一个步骤,就是 AAA 中的 Act 或者说 Given When Then 中的 When 部分。