В предыдущей статье мы разобрались, что такое TDD и какие тесты бывают. Сегодня у нас будет практика: мы напишем наши первые тесты.
Пишем unit-тесты
Давайте напишем наши первые unit-тесты. Создадим тестовое приложение My Test Application. Если вы ни разу не создавали своё приложение под Android, то в качестве шпаргалки пригодится статья «Как создать приложение для Android самому».
Приложение, которое вы увидите ниже, можно скачать и убедиться, что всё работает как надо.
Строковые ресурсы strings.xml и размеры:
<string name="app_name">My Test Application</string> <string name="email_label">Your Email address:</string> <string name="save">Save</string> <string name="email_hint">Enter your Email</string> <string name="invalid_email">Invalid email</string> <string name="valid_email">OK</string> <dimen name="activity_padding">16dp</dimen> <dimen name="main_margin">20dp</dimen>
Главный и единственный экран activity_main.xml:
<?xml version="1.0" encoding="UTF-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="@dimen/activity_padding"> <TextView android:id="@+id/titleTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/main_margin" android:text="@string/email_label" android:textAppearance="?android:attr/textAppearanceMedium" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> <EditText android:id="@+id/emailInput" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_margin="@dimen/main_margin" android:hint="@string/email_hint" android:inputType="textEmailAddress" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/titleTextView" /> <Button android:id="@+id/saveButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_margin="@dimen/main_margin" android:text="@string/save" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/emailInput" /> </androidx.constraintlayout.widget.ConstraintLayout>
Создайте класс, проверяющий введённый email:
package com.example.mytestapplication import android.text.Editable import android.text.TextWatcher import java.util.regex.Pattern class EmailValidator : TextWatcher { internal var isValid = false override fun afterTextChanged(editableText: Editable) { isValid = isValidEmail(editableText) } override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) = Unit override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) = Unit companion object { /** * Паттерн для сравнения. */ private val EMAIL_PATTERN = Pattern.compile( "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + "\\@" + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + "(" + "\\." + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + ")+" ) fun isValidEmail(email: CharSequence?): Boolean { return email != null && EMAIL_PATTERN.matcher(email).matches() } } }
Сам главный экран MainActivity:
package com.example.mytestapplication import android.os.Bundle import android.widget.Button import android.widget.EditText import android.widget.Toast import androidx.appcompat.app.AppCompatActivity class MainActivity : AppCompatActivity() { private val emailValidator = EmailValidator() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) findViewById<EditText>(R.id.emailInput).addTextChangedListener(emailValidator) findViewById<Button>(R.id.saveButton).setOnClickListener { if (emailValidator.isValid) { Toast.makeText(this@MainActivity, getString(R.string.valid_email), Toast.LENGTH_SHORT).show() } else { val errorEmail = getString(R.string.invalid_email) findViewById<EditText>(R.id.emailInput).error = errorEmail Toast.makeText(this@MainActivity, errorEmail, Toast.LENGTH_SHORT).show() } } } }
Убедимся, что всё работает уже на этом этапе, хотя главное — это написание тестов. Для этого у нас есть две автоматически сгенерированные папки, помимо основной.
Вы уже знаете, что папка, помеченная androidTest, предназначена для инструментальных тестов (загляните в неё ради интереса). Нам нужна папка, помеченная просто test. Сейчас там находится единственный класс, созданный для примера: ExampleUnitTest. Давайте добавим свой класс для тестирования функционала нашего приложения, EmailValidatorTest:
class EmailValidatorTest { package com.example.mytestapplication import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Test class EmailValidatorTest { @Test fun emailValidator_CorrectEmailSimple_ReturnsTrue() { assertTrue(EmailValidator.isValidEmail("name@email.com")) } @Test fun emailValidator_CorrectEmailSubDomain_ReturnsTrue() { assertTrue(EmailValidator.isValidEmail("name@email.co.uk")) } @Test fun emailValidator_InvalidEmailNoTld_ReturnsFalse() { assertFalse(EmailValidator.isValidEmail("name@email")) } @Test fun emailValidator_InvalidEmailDoubleDot_ReturnsFalse() { assertFalse(EmailValidator.isValidEmail("name@email..com")) } @Test fun emailValidator_InvalidEmailNoUsername_ReturnsFalse() { assertFalse(EmailValidator.isValidEmail("@email.com")) } @Test fun emailValidator_EmptyString_ReturnsFalse() { assertFalse(EmailValidator.isValidEmail("")) } @Test fun emailValidator_NullEmail_ReturnsFalse() { assertFalse(EmailValidator.isValidEmail(null)) } }
Давайте разбираться:
- Все методы, предполагающие тестирование, должны помечаться аннотацией @Test. Так среда разработки понимает, что это методы для тестирования.
- Названия методов должны описывать то, что они тестируют, и записываться в camel_Snake_Case.
- Названия тестов должны быть в одном стиле, чтобы было проще искать их, если перед глазами только лог (например, при CI/CD). Главное, чтобы все в команде разработчиков называли тесты единообразно.
- Действует правило: одно Утверждение (assert) — один тест.
- Мы проверяем все случаи, которые придут нам в голову. Допускаем, что в нашем тестовом классе проверяются не все возможные Утверждения, но мы точно проверяем все основные.
Как мы проверяем наши Утверждения? Мы используем метод assert из пекеджа org.junit (зависимость testImplementation 'junit:junit:4.+' в Gradle). Там есть довольно много методов, проверяющих разные значения, но нам для этого примера достаточно двух: assertTrue и assertFalse. Они принимают на вход значение и проверяют, совпадает ли оно с нашим Утверждением. В качестве значения мы передаем результат проверки класса EmailValidator.
На будущее
Помимо assertTrue и assertFalse, существуют методы:
- assertEquals;
- assertNotEquals;
- assertArrayEquals;
- assertNull;
- assertNotNull;
- assertSame.
Названия методов говорят сами за себя.
Осталось запустить тесты и посмотреть, как они выполняются. Для этого достаточно кликнуть на EmailValidatorTest правой кнопкой мыши:
Тест сразу запустится, и вы увидите результат. Если какие-то тесты не пройдут проверку Утверждений, вы сразу увидите это и сможете исправить или код, или тест — в зависимости от того, что именно пошло не так.
Конфигурация для прогона ваших тестов создалась автоматически, её можно найти в окне конфигураций запуска (там всегда есть как минимум одна конфигурация app для запуска приложения):
Не забудьте сменить конфигурацию, если хотите запустить приложение, а не тесты:
Есть опция запуска с отображением покрытия вашего приложения тестами:
После прогона тестов откроется дополнительное окно, которое показывает, насколько ваше приложение покрыто тестами:
Через двойной щелчок можно посмотреть, какие классы и как покрыты тестами:
Мы можем покрыть тестами класс EmailValidator, потому что он написан на чистом Kotlin без привязки к жизненному циклу Activity, но нет никакой возможности проверить работу методов нашей Activity. Для этого JUnit уже не подойдёт, как бы вы этого ни хотели. Для проверки работы Activity/Fragment нужны другие инструменты и виды тестов (мы ещё дойдём до этого на нашем курсе). Это ещё раз доказывает, что благодаря тестированию мы делаем свой код лучше. В этом примере благодаря тестированию мы:
- вынесли логику проверки в отдельный класс,
- сделали наш код переиспользуемым для любой Activity или Fragment.
Читайте больше полезных статей для начинающих Android-разработчиков:
- «Тестирование в Android. Часть 1: введение»
- «Делаем приложение на Android доступным для всех»
- «Flow: асинхронный «поток» в Kotlin»
- «Android: парсим JSON правильно»
- «Эксперименты с новыми API Android 12: Render Effect»
А если затянет — приходите на факультет Android-разработки. Во время учебы вы разработаете Android-приложение и выложите его в Google Play, даже если никогда не программировали. А также освоите языки Java и Kotlin, командную разработку, Material Design и принципы тестирования.
Комментарии