Testing

“В теории после каждого внесения исправлений необходимо запустить весь банк тестовых случаев, которые ранее запускались в системе, чтобы убедиться, что она каким-то непонятным образом не оказалась повреждена” — Фред Брукс, “Мифический человеко-месяц”

Не так давно один менеджер попросил меня подсказать ему надежный ресурс, где можно было бы найти четкое описание разницы между интеграционными и модульными тестами. Я ответил, что это очевидно, и он может найти соответствующее описание в Википедии или на Stack Overflow. 

Спустя некоторое время этот менеджер прислал мне сообщение с кратким описанием разницы между тестами, чтобы я мог перепроверить и одобрить его. Я заинтересовался, почему это вдруг необходимо. Оказалось, что один из разработчиков программного обеспечения утверждал, будто бы модульные тесты — это самое основное, и в интеграционных тестах нет никакой нужды. И времени читать материалы на эту тему у него тоже не было. Так что менеджер решил составить лаконичное описание, которое помогло бы разработчику изменить мнение. Я предложил отправить ему картинку, где была бы видна наглядная разница. Изучить визуальную схему можно быстрее и, возможно, эффективнее, чем читать статью. 

Я начал искать такую картинку, но быстрый поиск в Google мне ничем не помог. В конечном счете я сделал диаграмму сам, используя Keynote на своем MacBook. 

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

Тотальное тестирование системы (TST — Total System Testing)

Мне нравится философия TST. Не существует ненужных тестов. Многие разработчики говорят, что у них нет времени на какие-то виды тестирования. Представьте себе врача, который прописал вам много лекарств и рекомендовал принимать их каждый день, даже не взглянув на ваши анализы. Или представьте, что вы купили автомобиль, который не прошел все необходимые тесты. Или что вы на самолете, который не был должным образом испытан перед полетом. Не особенно хорошая ситуация, верно? 

Всегда необходимо стремиться к тотальному и исчерпывающему тестированию системы. Да, 100%-ое покрытие невозможно, но к нему следует стремиться. Модульные тесты проверяют только изолированные и небольшие части системы. Кто будет проверять их взаимодействие? Кто будет проверять, как они связываются между собой? Именно для этого и разрабатываются системные тесты. И что насчет поставки?

Делая поставку с какого-то сервиса с публичным API, вы обязаны протестировать этот API интеграционными тестами. Иначе вам придется нанимать команду ручных тестировщиков всякий раз, когда кто-нибудь будет вносить изменения в систему. И, конечно же, разработчики скажут, что там всё ОК. Всё изолировано и одна конечная точка не затрагивает других. Узнаете себя? Но где гарантии? Так вот, интеграционные тесты — это и есть гарантия стабильности работы системы. 

И в то же время вы должны помнить, что мы работаем с бизнесом и для бизнеса. В нашей реальности тотальное тестирование невозможно. Уместней будет использовать “Пирамиду тестов” Майка Кона или “Трофей тестирования” Кента С. Доддса. 

Стоит помнить: автоматизация должна быть подобна Железному Человеку, а не Альтрону.

Я представлю вам список тестов, которые, по моему мнению, должны писать разработчики. Не тестировщики-автоматизаторы. Они дополняют тестовую среду тестированием масштабируемости, нагрузочным тестированием, стресс-тестированием, тестированием производительности и т.д. Я говорю о нас, о разработчиках.

Модульное тестирование (юнит-тестирование, блочное тестирование, тестирование компонентов)

Модульные тесты используются для тестирования какого-либо одного логически выделенного и изолированного элемента системы. В большинстве случаев это метод класса или простая функция (хотя это может быть и класс целиком). Изоляция тестируемого блока достигается с помощью заглушек (stubs), манекенов (dummies) и макетов (mockups). 

Системное тестирование (сервисное тестирование)

“В течение нескольких лет я успешно использую следующее
эмпирическое правило для планирования программистских задач:
1/3 планирования;
1/6 написания кода;
1/4 компонентных тестов и раннего системного тестирования;
1/4 системного тестирования всех наличных компонентов”

Фред Брукс, “Мифический человеко-месяц”

Это комплексный тест, который проверяет сразу несколько компонентов. В этом случае система выступает в качестве черного ящика. Мы можем сказать, что это модульный тест, где модуль — набор компонентов. Набор имеет единый фасад, который предоставляет соответствующий API. Методы этого API — это как раз то, что мы должны охватить тестами. Изоляция набора достигается с помощью заглушек, манекенов и макетов. Связанность компонентов и формат коммуникации между ними проверяются с помощью так называемых шпионов (spies).

Интеграционное тестирование (тестирование контрактов, тестирование на основе API)

Это, на самом деле, разновидность системного тестирования. Чаще всего данный термин используется для тестов, которые покрывают публичный API сервиса. Основное внимание здесь уделяется тестированию взаимодействия различных систем по принципу “сервис-клиент”. Например, методы уровня доступа к данным (Data Access Layer) покрываются системными тестами. Методы контроллера, вызывающие функции для вычисления бизнес-информации (Business Layer), также покрываются системными тестами.

Однако уже обработчики HTTP-запросов, которые вызывают методы контроллеров, покрываются интеграционными тестами. При таком тестировании запросы должны быть сделаны так же, как это будет делать конечный пользователь этой службы (например, одностраничное приложение или тестировщик, использующий Postman/Swagger). Это означает, что на самом деле для таких тестов необходимо воссоздать практически полноценное рабочее окружение. Самое сложное — изолировать тесты и генерировать тестовые данные. Для формирования такого окружения используются шаблоны тестовой платформы (TestBed) и фикстуры (Fixture, также Scaffolding, “строительные леса”).

Функциональное тестирование (сквозное тестирование, UI-тестирование, пошаговое тестирование)

Этот тест разработан для эмуляции поведения конечного пользователя системы. На самом деле, вы должны написать робота, который будет использовать вашу систему в тестовой среде. Чаще всего этот термин используется относительно GUI (т.е. взаимодействия пользователя и графического интерфейса системы).

Одним из самых популярных шаблонов, облегчающих написание таких тестов, является объект страницы (Page Object, также Screen Object). Хорошей практикой является реализация таких тестов в стиле headless browser, чтобы они могли выполняться без графического интерфейса в рамках процесса CI (Continuous Integration). Большинство этих тестов пишутся инженерами по автоматизации, но базовый набор следует добавлять разработчику. 

Дымовое тестирование (проверка работоспособности)

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

“Термин “дымовое тестирование” происходит из сферы испытаний оборудования: если при включении питания устройство начинает дымиться, то это указывает на серьезную проблему.” — Философия DevOps. Искусство управления IT.

Учебное тестирование

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

Регрессионное тестирование

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

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

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

Тестирование на проникновение (Pentest)

Система проверяется на наличие различных уязвимостей. Хорошим примером являются тесты, которые проверяют экранирование SQL-команд (защита от инъекций), доступность данных, авторизацию с истекшим токеном и т.д. Сложность написания таких тестов заключается в учете всех узких мест. Обычно для этого используется публичная информация об известных уязвимостях и эксплойтах платформы.

Фаззинг-тест (Fuzztest)

Чаще всего является своего рода системным тестированием или проверкой на уязвимость. Идея заключается в подаче случайного, заведомо неверного или неожиданного потока данных на вход системы. Целью такого теста является попытка выявить нарушения логики валидации и верификации, логики приложения в граничных случаях, внезапные сбои на сервере, а также попытки обнаружить утечку памяти или утечку информации о внутреннем устройстве системы через необработанные сообщения об ошибках (stacktrace).

Тестовые шаблоны

Заглушка (Stub, Dummy, Noop) — это функция или метод класса, который подменяет реализацию исходной функции и, не выполняя никаких осмысленных действий, возвращает пустой результат или тестовые данные.

/* Очень примитивная реализация заглушки */

function foo(msg) {
  return System.callExternalAPI(msg);
}

function bar() {
  return foo('Specific message');
}

function stub() {
  const stubFunc = (arg) => {
    stubFunc.calls++;
    stubFunc.args.push(arg);
    return arg
  };
  stubFunc.args = [];
  stubFunc.calls = 0;
  return stubFunc;
}

// ...

describe('function bar()', () => {
  const originalFoo = foo;
  
  before(() => {
    foo = stub();
  });
  
  after(() => {
    foo = originalFoo;
  });
  
  it('should call foo() function', () => {
    bar();
    assertEquals(foo.calls, 1);
    assertEquals(foo.args, ['Specific message']); // проверка непрямого вывода
  });
});

Макет (Mockup)— это экземпляр объекта, который представляет собой определенную фиктивную реализацию интерфейса. Как правило, макет предназначен для замены исходного системного объекта исключительно для целей тестирования взаимодействия и изоляции тестируемого компонента. Методы объекта при этом зачастую будут заглушками.

/* Очень примитивная реализация макета */

function foo(msg) {
  return System.callExternalAPI(msg);
}

function bar() {
  return foo('Specific message');
}

function stub(result) {
  const stubFunc = (arg) => {
    stubFunc.calls++;
    stubFunc.args.push(arg);
    return result
  };
  stubFunc.args = [];
  stubFunc.calls = 0;
  return stubFunc;
}

// ...

describe('function bar()', () => {
  const originalSystem = System;
 
  const mock = {
    callExternalAPI: stub('System message') // или просто какая-то чистая реализация вроде (msg) => 'System message'
  };
  
  before(() => {
    System = mock;
  });
  
  after(() => {
    System = originalSystem;
  });
  
  it('should call and return result of foo() function', () => {
    const result = bar();
    // Проверка непрямого ввода
    assertEquals(result, 'System message');
    
    // Проверка непрямого вывода
    assertEquals(System.callExternalAPI.calls, 1);
    assertEquals(System.callExternalAPI.args, ['Specific message']);
  });
});

Шпион (Spy) — это объект-обертка, по типу прокси, который прослушивает вызовы и хранит информацию об этих вызовах (аргументы, количество вызовов, контекст) исходного системного объекта. Сохраненные шпионом данные в дальнейшем используются в тестах.

/* Очень примитивная реализация шпиона */

function foo(msg) {
  return System.callExternalAPI(msg);
}

function bar() {
  return foo('Specific message');
}

function spy(instance, method) {
  const original = instance[method];
  const internals = { calls: 0, args: [] };

  instance[method] = (...args) => {
    internals.calls++;
    internals.args.push(args);
    return Reflect.apply(original, instance, args);
  };
  
  internals.restore = () => {
    instance[method] = original;
  };
  
  return internals;
}

// ...

describe('function bar()', () => {
 
  let spyInstance;
  
  before(() => {
    spyInstance = spy(System, 'callExternalAPI');
  });
  
  after(() => {
    spyInstance.restore();
  });
  
  it('should call and return result of foo() function', () => {
    const result = bar();
    // Проверка непрямого ввода
    assertEquals(result, 'System message');
    
    // Проверка непрямого вывода
    assertEquals(spyInstance.calls, 1);
    assertEquals(spyInstance.args, ['Specific message']);
  });
});

Тестовая платформа (TestBed) — это специально воссозданное тестовое окружение, платформа для тестирования (возможно, представляющая собой сочетание макетов, заглушек и шпионов). Она используется для комплексного тестирования отдельных наборов компонентов или всей системы в целом. Она также может быть использована в качестве площадки для экспериментов.

Примерами в JavaScript могут служить lab и hapi.js server.inject, supertest и express.js, angular 2 testbed для компонентов, enzyme и react-testing-library для react.js компонентов, а также sandbox в sinonjs.

Фикстура (Fixture) — это механизм, который приводит объект или всю систему в определенное состояние и фиксирует это состояние для проведения тестов. Чаще всего понятие “фикстура” относится к тестовым данным, необходимым для правильного запуска тестов, а также к механизмам загрузки/выгрузки этих данных в репозиторий (т.е. основная цель фикстур — привести системные данные в определенное (фиксированное) состояние, которое будет точно известно во время выполнения теста).

// fixtures/users.js

exports.Users = [
  { name: 'Woody' },
  { name: 'Buzz' },
  { name: 'Steve Holt' }
];

// ...

describe('User', () => {
  const loader = new FixtureLoader('testDataSource');
  
  before(done => {
    loader.resetDB(error => {
      if (!!error) return done(error);
      loader.load(path.join(__dirname, './fixtures/users.js'), done);
    });
  });
  
  // ...
});

Объект страницы или экранный объект (Page Object, Screen Object) — это объект, структура которого повторяет элементы страницы. Объект предоставляет методы работы с соответствующей страницей пользовательского интерфейса (нажатие кнопок, заполнение полей, переключение на другие страницы) и методы доступа к информации на этой странице (заголовок, различные виды текста, теги). Одним из самых популярных инструментов в этой области является Selenium WebDriver и различные обертки над ним.

import PageObject from '../page-objects/abstract';

class ProjectPage extends PageObject {
  constructor() {
    super();
    this.route = '#/projects';
  }
  
  openProject(id='') {
    this.browser.get(`${this.url}${this.route}/${id}`);
  }
  
  getHeader() {
    return this.getElement('div[data-testid="header"]');
  }
}

// ...

describe('Project page', () => {
  const page = new ProjectPage();
  
  beforeAll(() => {
    const loginPage = new LoginPage();
    loginPage.open();
    loginPage.loginAsAdmin();
    page.wait(() => page.open());
  });

Проверки (Asserts) —  это набор функций, которые позволяют вам сравнивать результаты выполнения двух или более функций. Они могут обеспечить возможность сравнения структур вглубь, используя механизмы интроспекции для проверки объектов на наличие определенных свойств.

Заключение

Все, что написано выше, представляет собой мое скромное субъективное мнение и интерпретацию.

Читайте также:


Перевод статьи Artur Basak: A Visual Tutorial on Every Type of Test You Can Write

Предыдущая статьяКто на свете всех сильнее - Java, Go и Rust в сравнении
Следующая статьяКак искусственный интеллект меняет финансовый сектор?