Halo semuanya! Nama saya Sasha Zimin, saya bekerja sebagai pengembang iOS di kantor London
Badoo . Badoo memiliki hubungan yang sangat dekat dengan manajer produk, dan saya mengambil alih kebiasaan menguji semua hipotesis yang saya miliki tentang produk. Jadi, saya mulai menulis split test untuk proyek saya.
Kerangka kerja yang akan dibahas dalam artikel ini ditulis untuk dua tujuan. Pertama, untuk menghindari kemungkinan kesalahan, lebih baik tidak memiliki data dalam sistem analisis daripada data yang salah (atau bahkan data yang dapat diartikan secara salah dan dipecah menjadi kayu bakar). Kedua, untuk menyederhanakan implementasi setiap tes selanjutnya. Tapi mari kita mulai dengan apa tes split.
Saat ini, ada jutaan aplikasi yang memecahkan sebagian besar kebutuhan pengguna, sehingga setiap hari menjadi lebih sulit untuk menciptakan produk kompetitif baru. Ini telah mengarahkan banyak perusahaan dan perusahaan rintisan untuk melakukan berbagai penelitian dan percobaan pada awalnya untuk mengetahui fitur mana yang membuat produk mereka lebih baik dan mana yang dapat ditiadakan.
Salah satu alat utama untuk melakukan eksperimen tersebut adalah pengujian terpisah (atau pengujian A / B). Pada artikel ini saya akan memberi tahu bagaimana itu dapat diterapkan pada Swift.
Semua demo proyek tersedia di
sini . Jika Anda sudah memiliki gagasan tentang pengujian A / B, maka Anda dapat langsung menuju
ke kode .
Pengantar Singkat untuk Pengujian Split
Pengujian terpisah, atau pengujian A / B (istilah ini tidak selalu benar, karena Anda dapat memiliki lebih dari dua kelompok peserta), adalah cara untuk memeriksa berbagai versi produk pada kelompok pengguna yang berbeda untuk memahami versi mana yang lebih baik. Anda dapat membacanya di
Wikipedia atau, misalnya, dalam artikel
ini dengan contoh nyata.
Di Badoo, kami menjalankan banyak tes terpisah pada saat yang bersamaan. Misalnya, setelah kami memutuskan bahwa halaman profil pengguna di aplikasi kami terlihat usang, dan juga ingin meningkatkan interaksi pengguna dengan beberapa spanduk. Oleh karena itu, kami meluncurkan pengujian terpisah dengan tiga kelompok:
- Profil lama
- Versi Profil Baru 1
- Versi Profil Baru 2
Seperti yang Anda lihat, kami memiliki tiga opsi, lebih seperti pengujian A / B / C (dan itulah mengapa kami lebih suka menggunakan istilah "pengujian terpisah").
Jadi pengguna yang berbeda melihat profil mereka:
Di konsol Product Manager, kami memiliki empat grup pengguna yang dibentuk secara acak dan memiliki nomor yang sama:
Mungkin Anda bertanya mengapa kami memiliki control dan control_check (jika control_check adalah salinan dari logika grup kontrol)? Jawabannya sangat sederhana: perubahan apa pun memengaruhi banyak indikator, jadi kami tidak pernah bisa benar-benar yakin bahwa perubahan tertentu adalah hasil dari tes split, dan bukan tindakan lainnya.
Jika Anda berpikir bahwa beberapa indikator telah berubah karena tes split, Anda harus memeriksa ulang bahwa mereka sama di dalam grup kontrol dan control_check.
Seperti yang Anda lihat, pendapat pengguna mungkin berbeda, tetapi bukti empiris adalah bukti yang jelas. Tim manajer produk menganalisis hasil dan memahami mengapa satu opsi lebih baik dari yang lain.
Pengujian Split dan Swift
Tujuan:
- Buat perpustakaan untuk sisi klien (tanpa menggunakan server).
- Simpan opsi pengguna yang dipilih di penyimpanan permanen setelah dihasilkan secara tidak sengaja.
- Kirim laporan tentang opsi yang dipilih untuk setiap tes split ke layanan analitik.
- Manfaatkan sepenuhnya kemampuan Swift.
PS Penggunaan perpustakaan seperti itu untuk pengujian terpisah dari bagian klien memiliki kelebihan dan kekurangan. Keuntungan utama adalah Anda tidak perlu memiliki infrastruktur server atau server khusus. Dan kerugiannya adalah bahwa jika terjadi kesalahan selama percobaan, Anda tidak dapat memutar kembali tanpa mengunduh versi baru di App Store.
Beberapa kata tentang implementasinya:
- Selama percobaan, opsi untuk pengguna dipilih secara acak sesuai dengan prinsip kemungkinan yang sama.
- Layanan pengujian terpisah dapat menggunakan:
- Setiap penyimpanan data (misalnya, UserDefaults, Realm, SQLite atau Core Data) sebagai ketergantungan dan menyimpan nilai yang diberikan kepada pengguna (nilai variannya) ke dalamnya.
- Layanan analitik apa pun (misalnya, Amplitude atau Facebook Analytics) sebagai dependensi dan kirim versi saat ini saat pengguna menghadapi tes split.
Berikut adalah diagram kelas-kelas masa depan:
Semua tes split akan disajikan menggunakan
SplitTestProtocol , dan masing-masing akan memiliki beberapa opsi (grup) yang akan disajikan dalam
SplitTestGroupProtocol .
Tes terpisah harus dapat memberi tahu analis tentang versi saat ini, oleh karena itu,
AnalyticsProtocol akan menjadi ketergantungan.
Layanan
SplitTestingService akan menyimpan, menghasilkan opsi, dan mengelola semua tes split. Dialah yang mengunduh versi pengguna saat ini dari penyimpanan, yang ditentukan oleh
StorageProtocol , dan juga melewati
AnalyticsProtocol ke
SplitTestProtocol .
Mari kita mulai menulis kode dengan
dependensi AnalyticsProtocol dan
StorageProtocol :
protocol AnalyticsServiceProtocol { func setOnce(value: String, for key: String) } protocol StorageServiceProtocol { func save(string: String?, for key: String) func getString(for key: String) -> String? }
Peran analitik adalah merekam suatu peristiwa satu kali. Misalnya, untuk memperbaikinya pengguna
A berada dalam kelompok
biru selama tes split
button_color , ketika ia melihat layar dengan tombol ini.
Peran repositori adalah menyimpan opsi tertentu untuk pengguna saat ini (setelah
SplitTestingService menghasilkan opsi ini) dan kemudian membacanya kembali setiap kali program mengakses tes split ini.
Jadi, mari kita lihat
SplitTestGroupProtocol , yang mencirikan serangkaian opsi untuk tes pemisahan tertentu:
protocol SplitTestGroupProtocol: RawRepresentable where RawValue == String { static var testGroups: [Self] { get } }
Karena
RawRepresentable di mana RawValue adalah string, Anda dapat dengan mudah membuat varian dari string atau mengubahnya kembali menjadi string, yang sangat nyaman untuk bekerja dengan analitik dan penyimpanan.
SplitTestGroupProtocol juga berisi larik testGroup, yang dapat menunjukkan komposisi opsi saat ini (larik ini juga akan digunakan untuk pembuatan acak dari opsi yang tersedia).
Ini adalah prototipe dasar untuk tes split
SplitTestProtocol itu sendiri :
protocol SplitTestProtocol { associatedtype GroupType: SplitTestGroupProtocol static var identifier: String { get } var currentGroup: GroupType { get } var analytics: AnalyticsServiceProtocol { get } init(currentGroup: GroupType, analytics: AnalyticsServiceProtocol) } extension SplitTestProtocol { func hitSplitTest() { self.analytics.setOnce(value: self.currentGroup.rawValue, for: Self.analyticsKey) } static var analyticsKey: String { return "split_test-\(self.identifier)" } static var dataBaseKey: String { return "split_test_database-\(self.identifier)" } }
SplitTestProtocol mengandung:
- Tipe GroupType yang mengimplementasikan protokol SplitTestGroupProtocol untuk mewakili tipe yang mendefinisikan serangkaian opsi.
- Pengidentifikasi nilai string untuk analitik dan kunci penyimpanan.
- Variabel currentGroup untuk merekam instance spesifik SplitTestProtocol .
- Ketergantungan Analytics untuk metode hitSplitTest .
- Dan metode hitSplitTest , yang menginformasikan analis bahwa pengguna melihat hasil tes split.
Metode hitSplitTest memungkinkan Anda untuk memastikan bahwa pengguna tidak hanya dalam versi tertentu, tetapi juga melihat hasil pengujian. Jika Anda menandai pengguna yang belum mengunjungi bagian belanja sebagai "saw_red_button_on_purcahse_screen", ini akan merusak hasil.
Sekarang kami siap untuk
SplitTestingService :
protocol SplitTestingServiceProtocol { func fetchSplitTest<Value: SplitTestProtocol>(_ splitTestType: Value.Type) -> Value } class SplitTestingService: SplitTestingServiceProtocol { private let analyticsService: AnalyticsServiceProtocol private let storage: StorageServiceProtocol init(analyticsService: AnalyticsServiceProtocol, storage: StorageServiceProtocol) { self.analyticsService = analyticsService self.storage = storage } func fetchSplitTest<Value: SplitTestProtocol>(_ splitTestType: Value.Type) -> Value { if let value = self.getGroup(splitTestType) { return Value(currentGroup: value, analytics: self.analyticsService) } let randomGroup = self.randomGroup(Value.self) self.saveGroup(splitTestType, group: randomGroup) return Value(currentGroup: randomGroup, analytics: self.analyticsService) } private func saveGroup<Value: SplitTestProtocol>(_ splitTestType: Value.Type, group: Value.GroupType) { self.storage.save(string: group.rawValue, for: Value.dataBaseKey) } private func getGroup<Value: SplitTestProtocol>(_ splitTestType: Value.Type) -> Value.GroupType? { guard let stringValue = self.storage.getString(for: Value.dataBaseKey) else { return nil } return Value.GroupType(rawValue: stringValue) } private func randomGroup<Value: SplitTestProtocol>(_ splitTestType: Value.Type) -> Value.GroupType { let count = Value.GroupType.testGroups.count let random = Int.random(lower: 0, count - 1) return Value.GroupType.testGroups[random] } }
PS Di kelas ini kami menggunakan fungsi Int.random, diambil dari
di sini , tetapi dalam Swift 4.2 itu sudah bawaan secara bawaan.
Kelas ini berisi satu metode
fetchSplitTest publik dan tiga metode pribadi:
saveGroup ,
getGroup ,
randomGroup .
Metode randomGroup menghasilkan varian acak untuk pengujian pemisahan yang dipilih, sementara getGroup dan saveGroup memungkinkan Anda untuk menyimpan atau memuat varian untuk pengujian pemisahan khusus untuk pengguna saat ini.
Fungsi utama dan publik dari kelas ini adalah fetchSplitTest: ia mencoba untuk mengembalikan versi saat ini dari penyimpanan persisten dan, jika itu tidak berhasil, menghasilkan dan menyimpan versi acak sebelum mengembalikannya.
Sekarang kami siap untuk membuat tes split pertama kami:
final class ButtonColorSplitTest: SplitTestProtocol { static var identifier: String = "button_color" var currentGroup: ButtonColorSplitTest.Group var analytics: AnalyticsServiceProtocol init(currentGroup: ButtonColorSplitTest.Group, analytics: AnalyticsServiceProtocol) { self.currentGroup = currentGroup self.analytics = analytics } typealias GroupType = Group enum Group: String, SplitTestGroupProtocol { case red = "red" case blue = "blue" case darkGray = "dark_gray" static var testGroups: [ButtonColorSplitTest.Group] = [.red, .blue, .darkGray] } } extension ButtonColorSplitTest.Group { var color: UIColor { switch self { case .blue: return .blue case .red: return .red case .darkGray: return .darkGray } } }
Itu terlihat mengesankan, tetapi jangan khawatir: segera setelah Anda mengimplementasikan SplitTestProtocol sebagai kelas yang terpisah, kompiler akan meminta Anda untuk menerapkan semua properti yang diperlukan.
Bagian penting di sini adalah tipe
enum Group . Anda harus memasukkan semua grup Anda ke dalamnya (dalam contoh kami, merah, biru, dan darkGray), dan menentukan nilai string di sini untuk memastikan transfer yang benar ke analytics.
Kami juga memiliki ekstensi
ButtonColorSplitTest.Group , memungkinkan Anda untuk menggunakan potensi penuh Swift. Sekarang mari kita buat objek untuk
AnalyticsProtocol dan
StorageProtocol :
extension UserDefaults: StorageServiceProtocol { func save(string: String?, for key: String) { self.set(string, forKey: key) } func getString(for key: String) -> String? { return self.object(forKey: key) as? String } }
Untuk
StorageProtocol kami akan menggunakan kelas UserDefaults karena mudah diimplementasikan, tetapi dalam proyek Anda, Anda dapat bekerja dengan penyimpanan persisten lainnya (misalnya, saya memilih Keychain untuk saya sendiri, karena menyimpan grup untuk pengguna bahkan setelah penghapusan).
Dalam contoh ini, saya akan membuat kelas analitik fiktif, tetapi Anda dapat menggunakan analitik nyata dalam proyek Anda. Misalnya, Anda dapat menggunakan layanan
Amplitude .
// Dummy class for example, use something real, like Amplitude class Analytics { func logOnce(property: NSObject, for key: String) { let storageKey = "example.\(key)" if UserDefaults.standard.object(forKey: storageKey) == nil { print("Log once value: \(property) for key: \(key)") UserDefaults.standard.set("", forKey: storageKey) // String because of simulator bug } } } extension Analytics: AnalyticsServiceProtocol { func setOnce(value: String, for key: String) { self.logOnce(property: value as NSObject, for: key) } }
Sekarang kita siap untuk menggunakan tes split kami:
let splitTestingService = SplitTestingService(analyticsService: Analytics(), storage: UserDefaults.standard) let buttonSplitTest = splitTestingService.fetchSplitTest(ButtonColorSplitTest.self) self.button.backgroundColor = buttonSplitTest.currentGroup.color buttonSplitTest.hitSplitTest()
Buat saja instance Anda sendiri, ekstrak split test dan gunakan itu. Generalisasi memungkinkan Anda memanggil
buttonSplitTest.currentGroup.color.
Selama penggunaan pertama, Anda dapat melihat sesuatu seperti (
Log nilai sekali )
: split_test-button_color for key: dark_gray , dan jika Anda tidak menghapus aplikasi dari perangkat, tombolnya akan sama setiap kali Anda memulai.
Proses penerapan pustaka semacam itu membutuhkan waktu, tetapi setelah itu, setiap tes pemisahan baru di dalam proyek Anda akan dibuat dalam beberapa menit.
Berikut adalah contoh penggunaan mesin dalam aplikasi nyata: dalam analitik, kami mengelompokkan pengguna berdasarkan koefisien kompleksitas dan kemungkinan membeli mata uang game.

Orang yang belum pernah mengalami faktor kesulitan ini (tidak ada) mungkin tidak bermain sama sekali dan tidak membeli apa pun dalam permainan (yang logis), itulah sebabnya mengapa penting untuk mengirim hasil (versi yang dihasilkan) dari uji split ke server pada saat pengguna benar-benar menghadapi tes Anda.
Tanpa faktor kesulitan, hanya 2% pengguna yang membeli mata uang game. Dengan rasio kecil, pembelian sudah dilakukan 3%. Dan dengan faktor kesulitan tinggi, 4% pemain membeli mata uang. Ini berarti Anda dapat terus meningkatkan koefisien dan mengamati angka-angkanya. :)
Jika Anda tertarik untuk menganalisis hasil dengan keandalan maksimum, saya sarankan Anda untuk menggunakan
alat ini .
Terima kasih kepada tim yang luar biasa yang membantu saya mengerjakan artikel ini (terutama
Igor ,
Kelly dan
Hiro ).
Seluruh proyek demo tersedia di
tautan ini .