Тестирование документированного API с помощью утилиты Dredd от Apiary

  • Tutorial

Все мы любим ничего не делать работать с хорошо документированным API. С помощью стандартов API Blueprint или Swagger можно получить читаемую машиной и человеком документацию, а значит и инструменты проверки API на основе этой документации.


Apiary предлагает интерактивные инструменты для проверки API вручную, подставляя нужные параметры в формы, генерируемые на основе документации. Но можно извлечь гораздо больше пользы, если API будет проверяться автоматически. Это избавляет от необходимости писать отдельные тесты на каждый интерфейс, но накладывает определенные ограничения на структуру и качество самой документации.


image


В этом tutorial поговорим о утилите Dredd на примере API от GitHub.


image


Bootstrap


Я предполагаю, что у вас уже локально установлен Vagrant и Git. Я работаю в MacOS, все команды ниже выполнялись в ней.


Подготовим необходимые аккаунты и разрешения.


Зарегистрируйтесь и создайте новый проект в сервисе Apiary.


image


В созданном проекте откройте вкладку Tests > Tutorial. Здесь вам нужно записать значения переменных apiaryApiName и apiaryApiKey, которые пригодятся далее.


image


В настройках GitHub создайте токен для доступа к API со следующими разрешениями (scopes): gist, user:email. Подробнее про доступные разрешения можно почитать в документации к API GitHub.


Не забудьте скопировать токен. Он виден только один раз.


image


Создадим рабочий каталог.


mkdir ~/dredd_test && cd ~/dredd_test && git init


Virtual environment


Подготовим виртуальное окружение, чтобы не засорять свою ОС, а также чтобы задокументировать каждый сделанный шаг.


mkdir vagrant && touch vagrant/Vagrantfile && git add vagrant/


В корневом каталоге создадим файл .gitignore и добавим туда исключения:


echo 'vagrant/.vagrant' >> .gitignore && git add .gitignore


Отредактируем vagrant/Vagrantfile, добавим туда конфигурацию следующего содержания:


vagrant/Vagrantfile
Vagrant.require_version ">= 1.5"

Vagrant.configure("2") do |config|
    config.vm.hostname = "dredd.dev"
    config.vm.provider :virtualbox do |v|
        v.name = "dredd_test"
        v.customize [
            "modifyvm", :id,
            "--name", "dredd_test",
            "--memory", 2048,
            "--natdnshostresolver1", "on",
            "--natdnsproxy1", "on",
            "--cpus", 2,
        ]
    end

    # рекомендую начать с этого образа,
    # т.к. в официальных Ubuntu/Debian возникают проблемы с установкой пакетов npm
    config.vm.box = "centos/7"

    # IP-адрес виртуальной машины
    config.vm.network :private_network, ip: "192.168.99.105"
    config.ssh.forward_agent = true

    # С помощью переменных окружения можно запускать виртуальную машину,
    # указывая ей произвольный путь к каталогу с вашим проектом для монтирования
    if ENV['DREDD_GITHUB_TEST_PATH']
        config.vm.synced_folder "#{ENV['DREDD_GITHUB_TEST_PATH']}", "/var/dredd_test",
            :owner=> 'vagrant',
            :group=> 'vagrant'
    else
        # Путь к каталогу с проектом по-умолчанию и путь для монтирования внутри машины
        config.vm.synced_folder "~/dredd_test", "/var/dredd_test",
            :owner=> 'vagrant',
            :group=> 'vagrant'
    end

    # После разворачивания виртуалки первым внутри будет выполнен этот файл
    config.vm.provision :shell, path: "provision.sh"

    # Устанавливаем переменные окружения для гостевой ОС,
    # чтобы передавать GitHub API токен, и ключи Apiary снаружи
    config.vm.provision "shell" do |s|
        s.binary = true # Replace Windows line endings with Unix line endings.
        s.inline = %Q(sudo echo \
          "GITHUB_API_TOKEN=#{ENV['GITHUB_API_TOKEN']}\nAPIARY_API_KEY=#{ENV['APIARY_API_KEY']}\nAPIARY_API_NAME=#{ENV['APIARY_API_NAME']}" \
          > /etc/environment
          )
    end

    # Последняя команда, которая фактически запустит тестирование проекта сразу после старта VM
    config.vm.provision "shell" do |s|
        s.binary = true # Replace Windows line endings with Unix line endings.
        s.inline = %Q(cd /var/dredd_test && composer install && composer check)
    end
end

Теперь нужно подготовить файл с набором инструкций для подготовки виртуальной машины.


touch vagrant/provision.sh && git add vagrant/provision.sh


Поместим в него текст:


vagrant/provision.sh
#!/usr/bin/env bash

yum -y clean all

# Зарегистрируем репозитории пакетов EPEL (для nodejs etc) и webtatic (для php)
sudo rpm -Uvh https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
sudo rpm -Uvh https://mirror.webtatic.com/yum/el7/webtatic-release.rpm

# Регистрируем репозитории для npm
sudo curl --silent --location https://rpm.nodesource.com/setup_6.x | bash -

yum -y update

# Устанавливаем системные пакеты,
# менеджер зависимостей PHP composer,
# фреймворк тестирования API Dredd
# и фреймворк веб-приложений Express JS для прототипирования API
sudo yum -y install curl git nodejs npm php70w-cli --skip-broken \
&& curl -sS https://getcomposer.org/installer | sudo php -- --install-dir=/usr/bin --filename=composer \
&& yum -y install gcc-c++ make \
&& sudo npm cache clean \
&& sudo npm install dredd -g \
&& sudo npm install express -g \
&& echo "Done"

Как можно заметить, кроме желаемого Dredd, также будет установлен менеджер зависимостей PHP Composer. PHP нам пригодится в качестве примера для написания хуков при тестировании API. Вы можете воспользоваться и другими языками для этого, например тем же NodeJs, чтобы не плодить "зоопарк" технологий. Поскольку мой основной язык PHP, я покажу ниже как интегрировать dredd в Composer, чтобы не запоминать команды вызова.


Итак, у нас готово виртуальное окружение.


Blueprint for GitHub API


Настал черёд подготовить документацию к API. В качестве подопытной зверушки использовались следующие интерфейсы:



Dredd поддерживает два вида документации: API Blueprint и Swagger. Воспользуемся первым из них, так как этот стандарт базируется на Markdown-разметке и вы можете читать документацию к API в любимом git-сервисе: Gitlab, Github, Bitbucket etc. Кроме того, поддержка Swagger заявлена пока в beta-режиме.


В корне проекта создадим файл touch github-api.md && git add github-api.md и поместим в него документацию.
Описывать в деталях каждую строчку я не стану, но сделаю некоторые пояснения к структуре данного ниже листинга.


github-api.md
FORMAT: 1A
HOST: https://api.github.com/

# Github API test

Just a simple test of GitHub API

## Users Collection [/users]

### OAuth Non-Web authentication flow: load user data [GET /users/technoweenie]

+ Request JSON (application/json; charset=utf-8)

    + Headers

            Authorization: token 12345

+ Response 200 (application/json; charset=utf-8)

    + Headers

            Server: GitHub.com
            Date: Fri, 28 Jan 2017 02:30:40 GMT
            Content-Length: 1248
            Status: 200 OK
            X-RateLimit-Limit: 5000
            X-RateLimit-Remaining: 4992
            X-RateLimit-Reset: 1485553884
            Cache-Control: private, max-age=60, s-maxage=60
            Vary: Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding
            ETag: "123456701aaf50ed0c83ad4123456789"
            Last-Modified: Fri, 02 Dec 2016 07:32:00 GMT
            X-OAuth-Scopes: gist, user:email
            X-GitHub-Media-Type: github.v3; format=json
            Access-Control-Expose-Headers: ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval
            Access-Control-Allow-Origin: *
            Content-Security-Policy: default-src 'none'
            Strict-Transport-Security: max-age=31536000; includeSubdomains; preload
            X-Content-Type-Options: nosniff
            X-Frame-Options: deny
            X-XSS-Protection: 1; mode=block
            X-Served-By: q1234567cned3f5c4u15a34csddc1234
            X-GitHub-Request-Id: ABCD:EFGH:A5DA75D:2EF637A:12345678
            Connection: close

    + Body

            {
              "login": "technoweenie",
              "id": 21,
              "avatar_url": "https://avatars.githubusercontent.com/u/21?v=3",
              "gravatar_id": "",
              "url": "https://api.github.com/users/technoweenie",
              "html_url": "https://github.com/technoweenie",
              "followers_url": "https://api.github.com/users/technoweenie/followers",
              "following_url": "https://api.github.com/users/technoweenie/following{/other_user}",
              "gists_url": "https://api.github.com/users/technoweenie/gists{/gist_id}",
              "starred_url": "https://api.github.com/users/technoweenie/starred{/owner}{/repo}",
              "subscriptions_url": "https://api.github.com/users/technoweenie/subscriptions",
              "organizations_url": "https://api.github.com/users/technoweenie/orgs",
              "repos_url": "https://api.github.com/users/technoweenie/repos",
              "events_url": "https://api.github.com/users/technoweenie/events{/privacy}",
              "received_events_url": "https://api.github.com/users/technoweenie/received_events",
              "type": "User",
              "site_admin": true,
              "name": "risk danger olson",
              "company": "GitHub",
              "blog": "http://techno-weenie.net",
              "location": "Louisville, CO",
              "email": "technoweenie@gmail.com",
              "hireable": null,
              "bio": ":metal:",
              "public_repos": 164,
              "public_gists": 105,
              "followers": 2328,
              "following": 17,
              "created_at": "2008-01-14T04:33:35Z",
              "updated_at": "2017-01-24T10:45:13Z"
            }

    + Schema

            {
                "$schema": "http://json-schema.org/draft-04/schema#",
                "type": "object",
                "properties": {
                    "login": {
                        "type": "string"
                    },
                    "id": {
                        "type": "number"
                    },
                    "avatar_url": {
                        "type": ["string", "null"]
                    },
                    "gravatar_id": {
                        "type": ["string", "null"]
                    },
                    "url": {
                        "type": ["string", "null"]
                    },
                    "html_url": {
                        "type": ["string", "null"]
                    },
                    "followers_url": {
                        "type": ["string", "null"]
                    },
                    "following_url": {
                        "type": ["string", "null"]
                    },
                    "gists_url": {
                        "type": ["string", "null"]
                    },
                    "starred_url": {
                        "type": ["string", "null"]
                    },
                    "subscriptions_url": {
                        "type": ["string", "null"]
                    },
                    "organizations_url": {
                        "type": ["string", "null"]
                    },
                    "repos_url": {
                        "type": ["string", "null"]
                    },
                    "events_url": {
                        "type": ["string", "null"]
                    },
                    "received_events_url": {
                        "type": ["string", "null"]
                    },
                    "type": {
                        "type": ["string", "null"]
                    },
                    "site_admin": {
                        "type": "boolean"
                    },
                    "name": {
                        "type": ["string", "null"]
                    },
                    "company": {
                        "type": ["string", "null"]
                    },
                    "blog": {
                        "type": ["string", "null"]
                    },
                    "location": {
                        "type": ["string", "null"]
                    },
                    "email": {
                        "type": ["string", "null"]
                    },
                    "hireable": {
                        "type": ["boolean", "null"]
                    },
                    "bio": {
                        "type": ["string", "null"]
                    },
                    "public_repos": {
                        "type": ["number", "null"]
                    },
                    "public_gists": {
                        "type": ["number", "null"]
                    },
                    "followers": {
                        "type": ["number", "null"]
                    },
                    "following": {
                        "type": ["number", "null"]
                    },
                    "created_at": {
                        "type": ["string", "null"]
                    },
                    "updated_at": {
                        "type": ["string", "null"]
                    }
                }
            }

## Gists Collection [/gists]

### Load all Gists [GET /gists]

+ Request JSON (application/json; charset=utf-8)

+ Response 200 (application/json; charset=utf-8)
    + Attributes (array[Gist], optional)

### Creating new Gist [POST]

+ Request JSON (application/json; charset=utf-8)

    + Headers

            Accept: application/json
            Authorization: token 12345

    + Body

            {
                "description": "This is a simple test file for gist API check",
                "public": false,
                "files": {
                    "file1.md": {
                        "content": "# Hello, world\n- This is just a GitHub API testing result"
                    }
                }
            }

    + Schema

            {
                "$schema": "http://json-schema.org/draft-04/schema#",
                "type": "object",
                "properties": {
                    "files": {
                        "type": "object"
                    },
                    "description": {
                        "type": ["string"]
                    },
                    "public": {
                        "type": ["boolean"]
                    }
                },
                "required": [ "files" ]
            }

+ Response 201 (application/json; charset=utf-8)
    + Attributes (Gist)

### Load a Gist [GET /gists/{id}]

+ Request JSON (application/json; charset=utf-8)

+ Parameters
    + id: a123456bcd (string) - The ID of the Gist.

+ Response 200 (application/json; charset=utf-8)
    + Attributes (Gist)

### Delete a Gist [DELETE /gists/{id}]

+ Parameters
    + id: a123456bcd (string) - The ID of the Gist.

+ Response 204

# Data Structures

## Gist

This is a demo documentation example. Not all fields was included.

+ id: a123456bcd (string, required)
+ url: https://api.github.com/gists/{id} (string)
+ forks_url: https://api.github.com/gists/{id}/forks (string)
+ commits_url: https://api.github.com/gists/{id}/commits (string)
+ git_pull_url: https://gist.github.com/{id}.git (string)
+ public: false (boolean)

Первые две строчки нужны для парсеров, которые поймут версию стандарта и на какой хост по-умолчанию обращаться, чтобы вы могли проверять API в интерактивной форме.


Заголовки 1-го, 2-го,… уровней могут использоваться вариативно. Для анализаторов важно то, что вы укажете в квадратных скобках, например [GET /users/technoweenie]. Этот текст будет использован для составления интерактивных форм, например в Apiary:


image


После такой конструкции анализатор разметки будет ожидать детали ожидаемого запроса и ответа на данном интерфейсе. Для этого используем конструкции + Request ПроизвольноеНазвание (ожидаемый формат запроса) и + Response http-status-code (ожидаемый формат ответа)


Далее следуют вложенные уровни:


  • Headers — Примеры заголовков и их значений, которые могут приниматься или возвращаться.
  • Body — Пример тела запроса или ответа прежде всего для людей, которые будут читать документацию. Здесь вы можете приводить полный copy-n-paste из реального API, убирая лишь чувствительные данные. Для автоматизированного тестирования правильнее использовать Schema или Attributes.
  • Schema — Этот раздел уже важен для автоматического анализа и для автотестирования, но также может пригодиться для разработчиков. И Blueprint, и Swagger используют стандарт json-schema.org для описания типов данных.
    image
  • Parameters — Это альтернативный, упрощенный способ описания полей, если вам хочется просто быстро набросать простые примеры.
  • Attributes — На этом уровне Blueprint позволяет сослаться на уже составленную ранее общую структуру. В нашем примере это структура Gist и она описана в конце документа в виде вложенного уровня к заголовку Data Structures.

Таким образом, в приведённом примере показаны два различных способах описания структур данных в теле запросов и ответов: «навороченная» Schema и упрощенные Parameters и Attributes.


После того, как мы сохранили документ, его можно проверить в Apiary и Github на предмет читаемости и адекватности использования в ручном режиме.


Подготовка проекта composer


Этот шаг необязателен, но здесь он присутствует для более комплексного подхода, когда тестирование является неотъемлемой частью самого проекта.


В файл исключений git добавим новую строчку: echo 'vendor/' >> .gitignore


Создадим файл composer.json, а также папку для Dredd hooks с файлом будущих хуков внутри.


mkdir hooks && touch composer.json composer.lock hooks/github-api.php && echo '{}' > composer.lock && git add composer.* hooks/github-api.php


В файл composer.json поместим описание проекта:


composer.json
{
    "name": "kivagant/github-api-test",
    "type": "project",
    "license": "MIT",
    "description": "Github API test example with dredd and Apiary",
    "require": {
    },
    "require-dev": {
        "ddelnano/dredd-hooks-php": "^1.1"
    },
    "scripts": {
        "check": [
            "@test-mock",
            "@test"
        ],
        "test": "dredd --header=\"Authorization: token ${GITHUB_API_TOKEN}\" --config=./dredd-local.yml",
        "test-mock": "dredd --header=\"Authorization: token ${GITHUB_API_TOKEN}\" --config=./dredd.yml"
    }
}

Здесь стоит обратить внимание на следующие моменты:


  • В зависимостях уровня dev (посколько в production мы врядли будем запускать эти тесты) добавлен пакет ddelnano/dredd-hooks-php. Это часть проекта Dredd позволит нам перехватывать различные этапы выполнения тестов и манипулировать данными, чтобы, к примеру, передавать некий контекст выполнения между разными тестами (данные, полученные в одном тесте передавать в другой тест).
  • В разделе scripts добавлены две операции: test и test-mock, обе из которых будут выполнены при вызове команды composer check
  • Ниже приведены команды консольного запуска. Таким образом мы получаем упрощенный вызов этих обеих команд, заворачивая их в composer check, в котором у нас также могут вызываться юнит-тесты, проверка качества кода и так далее.
  • Команды консольного запуска будут вызывать утилиту dredd и передавать ей заголовок, который будет добавлен во все API. В него будет подставляться значение переменной окружения GITHUB_API_TOKEN, благодаря чему нам не нужно коммитить её значение в сам код, а хранить её где-нибудь в защищенном месте.
  • Два отдельных вызова dredd используют разные конфигурационные файлы. Правильнее было бы просто подменять некоторые параметры из этих файлов, но мир такой не идеальный. Одна из конфигураций обратиться напрямую на github. Вторая будет вызывать заглушку (мы напишем её ниже), которая например может являться вашим прототипом API ещё до того, как backend developers его имплементировали на практике. И вы сможете играть в TDD.

Конфигурируем dredd


Теперь создадим два файла конфигурации — для production и для макета API.


touch dredd.yml dredd-local.yml && git add dredd.yml dredd-local.yml


В первый файл dredd.yml запишем следующие параметры, назначение которых можно почитать в документаци:


dredd.yml
reporter: apiary
#custom:
#  apiaryApiKey: 12345 # better to use console argument
#  apiaryApiName: abcdefg
#header: ["Authorization: token 12345"] # better to use console argument
dry-run: null
hookfiles: hooks/*.php # каталог с hooks
language: vendor/bin/dredd-hooks-php # бинарник будет добавлен composer-ом
sandbox: false
server-wait: 3
init: false
names: false
only: []
output: []
sorted: false
user: null
inline-errors: false
details: false
method: []
color: true
level: info
timestamp: false
silent: false
path: []
hooks-worker-timeout: 5000
hooks-worker-connect-timeout: 1500
hooks-worker-connect-retry: 500
hooks-worker-after-connect-wait: 100
hooks-worker-term-timeout: 5000
hooks-worker-term-retry: 500
hooks-worker-handler-host: localhost
hooks-worker-handler-port: 61321
blueprint: github-api.md # путь к API Blueprint документации
endpoint: 'https://api.github.com' # тестируемый хост

В файл dredd-local.yml нужно поместить тот же самый текст, но в конце заменить параметр endpoint и добавить дополнительный параметр server, вот так:


dredd-local.yml
#... всё из dredd.yml, кроме endpoint
endpoint: 'http://localhost:3000' # тестируемый хост
server: node app.js # команда, с помощью которой будет запущен локальный "сервер" с прототипом API

В таком варианте Dredd изучит содержимое файла github-api.md и запустит все интерфейсы один за другим. Но этого недостаточно. В документации мы выполняем следующие операции:


  • GET для Users Collection
  • GET для списка Gists
  • POST, GET и DELETE для отдельного Gist.

Для первых двух случаев все хорошо "из коробки", однако для получения и удаления конкретного Gist нужно передавать идентификатор созданного ресурса в следующие запросы. Для этого нам пригодятся Dredd hooks.


Пишем Dredd hooks


Откроем файл hooks/github-api.php и добавим туда следующий код:


hooks/github-api.php
<?php

// Использовать это пространство имён
use Dredd\Hooks;

// Этот идентификатор используется в спецификациях в github-api.md
// Мы будем заменять его на лету на идентификатор,
// полученный после выполнения запроса на создание нового ресурса
const DEFAULT_GIST_ID = "a123456bcd";

// Это контекст теста со структурой данных,
// которые мы будем передавать между разными запросами 
$scope = [
    'lastGistId' => ''
];

// В этом хуке мы вы перехватываем результат создания нового Gist ПОСЛЕ вызова
Hooks::after("Gists Collection > Creating new Gist", function (&$transaction) use (&$scope) {
    $scope['lastGistId'] = ''; // обнулим возможное значение из других тестов, выполненных ранее
    if (!isset($transaction->real->body)) { // если нет тела, считаем что тест не пройден
        $transaction->fail = true;

        return;
    }
    $body = json_decode($transaction->real->body, true); // раскодируем JSON в ассоциативный массив
    if (!isset($body['id'])) { // если не существует идентификатор созданного Gist, тест не пройден
        $transaction->fail = true;

        return;
    }
    $scope['lastGistId'] = $body['id']; // сохраним идентификатор в контекст для следующих тестов
});

// В этом хуке, ПЕРЕД вызовом API чтения, в нескольких местах подменим данные
// идентификатора Gist из документации на полученный при создании нового
Hooks::before("Gists Collection > Load a Gist", function (&$transaction) use (&$scope) {
    $transaction->expected->body = str_replace(DEFAULT_GIST_ID, $scope['lastGistId'], $transaction->expected->body);
    $transaction->id = str_replace(DEFAULT_GIST_ID, $scope['lastGistId'], $transaction->id);
    $transaction->request->uri = str_replace(DEFAULT_GIST_ID, $scope['lastGistId'], $transaction->request->uri);
    $transaction->fullPath = str_replace(DEFAULT_GIST_ID, $scope['lastGistId'], $transaction->fullPath);
});

// В этом хуке, ПЕРЕД вызовом API удаления, в нескольких местах подменим данные
// идентификатора Gist из документации на полученный при создании нового
Hooks::before("Gists Collection > Delete a Gist", function (&$transaction) use (&$scope) {
    $transaction->expected->body = str_replace(DEFAULT_GIST_ID, $scope['lastGistId'], $transaction->expected->body);
    $transaction->id = str_replace(DEFAULT_GIST_ID, $scope['lastGistId'], $transaction->id);
    $transaction->request->uri = str_replace(DEFAULT_GIST_ID, $scope['lastGistId'], $transaction->request->uri);
    $transaction->fullPath = str_replace(DEFAULT_GIST_ID, $scope['lastGistId'], $transaction->fullPath);
});

Теперь у нас готово практически всё, но для полноты картины добавим ещё один шаг.


Подготовка прототипа API с помощью Express JS


В файл исключений git добавим новую строчку: echo 'node_modules/' >> .gitignore


Создадим файл будущего «сервера API»: touch app.js && git add app.js


В этот файл добавим следующее содержание, которое будет просто симулировать поведение github. Я не буду останавливаться на деталях.


app.js
var app = require('express')();

app.get('/', function(req, res) {
  res.json({message: 'Hello World!'});
});

app.get('/users/technoweenie', function(req, res) {
  res.set(
      {
            "Server": "GitHub-Mock.local",
            "Date": "Fri, 28 Jan 2017 02:31:55 GMT",
            "Content-Type": "application/json; charset=utf-8",
            "Content-Length": 1248,
            "Status": "200 OK",
            "X-RateLimit-Limit": 5000,
            "X-RateLimit-Remaining": 4992,
            "X-RateLimit-Reset": 1485553884,
            "Cache-Control": "private, max-age=60, s-maxage=60",
            "Vary": "Accept, Authorization, Cookie, X-GitHub-OTP, Accept-Encoding",
            "ETag": "\"123456701aaf50ed0c83ad4123456789\"",
            "Last-Modified": "Fri, 02 Dec 2016 07:32:00 GMT",
            "X-OAuth-Scopes": "gist, user:email",
            "X-Accepted-OAuth-Scopes": "",
            "X-GitHub-Media-Type": "github.v3; format=json",
            "Access-Control-Expose-Headers": "ETag, Link, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval",
            "Access-Control-Allow-Origin": "*",
            "Content-Security-Policy": "default-src 'none'",
            "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
            "X-Content-Type-Options": "nosniff",
            "X-Frame-Options": "deny",
            "X-XSS-Protection": "1; mode=block",
            "X-Served-By": "q1234567cned3f5c4u15a34csddc1234",
            "X-GitHub-Request-Id": "ABCD:EFGH:25DA75D:2EF637A:12345679"
      }
  ).json(
      {
          "login": "technoweenie",
          "id": 21,
          "avatar_url": "https://avatars.githubusercontent.com/u/21?v=3",
          "gravatar_id": "",
          "url": "https://api.github.com/users/technoweenie",
          "html_url": "https://github.com/technoweenie",
          "followers_url": "https://api.github.com/users/technoweenie/followers",
          "following_url": "https://api.github.com/users/technoweenie/following{/other_user}",
          "gists_url": "https://api.github.com/users/technoweenie/gists{/gist_id}",
          "starred_url": "https://api.github.com/users/technoweenie/starred{/owner}{/repo}",
          "subscriptions_url": "https://api.github.com/users/technoweenie/subscriptions",
          "organizations_url": "https://api.github.com/users/technoweenie/orgs",
          "repos_url": "https://api.github.com/users/technoweenie/repos",
          "events_url": "https://api.github.com/users/technoweenie/events{/privacy}",
          "received_events_url": "https://api.github.com/users/technoweenie/received_events",
          "type": "User",
          "site_admin": true,
          "name": "risk danger olson",
          "company": "GitHub",
          "blog": "http://techno-weenie.net",
          "location": "Louisville, CO",
          "email": "technoweenie@gmail.com",
          "hireable": null,
          "bio": ":metal:",
          "public_repos": 164,
          "public_gists": 105,
          "followers": 2328,
          "following": 17,
          "created_at": "2008-01-14T04:33:35Z",
          "updated_at": "2017-01-24T10:45:13Z"
      }
  );
});

app.get('/gists', function(req, res) {
    res.set(
        {
            "Server": "GitHub-Mock.local",
            "Date": "Fri, 28 Jan 2017 02:31:55 GMT",
            "Content-Type": "application/json; charset=utf-8"
        }
    ).json(
        [
            {
                "url": "https://api.github.com/gists/12345678c62ca49gf313ahd156781234",
                "forks_url": "https://api.github.com/gists/12345678c62ca49gf313ahd156781234/forks",
                "commits_url": "https://api.github.com/gists/12345678c62ca49gf313ahd156781234/commits",
                "id": "12345678c62ca49gf313ahd156781234",
                "git_pull_url": "https://gist.github.com/12345678c62ca49gf313ahd156781234.git",
                "git_push_url": "https://gist.github.com/12345678c62ca49gf313ahd156781234.git",
                "html_url": "https://gist.github.com/12345678c62ca49gf313ahd156781234",
                "files": {
                    "file.html": {
                        "filename": "file.html",
                        "type": "text/html",
                        "language": "HTML",
                        "raw_url": "https://gist.githubusercontent.com/anonymous/12345678c62ca49gf313ahd156781234/raw/1234ac2e64b06898787920a14f85511be/file.html",
                        "size": 45934
                    }
                },
                "public": true,
                "created_at": "2017-01-28T01:29:29Z",
                "updated_at": "2017-01-28T01:29:29Z",
                "description": "just a test",
                "comments": 0,
                "user": null,
                "comments_url": "https://api.github.com/gists/12345678c62ca49gf313ahd156781234/comments",
                "truncated": false
            }
        ]
    );
});

app.get('/gists/:id', function(req, res) {
    res.set(
        {
            "Server": "GitHub-Mock.local",
            "Date": "Fri, 28 Jan 2017 02:31:55 GMT",
            "Content-Type": "application/json; charset=utf-8"
        }
    ).json(
        {
            "url": "https://api.github.com/gists/" + req.params.id,
            "forks_url": "https://api.github.com/gists/" + req.params.id + "/forks",
            "commits_url": "https://api.github.com/gists/" + req.params.id + "/commits",
            "id": req.params.id,
            "git_pull_url": "https://gist.github.com/" + req.params.id + ".git",
            "git_push_url": "https://gist.github.com/" + req.params.id + ".git",
            "html_url": "https://gist.github.com/" + req.params.id,
            "files": {
                "file.html": {
                    "filename": "file.html",
                    "type": "text/html",
                    "language": "HTML",
                    "raw_url": "https://gist.githubusercontent.com/anonymous/" + req.params.id + "/raw/12345678910abc/file.html",
                    "size": 45934
                }
            },
            "public": true,
            "created_at": "2017-01-28T01:29:29Z",
            "updated_at": "2017-01-28T01:29:29Z",
            "description": "just a test",
            "comments": 0,
            "user": null,
            "comments_url": "https://api.github.com/gists/" + req.params.id + "/comments",
            "truncated": false
        }
    );
});

app.post('/gists', function(req, res) {
    res.status(201).set(
        {
            "Server": "GitHub-Mock.local",
            "Date": "Fri, 28 Jan 2017 02:31:55 GMT",
            "Content-Type": "application/json; charset=utf-8"
        }
    ).json(
        {
            "url": "https://api.github.com/gists/d7b4325ac95ca49ef310aed14dfa8b15",
            "forks_url": "https://api.github.com/gists/d7b4325ac95ca49ef310aed14dfa8b15/forks",
            "commits_url": "https://api.github.com/gists/d7b4325ac95ca49ef310aed14dfa8b15/commits",
            "id": "d7b4325ac95ca49ef310aed14dfa8b15",
            "git_pull_url": "https://gist.github.com/d7b4325ac95ca49ef310aed14dfa8b15.git",
            "git_push_url": "https://gist.github.com/d7b4325ac95ca49ef310aed14dfa8b15.git",
            "html_url": "https://gist.github.com/d7b4325ac95ca49ef310aed14dfa8b15",
            "files": {},
            "public": false
        }
    );
});
app.delete('/gists/:id', function(req, res) {
    res.status(204).set(
        {
            "Server": "GitHub-Mock.local",
            "Date": "Fri, 28 Jan 2017 02:31:55 GMT"
        }
    ).end();
});

app.listen(3000);

Также в корень добавим файл с описанием проекта nodejs, чтобы можно было вызывать установку пакетов отдельно от виртуального окружения:


touch package.json && git add package.json


Добавим достаточно очевидное содержимое:


package.js
{
  "name": "dredd_test",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "MIT",
  "private": true,
  "description": "",
  "dependencies": {
    "dredd ": "^2.2.5",
    "express": "^4.14.0"
  }
}

(Впрочем, стоит упомянуть, что без флага "private": true, npm начнёт проверять package.js на предмет соответствия требованиям и упадёт с великолепной ошибкой npm ERR! code 1. Хорошо, хоть перед ней следует WARN, который оказывается фатальной ошибкой.)


Запуск тестов


cd vagrant/
vagrant box update
DREDD_GITHUB_TEST_PATH=~/dredd_test GITHUB_API_TOKEN=<your-token> APIARY_API_KEY=<your-key> APIARY_API_NAME=<your-project-name> vagrant up

После выполнения вы должны увидеть в конце примерно такой «выхлоп»:


Результат запуска
info: Configuration './dredd.yml' found, ignoring other arguments.
info: Beginning Dredd testing...
info: Found Hookfiles: 0=hooks/github-api.php
info: Spawning `vendor/bin/dredd-hooks-php` hooks handler process.
info: Hooks handler stdout: Starting server

info: Successfully connected to hooks handler. Waiting 0.1s to start testing.
pass: GET /users/technoweenie duration: NaNms
pass: GET /gists duration: NaNms
pass: POST /gists duration: NaNms
pass: GET /gists/1f892b1c9fef3f3fc09fc66c11ad2027 duration: NaNms
pass: DELETE /gists/1f892b1c9fef3f3fc09fc66c11ad2027 duration: NaNms
info: Sending SIGTERM to hooks handler process.
complete: 5 passing, 0 failing, 0 errors, 0 skipped, 5 total
complete: Tests took 7286ms
complete: See results in Apiary at: https://app.apiary.io/githubtest5/tests/run/bcb8184a-2365-4459-b898-20e9bea8a389
> dredd --header="Authorization: token ${GITHUB_API_TOKEN}" --config=./dredd-local.yml
info: Configuration './dredd-local.yml' found, ignoring other arguments.
info: Starting backend server process with command: node app.js
info: Waiting 3 seconds for backend server process to start.
info: Beginning Dredd testing...
info: Found Hookfiles: 0=hooks/github-api.php
info: Spawning `vendor/bin/dredd-hooks-php` hooks handler process.
info: Hooks handler stdout: Starting server

info: Successfully connected to hooks handler. Waiting 0.1s to start testing.
pass: GET /users/technoweenie duration: NaNms
pass: GET /gists duration: NaNms
pass: POST /gists duration: NaNms
pass: GET /gists/d7b4325ac95ca49ef310aed14dfa8b15 duration: NaNms
pass: DELETE /gists/d7b4325ac95ca49ef310aed14dfa8b15 duration: NaNms
info: Sending SIGTERM to hooks handler process.
complete: 5 passing, 0 failing, 0 errors, 0 skipped, 5 total
complete: Tests took 2779ms
complete: See results in Apiary at: https://app.apiary.io/githubtest5/tests/run/625c2477-7384-48fa-8bb9-25e27af2a908
info: Sending SIGTERM to backend server process.
info: Backend server process was killed.

Как можно видеть выше, успешно завершились два вида тестов: сначала на локальном прототипе, затем на полноценном production. При этом на gist.github.com был создан, загружен и удалён один документ.


Теперь вы можете заходить по ссылкам из каждого результата тестирования и смотреть его в удобном GUI.


https://app.apiary.io/githubtest5/tests/run/625c2477-7384-48fa-8bb9-25e27af2a908
image


А что если нет?

Если вместо этого вы видите какие-то ошибки, возможно provision.sh не смог установить какие-то пакеты (чаще всего бывают проблемы с npm, ему иногда не хватает памяти). Зайдите в vagrant ssh, перейдите под sudo и проверьте, доступны ли команды dredd и express. Если нет — повторите их установку вручную.
Альтернативно, можно попробовать перезапустить provision-скрипт вот так (provision вместо up в конце):
DREDD_GITHUB_TEST_PATH=~/dredd_test GITHUB_API_TOKEN=<your-token> APIARY_API_KEY=<your-key> APIARY_API_NAME=<your-project-name> vagrant provision


После первого запуска можно заходить внутрь vagrant ssh, затем переходить к проекту и тестировать его cd /var/dredd_test && composer check.


Осталось закоммитить результат и переработать под собственные цели.


git add . && git commit -m 'Dredd testing project initial state'


Правки, дополнения и замечания приветствуются.
Вы можете найти весь исходный код в репозитории.

Онлайн школа EnglishDom
Лидер в онлайн образовании

Похожие публикации

Комментарии 3

    0
    А этот apiary можно использовать self-hosted, бесплатно на своем собственном серевре в интранете?
      0
      Насколько мне известно, это облачное решение. Но утилиту вы можете использовать локально, выгрузка на apiary лишь даёт приятный визуальный интерфейс для просмотра результатов теста от dredd. Для Blueprint или Swagger есть множество бесплатных утилит, думаю можно собрать себе стек по душе.
        0
        Apiary — это онлайн сервис для стандартов документации API Blueprint и OpenAPI (Swagger). А Dredd — это инструмент тестирования нацеленный на API Blueprint, а позже еще и на Swagger, что говорит о том, что можно писать тестируемую документацию локально и спокойно локально тестировать.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

      Самое читаемое