yaml + ejs + faker + ava + суперагент

Чтобы сдержать обещание клиентам, что наш веб-сервер никогда не сломается, мы щедро пишем все больше и больше кода, чтобы покрыть каждый уголок остальных API. Он становится даже больше, чем код самого веб-сервера. Эти жесткие коды наконец превратились в кошмар.

Я пытаюсь переписать свое старое тестирование API так, чтобы оно было удобнее в обслуживании. Я трачу большую часть времени на следование идеям, а не на жестком коде.

  • Быть декларативным
  • Принимает глобальные переменные
  • Принимает сгенерированные поддельные данные
  • Вложенные тесты
  • Парсер (с именем chef) тестовых файлов
  • Умный парсер

Быть декларативным

Декларативное тестирование является ключом к этому решению, оно значительно улучшает сопровождение тестирования. Этот файл объявляет HTTP и Утверждения.

{
  "name": "allow anonym to query user"
  
  "method": "GET"
  "api": "/user"
  
  "headers": {
    "accept": "application/json"
  },
  
  "query": {
    "id": 1
  },
  
  "assert": {
    "status": 200,
    "body": {
      "id": 1
    }
  }
  ...
}

Подождите, этот JSON содержит слишком много кавычек « и фигурных скобок {, что ухудшает читаемость. YAML кажется лучшим решением.

name: allow anonym to query user
method: GET
api: /user
headers: 
  accept: application/json
query: 
  id: 1
assert: 
  status: 200
  body: 
    id: 1
...

Кстати, YAML поддерживает привязку для уменьшения избыточного кода. Вы можете определить конфигурации сверху, которые можно будет повторно использовать позже.

user_id: &id 100
name: allow anonym to query user
method: GET
api: /user
headers: 
  accept: application/json
query: 
  id: *id
assert: 
  status: 200
  body: 
    id: *id
...

Принимает глобальные переменные

Я поместил все тесты, относящиеся к одному API, в один тестовый файл и получил:

api/users.yml
api/me.yml
api/wallets.yml
...

Но некоторые файлы YML имеют одинаковые конфигурации, такие как Авторизация, userId. Должен быть один способ вставки глобальных переменных в файлы тестирования. Эти тестовые файлы должны служить шаблонами, с помощью которых шаблонизатор может обрабатывать. Под рукой есть множество мощных шаблонизаторов, таких как EJS, Pug. Я взял EJS.

#api/me.yml
user_id: &id <%-logined_user_id%>
name: allow logined user to query me
method: GET
api: /me
headers: 
  accept: application/json
  authorization: <%-logined_user_authorization%>  
assert: 
  status: 200
  body: 
    id: *id
...

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

ejs.render(text, {
  logined_user_id: 10,
  logined_user_authorization: "Bearer ..."
})

Принимает сгенерированные поддельные данные

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

#api/comment.yml
user_id: &id <%-logined_user_id%>
name: allow logined user to post comment
method: POST
api: /comments
headers: 
  accept: application/json
  authorization: <%-logined_user_authorization%>  
body:
  text: <%-comment_text%>
assert: 
  status: 200
...

Faker генерирует случайный контент.

ejs.render(text, {
  logined_user_id: 10,
  logined_user_authorization: "Bearer ...",
  comment_text: faker.random.words()
})

Вложенные тесты

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

- name: test1
  method: PATCH
  api: /me
  ..
  after: 
    - name: test2
      method: GET
      api: /me

Парсер (с именем chef) тестовых файлов

После написания наших файлов декларативного тестирования мне нужен синтаксический анализатор, чтобы подготовить это тестирование к любым библиотекам assert, таким как mocha, chai или ava, и отправить HTTP-запрос с любыми библиотеками, такими как superagent, "принести". Здесь я выбираю ava и superagent, и что нужно сделать повару:

  • Прочитать все файлы YML
  • Рендеринг файлов YML как шаблонов с глобальными переменными
  • Плоские вложенные тесты в массив, ожидающий запуска
  • Запустить тест с ava
  • отправить HTTP-запрос
  • Утверждать

Первая версия этого парсера выглядит так:

ava(name, async t => {
  let req = superagent(test.method, host + test.api);
  if (test.headers['Content-Type']) {
    req = req.set('Content-Type', test.headers['Content-Type'])
  }
  if (test.headers['Authorization']) {
    req = req.set('Authorization', test.headers['Authorization'])
  }
  if (test.query) {
    req = req.query(test.query)
  }
  if (test.body) {
    req = req.send(test.body)
  }
  
  ... other http configuration
  
  const res = await req
  t.is(res.status, test.assert.status)
  if (reqeust.body.id) {
    t.is(res.body.id, reqeust.body.id)  
  }
  
  ... other ava asserts
})

Умный парсер

Есть много жесткого кода для конфигурации HTTP и утверждений. Кажется, что обслуживание не улучшается. Мне нужно оптимизировать декларативное тестирование, чтобы оно было адаптивным к суперагенту и ava для уменьшения этого грязного кода.

#api/user.yml
...
superagent: 
  set:
    accept: application/json
  query: 
    limit: 10
ava: 
  is:
    status: 200
    body.0.id: 1
...

Идея очень проста: просто поместите контекст, функцию и параметры в файл YML.

# yml
context:
  function:
    parameter

А парсер YML переводит их в вызов функции Javascript.

context.function(parameter)

Если эти функции нужно запустить по порядку. Вы можете немного изменить YML следующим образом:

# yml
context:
  - function1:
    parameter:
  - function2:
    parameter

Суперагент имеет несколько функций, таких как set, query, send и т. Д., А ava имеет is , true, not и т. д. Независимо от того, что они имеют, файл настроек YML использует эти имена функций как ключи и параметры как значения. И парсер становится меньше, но умнее.

ava(name, async t => {
  let req = superagent(test.method, host + test.api);
  
  //superagent
  Object.keys(test.superagent).forEach(item => {
    req = req[item](test.superagent[item]);
  });
  
  const res = await req
  //ava assert
  Object.keys(test.ava).forEach(func => {
    const asserts = test.ava[func] || {};
    
    Object.keys(asserts).forEach(keypath => {
      try {
        t[func](valueInPath(res, keypath), asserts[keypath]);
      } catch (e) {
        t.fail();
      }
    });
  });
})

Теперь шеф-повар очень весело громит тестирование API. Что вы рассказываете о тестировании API?