Testing all product analytics events before each release is usually time consuming. This can be automated. I show you exactly how, using the example of an iOS application.
- , ? ? ? ?
, , .
, . , , , . , . :
( Push) . , , - , .
, . โ .
.
โ : . , . , , .
. , .
, 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 Foundation
import MetricExampleCore
import 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 Foundation
import MetricExampleCore
import 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 Foundation
import 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 Foundation
import 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.isUITesting
ProcessInfo.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 XCTest
class 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- . . , . . , . , .
:
. , , .
CI, UI- , , Slack.
:
UI- . .
UI- , UI-.
, , . , : . .
.