Probar todos los eventos de análisis de productos antes de cada lanzamiento suele llevar mucho tiempo. Esto se puede automatizar. Te muestro exactamente cómo, usando el ejemplo de una aplicación iOS.
- , ? ? ? ?
, , .
, . , , , . , . :
( 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.sendMetricsToPasteboardUI- : --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-.
, , . , : . .
.