#android #tdd #android-testing
#Android #tdd #android-тестирование
Вопрос:
Недавно я начал заниматься тестированием (TDD), и мне было интересно, может ли кто-нибудь пролить некоторый свет на практику, которой я занимаюсь. Например, я проверяю, доступен ли поставщик местоположения, я реализую класс contract (источник данных) и оболочку, вот так:
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
and 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 — тестовая реализация не должна занимать слишком много времени.
Итак, я подумал, было бы лучше (и все еще в рамках 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
. Но, я думаю, это противоречит цели теста — поскольку он фокусируется только на самом контракте — а не на фактическом классе, который его использует.
Я все еще думаю, что, возможно, первая практика все еще является правильным способом. На начальном этапе просто требуется время для ознакомления с макетированием классов Android. Но я хотел бы знать, что думают эксперты по TDD.
Ответ №1:
Работая в обратном направлении … это выглядит немного странно:
// Given
`when`(dataSource.isLocationAvailable()).thenReturn(false)
// When
dataSource.isLocationAvailable().subscribe(observer)
Вы mock(LocationDataSource)
разговариваете с TestObserver
. Этот тест не совсем бесполезен, но, если я не ошибаюсь, запуск не сообщает вам ничего нового; если код компилируется, значит, контракт выполнен.
На языке, где у вас есть надежная проверка типов, выполняемые тесты должны иметь объект тестирования, который является производственной реализацией. Итак, в вашем втором примере, если observer
бы вы были объектом тестирования, это было бы «нормально».
Я бы не стал проходить этот тест при проверке кода — если только не происходит жуткая рекурсия на расстоянии, нет причин имитировать вызов метода, который вы собираетесь выполнить в самом тесте.
// When
BehaviorSubject.createDefault(false).subscribe(testSubject);
времени, которое я потратил на выяснение того, как издеваться над LocationManager, было достаточно, чтобы (я думаю) нарушить одно из общих правил TDD — тестовая реализация не должна занимать слишком много времени.
Правильно — ваш текущий дизайн борется с вами, когда вы пытаетесь его протестировать. Это симптом; ваша задача как разработчика — определить проблему.
В этом случае код, который вы пытаетесь протестировать, слишком тесно связан с LocationManager
. Обычно создается интерфейс / контракт, за которым вы можете скрыть конкретную реализацию. Иногда этот шаблон называется seam
.
LocationManager::isProviderEnabled
извне это просто функция, которая принимает String
и возвращает логическое значение. Поэтому вместо того, чтобы писать свой метод в терминах LocationManager
, напишите его в терминах возможностей, которые он вам даст:
class LocationUtil(isProviderEnabled: (String) -> boolean ) : LocationDataSource {
private var isAvailableSubject: BehaviorSubject<Boolean> =
BehaviorSubject.createDefault(isProviderEnabled(provider))
override fun isAvailable(): Observable<Boolean> = locationSubject
}
По сути, мы пытаемся приблизить «трудные для тестирования» части к границам, где мы будем полагаться на другие методы для устранения рисков.
Комментарии:
1. » запуск не сообщает вам ничего нового; » — Идеальный способ описать второй сценарий — и почти все, что я видел в модульных тестах (по крайней мере, с Android). Спасибо, что предложили функцию более высокого порядка в качестве параметров и ссылку на границы, я протестирую это. Я действительно думаю, что это облегчило бы мне тестирование различных сценариев с классом util. Однако мне интересно, является ли это обычной практикой для Android. Я просмотрел ряд руководств и лучших практик, но не нашел ни одной (пока), которая делала бы это таким образом. Тем не менее, большое вам спасибо.