Як ми перевіряємо працездатність серверного коду без мобільних клієнтів


Badoo — це сервіс знайомств, який доступний у вигляді сайту і мобільних додатків під основні платформи. На початку минулого року ми глобально переробили сайт, в результаті чого він перетворився в «товстого клієнта» і став працювати так само, як і мобільні додатки: викликати команди на сервері і одержувати від нього відповіді згідно з протоколом, який описує взаємодію клієнтської і серверної частин. Ці дві частини робляться різними розробниками, і, як правило, внутрішня частина робиться вже після того, як вона буде готова. При цьому є проблема: як розробник нової фічі може переконатися, що серверна частина працює коректно, якщо клієнта для неї поки немає і перевірити її не на чому?
Для вирішення цієї проблеми в будь-серверної задачі у нас обов'язково повинні бути написані інтеграційні тести, про які я розповім в цій статті.
Що це таке і як це працює?
У нашому випадку ці тести являють собою надбудову над PHPUnit, завдяки якій тест стає додатком-клієнтом, яке звертається до сервера за протоколом. При цьому є можливість налаштувати, до якого саме сервера ми хочемо звернутися. Це може бути:
  • майданчик розробника;
  • «шот» — спеціальна площадка з «бойової» базою, на яку викладається код створюваної системи;
  • «стейджинг».
У першому випадку і клієнт, і сервер працюють в рамках одного процесу PHP, а в інших це буде повноцінний клієнт-сервер, коли тест відправляє запити на інші сервера.
Ось приклад подібного тесту, який перевіряє, що користувач, який подарував подарунок користувачу, побачить цей подарунок в його профілі:
class ServerGetUserGiftsTest extends BmaFunctionalTestCase
{
public function testGiftsSending()
{
// Given
$ClientGiftSender = $this->getLoginedConnection(
\BmaFunctionalConfig::USER_TYPE_NEW,
[
'app_build' => 'Android',
'supported_features' => [
\Mobile\Proto\Enum\FeatureType::ALLOW_GIFTS,
],
]
);
$ClientGiftReceiver = $this->getLoginedConnection();

$gift_type = 1;
$gift_add_result = $ClientGiftSender->QaApiClient->addGiftToUser(
$ClientGiftReceiver->getUserId(),
$ClientGiftSender->getUserId(),
$gift_type
);
$this->assertGiftAddSuccess($gift_add_result, "Precondition failed: cannot add gift from sender to receiver");

// When
$Response = $ClientGiftSender->ServerGetUser(
[
'user_id' => $ClientGiftReceiver->getUserId(),
'client_source' => \Mobile\Proto\Enum\ClientSource::OTHER_PROFILE,
'user_field_filter' => [
'projection' => [\Mobile\Proto\Enum\UserField::RECEIVED_GIFTS],
],
]
);

// Then
$this->assertResponseHasMessageType(\Mobile\Proto\Enum\MessageType::CLIENT_USER, $Response);
$user_received_gifts = $Response->CLIENT_USER['received_gifts'];

$this->assertArrayHasKey('gifts', $user_received_gifts, "No gifts list at received_gifts field");
$this->assertCount(1, $user_received_gifts['gifts'], "received Unexpected gifts count");

$gift_info = reset($user_received_gifts['gifts']);
$this->assertEquals($ClientGiftSender->getUserId(), $gift_info['from_user_id'], "Wrong from_user_id value");
}
}

Давайте розберемо цей приклад по частинах.
Кожен тест успадковується від класу BmaFunctionalTestCase — спадкоємця PHPUnit_Framework_TestCase. У ньому реалізовано декілька допоміжних методів, головним з яких є можливість отримання об'єкта клієнта, через який можна відправляти запити до сервера:
$ClientGiftSender = $this->getLoginedConnection(
\BmaFunctionalConfig::USER_TYPE_MALE,
[
'app_build' => 'Android',
'supported_features' => [\Mobile\Proto\Enum\FeatureType::ALLOW_GIFTS],
]
);

Тут ми можемо «представитися» конкретною версією клієнта зі своїм набором підтримуваних функцій. Після виконання цього методу у нас з'являється об'єкт, який дозволяє відправляти запити від імені зареєстрованого користувача, що використовує певний додаток.
Цього зареєстрованого користувача ми беремо із спеціального пулу тестових користувачів. У ньому є деяка кількість «чистих» користувачів, тобто всі вони мають один і той же початковий стан. Коли в тесті викликається метод getLoginedConnection(), вибирається один з цих користувачів, і він блокується для використання іншими тестами. Блокування потрібна для того, щоб ми завжди мали справу з користувачами у відомому нам стані. Після блокування з цим користувачем можна проводити будь-які маніпуляції, а після закінчення роботи тесту запускається механізм очищення, який приведе користувача в початкове «чисте» стан, і той знову буде доступний для використання в тестах. Всі тестові користувачі знаходяться в одній локації, в якій немає реальних користувачів. Тому, з одного боку, ми в тестах маємо справу з передбачуваним оточенням, а з іншого — реальні користувачі не бачать тестових.
Як правило, ми не можемо запускати перевірку відразу після отримання об'єкта клієнта: потрібно створити оточення, необхідне тесту (в даному прикладі — відправити подарунок іншому користувачеві). Робити це ми можемо «чесно», відправляючи запити серверу через об'єкт клієнта, але це не завжди можливо. У разі подарунка «чесний» шлях був би занадто складним: нам потрібно поповнити рахунок користувача, отримати список доступних подарунків, відправити його і дочекатися, поки він буде оброблений скриптом відправлення. Все це ускладнить тест і збільшить час його розробки та виконання.
Щоб це спростити, ми використовуємо внутрішній інструмент під назвою QaAPI (про нього вже розповідав мій колега Дмитро Марущенко, презентацію та відео можна знайти на «Хабре»). Він складається з безлічі невеликих методів, кожен з яких дозволяє здійснювати окремі дії над користувачами в обхід стандартних механізмів або отримати якісь відомості про користувача. З його допомогою можна додати користувачеві фотографії і відразу отмодерировать їх, минаючи черги і перевірку модераторами; змінити значення окремих полів в його профілі, проголосувати за інших користувачів «Знайомствах» і т. д.
У цьому прикладі ми просто даруємо подарунок без поповнення рахунку і в обхід черг:
$gift_add_result = $ClientGiftSender->QaApiClient->addGiftToUser(
$ClientGiftReceiver->getUserId(),
$ClientGiftSender->getUserId(),
$gift_type_id
);
$this->assertGiftAddSuccess($gift_add_result, "Precondition failed: cannot add gift from sender to receiver");

Дуже важливо перевіряти відповіді QaAPI, адже в разі помилки користувач буде зовсім не в тому стані, який ми очікуємо отримати, і подальші перевірки будуть безглузді. Якщо говорити про нашому прикладі, то було б дивно перевіряти наявність подарунка в профілі, якщо ми не змогли його подарувати.
Якщо ми з якихось причин не хочемо «чесно» приводити користувача в потрібний стан, то ми можемо використовувати видалені mock-об'єкти. На відміну від локальних, вони бувають одноразові (діють тільки на одну команду) і постійні (що працюють до кінця виконання тесту).
Технічно mock-об'єкти реалізовані з допомогою іншого нашого рішення, SoftMocks. Воно використовується або безпосередньо (на майданчику розробника, коли тест працює в рамках одного процесу), або через «прокладку» у вигляді memcache (на віддаленій майданчику). У другому випадку під час роботи тесту ми кладемо інформацію про новому mock-об'єкті в масив одноразових чи постійних mock-об'єктів, а перед відправленням запиту на сервер об'єднуємо ці два масиви і кладемо їх у memcache, звідки їх зможе забрати серверна частина.
Ми часто використовуємо такі mock-об'єкти для перевірок лексем, коли потрібно переконатися, що у відповіді прийде потрібний нам текст. Це можна зробити «чесно», але це буде не дуже зручно: тексти можуть змінюватися з часом (і це буде ламати тест), плюс на різних мовах вони можуть бути різні. Щоб уникнути цих проблем, ми замінюємо лексеми на якісь визначені значення або навіть на шляху до текстів.
У цілому використання mock-об'єктів робить тест більш швидким, т. до. дозволяє позбутися від одного або декількох віддалених викликів, але додає залежно від серверного коду і робить їх менш надійними: вони частіше ламаються і більше «брешуть».
Після створення потрібного оточення ми можемо відправити серверу запит і отримати відповідь:
$Response = $ClientGiftSender->ServerGetUser(
[
'user_id' => $ClientGiftReceiver->getUserId(),
'user_field_filter' => [
'projection' => [\Mobile\Proto\Enum\UserField::RECEIVED_GIFTS],
],
]
);

У таких тестах код сервера являє для нас чорний ящик: ми не знаємо, що там відбувається і який саме код обробляє наш запит. Все, що ми можемо зробити — це перевірити відповідність відповіді сервера нашим очікуванням.
Наш протокол дозволяє серверу повертати різні типи відповідей на одну і ту ж команду. Команди можуть повертати відповідь різних типів. Наприклад, помилку може повернути практично будь-яка команда. З цієї причини ми починаємо перевірку відповіді з того, є чи там повідомлення очікуваного типу:
$this->assertResponseHasMessageType(\Mobile\Proto\Enum\MessageType::CLIENT_USER, $Response);

Після того як ми переконалися в наявності потрібного повідомлення, можна більш детально перевірити відповідь і переконатися, що в ньому є наш подарунок:
$user_received_gifts = $Response->CLIENT_USER['received_gifts'];

$this->assertArrayHasKey('gifts', $user_received_gifts, "No gifts list at received_gifts field");
$this->assertCount(1, $user_received_gifts['gifts'], "received Unexpected gifts count");

$gift_info = reset($user_received_gifts['gifts']);
$this->assertEquals($ClientGiftSender->getUserId(), $gift_info['from_user_id'], "Wrong from_user_id value");

Для команд, які модифікують стан користувача, недостатньо перевірити відповідь сервера. Наприклад, якщо ми відправляємо команду на видалення подарунка, то мало отримати Success у відповіді — треба ще перевірити, що подарунок дійсно видалено. Для цього можна викликати інші команди і перевірити їх відповіді, або скористатися тим же QaAPI, викликавши метод, що повертає стан параметра, який ми хочемо перевірити. У прикладі з видаленням подарунка ми могли б викликати QaAPI-метод, що повертає список подарунків і перевірити, що в ньому немає тільки що віддаленого.
Які переваги?
Головне достоїнство таких тестів — розуміння того, що новий функціонал працює так, як ми очікуємо. Якщо ми описали сценарій у вигляді такого тесту і він пройшов, то ми розуміємо, що весь функціонал працює і може бути використаний реальним додатком-клієнтом.
Інший важливий плюс: ми можемо провести регрессионное тестування і переконатися, що внесені зміни не зламають старих клієнтів, для яких новий функціонал буде недоступний. Дані тести дозволяють нам це зробити через вказівку різних версій програми (це старий шлях, який ми використовували для версионирования раніше) і певного набору функцій, підтримуваних клієнтом (це новий шлях, який ми використовуємо зараз).
Які недоліки?
Головними недоліками цих тестів є тривалий час роботи і нестабільність, що випливають з їх високого рівня. Хоча тести зазвичай перевіряють результати однієї команди протоколу, для них створюється повноваге оточення, яке працює з тими ж базами і сервісами, що і у звичайних клієнтів. Все це, а так само «чесне» відтворення оточення, потребує інших запитів (часто не одного-двох) до сервера, вимагає часу.
Деякі фічі вимагають складної ініціалізації, яка збільшує розмір тестових методів. Адже перед викликом досліджуваного методу потрібно не тільки надсилати запити для ініціалізації, але і перевірити, що вони відпрацювали так, як ви очікували. Наприклад, якщо ви хочете перевірити роботу чату, то вам потрібно отримати двох клієнтів, дати їм можливість «чатитися» один з одним, відправити повідомлення і перевірити, що воно дійсно вирушило. Буває, що деякі речі відбуваються з затримкою і вам потрібно дочекатися доставки даних.
З-за цієї складності тести стають дуже крихкими»: поломка у відтворенні оточення зламає вам тест, і хоча проблема не відноситься до того, що ви перевіряєте, ваш тест падає. Такі тести не вкажуть вам, що саме зламалося, ви зрозумієте, що щось не працює. Конкретний метод, зміна якого поламало тест, доведеться шукати самостійно, а іноді зробити це буває дуже непросто.
Висновок
Незважаючи на перераховані мінуси, ці тести вирішують свої завдання і дозволяють розробникам писати тести в тому ж вигляді, що і звичні всім unit-тести.
Віктор Пряжников, розробник відділу Features
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.