Предисловие:

Раньше я думал, что разработчик пишет модульные тесты только для отлова ошибок, но моя мысль была лишь одним из ее пунктов. Написание модульных тестов может помочь обнаружить ошибки во время модульного тестирования, но оно может принести и другие, более важные и полезные преимущества. Здесь я представляю некоторые преимущества, которые я думаю, не очень известны, но они важны.

Кратко известные преимущества модульных тестов:

Модульные тесты сокращают ручное тестирование, вам не нужно создавать, запускать и вручную запускать (не)определенные сценарии (что в моем опыте разработки для Android обычно тратило мое время с 10 до 6 минут). Тестируемость предоставляет вам возможность настройки, и это позволяет вам тестировать различные сценарии за считанные секунды, поэтому при отладке и изменении исходных кодов вы значительно экономите время. Кроме того, вы застрахованы от побочных эффектов ручного тестирования серверной части, базы данных и изменений в файлах… с помощью имитации или базы данных в памяти (изолированной). С помощью модульного тестирования разработчики могут запускать и повторять многочисленные тесты, которые будут проверены самими собой (автоматизировать), и их можно запланировать на любое время. Модульные тесты позволяют вам узнать, какова ситуация с FUT (тестируемая функция), вы можете получить быструю обратную связь от модульных тестов, которые избавят вас от выпуска и процесса контроля качества, потому что вы видите, какие модульные тесты нарушены изменениями. Также утешает и обнадеживает множество успешных модульных тестов.

Вкратце:
 – Поддержка различных сценариев
 – Автоматическое тестирование
 – Повторяющееся тестирование
 – Изолированное тестирование
 – Быстрая обратная связь

1-Утверждаемые документы:

Функция показывает, как составление функций и операторов обеспечивает желаемую функциональность, но модульный тест представляет по заданному состоянию, что является выходом FUT (тестируемая функция) или каков побочный эффект вызова функции путем предоставления данных. Модульное тестирование может документировать функциональность функции, потому что фикстура, упражнение и утверждение представляют отношение состояния, данных, операции и вывода в кейсе. Данные и конфигурации показывают состояние, упражнение показывает влияние операции, а утверждение показывает ожидания. Поэтому важно, что и какие данные представлены в модульном тесте.

Модульный тест можно рассматривать как случай, который показывает, как функция передает состояние или данные на выход, или вы видите, что является результатом вызова функции с заданными параметрами. Таким образом, модульный тест представляет отношение данных и функции к результату. . Посмотрите на следующие модульные тесты.

fun test_ab_1_2(){
  val result = ab(1, 2)
  assertTrue(result == 3)
}

fun test_ab_2_3(){
  val result = ab(2, 3)
  assertTrue(result == 5)
}

fun test_ab_3_2(){
  val result = ab(3, 2)
  assertTrue(result == 5)
}

Увидев приведенные выше функции, вы догадываетесь, что функция ab имеет функциональность суммы, вы правы, увидев модульный тест с различными параметрами, мы найдем общее представление о том, какова функциональность функции, хотя ее имя может быть лжецом.

fun test_sum(){
  //The function tell us if given 2, 3 then result must have 5
  val result  = sum(2, 3)
  assertTrue(result == 5)
}

Переименовав функцию ab в sum, читатели быстрее поймут, какова функциональность этой функции.

Я предлагаю вам использовать шаблон объекта-матери (построители тестовых данных, генератор данных фикстур) для тестов, чтобы иметь более читаемые, сфокусированные модульные тесты, потому что пример может показать, что имеет значение, а что нет. Упомянутые шаблоны могут уменьшить неоднозначность роли данных. Посмотрите на следующий тест.

fun test_isValidDeeplink(){
    val whiteList = listOf(
        "google.com", "amazon.com"
    )
    val deeplink = "amazon.com/orders"
    val actual = isValidDeeplink(deeplink, whiteList)
    assertTrue(actual)
}

У новых читателей, увидевших приведенный выше модульный тест, могут возникнуть следующие вопросы: Почему URL-адреса не имеют схемы? почему они заканчиваются разными путями? какова роль google и amazon? Верен ли результат из-за пути или только из-за хоста? мы можем использовать именованное значение и название модульного теста, чтобы раскрыть наше намерение в результате тестирования:

fun test_isValidDeeplink_valid_deeplink_by_path(){
   val just_aUrl1 = "google.com/users"
   val just_aUrl2 = "amazon.com/orders"
   val whiteList = arrayListOf(justAUrl_1,just_aUrl2)
   val deeplink = "amazon.com/orders"
   val actual = isValidDeeplink(deeplink, whiteList)
   assertTrue(actual)
}

Другой пример: увидев следующий тест, у меня возникают некоторые вопросы, почему после местоположения есть версия со значением 21? Почему Амазон? (и более)

fun test_replaceLocationInUrl_in_center_of_url() {
    val url = "amazone.com/location=#LOCMARK/version=21"
    val actual = replaceLocationInUrl(url, "40.67654")
    val expected = "amazone.com/location=xasdsd/version=21"
    assertThat(actual).isEqualTo(expected)
}

Но мы можем уменьшить неоднозначность роли данных, добавив значимые переменные и используя некоторые генераторы данных, как показано ниже. Следующий код просто показывает замену значения местоположения на #LOCMARK, другие данные, такие как aParam и locationValue, не имеют значения. Больше нет параметра запроса версии, значение местоположения и базовый URL не имеют значения.

fun test_replaceLocationInUrl_in_center_of_url() {
    val locationValue = aString()
    val aParam = "${aString()}=${aString()}"
    val url = "${aUrl()}/location=#LOCMARK/${aParam}"
    val actual = replaceLocationInUrl(url, locationValue)
    val expected = basicUrl.plus("/location=$locationValue")
    assertThat(actual).isEqualTo(expected)
}

Примечание: Когда что-то не имеет значения, лучше не быть замеченным. (Fom XUnit Test Patterns: рефакторинг тестового кода Джерарда Месароса)

Уменьшите количество технических деталей в модульном тестировании, чтобы улучшить документацию за счет увеличения абстракции.

Рассматривая модульное тестирование как документ, мы могли бы получить подтверждаемый документ, который говорит нам, является ли документ действительным или нет. Когда модульный тест терпит неудачу, это сигнал о том, что что-то не так, либо функция, либо модульный тест.

Подводя итог:

  • Модульный тест показывает взаимосвязь между данными, упражнениями и выходными данными.
  • Модульный тест можно рассматривать как пример.
  • Модульный тест показывает, как FUT должен работать.
  • Несколько модульных тестов для одной функции вызывают разные реакции.
  • Модульный тест может быть подтверждаемым документом, который может показать конфликты между функциональностью и документом.

Вы можете увидеть некоторые функции генератора данных в моей сути, которые, возможно, помогут вам написать более целенаправленные модульные тесты.



2-Побочные эффекты:

Коды зависят друг от друга, почти каждый код зависит от данных или функций. Небольшое изменение может сделать некоторые функции недействительными. Изменения имеют волновой эффект. В небольшом проекте мы можем запоминать функции и классы и предсказывать изменения или побочные эффекты, но когда проект растет и над большим проектом работает несколько человек, вы не можете вручную запускать и тестировать свои изменения, потому что продукт большой и маленький. изменение может иметь большое влияние, вы должны предоставить версию команде контроля качества и сообщить им, в чем заключались ваши изменения, чтобы устранить возможные побочные эффекты. Работать над таким проектом ужасно, страшно и такой процесс замедляет развитие. Кто знает, каков побочный эффект небольшого изменения в функции, я могу предсказать изменения для двух или трех уровней по использованию функции, но чем больше слоя мы уходим, тем больше неопределенности появляется в нашем прогнозе.

Когда у вас есть проект с множеством модульных тестов, вы можете применить свои изменения, а затем запустить и увидеть результат модульных тестов. Это дает вам уверенность в сценариях тестирования с учетом новых изменений, и вам не нужно проводить ручное тестирование для каждого небольшого изменения.

Предположим, что функции C,иB. Когда C зависит от B, то сбой в B вызовет сбой в функции C, если утверждение находится на выходе.
In в следующем примере getLeapYearColor зависит от isLeapYear, а утверждение выполняется на выходе getLeapYearColor. Наличие дюжины модульных тестов говорит нам о том, что getLeapYearColor потерпел неудачу, потому что isLeapYear действительно потерпел неудачу, это хороший сигнал, который приведет нас к основной причине ошибки.

fun getLeapYearColor(date: Calendar): Int {
    val isLeapYear = date.isLeapYear(date.getYear())
    return if (isLeapYear) {
        Color.PURPLE
    } else {
        Color.BLACK
    }
}

fun test_getMonthColor_leapYear() {
    val date = Calendar.getInstance()
    date.setDate(1399, 1, 1)
    assertEquals(getLeapYearColor(date), Color.PURPLE)
}

Я слышал историю, в видеоигре разработчик изменил алгоритм своей баллистической ракеты, после чего главное меню игры перестало работать (вылетело), ​​очень сложно предсказать изменение зоны.

3-Защитные изменения:

Модульные тесты могут защищать коды от ошибок, вызванных новыми изменениями (преднамеренными или непреднамеренными) в функции или других функциях, проверяя поведение или выходные данные FUT.

Если модульные тесты запускаются после каждого изменения, разработчик находит уверенность в том, что изменения не вносятся, потому что разработчик защитил выходные данные или поведение FUT с помощью заданных данных и ожидаемого результата. Несомненно, если функциональность функции изменилась, возможно, модульный тест недействителен.

В следующем тесте ожидается, что 1400–2–1 будет после 1400–1-30. Если isAfterDate возвращает false, тогда модульный тест должен сообщить об этом своему исполнителю.

fun Date.isAfterDate(date: BaseCalendar): Boolean

fun test_isAfterDate_isTrue() {
    val date = newCalendar(1400, 2, 1)
    assert(date.isAfterDate(newCalendar(1400, 1, 30)==true)
}

При каждом новом запуске модульный тест проверяет функциональность FUT на соответствие заданным ожиданиям. Необходимо сообщать о любом конфликте между функциональностью FUT и ожиданиями модульного тестирования.

Посмотрите на следующую функцию. В продакшене разработчик временно закомментировал вызов userDidLogin (по какой-либо причине) и забыл раскомментировать userDidLogin, эта ошибка не ломает приложение, но вносит дисфункциональность в аналитику. данные.

class LoginFragment(
    private val eventLogger: EventLogger,
    private val userRepository: UserRepository
) {
    fun submitLogin(userName: String, password: String): User? {
        val user = userRepository.login(userName, password)
        if (user != null) {
            //eventLogger.userDidLogin()
        }
        return user
    }
}

Когда вы изменяете код и модульный тест завершается с ошибкой, обновляя этот модульный тест, вы фиксируете/подтверждаете свои изменения.

Модульные тесты могут выявить эти ошибки, например, в FUT.

fun test_login_userDidLogin_mustBeCalled_whenLoginIsSuccessful() {
    val eventLogger = mock(EventLogger::class.java)
    val userRepository = mock(UserRepository::class.java)
    `when`(userRepository.login(any(), any())).thenReturn(User())
    val loginFragment = LoginFragment(eventLogger, userRepository)
    val user = loginFragment.submitLogin(anyString(), anyString())
    verify(eventLogger, times(1)).userDidLogin()
}

Взгляд 4-тестеров на SUT:

Когда мы разрабатываем код, в большинстве случаев мы просто добиваемся функциональности, и это обязанность вызывающей стороны, чтобы настроить себя, в основном разработчики клиентов предполагают, что данные, поступающие от бэкэнда, действительны, и мы должны просто показать их … Но в модульном тестировании мы обнаруживаем вид тестировщика, тестер представляет SUT с FUT в различных состояниях и сценариях.

Следующие коды работают в обычном сценарии, и onViewCreate будет вызываться один раз.

class HomeFragment(private val viewModel: HomeViewModel) : Fragment() {
    private val productList = ArrayList<ProductData>()
    fun onViewCreate() {
        val products = viewModel.fetchProductList()
        productList.addAll(products)
        showProductList()
    }
    fun shownProductList() = productList
}

В производственной среде легко обмануть себя, что проблемных сценариев не будет, поэтому нет необходимости с этим справляться. В производстве программист должен достичь желаемой функциональности, но в тестировании тестировщик должен проверять функциональность программы с другими параметрами, у тестировщика другая точка зрения, и тестер легко спрашивает, что произойдет, если функция будет вызываться с другими параметрами, или что произойдет, если эта функция будет вызвана дважды (ОС или приложение)

fun test_onViewCreate_calledTwice() {
    val viewModel = mock(HomeViewModel::class.java)
    val data = arrayListOf(ProductData(), ProductData())
    `when`(viewModel.fetchProductList()).thenReturn(data)
    val homeFragment = HomeFragment(viewModel)
    homeFragment.onViewCreate()
    homeFragment.onViewCreate()
    //this test will fails
    assert(homeFragment.shownProductList().size == 2)
}

Другой пример: в приложении строка даты поступает из бэкенда и сокращается в формате гггг-мм-дд. Немедленно разработчик перейдет к коду.

fun parseDate(date: String): Date {
    return SimpleDateFormat("yyyy-MM-dd").parse(date)
}

Но тестер будет тестировать функцию с другой строкой формата даты, как показано ниже:

fun test_parseDate() {
    val actual =  parseDate("2000/12/21")
    //assert....
}

Точка зрения тестировщика изменяет нас с унифицированной на информированную, по крайней мере, разработчик знает, что проблема возникнет, если будет предоставлен этот ввод или разработчик решит улучшить функцию.

В конце концов:
Модульные тесты не удаляют работы по контролю качества, но они могут помочь вам создать более стабильный продукт и сэкономить время.

Если у вас есть какие-либо аргументы или комментарии по поводу поста или вы хотите поделиться своими мыслями, сообщите мне об этом в комментариях.

Спасибо за прочтение.