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

时间:2019-03-26 14:07:43

标签: android tdd android-testing

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

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-测试实施不应消耗太多时间。

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

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

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

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

(显然)这将提供相同的结果,而不会遇到模拟LocationManager的麻烦。但是,我认为这违背了测试的目的-因为它只关注合同本身-而不是使用合同的实际类。

我仍然认为也许第一次练习仍然是正确的方法。最初,只需花费一些时间就可以熟悉Android类的 mocking 。但是我很想知道TDD的专家们的想法。

1 个答案:

答案 0 :(得分:1)

Working backwards... this looks a little weird:

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

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

You've got a mock(LocationDataSource) talking to a TestObserver. That test isn't completely without value, but if I'm not mistaken running tells you nothing new; if the code compiles, then the contract is satisfied.

In a language where you have reliable type checking, executed tests should have a test subject that is a production implementation. So in your second example, if observer were a test subject, that would be "fine".

I wouldn't pass that test in a code review -- unless there is spooky recursion at a distance going on, there's no reason to mock a method call that you are going to be making in the test itself.

// 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.

Right - your current design is fighting with you when you try to test it. That's a symptom; your job as the designer is to identify the problem.

In this case, the code you are trying to test it too tightly coupled to the LocationManager. It is common to create an interface/contract that you can hide a specific implementation behind. Sometimes this pattern is called a seam.

LocationManager::isProviderEnabled, from the outside, is just a function that takes a String and returns a boolean. So instead of writing your method in terms of the LocationManager, write it in terms of the capability that it will give you:

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

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

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

In effect, we're trying to push the "hard to test" bits closer to the boundaries的实例,在此我们将依靠其他技术来解决风险。