Pull to refresh
110.05
Циан
В топ-6 лучших ИТ-компаний рейтинга Хабр.Карьера

Используем UI-тесты для поиска утечек

Level of difficultyEasy
Reading time6 min
Views2.3K

Утечки памяти — штука неприятная. Они могут приводить к снижению производительности, а в некоторых случаях и к падению приложения. Поэтому их определённо стоит находить и исправлять. Проблема в том, что если вы достаточно хорошо проработали архитектуру вашего проекта или у вас опытные разработчики, то утечки будут появляться редко. Даже очень редко. Как следствие, ручной поиск утечек, на который надо потратить кучу времени, почти всегда будет с нулевым результатом. Чтобы избежать лишней траты времени, представляю вам простой и довольно очевидный способ — встраивание поиска утечек памяти в UI-тесты.

По всем правилам приличия представлюсь — меня зовут Перевалов Данил, а теперь давайте перейдём к теме.

А чё сразу UI-тесты то?

Изначально мы не планировали использовать UI-тесты. Поиск утечек осуществлялся на «ручном приводе». 

Для поиска утечек мы, как и все остальные, используем библиотеку LeakCanary. Эта библиотека хороша почти во всём, разве что сама код не пишет. 

Поэтому наш изначальный план был прост — включить LeakCanary в сборки для разработчиков и тестировщиков. Затем, когда разработчик или тестировщик замечает утечку, он заводит баг. 

Но со временем вскрылись некоторые нюансы:

  • LeakCanary влияет на производительность, а ручное тестирование и так — процесс не быстрый. Не очень хочется его дополнительно растягивать.

  • LeakCanary зачастую тратит слишком много времени на создание дампов памяти, а, напомню, ручное тестирование и так — процесс не быстрый.

  • Утечки часто остаются незамеченными или, возможно, даже игнорируются. Об этом история предпочитает умалчивать.

Ситуацию нужно было как-то исправлять. И что обычно делают люди, когда ручной способ не работает? Правильно, они автоматизируют. Самым очевидным и простым решением нам показалось — просто прикрутить поиск утечек к UI-тестам.

Первый заход

Вот только… На тот момент, когда нам это пришло в голову, нативные UI-тесты для Android в нашей компании только зарождались как явление. Следствием этого было то, что их было мало, и они покрывали небольшое количество сценариев пользователя. Поэтому было принято волевое решение — написать отдельные UI-тесты для поиска утечек.

Специализированные тесты

Суть такого теста проста: 

  1. Открываем приложение.

  2. Открываем экран.

  3. Затем ещё один.

  4. И ещё один.

  5. Короче говоря, открываем как можно больше экранов.

  6. Закрываем экраны. Обычно это делает сам фреймворк тестов.

  7. Запускаем поиск утечек с помощью LeakCanary.

Как вы могли заметить, основная цель такого теста — открыть как можно больше экранов. Затем все экраны закрываются, и запускается поиск утечек.

Немного про LeakCanary

Для начала, как ни странно, нужно подключить, инициализировать и включить проверки в LeakCanary. В целом, достаточно просто подключить LeakCanary, и всё это произойдёт автоматически.

Но есть нюансы:

  • В общем случае, поиск утечек через LeakCanary должен быть включён только в UI-тестах. Однако когда разработчик исправит утечку, он захочет проверить — действительно ли он избавился от неё. Для этого придётся включать LeakCanary в debug-сборке. При этом желательно, чтобы по умолчанию LeakCanary был выключен, но можно было его включить в нужный момент. 

  • Существуют утечки в системе, сторонних библиотеках и прочем, на что вы не сможете повлиять. 

Чтобы свободно включать и выключать поиск утечек, а также избежать постоянных падений тестов из-за того, что, например, в Samsung утекает менеджер клавиатуры, я рекомендую завести Config. Через него можно как включать и выключать поиск утечек, так и добавлять собственные исключения, если это потребуется.

LeakCanary.config = LeakCanary.config.copy(
   dumpHeap = isEnabled,
   referenceMatchers = getKnownReferences()
)

Подключаем LeakCanary к тестам

К счастью, LeakCanary умеет работать и с UI-тестами. Для того чтобы он запустил проверку, достаточно вызвать определённый код по завершении теста.

LeakAssertions.assertNoLeaks()

Поэтому в конце теста, если вы используете Espresso, то в методе с аннотацией After запустим поиск утечек у LeakCanary. 

@After
fun after() {
   // Запускаем поиск утечек
   LeakAssertions.assertNoLeaks()
}

У нас же используется Kaspresso, и мы сделали обёртку над его Rule, чтобы можно было добавлять в init и after секции свой код для всех тестов сразу. Что-то вроде такого (изменил код, чтобы блок с этим кодом имел хоть сколько-нибудь адекватные размеры):

class LeakKaspressoRule(
   testClassName: String
) : TestRule {

   val kaspressoRule = KaspressoRule(testClassName)

   override fun apply(base: Statement, description: Description): Statement {
       return kaspressoRule.apply(base, description)
   }

   fun before(actions: BaseTestContext.() -> Unit) = After(kaspressoRule.before {
       // наш код
       actions(this)
   })

   class After(
       private val after: AfterTestSection<Unit, Unit>
   ) {

       fun after(actions: BaseTestContext.() -> Unit) = Init(after.after {
           // наш код
           actions(this)
       })
   }

   class Init(
       private val init: InitSection<Unit, Unit>
   ) {

       fun run(steps: TestContext<Unit>.() -> Unit) = init.run {
           steps(this)
           // наш код
       }
   }
}

В класс After Kaspresso добавляем поиск утечек.

after.after {
   LeakAssertions.assertNoLeaks()
   actions(this)
}

И в целом, это всё. Рассмотрим пример подобного теста.

class LeakAuthUiTest {

    @get:Rule
    val leakKaspressoRule = LeakKaspressoRule(javaClass.simpleName)

    @Test
    fun testLeakOnAuth() {
        leakKaspressoRule.before {
        }.after {
        }.run {
            step("Открываем личный кабинет") { ... }
            step("Открываем авторизацию")  { ... }
            step("Открываем регистрацию")  { ... }
            step("Открываем восстановление пароля")  { ... }
        }
    }
}

Всё происходит так, как я описал выше — просто открываем экраны, а по завершении теста внутри LeakKaspressoRule запускается поиск утечек памяти.

Теперь давайте немного поговорим о запуске и поддержке таких тестов.

Запуск и поддержка

Наши тесты на поиск утечек памяти запускались с некоторой периодичностью. Скажем так, когда появлялось желание. Откровенно говоря, это желание появлялось нечасто — раз в месяц максимум.

Но это, в целом, работало. Мы написали несколько тестов, которые просто открывали стопку экранов из определённого сценария. Тесты запускались, иногда обнаруживались утечки. На исправление этих утечек заводились задачи. И самое главное, это не требовало много времени разработчика или тестировщика.

Но увы… Со временем возникла проблема поддержки таких тестов. Если тест затрагивает множество экранов, а ваше приложение активно развивается, то вам постоянно придётся корректировать такие тесты в соответствии с изменениями.

Так как тесты на поиск утечек памяти запускались не постоянно, то разработчики случайно ломали в них что-то и даже не замечали этого. Поэтому периодически приходилось создавать задачи на исправление этих тестов. Да и новые подобные тесты никто не хотел писать, ведь потом их нужно муторно поддерживать. Это продолжалось некоторое время. 

Но смею заметить, что это всё ещё был многократно менее трудозатратный способ поиска утечек, чем поиск вручную.

И вот когда в очередной раз мы собирались приступить к исправлению специализированных тестов, мы поняли: «Пора что-то менять».

Второй заход и пока что последний (ну или крайний, если пожелаете)

Количество обычных UI-тестов к тому моменту у нас перевалило за сотню, и суммарно обычные UI-тесты покрывали куда большее количество экранов и сценариев пользователя, чем те, что были заточены под поиск утечек.

Как следствие, нужда в отдельных тестах пропала. Поэтому мы решили попробовать поступить иначе.

Новая концепция

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

Важно, что это лишь возможность. Так или иначе, поиск утечек нехило увеличивает время теста. Нам же хотелось сохранить логику, когда перед слиянием в Git фичёвой ветки с главной веткой прогоняются UI-тесты, а для этого они должны прогоняться достаточно быстро.

Поэтому на нашем CI мы прикрутили следующую логику:

  • Если UI-тесты прогоняются перед слиянием, то они запускаются в обычном режиме, без поиска утечек. Запускаются не все тесты, а только те, что связаны с изменившимся в Git-ветке кодом.

  • Если UI-тесты прогоняются перед сборкой Release Candidate, то запускаются абсолютно все UI-тесты с поиском утечек. 

Как это сделано

Самый простой способ реализовать такое поведение — добавить флаг. Если в аргументы TestRunner передаётся флаг isLeakTest со значением true, это означает, что тесты нужно прогнать в режиме поиска утечек.

Для начала мы считываем значение флага из аргументов TestRunner и записываем полученное значение куда-либо, например, в статическую переменную.

class CianUiTestRunner : AllureAndroidJUnitRunner() {

   override fun onCreate(arguments: Bundle) {
       IS_LEAK_TEST = arguments.getString("isLeakTest") == "true"
   }
}

Затем, для Espresso, в методе с аннотацией After, запустим поиск утечек у LeakCanary, но только в случае, если флаг IS_LEAK_TEST в состоянии true. 

@After
fun after() {
   if (IS_LEAK_TEST) {
       // Запускаем поиск утечек
       LeakAssertions.assertNoLeaks()
   }
}

Для Kaspresso реализовать подобную логику тоже не слишком сложно.

after.after {
   if (IS_LEAK_TEST) {
       LeakAssertions.assertNoLeaks()
   }
   actions(this)
}

И в целом, это всё. Доработки вышли небольшими, не учитывая сотни написанных до этого тестов, но это работает.

Что в итоге?

А в итоге мы получили полностью автоматизированный поиск утечек, который происходит при сборке Release Candidate. Баги на исправление утечек редко, но появляются, а значит, схема работает. При этом для поиска утечек почти не требуется участия разработчика, разве что завести баги на выявленные утечки. Ну и потенциально править код при обновлении LeakCanary или Kaspresso, но у нас ещё ни разу такого не случалось. 

Пока что нас всё устраивает.

На самом деле, оба варианта имеют право на жизнь. 

Первый вариант, от которого мы в итоге отказались, хорошо подходит, когда у вас достаточно малое покрытие UI-тестами.

Второй вариант лучше почти во всём, но для него нужно высокое покрытие UI-тестами, что есть не у всех, да и нужно не всегда и не везде.

Tags:
Hubs:
Total votes 6: ↑6 and ↓0+6
Comments0

Articles

Information

Website
www.cian.ru
Registered
Founded
Employees
1,001–5,000 employees
Location
Россия