我最近开始进行测试(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
}
现在,在测试时,我不确定如何继续。我做的第一件事是模拟LocationManager
和isProviderEnabled
方法:
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的专家们的想法。
答案 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的实例,在此我们将依靠其他技术来解决风险。