Всем доброго времени суток!
О чем пойдет речь: практическое решение задачи — как заработать кучу виртуальных денег в популярной Android игре — CSR Racing, приложив минимум усилий (< 2х часов свободного времени). Фактически, как использовать обратную сторону медали автоматизированного тестирования и наглядный пример того, что же такое на самом деле ночная автоматизация и какую выгоду можно от нее получить.
Статья ориентирована на начинающих QA специалистов, геймеров и любителей виртуального драг рейсинга, критика приветствуется :)
Итак, как же автору удалось одновременно обзавестись крутой тачкой в игре и найти критический баг.
1. Небольшое лирическое предисловие о том, как вообще родилась эта идея. Обзор объекта тестирования.
Началось все с того, что Barnes & Noble включили в свою платформу Google Play и как результат, все пользователи Nook-в, включая меня, получили возможность устанавливать любые приложения из Google Play. Затем у меня сломалась машина и, чтобы не скучать в маршрутке/метро/автобусе, я начал брать с собой рабочий Nook HD+. Быстро была найдена интересная игра автомобильной тематики — CSR Racing (в категории от 5 до 10 млн установок). Она бесплатная и обладает превосходной 3D графикой и геймплеем.
Коварство разработчиков крылось в том, что если вы не делаете in-app-purchases, то у вас имеется только 10 единиц топлива (1 единица на заезд) и для получения следующей единицы топлива необходимо подождать 7 минут. Экономика игры построена на двух принципах: вы можете получать деньги от заездов или покупать деньги за реальные доллары, причем расценки такие:
— Игровые 56 000$ — это совсем небольшая сумма. Нитро 4-го уровня стоит 72, 591$ на Ford Mustang Boss 302, т. е. потратив 10$ реальных денег, мы не сможем даже нитро купить 4-го уровня. Так не пойдет! :)
Получать деньги за победные заезды здорово — но это (на 3-м уровне) ~ 5.700 каждые 7 минут, при стоимости того же нитро в 72, 591. Капля в море.
2. Стратегия действий, для доминирования в игре.
- Изучить объект тестирования;
- Найти дефекты или логические просчеты, которые могут открыть дверь к халявному обогащению;
- Если найдутся дефекты и их можно будет эффективно автоматизировать — сделать это. Если нет — рассмотреть вариант использования найденных дефектов в «ручном» режиме;
- За счет найденных дефектов заполучить машину превосходящего класса;
- За счет крутой тачки свести проблемы обогащения (и соответственно развития на текущем уровне) к минимуму.
3. Доступные стратегии обогащения через автоматизацию, без детального изучения объекта тестирования.
Примечание: Я все это проделывал на Tier 3, где за проигрыш начисляют 425$. На Tier 1 эта сумма всего 60$ :) (Уточнил: можно найти рейс на Tier 1 и с 325$ призовых. Битва с 2-3 crew, что делает разумным автоматизацию проигрыша, даже на первом уровне).
1) Любая победа приносит хорошие деньги, т. к. взамен мы тратим всего лишь 1 единицу топлива. В игре все упирается в топливо. Если его нет, вам не испытать новенькую турбину в состязании. 3 раза в 24 часа можно отправить email другу «в никуда» и получить 3 ед топлива. На общем фоне не сильно меняет картину.
2) Топливо можно получать за золотые фишки, которые переодически даются за определенные ачивки, но это расточительно Нитро за 72, 591 стоит 8 золотых фишек, а заправить 10 ед топлива — 2 фишки. Жуть!. Гипотетически, используя возможности получения доптоплива, можно удлиннить победную серию.
4. Дополнительно найденная стратегия при детальном изучении объекта тестирования.
3) В результате хорошего ad-hoc тестирования пограничных значений топливного бака было обнаружено, что на последней единице топлива можно переигрывать проигранный рейс бесконечно долго. Если вы проигрываете его, единица добавленного топлива через 7 минут будет потрачена, но дальше можно будет продолжать переигрывать рейс снова и снова на «сухом баке». Что примечательно, за проигрыш вы тоже получаете деньги (бонус за удачный старт, бонус за удачное переключение, бонус за просто участие в рейсе). В рейсе с призовыми в 3000, максимум получилось выжать на проигрыше — 1000 (за счет череды удачных переключений с первой на вторую, со второй на первую и так бесконечно до финиша). Выиграть 1000 на проигрыше достаточно тяжело и требует предельной концентрации. Выиграть 500 — не проблема вообще, достаточно одной удачно переключенной передачи. Время рейса — 12 секунд. Итого получаем (с паузами на загрузку и обработку диалогов) 45 секунд на одну итерацию. 7 минут = 420 секунд, а это 9 полных итераций и, в свою очередь, это 4500 100% заработанных игровых денег, что на 1500 больше, если просто выиграть заезд и подождать 7 минут до следующей единицы топлива. Неплохо :)
Вот тут-то ладошки и вспотели, сами потянулись написать какой-нить автотест. Только вот на чем его писать?
5. Выбор инструмента реализации.
- Хотел было сразу взяться за Google UiAutomator (статья в нашем блоге и документация Google), но у него ограничение по минимальной версии платформы — Android 4.1. Сразу отпадает.
- Старый добрый adb shell input swipe/tap. Тоже не работает на Android 4.0.3. Более того, проблема возникнет, если необходимо продолжительное нажатие (6-8 секунд удержание педали газа в начале заезда).
- Robotium Solo, методом черного ящика, с последующим Robotium тестом. В принципе, допустимо, но придется потратиться на получения apk, создание проекта, написания теста, переподписывание apk, его повторную установку. Ну и плюс все сложности, при работе с Robotium.
- Monkey Runner? Блин, как я не пытался — не быть нам с Python вместе. Также проблема очень медленного старта теста.
- Старый добрый event-driven подход, который выручал нас с тех пор, когда у эмулятора Андроид была красненькая шкурка.
Предпочтение было отдано #5, так, как это самый простой подход не требующий даже блокнонта, можно прямо в терминале написать весь тест: D
6. Выявление маршрута автоматизации необходимого процесса.
Чтобы что-то успешно автоматизировать на продолжительном отрезке времени, необходимо найти циклическую итерацию и запихнуть ее в while (true). В нашем случае этот маршрут состоит из следующих взаимодействий с экраном:
Предусловие: запущена гонка.
- Нажать и держать педаль газа 6 секунд — создаем файл acceleration.sh;
- Переключить передачу минимум 2 раза (расчитываем время и дотягиваем его тестами), чтобы получить «Perfect shift» хотя бы раз. Это даст нам 500$ за гонку. Создаем файл shift_up.sh;
- Нажать на кнопку «Переиграть рейс». Создаем файл restart_race.sh;
- Подавить негативный диалог с вопросом о походе к механику или о необходимости попытаться на более простом уровне. Добавим этот клик в файл restart_race.sh.
- Вернуться к шагу 1. Создаем файл scenario.sh с бесконечным циклом.
Хм. не самый сложный алгоритм: D
7. Реализация
Для упрощения всех кликов внутри выше описанных файлов, создадим еще один файл — click.sh — и запишем в него сигнатуру клика для нашего устройства. Сигнатура служебного Nook HD+ доподлинно известна (и скорее всего сработает на большинстве 4.0.3 девайсов):
1. Touch down:
sendevent /dev/input/event2 1 330 1 sendevent /dev/input/event2 3 48 14 sendevent /dev/input/event2 3 53 $2 sendevent /dev/input/event2 3 54 $3 sendevent /dev/input/event2 0 0 0
2. Touch up:
sendevent /dev/input/event2 1 330 0 sendevent /dev/input/event2 3 48 0 sendevent /dev/input/event2 3 53 $2 sendevent /dev/input/event2 3 54 $3 sendevent /dev/input/event2 0 0 0
3. Параметр $1 зарезирвируем для вида действия — нажатие экрана (Touch down) или его отпускание (Touch up).
4. Конечный click.sh (не забудте записать этот файл на девайс) принимает 3 параметра — вид нажатия, X, Y и выглядит следующим образом:
case $1 in click_down) sendevent /dev/input/event2 1 330 1 sendevent /dev/input/event2 3 48 14 sendevent /dev/input/event2 3 53 $2 sendevent /dev/input/event2 3 54 $3 sendevent /dev/input/event2 0 0 0 ;; click_up) sendevent /dev/input/event2 1 330 0 sendevent /dev/input/event2 3 48 0 sendevent /dev/input/event2 3 53 $2 sendevent /dev/input/event2 3 54 $3 sendevent /dev/input/event2 0 0 0 ;; *) echo Wrong parameter: $1;; esac
Вызов нажатия в 100 200:
adb shell /data/click.sh click_down 100 200 adb shell /data/click.sh click_up 100 200
Обязательно сделать этому скрипту:
adb shell chmod 777 /data/click.sh
А также интерфейсу, который отвечает за сенсорный экран (в нашем случае event2):
adb shell chmod 777 /dev/event2
Иначе клики работать не будут.
С помощью getevent можно получить координаты всех нужных кнопок и запрограммировать наши файлы-функции (далее координаты приведены для Nook HD+ в обратно-ландшафтной ориентации):
#Файл acceleration.sh echo accelerating! adb shell /data/./click.sh click_down 784 82 sleep 5 # имитация продолжительного удержания педали газа adb shell /data/./click.sh click_up 784 82
# Файл shift_up.sh echo "shift up!" adb shell /data/./click.sh click_down 1017 473 adb shell /data/./click.sh click_up 1017 473
# Файл restart_race.sh echo click restart adb shell /data/./click.sh click_down 1122 1716 sleep .5 adb shell /data/./click.sh click_up 1122 1716 sleep 4 # ожидаем появление возможного негативного диалога echo suppress possible negative dialog adb shell /data/./click.sh click_down 913 1064 sleep .5 adb shell /data/./click.sh click_up 913 1064
# Сам сценарий авто теста scenario.sh while true; do echo "-------------------------------------------------" echo "Iteration started at: " `date +%d-%m-%y-%H:%M:%S` ./acceleration.sh sleep 1 ./shift_up.sh sleep 1.4 ./shift_up.sh sleep 1.4 ./shift_up.sh sleep 25 echo "Long race sleep is over" ./restart_race.sh sleep 8 done
Для Windows можно заменить все файлы, кроме того, который записывается на устройство (click.sh), на bat-файлы.
8. Анализ результатов.
1) Самое печальное, что даже под таким простым автоматическим нагрузочным тестированием был найден критический дефект в приложении, который приводит к сбою работы и неожиданному выходу из приложения (это я пытался на русском написать «краш приложения»). Более того, если продолжать игнорировать подобный дефект, через двое суток (в моем случае) это приведет к потере всего вашего накопления, достижений, машин. В общем игра загрузится так, как будто вы первый раз ее установили. Пойманный текст ошибки (вероятно не в нем причина вайпа аккаунта, но этот приводил к регулярным вылетам из игры):
W/Unity (18032): Player race name requested but nothing or user100000 was returned, defaulting to "You" W/Unity (18032): W/Unity (18032): (Filename: ./Runtime/ExportGenerated/AndroidManaged/UnityEngineDebug.cpp Line: 43) W/Unity (18032): W/Unity (18032): Player race name requested but nothing or user100000 was returned, defaulting to "You" W/Unity (18032): W/Unity (18032): (Filename: ./Runtime/ExportGenerated/AndroidManaged/UnityEngineDebug.cpp Line: 43) W/Unity (18032): F/Looper (18032): Could not create wake pipe. errno=24 F/libc (18032): Fatal signal 11 (SIGSEGV) at 0xdeadbaad (code=1) F/Looper (18032): Could not create wake pipe. errno=24 F/libc (18032): Fatal signal 11 (SIGSEGV) at 0xdeadbaad (code=1) D/DeviceManagerBroadcastReceiver( 384): Updated Battery Level: 73 D/DeviceManagerBroadcastReceiver( 384): Action: android.intent.action.BATTERY_CHANGED D/DeviceManagerBroadcastReceiver( 384): Set Alarm: false D/BatteryService( 230): PROCESSVALUES : update plugged:false was:false stat:3 zone:16 was:16 send low:false I/ActivityManager( 230): Process com.naturalmotion.csrracing (pid 18032) has died. W/ActivityManager( 230): Scheduling restart of crashed service com.naturalmotion.csrracing/com.bossalien.racer01.CSRNotificationService in 5000ms W/ActivityManager( 230): Scheduling restart of crashed service com.naturalmotion.csrracing/com.bossalien.racer01.CSRNotificationManager$NotificationIntent in 15000ms W/ActivityManager( 230): Force removing ActivityRecord{413efa08 com.naturalmotion.csrracing/com.bossalien.racer01.CSRPlayerActivity}: app died, no saved state I/WindowManager( 230): WIN DEATH: Window{4188c6a0 com.naturalmotion.csrracing/com.bossalien.racer01.CSRPlayerActivity paused=false} W/WindowManager( 230): Force-removing child win Window{4188caa0 SurfaceView paused=false} from container Window{4188c6a0 com.naturalmotion.csrracing/com.bossalien.racer01.CSRPlayerActivity paused=false} W/WindowManager( 230): Failed looking up window W/WindowManager( 230): java.lang.IllegalArgumentException: Requested window android.os.BinderProxy@41ef62f0 does not exist W/WindowManager( 230): at com.android.server.wm.WindowManagerService.windowForClientLocked(WindowManagerService.java:7189) W/WindowManager( 230): at com.android.server.wm.WindowManagerService.windowForClientLocked(WindowManagerService.java:7180) W/WindowManager( 230): at com.android.server.wm.WindowState$DeathRecipient.binderDied(WindowState.java:1551) W/WindowManager( 230): at android.os.BinderProxy.sendDeathNotice(Binder.java:417) W/WindowManager( 230): at dalvik.system.NativeStart.run(Native Method) I/WindowManager( 230): WIN DEATH: null
2) Мне таки удалось заработать около 500 000$, что почти 100 реальных долларов в эквиваленте :) Данный подход был использован на Tier 3, так как на предыдущих в нем не было необходимости и стоимость запчастей была приемлемой. На Tier 3 мне удалось купить McLaren SLR за 260 000$ (для следующего Tier 4) и Ford Mustang Boss 302 за 120 000$ + большинство апгрейдов к нему.
Видео результат (прошу прощения за низкое качество)
3) Интересный побочный вывод. Даже простейшая нагрузочная ночная автоматизация может вскрывать серьезные проблемы, а чтобы это было еще более эффективно, разумно делать так (в Ubuntu):
Открываем терминал с тремя вкладками. В первой запускаем сам сценарий. Во второй:
adb logcat -v time > logOutput.txt (сохраняем в файл журнал ошибок с подопытного устройства)
В случае с Nook HD+ (так как я наверняка знаю, какое сообщение говорит о состоянии батареи):
D/DeviceManagerBroadcastReceiver( 384): Updated Battery Level: 73
Можно еще добавить следующую запись:
adb logcat -v time | grep "Updated Battery"
Что даст нам следующий вывод:
05-13 03:27:28.176 D/DeviceManagerBroadcastReceiver( 5126): Updated Battery Level: 56 05-13 03:27:33.285 D/DeviceManagerBroadcastReceiver( 5126): Updated Battery Level: 56 05-13 03:27:43.449 D/DeviceManagerBroadcastReceiver( 5126): Updated Battery Level: 56 05-13 03:27:48.535 D/DeviceManagerBroadcastReceiver( 5126): Updated Battery Level: 56 05-13 03:27:58.684 D/DeviceManagerBroadcastReceiver( 5126): Updated Battery Level: 56 05-13 03:28:08.848 D/DeviceManagerBroadcastReceiver( 5126): Updated Battery Level: 56
Фактически такой подход поможет нам измерять расход батареи под постоянной нагрузкой на продолжительном промежутке времени.
Самые внимательные сразу скажут: как это можно доверять таким значениям, если устройство подключено по USB проводу к компьютеру и постоянно заряжается! И они будут правы. Чтобы обеспечить подобный замер, необходимо перевести ваш ADB в режим работы по WiFi.
При желании можно еще добавить забор скриншотов с помощью утилиты screencap (идет сейчас в поставке). Простой shell script для взятия скриншотов каждые 3 секунды (если убрать паузу, можно получить видео с 2-3 fps после сборки всех скриншотов в gif или avi):
while true; do adb shell /system/bin/screencap -p /sdcard/img.png #перезапись не тратит свободное место adb pull /sdcard/img.png "./`date`.png" sleep 3 done
Если вы захотите повторить это на вашем устройстве и у вас что-то не получится — пишите, разберемся. Если вам нравится подобный род деятельности и вы бы этим хотели заниматься по-серьезному — тоже пишите, разберемся =)
9. Можно ли было сделать этот тест еще круче?
Да, можно было бы еще добавить несколько простых, но классных штук:
- С помощью любого планировщика задач стартовать тест каждые 10 минут, если текущая Activity! = CSR Racing;
- Предусмотреть маршрут входа в нужный цикл зарабатывания виртуальных денег из нулевого состояния приложения (когда мы его загружаем с домашнего экрана);
- Оптимизировать сценарий по принципу: выиграй 9 гонок, на 10-й включай стратегию заработка на проигрыше в течении N минут (желательно меньше часа, чтобы не происходил краш приложения), затем подожди часок (чтобы заправился бензобак) и начинай снова. Такой сценарий приносил бы больше денег, так как был бы полностью автоматическим и более стабильным;
- Ваш вариант =)
ЗЫ: за 25 лайков готов написать статью, как записать и воспроизвести пользовательское взаимодействие с Android устройством на shell event-ах (Windows & Ubuntu). Да, да, такой я алчный негодяй ^__^
Релоцировались? Теперь вы можете комментировать без верификации аккаунта.