Найти тему
Nuances of programming

Поиск утечек памяти с помощью автоматизированных тестов

Источник: Nuances of Programming

Процесс управления памятью может стать фактором, весьма затрудняющим работу в приложениях iOS. В связи с этим нужно уделять пристальное внимание размещенным в памяти object instances и обеспечивать их корректное высвобождение.

Как известно, экземпляры высвобождаются из памяти посредством механизма автоматического подсчета ссылок (Automatic Reference Counting или ARC). В iOS существуют 2 типа таких ссылок: сильные и слабые.

Сильные ссылки  —  это тип ссылки по умолчанию. Рассмотрим простой пример объявления объекта. Допустим, у нас есть класс Person , и мы объявляем объект следующим образом:

var person = Person()

Здесь мы создаем сильную ссылку на Person . При каждом ее создании число ссылок на объект увеличивается на 1. Что это значит? И зачем нам это нужно? Дело в том, что ARC не будет удалять объект из памяти до тех пор, пока число ссылок на него не приравняется к 0.

Иначе себя ведут слабые ссылки, которые не увеличивают общее число ссылок на объект. Вашему вниманию  —  очередной пример:

var person = Person()
weak var anotherPerson: Person? = person

Здесь создается слабая ссылка на тот же самый объект person , так что теперь у нас есть сильная ссылка person и слабая  —  anotherPerson . Это значит, что при наличии 2 ссылок на один и тот же объект общее их число по-прежнему равняется 1, поскольку одна из них  —  слабая. Также важно знать, что слабые ссылки являются необязательными, поскольку доступ к ним можно получить даже после их удаления из памяти.

Поиск циклов retain и утечек памяти с помощью тестов

Цикл retain возникает, когда 2 объекта имеют сильную ссылку друг на друга. В результате этого они удерживают друг друга в памяти, поскольку число ссылок на них никогда не будет равно 0. Следовательно, наша задача  —  нарушить этот цикл, позволив ARC освободить область памяти, занятую такими объектами.

Предположим, у нас есть два объекта. Один из них имеет тип HTTPClient и при этом состоит во втором объекте UserProfileLoader .

В данном примере UserProfileLoader содержит метод loadProfile , вызывающий метод get в созданном client типа HTTPClient . Как видно, реализация get в HTTPClientSpy сохраняет замыкание в свойстве completion , что делает его доступным для тестов.

Представленная выше реализация, несомненно, содержит цикл retain , но вот для какой цели и в каком месте так сразу и не скажешь. Попробуем разобраться. UserProfileLoader содержит сильную ссылку на объект HTTPClient в свойстве client . С другой стороны, HTTPClient также имеет сильную ссылку на UserProfileLoader . “Где же она?” — спросите вы, поскольку может показаться, что никакого UserProfileLoader в HTTPClientSpy не существует. Однако он там точно есть. Обратим внимание на метод get . В свойстве completion он хранит полученное замыкание, которое содержит сильную ссылку на объект UserProfileLoader в вызове метода map . Как видно, мы добавили сильную ссылку с помощью явного использования self .

Как правило, ошибки подобного рода встречаются во многих базах кода. Порой они совсем не очевидны, так что никто от них не застрахован. Но мы можем сократить риск их появления с помощью автоматизированных тестов. Как же? Для этого необходимо убедиться, что тестируемая система (System Under Test или SUT) и ее составляющие высвобождаются должным образом.

Начнем с написания первого теста, который задаст нам правильный вектор. В этом примере экземпляр UserProfileLoader будет нашим SUT.

Мы располагаем исходными данными, и теперь нам необходимо где-то сохранить результат загрузки для создания утверждений, поскольку, как вы помните, метод loadProfile завершится замыканием типа (Result) -> Void . Сохраним этот результат в тесте.

В вышеуказанном тесте замыкание загрузки никогда не будет выполнено. Причина в том, что loadProfile вызывает реализацию get . В экземпляре HTTPClientSpy метод get сохраняет полученное замыкание в свойстве. Для получения результата нужно вызвать в тесте этот сохраненный блок completion .

Вот теперь мы можем сделать утверждения с помощью массива capturedResults . Метод loadProfile будет всегда завершаться с успешным результатом, содержащим экземпляр UserProfile . Именно этого нам и нужно ожидать в тесте.

Тест прошел! Гарантирует ли он корректное высвобождение SUT и его составляющих компонентов? Пока нет. Добавим еще утверждений для большей уверенности в высвобождении теста из памяти.

Класс MemoryLeaksTests расширяется из класса XCTestCase , предоставляющего метод addTearDownBlock . Этот метод получает блок типа () -> Void, который будет выполняться каждый раз по завершении тестовой функции. Мы могли бы добавить блок teardown сразу после создания SUT и его составляющих.

Теперь блоки на своих местах. Обратите внимание, что в них мы добавили слабые ссылки на экземпляры sut и client , чтобы обойтись без дальнейшего увеличения их числа. Теперь зеленый тест уже не зеленый, потому что утверждения блоков teardown не срабатывают.

Для устранения цикла retain стоит воздержаться от явного использования self в методе loadProfile . И у нас есть несколько вариантов:

  1. Применить в замыкании слабую ссылку и добавить блок guard let для развертывания результата.
  2. Превратить метод map в статический.
  3. Переместить map в статический метод класса-преобразователя.

В следующем примере мы воспользовались 2 вариантом, чтобы нарушить цикл retain и успешно пройти тест.

Теперь тест проходит, но при этом выглядит немного неряшливо, поэтому стоит провести рефакторинг и немного его почистить. Допустим, нам понадобились дополнительные тестовые функции. Тогда мы бы создали SUT и его компоненты, а также добавили бы для каждого из них блоки tear down , что свидетельствовало бы о повторении.

Для рефакторинга этого теста создадим несколько фабричных методов. Первым станет makeSUT , который вернет SUT и его компоненты.

Вот теперь тест стал более чистым и читаемым. К тому же у нас появилось еще одно дополнительное и большое преимущество. Присмотревшись к фабричному методу makeSUT , вы увидите, что он добавляет блоки tear down . Это значит, что по завершении каждый тест, который создает SUT, используя этот метод, будет автоматически проверять корректное высвобождение данного SUT и его объектов из памяти. Если SUT и его компоненты не будут удалены из памяти, то тест покажет ошибку при вызове makeSUT , поскольку мы передаем в XCTAssertNil параметры file и line .

-2

Данный подход позволит уверенно проводить рефакторинг и послужит гарантией правильного управления памятью.

Читайте также:

Читайте нас в Telegram , VK

Перевод статьи Luis Piura : Find potential memory leaks with automated tests