مرحبا بالجميع! اسمي ساشا زيمين ، أعمل كمطورة لنظام iOS في مكتب
Badoo بلندن. لدى Badoo علاقة وثيقة للغاية مع مديري المنتجات ، وقد اعتدت على اختبار جميع الفرضيات التي لدي بشأن المنتج. لذلك ، بدأت في كتابة اختبارات الانقسام لمشاريعي.
تمت كتابة الإطار الذي سيتم مناقشته في هذه المقالة لغرضين. أولاً ، لتجنب الأخطاء المحتملة ، من الأفضل عدم وجود بيانات في نظام التحليلات من البيانات غير الصحيحة (أو حتى البيانات التي يمكن تفسيرها بشكل غير صحيح وكسرها في الحطب). ثانيًا ، لتبسيط تنفيذ كل اختبار لاحق. ولكن دعونا نبدأ بالاختبارات المنقسمة.
في الوقت الحاضر ، هناك الملايين من التطبيقات التي تحل معظم احتياجات المستخدمين ، لذلك يصبح إنشاء منتجات تنافسية جديدة أكثر صعوبة كل يوم. وقد أدى هذا بالعديد من الشركات والشركات الناشئة إلى إجراء العديد من الأبحاث والتجارب في البداية لمعرفة الميزات التي تجعل منتجهم أفضل وأيها يمكن الاستغناء عنه.
واحدة من الأدوات الرئيسية لإجراء مثل هذه التجارب هي اختبار الانقسام (أو اختبار A / B). في هذه المقالة سأخبرك كيف يمكن تنفيذها على Swift.
جميع العروض التوضيحية للمشروع متاحة
هنا . إذا كانت لديك بالفعل فكرة حول اختبار أ / ب ، فيمكنك الانتقال مباشرةً
إلى الرمز .
مقدمة موجزة لاختبار الانقسام
اختبار التقسيم ، أو اختبار أ / ب (هذا المصطلح ليس دائمًا صحيحًا ، لأنه يمكن أن يكون لديك أكثر من مجموعتين من المشاركين) ، هي طريقة للتحقق من الإصدارات المختلفة للمنتج على مجموعات مستخدمين مختلفة من أجل فهم الإصدار الأفضل. يمكنك أن تقرأ عنها في
ويكيبيديا أو ، على سبيل المثال ، في
هذه المقالة مع أمثلة حقيقية.
في Badoo ، نجري الكثير من الاختبارات المقسمة في نفس الوقت. على سبيل المثال ، بمجرد أن قررنا أن صفحة ملف تعريف المستخدم في تطبيقنا تبدو قديمة ، وأردنا أيضًا تحسين تفاعل المستخدم مع بعض اللافتات. لذلك ، أطلقنا اختبار التقسيم مع ثلاث مجموعات:
- ملف تعريف قديم
- إصدار ملف تعريف جديد 1
- إصدار ملف تعريف جديد 2
كما ترى ، كان لدينا ثلاثة خيارات ، أشبه باختبار A / B / C (ولهذا السبب نفضل استخدام مصطلح "اختبار الانقسام").
لذلك رأى مستخدمون مختلفون ملفاتهم الشخصية:
في وحدة تحكم مدير المنتج ، كان لدينا أربع مجموعات مستخدمين تم تشكيلها بشكل عشوائي ولها نفس الرقم:
ربما تسأل لماذا لدينا سيطرة و control_check (إذا كانت control_check نسخة من منطق مجموعة التحكم)؟ الجواب بسيط للغاية: أي تغيير يؤثر على العديد من المؤشرات ، لذلك لا يمكننا أن نكون على يقين مطلقًا من أن تغييرًا معينًا هو نتيجة اختبار الانقسام ، وليس إجراءات أخرى.
إذا كنت تعتقد أن بعض المؤشرات قد تغيرت بسبب اختبار التقسيم ، فيجب عليك التحقق جيدًا من أنها متطابقة داخل مجموعتي التحكم والتحكم.
كما ترى ، قد تختلف آراء المستخدمين ، ولكن الدليل التجريبي دليل واضح. يحلل فريق مديري المنتجات النتائج ويفهم سبب كون أحد الخيارات أفضل من الآخر.
اختبار الانقسام وسويفت
الأهداف:
- إنشاء مكتبة لجانب العميل (بدون استخدام خادم).
- احفظ خيار المستخدم المحدد في التخزين الدائم بعد إنشائه عن طريق الخطأ.
- إرسال تقارير حول الخيارات المحددة لكل اختبار تجزئة إلى خدمة التحليلات.
- حقق أقصى استفادة من قدرات Swift.
ملاحظة: إن استخدام مثل هذه المكتبة للاختبار المنفصل لجزء العميل له مزاياه وعيوبه. الميزة الرئيسية هي أنك لا تحتاج إلى بنية تحتية للخادم أو خادم مخصص. والعيب هو أنه إذا حدث خطأ ما أثناء التجربة ، فلا يمكنك التراجع دون تنزيل الإصدار الجديد في App Store.
بضع كلمات حول التنفيذ:
- أثناء التجربة ، يتم تحديد خيار المستخدم عشوائيًا وفقًا للمبدأ المحتمل على حد سواء.
- قد تستخدم خدمة الاختبار المجزأ:
- أي مخزن بيانات (على سبيل المثال ، UserDefaults أو Realm أو SQLite أو Core Data) كتبعية وحفظ القيمة المعينة للمستخدم (قيمة متغيره) فيه.
- أي خدمة تحليلات (على سبيل المثال ، Amplitude أو Facebook Analytics) كتبعية وإرسال الإصدار الحالي في اللحظة التي يواجه فيها المستخدم اختبار الانقسام.
هنا رسم تخطيطي للفصول المستقبلية:
سيتم تقديم جميع اختبارات التقسيم باستخدام
SplitTestProtocol ، وسيكون لكل منها العديد من الخيارات (المجموعات) التي سيتم تقديمها في
SplitTestGroupProtocol .
يجب أن يكون اختبار التقسيم قادرًا على إبلاغ المحلل بالنسخة الحالية ، وبالتالي ، سيكون لديه
AnalyticsProtocol كإعتماد.
ستقوم Service
SplitTestingService بحفظ وإنشاء الخيارات وإدارة جميع الاختبارات المقسمة. هو الذي يقوم بتنزيل الإصدار الحالي للمستخدم من التخزين ، والذي يتم تحديده بواسطة
StorageProtocol ، ويمرر
AnalyticsProtocol إلى
SplitTestProtocol .
لنبدأ في كتابة التعليمات البرمجية باستخدام
تبعيات AnalyticsProtocol و
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? }
دور التحليلات هو تسجيل حدث مرة واحدة. على سبيل المثال ، لإصلاح هذا المستخدم
أ في المجموعة
الزرقاء أثناء اختبار الانقسام
button_color ، عندما يرى شاشة مع هذا الزر.
يتمثل دور المستودع في حفظ خيار محدد للمستخدم الحالي (بعد قيام
SplitTestingService بإنشاء هذا الخيار) ثم قراءته مرة أخرى في كل مرة يصل البرنامج إلى اختبار التقسيم هذا.
لذا ، دعونا نلقي نظرة على
SplitTestGroupProtocol ، التي تميز مجموعة من الخيارات لاختبار تقسيم محدد:
protocol SplitTestGroupProtocol: RawRepresentable where RawValue == String { static var testGroups: [Self] { get } }
نظرًا لأن
RawRepresentable حيث يكون RawValue عبارة عن سلسلة ، يمكنك بسهولة إنشاء متغير من سلسلة أو تحويله مرة أخرى إلى سلسلة ، وهو أمر ملائم للغاية للعمل مع التحليلات والتخزين. يحتوي
SplitTestGroupProtocol أيضًا على مجموعة من testGroups ، والتي يمكن أن تشير إلى تكوين الخيارات الحالية (سيتم استخدام هذا الصفيف أيضًا لتوليد عشوائي من الخيارات المتاحة).
هذا هو النموذج الأساسي لاختبار
SplitTestProtocol نفسه :
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 على:
- نوع GroupType الذي يقوم بتنفيذ بروتوكول SplitTestGroupProtocol لتمثيل نوع يحدد مجموعة من الخيارات.
- معرّف قيمة السلسلة للتحليلات ومفاتيح التخزين.
- المتغير CurrentGroup لتسجيل نسخة معينة من SplitTestProtocol .
- تبعية التحليلات لطريقة hitSplitTest .
- وطريقة hitSplitTest التي تبلغ المحلل أن المستخدم رأى نتيجة اختبار التقسيم.
تسمح لك طريقة hitSplitTest بالتأكد من أن المستخدمين ليسوا فقط في إصدار معين ، ولكنهم شاهدوا أيضًا نتيجة الاختبار. إذا وضعت علامة على مستخدم لم يقم بزيارة قسم التسوق باسم "saw_red_button_on_purcahse_screen" ، فسيؤدي ذلك إلى تشويه النتائج.
نحن الآن جاهزون لـ
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 في هذه الفئة نستخدم الدالة Int.random المأخوذة من
هنا ، ولكن في Swift 4.2 ، تم تضمينه بالفعل بشكل افتراضي.
تحتوي هذه الفئة على طريقة
fetchSplitTest عامة وثلاث طرق خاصة:
saveGroup و
getGroup و
randomGroup .
تنشئ طريقة randomGroup متغيرًا عشوائيًا لاختبار التقسيم المحدد ، بينما تتيح لك getGroup و saveGroup حفظ المتغير أو تحميله لاختبار تقسيم محدد للمستخدم الحالي.
الوظيفة الرئيسية والعامة لهذه الفئة هي fetchSplitTest: فهي تحاول إرجاع الإصدار الحالي من التخزين الدائم ، وإذا لم تعمل ، فإنها تنشئ نسخة عشوائية وتحفظها قبل إرجاعها.
نحن الآن جاهزون لإنشاء أول اختبار تجزئة:
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 } } }
يبدو الأمر مثيرًا للإعجاب ، ولكن لا تقلق: بمجرد تنفيذ SplitTestProtocol كفئة منفصلة ، سيطلب منك المترجم تنفيذ جميع الخصائص الضرورية.
الجزء المهم هنا هو نوع
مجموعة التعداد . يجب عليك وضع جميع مجموعاتك فيه (في المثال الخاص بنا ، الأحمر والأزرق والظلام الداكن) ، وتحديد قيم السلسلة هنا لضمان النقل الصحيح إلى التحليلات.
لدينا أيضًا ملحق
ButtonColorSplitTest.Group ، مما يسمح لك باستخدام الإمكانات الكاملة لـ Swift. الآن دعنا ننشئ الكائنات لـ
AnalyticsProtocol و
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 } }
بالنسبة لـ
StorageProtocol ، سنستخدم فئة UserDefaults لأنه سهل التنفيذ ، ولكن في مشاريعك يمكنك العمل مع أي تخزين مستمر آخر (على سبيل المثال ، اخترت Keychain لنفسي ، لأنه يحفظ المجموعة للمستخدم حتى بعد الحذف).
في هذا المثال ، سوف أقوم بإنشاء فئة تحليلات وهمية ، ولكن يمكنك استخدام تحليلات حقيقية في مشروعك. على سبيل المثال ، يمكنك استخدام خدمة
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) } }
الآن نحن على استعداد لاستخدام اختبار الانقسام:
let splitTestingService = SplitTestingService(analyticsService: Analytics(), storage: UserDefaults.standard) let buttonSplitTest = splitTestingService.fetchSplitTest(ButtonColorSplitTest.self) self.button.backgroundColor = buttonSplitTest.currentGroup.color buttonSplitTest.hitSplitTest()
ما عليك سوى إنشاء مثيلك الخاص ، واستخراج اختبار الانقسام واستخدامه. تتيح لك التعميمات استدعاء
buttonSplitTest.currentGroup.color.
أثناء الاستخدام الأول ، يمكنك رؤية شيء مثل (
قيمة التسجيل مرة واحدة )
: split_test-button_color للمفتاح: dark_gray ، وإذا لم تقم بإزالة التطبيق من الجهاز ، فسيكون الزر هو نفسه في كل مرة تبدأ فيها.
تستغرق عملية تنفيذ هذه المكتبة بعض الوقت ، ولكن بعد ذلك ، سيتم إنشاء كل اختبار تقسيم جديد داخل مشروعك في غضون دقيقتين.
في ما يلي مثال على استخدام المحرك في تطبيق حقيقي: في التحليلات ، قمنا بتقسيم المستخدمين حسب معامل التعقيد واحتمال شراء عملة اللعبة.

الأشخاص الذين لم يواجهوا عامل الصعوبة هذا مطلقًا (لا شيء) ربما لا يلعبون على الإطلاق ولا يشترون أي شيء في الألعاب (وهو أمر منطقي) ، وهذا هو السبب في أنه من المهم إرسال النتيجة (الإصدار الذي تم إنشاؤه) من اختبار التقسيم إلى الخادم في وقت يواجه المستخدمون اختبارك حقًا.
بدون عامل صعوبة ، اشترى 2٪ فقط من المستخدمين عملة اللعبة. بنسبة صغيرة ، تم إجراء عمليات الشراء بالفعل بنسبة 3 ٪. ومع وجود صعوبة عالية ، اشترى 4٪ من اللاعبين العملة. هذا يعني أنه يمكنك الاستمرار في زيادة المعامل ومراقبة الأرقام. :)
إذا كنت مهتمًا بتحليل النتائج بأقصى قدر من الموثوقية ، أنصحك باستخدام
هذه الأداة .
بفضل الفريق الرائع الذي ساعدني في العمل على هذا المقال (وخاصة
إيجور ،
كيلي وهيرو ).
المشروع التجريبي بأكمله متاح على
هذا الرابط .