Тестирование в Android. Часть 2: unit-тесты

Продолжаем говорить о тестировании Android-приложений и переходим к написанию первых тестов
2 минуты4532

В предыдущей статье мы разобрались, что такое 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-разработки. Во время учебы вы разработаете Android-приложение и выложите его в Google Play, даже если никогда не программировали. А также освоите языки Java и Kotlin, командную разработку, Material Design и принципы тестирования.

androidпрограммированиеразработкамобильные приложения
Нашли ошибку в тексте? Напишите нам.
Спасибо,
что читаете наш блог!