Автоматизация функционального тестирования REST API

Post on 09-Aug-2015

98 views 2 download

Transcript of Автоматизация функционального тестирования REST API

Автоматизация функционального тестирования REST API

секреты, тонкости и подводные камни

Павел Асанов

QA Lead

90 городов

700 тыс. пользователей

12 млн просмотров

1 млн отзывов

3 млн посетителей

API как продукт

API как продукт

80%

20%

Регрессия

3 месяца 5 минут

● Ресурсы, однозначно определяемые по URL

● Представления ресурсов (JSON)

● Методы работы с ресурсами

REST - это...

POST /reviews GET /reviews/666PUT /reviews/666PATCH /reviews/666DELETE /reviews/666

Что тестируем?

1. Логика

отсутствие обязательных параметров

невалидный протокол

параметры запроса

скрытыйобычный скрытый с особым типом

авторизованный гость

БАК

удалённыйзаблокированный

забаненный

филиал БАК

автор неактивированный удалённый

обычный

корректные

с фото

невалидные значения

параметров

невалидные типы

параметров

БАК

expired

другой юзер

приватныйконтент

пользователь

аксесс токен

отсутствие обязательных параметров

невалидный протокол

параметры запроса

скрытыйобычный скрытый с особым типом

авторизованный гость

БАК

удалённыйзаблокированный

забаненный

филиал БАК

автор неактивированный удалённый

обычный

корректные

с фото

невалидные значения

параметров

невалидные типы

параметров

БАК

expired

другой юзер

201

Авторизованный пользователь добавляет отзыв с фото

приватныйконтент

пользователь

аксесс токенPOST

отсутствие обязательных параметров

невалидный протокол

параметры запроса

скрытыйобычный скрытый с особым типом

авторизованный гость

БАК

удалённыйзаблокированный

забаненный

филиал БАК

автор неактивированный удалённый

обычный

корректные

с фото

невалидные значения

параметров

невалидные типы

параметров

БАК

expired

другой юзер

200

Авторизованный владелец Бизнес-аккаунта запрашивает приватное обращение

приватныйконтент

пользователь

аксесс токен

GET

отсутствие обязательных параметров

невалидный протокол

параметры запроса

скрытыйобычный скрытый с особым типом

авторизованный гость

БАК

удалённыйзаблокированный

забаненный

филиал БАК

автор неактивированный удалённый

обычный

корректные

с фото

невалидные значения

параметров

невалидные типы

параметров

БАК

expired

другой юзер

401

Гость добавляет отзыв

приватныйконтент

пользователь

аксесс токен

POST

отсутствие обязательных параметров

невалидный протокол

параметры запроса

скрытыйобычный скрытый с особым типом

авторизованный гость

БАК

удалённыйзаблокированный

забаненный

филиал БАК

автор неактивированный удалённый

обычный

корректные

с фото

невалидные значения

параметров

невалидные типы

параметров

БАК

expired

другой юзер

403

Авторизованный пользователь запрашивает чужое приватное обращение

приватныйконтент

пользователь

аксесс токен

GET

отсутствие обязательных параметров

невалидный протокол

параметры запроса

скрытыйобычный скрытый с особым типом

авторизованный гостьаксесс токен

БАК

удалённыйзаблокированный

забаненный

филиал БАК

автор неактивированный удалённый

обычный

корректные

с фото

невалидные значения

параметров

невалидные типы

параметров

БАК

expired

другой юзер

500 400 400 404

Невалидный запрос

приватныйконтент

пользователь

2. Данные и их формат

{code: 200status: "success"review: {

filial_id: "985690699651034"

text: "Для меня Code Fest - это глоток свежего воздуха. Только надо баги пофиксить."

is_recommended: false

project_id: 1

source: "flamp"

rating: 4

date_created: "2011-03-28T05:30:50+04:00"

user_id: 171

photo: null

}}

{code: 200status: "success"review: {

filial_id: "985690699651034"

text: "Для меня Code Fest - это глоток свежего воздуха. Только надо баги пофиксить."

is_recommended: false

project_id: 1

source: "flamp"

rating: 4

date_created: "2011-03-28T05:30:50+04:00"

user_id: 171

photo: null

}}

точное значение

{code: 200status: "success"review: {

filial_id: "985690699651034"

text: "Для меня Code Fest - это глоток свежего воздуха. Только надо баги пофиксить."

is_recommended: false

project_id: 1

source: "flamp"

rating: 4

date_created: "2011-03-28T05:30:50+04:00"

user_id: 171

photo: null

}}

точное значение

тип значения и его диапазон

{code: 200status: "success"review: {

filial_id: "985690699651034"

text: "Для меня Code Fest - это глоток свежего воздуха. Только надо баги пофиксить."

is_recommended: false

project_id: 1

source: "flamp"

rating: 4

date_created: "2011-03-28T05:30:50+04:00"

user_id: 171

photo: null

}}

точное значение

enum

тип значения и его диапазон

{code: 200status: "success"review: {

filial_id: "985690699651034"

text: "Для меня Code Fest - это глоток свежего воздуха. Только надо баги пофиксить."

is_recommended: false

project_id: 1

source: "flamp"

rating: 4

date_created: "2011-03-28T05:30:50+04:00"

user_id: 171

photo: null

}}

точное значение

enum

тип значения и его диапазон

формат значения

{code: 200status: "success"review: {

filial_id: "985690699651034"

text: "Для меня Code Fest - это глоток свежего воздуха. Только надо баги пофиксить."

is_recommended: false

project_id: 1

source: "flamp"

rating: 4

date_created: "2011-03-28T05:30:50+04:00"

user_id: 171

photo: null

}}

точное значение

наличие атрибута

enum

тип значения и его диапазон

формат значения

3. Изменение состояния системы

● После DELETE сущность стала недоступна

● После логаута access token удаляется

● После DELETE сущность стала недоступна

● После логаута access token удаляется

запрос сущности возвращает 404

● После DELETE сущность стала недоступна

● После логаута access token удаляется

запрос с данным токеном возвращает 401

запрос сущности возвращает 404

● После DELETE сущность стала недоступна

● После логаута access token удаляется

запрос с данным токеном возвращает 401или

запрос сущности возвращает 404

access token отсутствует в БД

Глава первая, в которой мы знакомимся с JSON-schema

JSON-схема — это…

1) готовая документация + примеры

header: "Информация об отзыве",description: "Информация об отзыве",request: { type: "GET", url: "https://flamp.ru/api/2.0/reviews/{id}", properties: {

id: { type: "integer", required: true,

examples: ["1"], description: "Идентификатор отзыва" }

}},

response: { type: "object", required: true, properties: {

code: "{{ common/code }}", status: "{{ common/status }}", review: "{{ objects/review }}"

}},

...

2) валидация запроса на сервере

3) валидация ответа на сервере в dev-режиме

+ простейший smoke-тест+ полная проверка формата

+ простейший smoke-тест+ полная проверка формата

- white-box- схема может содержать ошибки- нельзя использовать на продакшне

Глава вторая, в которой мы добавляем assertions в JSON-

schema

assertions: [ {

name: "View Review #1", request: { id: 1 }, response: { status: "success", review: {

id: 1, date_created: "_is_datetime", date_edited: "_is_datetime|null"

} }

},]

assertions: [{ name: "View Review #99999999", request: { id: 99999999 }, response: { status: "error" }

}]

Flamposcope

+ smoke testing+ выполняется ~20 сек+ тесты пишет разработчик

+ smoke testing+ выполняется ~20 сек+ тесты пишет разработчик

- только GET- захардкоженные данные- тесты пишет разработчик

Глава третья, в которой мы хотели прикрутить

к JSON-схеме UI

+ удобный UI+ нет кода+ автоподстановка из схемы

+ удобный UI+ нет кода+ автоподстановка из схемы

- серый ящик- фикстуры- поддержка UI

Глава четвёртая, в которой мы начали писать на PHP

Параметризованные тестыhttps://flamp.ru/api/2.0/filials/?what=кафе&project=1&lon=71&lat=65&radius=1000&sort=relevance&limit=12&page=5&with_markers=false&building_id=1&metarubric=323&depth=1&scopes=reviews&access_token=fa07c31442508e2248fcd634a786d409428ca50d

Параметризованные тестыhttps://flamp.ru/api/2.0/filials/?what=кафе&project=1&lon=71&lat=65&radius=1000&sort=relevance&limit=12&page=5&with_markers=false&building_id=1&metarubric=323&depth=1&scopes=reviews&access_token=fa07c31442508e2248fcd634a786d409428ca50d

[params_set_1] => 200,[params_set_2] => 404,[params_set_3] => 401,[params_set_4] => 403,[params_set_5] => 400,[params_set_6] => 500,...

Параметризованные тестыhttps://flamp.ru/api/2.0/filials/?what=кафе&project=1&lon=71&lat=65&radius=1000&sort=relevance&limit=12&page=5&with_markers=false&building_id=1&metarubric=323&depth=1&scopes=reviews&access_token=fa07c31442508e2248fcd634a786d409428ca50d

[params_set_1] => 200,[params_set_2] => 404,[params_set_3] => 401,[params_set_4] => 403,[params_set_5] => 400,[params_set_6] => 500,...

набор N

...

набор 1 тест

2fingers =PHPUnit + сomposer + http-клиент

(Guzzle поверх cURL) + данные

данные

dataProvider

параметры

проверки

Структура теста

test

Тестовые наборыТестовые наборыТестовые наборы

Данные

Установка параметров запроса

источник данных

Проверки

если success

ожидаемый код

ожидаемый результат

фактический результат

ожидаемый кодрольпараметры

запросаобъект

запрос

код ответа

ожидаемый ответ

тело ответа

Данные

фикстуры дамп

● реальные

● случайные, но однородные

● уникальные

Требования к данным

Работа с БД

$entity = Db()->entity('review')->forFilial('141265770608749')

->isHidden(false)->withPhoto(true)->getRandomEntity();

$user_id = Db()->user()->withStatus(1)->getRandomUser()->id;

$article = Db()->table('articles')->isPublished(true)->getRow();

Всегда ли?

'bacs' => [

'flamp_nsk' => 1,

'flamp_krsk' => 4,

'flamp_msk' => 13,

],

'filials' => [

'pac' => '141265770608749',

'pac2' => '141265771836316',

'bac' => '141265771910841',

'simple' => '141265771459351',

],

config.php

assertions

$this->assertEquals($user_id, $actual->user_id, “incorrect user_id”);

$this->assertEquals($project_id, $actual->project_id, “incorrect project_id”);

$this->assertEquals($id, $actual->id, “incorrect id”);

...

$this->assert($expected, $actual);

// Фактический результат$actual = $this->getResponseBody();

// Ожидаемый результат$expected = [

'user' => CHECK_STRING_NOT_EMPTY,'access_token' => CHECK_STRING_NOT_EMPTY

];

// Сравниваем ФР и ОР$this->assert($expected, $actual);

assertions

// Ожидаемый результат $expected = [ 'filial_id' => $filial_id, 'user_id' => $user_id, 'text' => $text, 'date_created' => CHECK_DATETIME_FORMAT, 'date_edited' => null, 'comments_count' => 0, 'likes_score' => 0, 'source' => CHECK_SOURCE, 'filial' => CHECK_NOT_NULL, 'user' => CHECK_EXIST, 'comments' => CHECK_NOT_NULL, 'official_answer' => null, 'additional_data' => [ 'is_liked' => false, 'is_subscribed_to_comments' => false ], 'url' => CHECK_STRING_NOT_EMPTY, 'id' => CHECK_POSITIVE ];

// Ожидаемый результат $expected = [ 'filial_id' => $filial_id, 'user_id' => $user_id, 'text' => $text, 'date_created' => CHECK_DATETIME_FORMAT, 'date_edited' => null, 'comments_count' => 0, 'likes_score' => 0, 'source' => CHECK_SOURCE, 'filial' => CHECK_NOT_NULL, 'user' => CHECK_EXIST, 'comments' => CHECK_NOT_NULL, 'official_answer' => null, 'additional_data' => [ 'is_liked' => false, 'is_subscribed_to_comments' => false ], 'url' => CHECK_STRING_NOT_EMPTY, 'id' => CHECK_POSITIVE ];

точное значение

точное значение

// Ожидаемый результат $expected = [ 'filial_id' => $filial_id, 'user_id' => $user_id, 'text' => $text, 'date_created' => CHECK_DATETIME_FORMAT, 'date_edited' => null, 'comments_count' => 0, 'likes_score' => 0, 'source' => CHECK_SOURCE, 'filial' => CHECK_NOT_NULL, 'user' => CHECK_EXIST, 'comments' => CHECK_NOT_NULL, 'official_answer' => null, 'additional_data' => [ 'is_liked' => false, 'is_subscribed_to_comments' => false ], 'url' => CHECK_STRING_NOT_EMPTY, 'id' => CHECK_POSITIVE ];

точное значение

тип значения и его диапазон

точное значение

// Ожидаемый результат $expected = [ 'filial_id' => $filial_id, 'user_id' => $user_id, 'text' => $text, 'date_created' => CHECK_DATETIME_FORMAT, 'date_edited' => null, 'comments_count' => 0, 'likes_score' => 0, 'source' => CHECK_SOURCE, 'filial' => CHECK_NOT_NULL, 'user' => CHECK_EXIST, 'comments' => CHECK_NOT_NULL, 'official_answer' => null, 'additional_data' => [ 'is_liked' => false, 'is_subscribed_to_comments' => false ], 'url' => CHECK_STRING_NOT_EMPTY, 'id' => CHECK_POSITIVE ];

точное значение

enum

тип значения и его диапазон

точное значение

// Ожидаемый результат $expected = [ 'filial_id' => $filial_id, 'user_id' => $user_id, 'text' => $text, 'date_created' => CHECK_DATETIME_FORMAT, 'date_edited' => null, 'comments_count' => 0, 'likes_score' => 0, 'source' => CHECK_SOURCE, 'filial' => CHECK_NOT_NULL, 'user' => CHECK_EXIST, 'comments' => CHECK_NOT_NULL, 'official_answer' => null, 'additional_data' => [ 'is_liked' => false, 'is_subscribed_to_comments' => false ], 'url' => CHECK_STRING_NOT_EMPTY, 'id' => CHECK_POSITIVE ];

формат значения

точное значение

enum

тип значения и его диапазон

точное значение

// Ожидаемый результат $expected = [ 'filial_id' => $filial_id, 'user_id' => $user_id, 'text' => $text, 'date_created' => CHECK_DATETIME_FORMAT, 'date_edited' => null, 'comments_count' => 0, 'likes_score' => 0, 'source' => CHECK_SOURCE, 'filial' => CHECK_NOT_NULL, 'user' => CHECK_EXIST, 'comments' => CHECK_NOT_NULL, 'official_answer' => null, 'additional_data' => [ 'is_liked' => false, 'is_subscribed_to_comments' => false ], 'url' => CHECK_STRING_NOT_EMPTY, 'id' => CHECK_POSITIVE ];

формат значения

точное значение

enum

тип значения и его диапазон

наличие атрибута

точное значение

Примеры

public function providerGetBlogAuthors()

{

// запись блога $article = Db()->table('articles')->isPublished(true)->hasAuthor(true)->getRow();

// неопубликованная запись блога $article_not_published = Db()->table('articles')->isPublished(false)->getRow();

return [

// опубликованная [$article, Config()->roles->user, 200],

// неопубликованная под авторизованным юзером [$article_not_published, Config()->roles->user, 404],

// неопубликованная под гостем [$article_not_published, Config()->roles->guest, 404],

];

}

public function testGetBlogAuthors($article, $user_id, $expected_code)

Формируем тестовые наборы

/**

* @dataProvider providerGetBlogAuthors

*/

public function testGetBlogAuthors($article, $user_id, $expected_code)

{

// Задаём http-метод, метод API, параметры запроса $this->http_method = 'GET';

$this->method = "blogs/{$article->id}/authors";

// Выполняем запрос и проверяем коды ответа $this->asUser($user_id)->send();

$this->waitFor($expected_code);

Получаем авторов записи блога

// проверяем поля в ответе if ($expected_code === 200) {

$actual = $this->getResponseBody()->authors;

// авторов может быть много, проверяем первого $expected[0] = [

'name' => CHECK_STRING_NOT_EMPTY,

'user' => CHECK_NOT_NULL,

];

$this->assert($expected, $actual);

}

Получаем авторов записи блога

public function testDeleteReview($entity_id, $cause, $user_id, $expected_code)

{

// Задаём http-метод, метод API, параметры запроса $this->http_method = 'DELETE';

$this->method = "reviews/{$entity_id}";

$this->params = [

'cause' => $cause

];

// Выполняем запрос и проверяем коды ответа $this->asUser($user_id)->send();

$this->waitFor($expected_code);

// Проверка на повторное удаление сущности if ($expected_code === 202) {

$this->asUser($user_id)->send();

$this->waitFor(404);

}

}

Удаляем отзыв

Запуск и отладка

Запуск и отладка

Приёмочное тестирование фичи

10-20 сек

Регрессия

1400 тестов

35 000 проверок

5 минут 1400 тестов

Регрессия

1400 тестов

35 000 проверок

5 минут 1400 тестов

28 860 минутвместо

Процессы

● отдельный репозиторий

● все пушат, мёржат, коммитят

● новая фича покрывается при тестировании,или даже параллельно с разработкой :)

Flamp API

JSON-schema

Flamposcope

2fingers

Луковица качества API

Вопросы?