正确(和简化)数据源测试

Proper (and Simplified) Testing of a Data Source

我最近开始进行测试 (TDD),想知道是否有人可以阐明我正在做的练习。例如,我正在检查位置提供程序是否可用,我实现了一个合同(数据源)class 和一个包装器,如下所示:

LocationDataSource.kt

interface LocationDataSource {

  fun isAvailable(): Observable<Boolean>

}

LocationUtil.kt

class LocationUtil(manager: LocationManager): LocationDataSource {

  private var isAvailableSubject: BehaviorSubject<Boolean> = 
      BehaviorSubject.createDefault(manager.isProviderEnabled(provider))

  override fun isAvailable(): Observable<Boolean> = locationSubject

}

现在,在测试时,我不确定如何进行。我做的第一件事是模拟 LocationManagerisProviderEnabled 方法:

class LocationTest {

  @Mock
  private lateinit var context: Context

  private lateinit var dataSource: LocationDataSource
  private lateinit var manager: LocationManager

  private val observer = TestObserver<Boolean>()

  @Before
  @Throws(Exception::class)
  fun setUp(){
    MockitoAnnotations.initMocks(this)

    // override schedulers here

    `when`(context.getSystemService(LocationManager::class.java))
        .thenReturn(mock(LocationManager::class.java))

    manager = context.getSystemService(LocationManager::class.java)
    dataSource = LocationUtil(manager)
  }

  @Test
  fun isProviderDisabled_ShouldReturnFalse(){
    // Given
    `when`(manager.isProviderEnabled(anyString())).thenReturn(false)

    // When
    dataSource.isLocationAvailable().subscribe(observer)

    // Then
    observer.assertNoErrors()
    observer.assertValue(false)
  }

}

这行得通。然而,在我研究如何做 这个和那个 的过程中,我花在弄清楚如何模拟 LocationManager 上的时间足以(我认为)打破其中一个TDD 中的通用规则 -- 测试实施不应消耗太多时间。

所以我想,是否最好(并且仍在 TDD 范围内)只测试合约 (LocationDataSource) 本身?模拟 dataSource 然后将上面的测试替换为:

@Test
fun isProviderDisable_ShouldReturnFalse() {
    // Given
    `when`(dataSource.isLocationAvailable()).thenReturn(false)

    // When
    dataSource.isLocationAvailable().subscribe(observer)

    // Then
    observer.assertNoErrors()
    observer.assertValue(false)
}

这将(显然)提供相同的结果,而无需经历模拟 LocationManager 的麻烦。但是,我认为这违背了测试的目的——因为它只关注合约本身——而不是使用它的实际 class。

我还是觉得,也许先练才是正道。最初,只需要时间来熟悉 Android classes 的 mocking。但我很想知道 TDD 专家的想法。

向后工作...这看起来有点奇怪:

// Given
`when`(dataSource.isLocationAvailable()).thenReturn(false)

// When
dataSource.isLocationAvailable().subscribe(observer)

您有一个 mock(LocationDataSource) 正在与一个 TestObserver 交谈。该测试并非完全没有价值,但如果我没记错的话 运行 没有告诉您任何新内容;如果代码 编译 ,则合同满足。

在具有可靠类型检查的语言中,执行的测试应该有一个作为生产实现的测试主题。所以在你的第二个例子中,如果 observer 是一个测试对象,那就是 "fine".

我不会在代码审查中通过该测试 - 除非在一定距离内发生令人毛骨悚然的递归,否则没有理由模拟您将在测试本身中进行的方法调用。

// When
BehaviorSubject.createDefault(false).subscribe(testSubject);

the time I spent figuring out how to mock the LocationManager was big enough to (I think) break one of the common rules in TDD -- a test implementation should not consume too much time.

是的 - 当您尝试测试时,您当前的设计正在与您作对。这是一个症状;作为设计师,您的工作是找出问题所在。

在这种情况下,您要测试的代码与 LocationManager 耦合得太紧了。 common 创建一个 interface/contract 可以隐藏特定的实现。有时这种模式被称为 seam.

LocationManager::isProviderEnabled,从外面看,只是一个接受 String 和 returns 布尔值的函数。因此,与其根据 LocationManager 来编写方法,不如根据它将为您提供的功能来编写方法:

class LocationUtil(isProviderEnabled: (String) -> boolean ) : LocationDataSource {

  private var isAvailableSubject: BehaviorSubject<Boolean> = 
      BehaviorSubject.createDefault(isProviderEnabled(provider))

  override fun isAvailable(): Observable<Boolean> = locationSubject
}

实际上,我们正试图将 "hard to test" 位推向更接近 boundaries 的位置,我们将依靠其他技术来解决风险。