Добавить в корзинуПозвонить
Найти в Дзене

Unit-тесты. Основные методы, которые я использую. Часть 1.

Думаю, каждый начинающий разработчик встречается с тестами. Я помню свою первую встречу и она была ужасна. Я вообще не понимала, что происходит. Что ещё за мокито и почему нельзя создать нормальный объект? Как тестить многопоточность? А базу данных? Даже сейчас, спустя 4 месяц, синтаксис тестов меня немного пугает.
Сегодня возьмём для примера самый простой класс и попробуем его

Думаю, каждый начинающий разработчик встречается с тестами. Я помню свою первую встречу и она была ужасна. Я вообще не понимала, что происходит. Что ещё за мокито и почему нельзя создать нормальный объект? Как тестить многопоточность? А базу данных? Даже сейчас, спустя 4 месяц, синтаксис тестов меня немного пугает.

Сегодня возьмём для примера самый простой класс и попробуем его протестировать:

class Cat(private val settings: Settings) { // В конструктор кота передаём настройки
fun catSpeaking(): String { // Метод, который нам возвращает строку
val sound = settings.getCatSound() // Получаем звук, который издаём кот
return "Cat says $sound"
}

В юнит-тестах я обычно использую Mockito: https://site.mockito.org/. Это такая потрясающая библиотека, которая позволяет сделать различные проверки + замокать класс. Обратите внимание, что зависимость надо прописать именно в testImplementation.

Что означает "замокать класс". В примере выше мы тестируем именно класс Cat. Класс настроек мы не тестируем. Нам в принципе всё равно, что там происходит. Поэтому для этого класса мы можем не создавать реальный объект, а просто замокать его. Т.е. просто создадим некий имитатор объекта. Если мы создадим реальный объект, то надо будет также создавать ещё всё, что требует второй класс, а нам это не нужно.

class CatTest {
private val settings: Settings = mock() // Создаём имитацию настоящего объекта
val cat = Cat(settings) // Тестируемый класс надо создавать обычным способом, потому что нас интересует этот класс и надо иметь доступ ко всем параметрам и методам класса.
}

Но тут есть подвох, если вы решили использовать мокито в инструментальных тестах (те, которые на устройстве запускаются). Я очень долго не могла понять, почему у меня не работал mock. Оказалось, всё настолько просто, что даже немного стыдно: я забыла, что это инструментальный тест, и надо прописать зависимость для androidTestImplementation. И ещё заметьте, что тут не обычная зависимость мокито, а мокито-андроид.

Начинаем тестировать наш метод. В тестах всегда надо писать сверху @Test, иначе мы не сможем его запустить.

@Test
fun catSpeaking() { }

Смотрим на строку val sound = settings.getCatSound() . По сути, тут идёт вызов метода в другом классе, который нас вообще не интересует. Если бы мы хотели проверить работу метода getCatSound, то тестировали бы настройки. Так что тут нам не нужна настоящая работа метода и мы тоже можем его замокать. Т.е. создать имитацию работы метода и сразу вернуть результат.

`when`( settings.getCatSound() ).thenReturn("Meow") // когда в коде встречается settings.getCatSound(), то не надо выполнять метод, а просто надо сразу вернуть Meow.

Также, конечно, я использую стандартные assertTrue, assertNull, assertEquals и т.п. Это самые распространенные методы, которые встречаются у меня в тестах. Уже по названиям можно понять, что происходит. Итак, как выглядит весь тест:

@Test
fun catSpeaking() {
`when`( settings.getCatSound() ).thenReturn("Meow")
val result = cat. catSpeaking() // вызываем метод, который нужно проверить
assertNotNull(result) // проверяем, что результат не null
assertEquals("Cat says Meow", result) // проверяем. Слева то, что ожидаем. Справа то, что вернул нам метод.
}

Если хотим проверить, что будет выброшено исключение, то обычно мои методы выглядят как-то так:

@Test(expected = IllegalStateException::class) // исключение, которое ожидаем
fun catSpeaking() {
doAnswer {
throw IllegalStateException("Что-то пошло не так.")
}.`when`( settings ). getCatSound ()
val result = cat. catSpeaking()
assertNotNull(result)
}