Правильное (и упрощенное) тестирование источника данных

#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. Я просмотрел ряд руководств и лучших практик, но не нашел ни одной (пока), которая делала бы это таким образом. Тем не менее, большое вам спасибо.