Русский
Русский
English
Статистика
Реклама

Автоматизация тестирования продуктовой аналитики в мобильных приложениях

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

Вы когда-нибудь выпускали релиз, в котором случайно удалили код отправляющий некоторые важные события аналитики? Или забывали покрыть событиями новую фичу? А сколько времени ваши аналитики или тестировщики тратят на ручное тестирование перед каждым релизом? А если это приложение с тысячей событий?

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

Тестирование аналитики вручную

Когда пользователь совершает действие, то событие аналитики сразу отправляется в систему аналитики. И, к сожалению, это никак не отследить, чтобы протестировать. Только если модифицировать код приложения, чтобы вместе с отправкой события сделать дополнительное действие. Какие есть варианты:

  1. Можно отправить локальное уведомление (типа Push) с названием и параметрами события. Это неудобно, так как перекрывает интерфейс приложения, а также сложно тестировать цепочку событий из-за того, что каждое новое уведомление перекрывает старые.

  2. Добавить отладочный экран, на котором показан список всех отправленных событий. Но это тоже не очень удобно нужно постоянно переключаться между приложением и этим экраном.

  3. Либо события аналитики можно логировать и сразу отслеживать в консоли.

События аналитики в Console.appСобытия аналитики в Console.app

Третий вариант самый удобный: его несложно сделать и он позволяет фильтровать события. Также при тестировании приложения сразу видно, какие события отправляются. И если приложение содержит небольшое количество событий, то этим вариантом можно обойтись, не прибегая к автоматизации.

Это всё способы тестирования события аналитики вручную. Но если в приложении событий много, то такое тестирование будет не быстрым.

Для того чтобы автоматизировать тестирование, можно воспользоваться UI-тестами. С их помощью можно переходить между экранами, совершать действия и проверять, что определенные события с указанными параметрами отправляются.

Тестирование аналитики UI-тестами

У любого события есть имя, у некоторых бывают еще и параметры. Например, у успешность авторизации имя authorization и булевый параметр success.

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

На практике есть два способа передачи данных из приложения в UI-тесты:

  • Можно сохранить текстовые данные в невидимое текстовое поле или в свойство accessibilityLabel невидимой вьюшки. Но в этом случае меняется иерархия вьюшек, и это может привести к багам. Кроме того, не получится очистить список отправленных событий из UI-тестов.

  • Или можно сохранить текстовые данные в буфер обмена, к которому у UI-тестов есть доступ. Этот вариант лучше, так как иерархия вьюшек не изменяется. Буфер обмена можно очистить из UI-тестов, а еще это проще в реализации.

Когда приложение запущено в режиме UI-тестирования, то можно подменить сервис отправки событий аналитики. Например, вместо AppMetrica подставить свой сервис, который будет отправлять события в буфер обмена. Далее в UI-тестах происходит чтение текстовых данных из буфера, преобразование их в массив событий и проверка.

Так в итоге будет выглядеть UI-тест, проверяющий события аналитики на экране авторизации:

func testLoginSuccess() {    // Запустить приложение    launchApp()        // Проверить что отправилось событие показа экрана авторизации    analytics.assertContains(name: "open_login_screen")        // Успешно залогиниться    loginScreen.login(success: true)        // Проверить что отправилось событие успешной авторизации    analytics.assertContains("authorization", ["success": true])}

Доработки со стороны приложения

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

Базовые сущности

Представим событие аналитики в виде следующей структуры:

public struct MetricEvent: Equatable {     public let name: String        public let values: [String: AnyHashable]?     public init(name: String, values: [String: AnyHashable]? = nil) {        self.name = name        self.values = values    }}

Структура MetricEvent будет использоваться и в коде приложения, и в коде UI-тестов. Поэтому вынесем её в отдельный модуль MetricExampleCore. Для этого нужно создать новый Target типа Framework.

Событие что-то должно отправлять, поэтому объявим соответствующий протокол:

import MetricExampleCore /// Сервис отправки событий в аналитикуpublic protocol MetricService {        func send(event: MetricEvent)    }

В первой строчке импортируем модуль, в котором объявили структуру MetricEvent.

Сервисы отправки событий

Этому протоколу будут соответствовать классы, отправляющие события куда-либо. К примеру, класс для отправки событий в AppMetrica:

import Foundationimport MetricExampleCoreimport YandexMobileMetrica open class AppMetricaService: MetricService {     public init(configuration: YMMYandexMetricaConfiguration) {        YMMYandexMetrica.activate(with: configuration)    }     open func send(event: MetricEvent) {        YMMYandexMetrica.reportEvent(event.name, parameters: event.values, onFailure: nil)    }}

В нашем случае нужен класс, который отправляет события в буфер обмена. Создаем его:

import Foundationimport MetricExampleCoreimport UIKit final class MetricServiceForUITests: MetricService {     // Массив всех отправленных событий аналитики    private var metricEvents: [MetricEvent] = []     func send(event: MetricEvent) {        guard ProcessInfo.processInfo.isUITesting,              ProcessInfo.processInfo.sendMetricsToPasteboard else {            return        }                if UIPasteboard.general.string == nil ||           UIPasteboard.general.string?.isEmpty == true {            metricEvents = []        }         metricEvents.append(event)         if let metricsString = try? encodeMetricEvents(metricEvents) {            UIPasteboard.general.string = metricsString        }    }     private func encodeMetricEvents(_ events: [MetricEvent]) throws -> String {        let arrayOfEvents: [NSDictionary] = events.map { $0.asJSONObject }        let data = try JSONSerialization.data(withJSONObject: arrayOfEvents)        return String(decoding: data, as: UTF8.self)    }}

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

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

// MetricEvent.swift...    /// Представляет событие в виде словаря для передачи в JSONSerialization.data(withJSONObject:)    public var asJSONObject: NSDictionary {        return [            "name": name,            "values": values ?? [:]        ]    }...

Каждый UIViewController, который будет отправлять события, получит в инициализатор зависимость MetricService.

final class LoginViewController: UIViewController {        private let metricService: MetricService        init(metricService: MetricService = ServiceLayer.shared.metricService) {        self.metricService = metricService        super.init(nibName: nil, bundle: nil)    }    ...

Чтобы не передавать каждый раз вручную эту зависимость, можно использовать паттерн Service Locator и создать класс ServiceLayer. В нем будет создаваться и храниться MetricService, который будет передаваться во все контроллеры.

import Foundationimport YandexMobileMetrica final class ServiceLayer {        static let shared = ServiceLayer()        private(set) lazy var metricService: MetricService = {        if ProcessInfo.processInfo.isUITesting {            return MetricServiceForUITests()        } else {            let config = YMMYandexMetricaConfiguration(apiKey: "APP_METRICA_API_KEY")            return AppMetricaService(configuration: config)        }    }()}

Если приложение запущено в режиме UI-тестирования, то для отправки событий используется MetricServiceForUITests. В ином случае AppMetricaService.

Отправка событий

Осталось объявить все события, которые будут отправляться. Для этого нужно написать расширение MetricEvent:

import Foundationimport MetricExampleCore extension MetricEvent {        /// Пользователь перешел на экран авторизации    static var openLogin: MetricEvent {        MetricEvent(name: "open_login_screen")    }     /// Пользователь ввел логин и пароль и инициировал авторизацию.    ///    /// - Parameter success: Успешность запроса.    /// - Returns: Событие метрики.    static func authorization(success: Bool) -> MetricEvent {        MetricEvent(            name: "authorization",            values: ["success": success]        )    }}

Теперь события можно отправлять:

metricService.send(event: .openLogin)metricService.send(event: .authorization(success: true))metricService.send(event: .authorization(success: false))

Аргументы запуска

Я уже упоминал такие вещи, как:

ProcessInfo.processInfo.isUITestingProcessInfo.processInfo.sendMetricsToPasteboard

При запуске UI-тестов на аналитику будут передаваться два аргумента: --UI-TESTING и --SEND-METRICS-TO-PASTEBOARD. Первый показывает, что приложение запущено в режиме UI-тестирования. Второй что приложению разрешено отправлять события аналитики в буфер обмена. Чтобы получить доступ к этим аргументам, нужно написать расширение для ProcessInfo:

import Foundation extension ProcessInfo {    var isUITesting: Bool { arguments.contains("--UI-TESTING") }    var sendMetricsToPasteboard: Bool { arguments.contains("--SEND-METRICS-TO-PASTEBOARD") }}

Доработки со стороны UI-тестов

Теперь расскажу, как на стороне UI-тестов получить список отправленных событий из буфера обмена и проверить их.

Получение списка отправленных событий

Чтобы получить текстовые данные из буфера, используем UIPasteboard.general.string. Затем строку нужно преобразовать в массив событий (MetricEvent). В методе decodeMetricEvents строка преобразуется в объект Data и десериализуется в массив с помощью JSONSerialization:

/// Возвращает список всех событий аналитики произошедших с момента запуска приложенияfunc extractAnalytics() -> [MetricEvent] {    let string = UIPasteboard.general.string!    if let events = try? decodeMetricEvents(from: string) {        return events    } else {        return []    }} /// Преобразует строку с массивом событий в массив объектов [MetricEvent]private func decodeMetricEvents(from string: String) throws -> [MetricEvent] {    guard !string.isEmpty else { return [] }    let data = Data(string.utf8)     guard let arrayOfEvents: [NSDictionary] = try JSONSerialization.jsonObject(with: data) as? [NSDictionary] else {        return []    }     return arrayOfEvents.compactMap { MetricEvent(from: $0) }}

Далее массив словарей преобразуется в массив MetricEvent. Для этого у MetricEvent нужно добавить инициализатор из словаря:

/// Пытается создать объект MetricEvent из словаряpublic init?(from dict: NSDictionary) {    guard let eventName = dict["name"] as? String else { return nil }    self = MetricEvent(        name: eventName,        values: dict["values"] as? [String: AnyHashable])}

Теперь можно получить массив событий [MetricEvent] и проанализировать его.

Если в процессе тестирования понадобится очистить список событий, то тут поможет:

UIPasteboard.general.string = ""

Проверки списка событий

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

/// Проверяет наличие события с указанным именем/// - Parameters:///   - name: Название события///   - count: Количество событий с указанным именем. По умолчанию равно 1.func assertContains(    name: String,    count: Int = 1) {     let records = extractAnalytics()     XCTAssertEqual(        records.filter { $0.name == name }.count,        count,        "Событие с именем \(name) не найдено.")}

В итоге получился класс AnalyticsTestBase. Посмотреть его можно на GitHub AnalyticsTestBase.swift

Создадим класс, наследника XCTestCase, от которого будут наследоваться классы, тестирующие аналитику. Он создает класс AnalyticsTestBase для тестирования аналитики и метод launchApp, запускающий приложение.

import XCTestclass TestCaseBase: XCTestCase {        var app: XCUIApplication!    var analytics: AnalyticsTestBase!        override func setUp() {        super.setUp()                app = XCUIApplication()        analytics = AnalyticsTestBase(app: app)    }        /// Запускает приложение для UI-тестирования с указанными параметрами.    func launchApp(with parameters: AppLaunchParameters = AppLaunchParameters()) {        app.launchArguments = parameters.launchArguments        app.launch()    }}

Метод будет принимать AppLaunchParameters (параметры запуска приложения, о которых я говорил выше).

struct AppLaunchParameters {        /// Отправлять аналитику в UIPasteboard    private let sendMetricsToPasteboard: Bool        init(sendMetricsToPasteboard: Bool = false) {        self.sendMetricsToPasteboard = sendMetricsToPasteboard    }        var launchArguments: [String] {        var arguments = ["--UI-TESTING"]        if sendMetricsToPasteboard {            arguments.append("--SEND-METRICS-TO-PASTEBOARD")        }        return arguments    }}

В обычных UI-тестах приложение будет запускаться с параметрами:

AppLaunchParameters(sendMetricsToPasteboard: false)

А в UI-тестах на аналитику:

AppLaunchParameters(sendMetricsToPasteboard: true)

Теперь можно писать тесты на аналитику. Например, это тест на экран входа:

final class LoginAnalyticsTests: TestCaseBase {        private let loginScreen = LoginScreen()        func testLoginSuccess() {        launchApp(with: AppLaunchParameters(sendMetricsToPasteboard: true))                // Проверить что отправилось событие показа экрана входа        analytics.assertContains(name: "open_login_screen")                // Успешно залогинится        loginScreen.login(success: true)                // Проверить что отправилось событие успешной авторизации        analytics.assertContains("authorization", ["success": true])    }}

LoginScreen это Page Object, описывающий экран авторизации. Посмотреть его можно на GitHub LoginScreen.swift

Примеры

Example проект

iOS-проект, где используется автоматизированное тестирование аналитики UI-тестами.

Это простое приложение, состоящее из двух экранов: вход и меню. События отправляются при заходе на каждый экран, при авторизации и при выборе пункта меню.

Тест, покрывающий все эти события:

import XCTest final class AnalyticsTests: TestCaseBase {        private let loginScreen = LoginScreen()    private let menuScreen = MenuScreen()        // MARK: - Login        func testLoginSuccess() {        launchApp(with: AppLaunchParameters(sendMetricsToPasteboard: true))                analytics.assertContains(name: "open_login_screen")        loginScreen.login(success: true)         analytics.assertContains("authorization", ["success": true])    }        func testLoginFailed() {        launchApp(with: AppLaunchParameters(sendMetricsToPasteboard: true))                analytics.assertContains(name: "open_login_screen")        loginScreen.login(success: false)        analytics.assertContains("authorization", ["success": false])    }        // MARK: - Menu        func testOpenMenu() {        launchApp(with: AppLaunchParameters(sendMetricsToPasteboard: true))         loginScreen.login(success: true)        waitForElement(menuScreen.title)        analytics.assertContains(name: "open_menu_screen")    }        func testMenuSelection() {        launchApp(with: AppLaunchParameters(sendMetricsToPasteboard: true))                loginScreen.login(success: true)        waitForElement(menuScreen.title)         menuScreen.profileCell.tap()                analytics.assertContains("menu_item_selected", ["name": "Профиль"])                menuScreen.messagesCell.tap()        analytics.assertContains("menu_item_selected", ["name": "Сообщения"])    }}

Реальный проект

Пример UI-тестов на аналитику экрана авторизации из реального проекта LoginAnalyticsTests.swift

Пример, как мне, разработчику, помогли UI-тесты на аналитику. На одном проекте нужно было произвести рефакторинг и редизайн главного экрана приложения. Экран был сложным, с большим количеством событий аналитики. На тот момент в проекте я уже настроил тесты. После рефакторинга и редизайна запустил тесты и обнаружил, что некоторые события случайно удалил. Если бы не тесты на аналитику, эти события не попали бы в релиз.

Итоги

Плюсы подхода:

  1. Продуктовому аналитику или тестировщику не нужно проверять все события аналитики вручную. А это экономия времени и, соответственно, денег.

  2. Если у вас настроен CI, то UI-тесты на аналитику можно запускать по расписанию, например, раз в неделю или по команде из Slack.

Есть и минусы:

  1. UI-тесты выполняются относительно долго. Имеет смысл запускать их только в процессе регрессионного тестирования перед каждым релизом.

  2. UI-тесты на аналитику смогут написать только те тестировщики, которые имеют опыт написания нативных UI-тестов.

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

Поэтому в случае большого проекта есть смысл автоматизировать проверку событий аналитики.

Источник: habr.com
К списку статей
Опубликовано: 12.10.2020 12:10:07
0

Сейчас читают

Комментариев (0)
Имя
Электронная почта

Блог компании redmadrobot

Разработка под ios

Разработка мобильных приложений

Swift

Аналитика мобильных приложений

Ios

Тестирование

Автоматизация

Ui-тесты

Аналитика

Redmadrobot

Мобильное приложение

Ui-тестирование

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru