Что такое стаб в программировании
Перейти к содержимому

Что такое стаб в программировании

  • автор:

По хардкору: дублеры, моки, стабы

Сегодня о тестах. Пост для тех, кто знаком с RSpec, но не понимает, что такое «мокать» и «застабить». Коротко, по делу и с примерами.

Дублер (test double)

Объект-каскадер, подменяющий реальный объект системы во время тестов:

describe NotificationsController do # NotificationsController загружает последние уведомления # со стороннего сервиса по HTTP # с помощью NotificationsDatasource. let(:datasource) do double(:datasource, as_json:  notifications: [] >) end before do # Подменяем реальный NotificationsDatasource дублером, # чтобы не зависеть от внешнего сервиса в тестах: allow(NotificationsDatasource) .to receive(:new) .and_return(datasource) end describe "#index" do it "wraps notifications in 'data' key" do get :index, format: :json expect(json_response["data"].keys) .to have_key "notifications" end end end 

Стаб (stub)

Заглушка для метода или объекта, возвращающая заданное значение:

context "when attachment file is too large to email" do let(:max_file_size)  Attachment::MAX_FILE_SIZE > before do allow(attachment) .to receive(:file_size) .and_return(max_file_size + 1) end it "raises 'file is too large' error" do # . end end 

Внимательный читатель со звездочкой уже заметил, что и в предыдущем примере с NotificationsController был стаб. Все верно: стаб — это дублер с зашитыми ответами.

Мок (mock)

Стаб с ожиданиями, которые RSpec проверит в конце теста:

context "when cloning process cannot be performed" do before do allow(doctor).to receive(:clone)  raise "can't clone" > # стаб end it "notifies airbrake" do expect(Airbrake).to receive(:notify) # мок # Rspec застабит `Airbrake.notify` # и в конце этого `it do. end` блока # проверит, был ли он вызван. # Если вызова не было — ошибка и красный тест. # # Когда на собеседовании спросят, чем # отличается мок от стаба, отвечайте: # «Только мок может завалить тест». clone_poor_dolly end end 

Моки меняют порядок фаз в тесте. Вместо «Подготовка — Испытание — Проверка» получается «Проверка+Подготовка — Испытание». Если вам, как и мне, тяжело такое читать, используйте стаб с проверкой:

# мок it "notifies airbrake" do expect(Airbrake).to receive(:notify) # проверка + настройка clone_poor_dolly # испытание end # стаб + проверка it "notifies airbrake" do allow(Airbrake).to receive(:notify) # настройка clone_poor_dolly # испытание expect(Airbrake).to have_received(:notify) # проверка end 

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

  • Mocks Aren’t Stubs;
  • Thoughts on Mocking, Part 2;
  • Thoughts on Mocking.

P. S. Ещё больше постов о программировании, тестах и культуре разработки у меня в Телеграме.

⊙ 30 апреля 2016
Поделиться
Поделиться

  • Стоп-комментарии
  • ← →
  • По хардкору: instance_double, verify_partial_doubles

Тестовый курс

Практический курс о тестировании в Ruby и Rails. С наставником, теорией, домашкой и дополнительными материалами: статьями, советами, видео.

Для разработчиков знакомых с Ruby и RSpec, но не до конца понимающих что и как тестировать. Для тех, кто прочитал RSpec Book, но не может написать тест с нуля. Для тех, кто исправляет баг за 5 минут, а потом 2 часа пишет тест.

Популярное

  • Stylus → PostCSS
  • Анти-паттерны в тестах
  • По хардкору: что писать в комментарии к коммиту
  • Ищу стажеров (уже нашел)
  • По хардкору: describe и context
  • Три правила ревью
  • CSS инбоксы
  • Дезодоранты и полезные комментарии
  • Подростковое тестирование. Подборка

Из старого

  • 3д-печать и ФФФ
  • Баготерапия
  • Дефолтные стили браузеров
  • Просто не делай херни
  • Датчик есть, но его как бы нет
  • Большая маленькая ложь
  • Карма тестировщика
  • irb: echo on assignment
  • Как тестировать код, завязанный на рандом
  • Лайфхак: процедура завершения работы
  • RSpec: to be_within().of()
  • SPA на Реакте без Вебпака
  • RSpec: skip и pending
  • SSRF
  • Range в ActiveRecord
  • Не надо везде лепить восклицательные знаки!
  • Планирование преемственности
  • yakuake в iTerm
  • Мониторинг ключевых метрик приложения на Рельсах в Графане
  • Лайфхак: буферы для совещаний
  • Проблема в занятости, а не в лени
  • before + after → around
  • Лайфхак: талоны за дипворк
  • inbox.txt
  • RSpec & Rails: как проверить deliver_later(wait_until: . )
  • Стабы и моки юникс-сокетов
  • А у нас Faker
  • Фейковые данные в тестах
  • Как тестировать код, работающий с внешним АПИ. Заглушка на Синатре
  • На каких задачах сфокусироваться
  • Тормоза mc на m1, zsh и nvm
  • Как читать больше в эпоху цифрового максимализма
  • Менеджерский лайфхак: 1:1 в Телеграме
  • Задавать вопросы, а не перебирать варианты
  • Ищу толкового стажера на Рельсах
  • Достроить, чтобы обойти
  • Заглушка из Rack-приложения в одну строчку
  • Лайфхак: поработать в машине
  • Линтим Руби в Виме
  • Курс о профессиональном росте. Как расти самому, как растить команду
  • Сервисы головного мозга
  • «Что» и «Чтобы что» в пулреквестах
  • Где проходит грань в изолированности объекта тестирования?
  • Только то, что важно для проверки
  • Меньше — лучше
  • Пора валить?
  • Когда использовать double, а не instance_double?
  • RSpec: before и after хуки
  • Как застабить переменные окружения в RSpec
  • Что не нужно писать в it
  • RuboCop, RSpec и стайлгад
  • Как протестировать конфиг whenever
  • Один тест — одна логическая проверка
  • Обратная связь в тестах, guard и vim-test
  • Не проверяйте отдельно ключи
  • Не тестируйте attr_reader/_writer/_accessor
  • Ищу сообразительного стажёра
  • Только то, что влияет на проверку
  • Какой тест упадет, если удалим этот кусок кода?
  • Как отлаживать странное: в браузере на реальном Айфоне через Скайп, Зум или Хэнгаут
  • let и before
  • Впихнуть все в expect
  • Сэнди Метц
  • Конспект POODR. Reducing Costs with Duck Typing
  • Конспект POODR. Creating Flexible Interfaces
  • Конспект POODR. Managing Dependencies
  • Конспект POODR. Designing Classes with a Single Responsibility
  • Конспект POODR. Object-Oriented Design
  • Ищу толкового стажёра
  • Что почитать начинающему тимлиду
  • Советы
  • Сравнение CI-сервисов
  • Как тестировать коллбэки ActiveRecord моделей
  • Гигиена в тестах
  • Проблемы с subject
  • Стоит почитать
  • По хардкору: instance_double, verify_partial_doubles
  • Стоп-комментарии
  • Сегодня я узнал
  • Управление временем
  • Хороший доклад
  • К Оптимусу Прайму. Группировка CSS свойств
  • Объясни это
  • К Оптимусу Прайму. Сортировка CSS свойств по алфавиту
  • Два способа признаться в ненависти к человечеству
  • Реквием по ИЕ
  • Былинное программирование
  • Иисус и псевдоэлементы
  • Удаленные мудаки
  • Пусть страдают роботы
  • Как пользоваться успехом у женщин с помощью CSS
  • Воинствующие шаблонисты
  • Нет, это дизайнерский таск
  • Синдром бойлерной
  • Рабочие столы
  • Союз скобок

Моки и стабы

Существует категория классов, которые тестировать весьма просто. Если класс зависит только от примитивных типов данных и не имеет никаких связей с другими бизнес-сущностями, то достаточно создать экземпляр этого класса, «пнуть» его некоторым образом путем изменения свойства или вызова метода и проверить ожидаемое состояние.

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

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

// Модель представления, предназначенная для управления входом // пользователя в систему public class LoginViewModel < public LoginViewModel() < // Читаем имя последнего пользователя UserName = ReadLastUserName(); >// Имя пользователя; может быть изменено пользователем public string UserName < get; set; >// Логиним пользователя UserName public void Login() < // Не обращаем внимание на дополнительную логику, которая должна быть // выполнена. Считаем что нам достаточно просто сохранить имя текущего // пользователя SaveLastUserName(UserName); >// Читаем имя последнего залогиненного пользователя private string ReadLastUserName() < // Не важно, как она на самом деле реализована . // Просто возвращаем что-нибудь, чтобы компилятор не возражал return "Jonh Doe"; >// Сохраняем имя последнего пользователя private void SaveLastUserName(string lastUserName) < // Опять таки, нам не интересно, как она реализована >> 

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

ПРИМЕЧАНИЕ
Не нужно бросать в меня камнями с криками «Да кто сегодня вообще такую хрень написать можно? Ведь уже столько всего написано о вреде такого подхода, да и вообще, у нас есть юнити-шмунити и другие полезности, так что это нереальный баян двадцатилетней давности!». Кстати, да, это баян, но, во-первых, речь не юнитях и других контейнерах, а о базовых принципах, а во-вторых, подобное «интеграционное» тестирование все еще невероятно популярно, во всяком случае, среди многих моих «зарубежных» коллег.

Создания «швов» для тестирования приложения

Даже если не задумываться о том, какое количество новомодных принципов проектирования нарушает наша вью-модель, четко видно, что ее дизайн несколько … убог. Ведь даже если проектировать старым дедовским бучевским методом, то становится понятно, что всю работу по сохранению имени последнего пользователя, логику по работе с базой данных (или другим внешним источником данных) нужно спрятать подальше с глаз долой и сделать это «проблемой» кого-то другого и использовать уже этого «кого-то» в качестве «строительного блока» для получения более высокоуровневого поведения:

internal class LastUsernameProvider < // Читаем имя последнего пользователя из некоторого источника данных public string ReadLastUserName() < return "Jonh Doe"; >// Сохраняем это имя, откуда его можно будет впоследствии прочитать public void SaveLastUserName(string userName) < >> public class LoginViewModel < // Добавляем поле для получения и сохранения имени последнего пользователя private readonly LastUsernameProvider _provider = new LastUsernameProvider(); public LoginViewModel() < // Теперь просто вызываем функцию нового вспомогательного класса UserName = _provider.ReadLastUserName(); >public string UserName < get; set; >public void Login() < // Все действия по сохранению имени последнего пользователя также // делегируем новому классу _provider.SaveLastUserName(UserName); >> 

Пока что написание модульного теста все еще остается затруднительным, но становится понятным, как можно достаточно просто «подделать» реальную реализацию класса LastUsernameProvider и сымитировать нужное для нас поведение. Достаточно выделить методы этого класса в отдельный интерфейс или просто сделать их виртуальными и переопределить в наследнике. После чего останется лишь «прикрутить» нужный нам объект в нашу вью-модель.

Честно говоря, я не большой фанат изменений в дизайне только ради «тестируемости» кода. Как показывает практика, нормальный ОО дизайн либо уже является достаточно «тестируемым» или же требует лишь минимальных телодвижений, чтобы сделать его таковым. Некоторые дополнительные мысли по этому поводу можно найти в заметке «Идеальная архитектура» .

Даже не прибегая ни к каким сторонним библиотекам для «инджекта» зависимостей мы можем сделать это самостоятельно несколько простыми способами. Нужную зависимость можно передать через дополнительный конструктор, через свойство или создать фабричный метод, который будет возвращать интерфейс ILastUsernmameProvider.

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

// Выделяем методы в интерфейс internal interface ILastUsernameProvider < string ReadLastUserName(); void SaveLastUserName(string userName); >internal class LastUsernameProvider : ILastUsernameProvider < // Читаем имя последнего пользователя из некоторого источника данных public string ReadLastUserName() < return "Jonh Doe"; >// Сохраняем это имя, откуда его можно будет впоследствии прочитать public void SaveLastUserName(string userName) < >> public class LoginViewModel < private readonly ILastUsernameProvider _provider; // Единственный открытый конструктор создает реальный провайдер public LoginViewModel() : this(new LastUsernameProvider()) <>// "Внутренний" предназначен только для тестирования и может принимать "фейк" internal LoginViewModel(ILastUsernameProvider provider) < _provider = provider; UserName = _provider.ReadLastUserName(); >public string UserName < get; set; >public void Login() < _provider.SaveLastUserName(UserName); >> 

Поскольку дополнительный конструктор является внутренним (internal), то он доступен только внутри этой сборке, а также «дружеской» сборке юнит-тестов. Конечно, если тестируемые классы являются внутренними не будет не какой, но поскольку все «клиенты» внутреннего класса находятся в одной сборке, то и контролировать их проще. Подобный подход, основанный на добавлении внутреннего метода для установки «фальшивого» поведения является разумным компромиссом упрощения тестирования кода, не налагая ограничения на использования более сложных механизмов управления зависимостями, типа IoC контейнеров.

ПРИМЕЧАНИЕ
Одним из недостатков при работе с интерфейсами является падение читабельности, поскольку не понятно, сколько реализаций интерфейса существует и где находится реализация того или иного метода интерфейса. Такие инструменты, как Решарпер существенно смягчают эту проблему, поскольку поддерживают не только навигацию к объявлению метода (Go To Declaration), но также и навигацию к реализации метода (Go To Implementation):

Проверка состояния vs проверка поведения

Теперь давайте попробуем написать юнит-тест вначале для конструктора класса LoginViewModel, который получает имя последнего залогиненного пользователя, а потом юнит-тест для метода Login, после выполнения которого, имя последнего пользователя должно быть сохранено.

Для нормальной реализации этих тестов нам нужна «фейковая» реализация интерфейса, при этом в первом случае, нам нужно вернуть произвольное имя последнего пользователя в методе ReadLastUserName, а во втором случае – удостовериться, что вызван метод SaveLastUserName.

Именно в этом и отличаются два типа «фейковых» классов: стабы предназначены для получения нужного состояния тестируемого объекта, а моки применяются для проверки ожидаемого поведения тестируемого объекта.

Стабы никогда не применяются в утверждениях, они простые «слуги», которые лишь моделируют внешнее окружение тестового класса; при этом в утверждениях проверяется состояние именно тестового класса, которое зависит от установленного состояния стаба.

// Стаб возвращающее указанное имя последнего пользователя internal class LastUsernameProviderStub : ILastUsernameProvider < // Добавляем публичное поле, для простоты тестирования и // возможности повторного использования этого класса public string UserName; // Реализация метода очень простая - просто возвращаем UserName public string ReadLastUserName() < return UserName; >// Этот метод в данном случае вообще не интересен public void SaveLastUserName(string userName) < >> [TestFixture] public class LoginViewModelTests < // Тестовый метод для проверки правильной реализации конструктора вью-модели [Test] public void TestViewModelConstructor() < var stub = new LastUsernameProviderStub(); // "моделируем" внешнее окружение stub.UserName = "Jon Skeet"; // Ух-ты!! var vm = new LoginViewModel(stub); // Проверяем состояние тестируемого класса Assert.That(vm.UserName, Is.EqualTo(stub.UserName)); >> 

У моков же другая роль. Моки «подсовываются» тестируемому объекту, но не для того, чтобы создать требуемое окружение (хотя они могут выполнять и эту роль), а прежде всего для того, чтобы потом можно было проверить, что тестируемый объект выполнил требуемые действия. (Именно поэтому такой вид тестирования называется behaviortesting, в отличие от стабов, которые применяются для statebasedtesting).

// Мок позволяет проверить, что метод SaveLastUserName был вызван // с определенными параметрами internal class LastUsernameProviderMock : ILastUsernameProvider < // Теперь в этом поле будет сохранятся имя последнего сохраненного пользователя public string SavedUserName; // Нам все еще нужно вернуть правильное значение из этого метода, // так что наш "мок" также является и "стабом" public string ReadLastUserName() < return "Jonh Skeet";>// А вот в этом методе мы сохраним параметр в SavedUserName для public void SaveLastUserName(string userName) < SavedUserName = userName; >> // Проверяем, что при вызове метода Login будет сохранено имя последнего пользователя [Test] public void TestLogin() < var mock = new LastUsernameProviderMock(); var vm = new LoginViewModel(mock); // Изменяем состояние вью-модели путем изменения ее свойства vm.UserName = "Bob Martin"; // А затем вызываем метод Login vm.Login(); // Теперь мы проверяем, что был вызван метод SaveLastUserName Assert.That(mock.SavedUserName, Is.EqualTo(vm.UserName)); >
А зачем мне знать об этих отличиях?

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

Однако рано или поздно вам может надоесть это чудесное занятие по ручной реализации интерфейсов и вы обратите свое внимание на один из Isolation фреймворков, таких как Rhino Mocks, Moq или Microsoft Moles. Там эти термины встретятся обязательно и понимание отличий между этими типами фейков вам очень пригодится.

Я осознанно не касался ни одного из этих фреймворков, поскольку каждый из них заслуживает отдельной статьи и ИМО лишь усложнит понимание этих понятий. Но если вам все же интересно посмотреть на некоторые из этих фреймворков более подробно, то об одном из них я писал более подробно: “Microsoft Moles”.

Что такое криптор и стаб ?

Криптор (aka cryptor) — это тулза, которая предназначена для скрытия троянов, ботов и прочей нечисти от детектирования антивирусами.

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

Чаще всего такие крипторы полиморфны — т.е. код криптора в криптуемой программе каждый раз уникален, заполнен случайными инструкциями и бессмысленными вызовами функций API. Такие крипторы достаточно долго остаются недетектируемыми в силу уникальности каждого закриптованного файла. Такие крипторы тоже со временем детектируются, и если автор не чистит свой продукт, то криптор перестает быть уникальным.

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

Т.е. надеюсь поняли отличия в первом случае шифруется код программы, и стаб расшифровывает уже команды программы, а во втором случае шифруется сам файл программы и расшифровывается тоже сам файл, ну и понятно, что второй тип криптора это не что иное как ерунда, т.к. практически любой антивирусник спалит вирус при расшифровке ! 😉

Некоторые крипторы напрямую файл на диск не пишут, а запускают его из памяти, но это их не оправдывает, т.к. продвинутый антивирус словит это при запуске.

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

Тестирование для “чайников”.

Тестирование — это процесс в программировании, позволяющий проверить на корректность отдельные модули исходного кода программы.

Когда нужно и не нужно тестировать

Всегда ли нужно тестировать ваше приложение? Рекомендуем ответить вам самим на этот вопрос после следующего утверждения: “Любой долгосрочный проект без надлежащего покрытия тестами обречен рано или поздно быть переписанным с нуля”.

Однако существуют ситуации, когда необходимость в написании тестов не столь очевидна. Скорее всего тесты не нужны вам если:

  • Вы всегда пишете код для себя, создавая pet проект, не имея планов на его дальнейшее коммерческое использование или распространение в рамках сообщества.
  • Вы создаете рекламные одностраничники, продающие страницы, простые флеш-играми или баннеры — сложная верстка/анимация или большой объем статики. Никакой логики нет, только представление.
  • Создание статического сайта-визитки, т.е. 1–4 html-страницы с одной или несколькими формами для отправки данных. Тут закономерно нет никакой особенной логики, быстрее просто все проверить самостоятельно, так сказать «руками».
  • Вы делаете рекламный проект для выставки. Срок работы — от нескольких недель до месяца. В начале проекта не до конца известно, что именно должно получиться в конце. Задача проекта — отработать несколько дней на выставке в качестве презентации.

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

Основные термины (пирамида, TDD, BDD, stubs, mock)

Существует несколько подходов к написанию тестов. Первая модель — классика: сначала разработка, а затем тестирование “code first”. Это означает, что сначала происходит написание кода, затем мы тестируем продукт и отправляем его или на доработку, или переходим к следующей стадии разработки.

Другой подход можно назвать “test first” режимом. Это означает, что мы можем начать тестирование еще до написания самой функции — например, мы можем создать единичный тест или автоматически выполняемый набор тестов до того, как функция или какой-то кусок кода будет разработан и внедрен в приложение. Одним из наиболее популярных примеров здесь является Test-Driven Development.

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

Также наряду с термином ТDD вы можете услышать и о BDD подходе.

В основе Test-driven development (TDD) лежит 5 основных этапов:

  1. Сначала разработчик пишет несколько тестов.
  2. Затем разработчик запускает эти тесты и (очевидно) они терпят неудачу, потому что ни одна из этих функций еще не реализована.
  3. Далее разработчик действительно реализует эти тесты в коде.
  4. Если разработчик хорошо пишет свой код, то на следующем этапе он увидит, что тесты проходят.
  5. Разработчик может затем реорганизовать свой код, добавить комментарии так как он уверен, что если новый код что-то сломает, тогда тесты предупредят об этом.
test('равно 1 для массива с одним элементом', function () assert.equal(1,[1].length);>);

Behavior-driven development (BDD) — подход создан для того, чтобы исправить проблемы, которые могут возникнуть при использовании ТDD, а именно, обеспечить лучшее взаимопонимание внутри команды, т.е. не только для разработчиков, облегчить поддержку кода через наглядное представление о его функциональности, тесты и их результаты выглядят более “человечно”, облегчается процесс миграции при переходе на другой язык программирования.

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

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

it('должно вернуть 1, когда передан массив с одним элементов', function () [1].length.should.equal(1);>);

Еще одной важной концепцией тестирования является тестовая пирамида. Пирамида тестирования используется для распределения тестов по уровням приложения.

Каждое приложение можно разделить на несколько слоев. Рассмотрим типичное расслоение с уровнем компонентов, сервисами и пользовательским интерфейсом. Нижняя часть пирамиды покрыта модульными (unit) тестами. Они написаны в основном разработчиками и охватывают атомарные компоненты, такие как классы, методы и функции. Запускаются очень часто, работают быстро и их количество в рамках приложения велико.

Б. Страустрап в своей книге о C++ предлагает следующий подход к разделению кода на отдельные блоки: если «это» действие — сделайте метод. Если несколько действий объединены общим смыслом и/или процессом — объявите класс. Если придерживаться этого правила, то автоматически класс будет модулем вашего приложения.

Следующий уровень — интеграционные тесты. Т.е. когда идет проверка, не ломает ли новый функционал код, который уже написан ранее в рамках системы. Также тут мы можем иметь сценарии, которые охватывают более сложные функции, такие как тесты API. Запускаем реже, как правило, при мердже веток или объединении больших участков кода.

В верхней части находятся тесты пользовательского интерфейса (end to end) Они действуют так же, как конечный пользователь работает с приложением. Запускаем очень редко — несколько раз за проект. Работают очень медленно.

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

Еще один набор терминов, с которыми придется столкнуться в процессе написания тестов — это стабы (stubs) и моки (mock).

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

Если на вопрос: «будет ли этот компонент вести себя так же на другой машине?» вы отвечаете нет, то его необходимо “подменить” и тут вам на помощь как раз придут стабы и моки. Но есть и другая сторона медали, когда разработчик начинает увлекаться и приходит к тому, что подменяет вообще все. Соответственно тесты перестают проверять само приложение и начинают тестировать стабы, моки. Это в корне не верно. Если «живых» реализаций в тесте нет, то этот тест не тестирует ничего.

Иногда эти термины stubs и mock путают: разница в том, что стаб ничего не проверяет, а лишь имитирует заданное состояние. А мок — это объект, у которого есть ожидания. Например, что данный метод класса должен быть вызван определенное число раз. Иными словами, ваш тест никогда не сломается из-за «стаба», а вот из-за мока может.

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

Есть уже готовые фреймворки, которые предоставляют такой функционал: Sinon, Jasmine, enzyme, Jest, testdouble

Пример использования стаба (sinonjs.org)

it('resolves with the right name', done =>  const stub = sinon.stub(User.prototype, 'fetch') .resolves(< name: 'David' >) User.fetch() .then(user =>  expect(user.name).toBe('David') done() >)>)

Пишем тесты правильно (требования, оценка результата)

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

Есть несколько рекомендаций, придерживаясь которых, вы можете избежать данной проблемы.

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

2) Особое внимание уделите именованию тестов. Одна из лучших практик: добавьте к каждому проекту его собственный тестовый проект.

Если у вас, например, есть части системы your-project.models, your-project.controllers, то тесты для этих частей могут именоваться следующим образом: your-project.models.tests, your-project.controllers.tests

3)Такие же “логичные” походы используйте для именования тестовых классов или методов.

Например, один из вариантов именования для тестирования метода — __ .

Метод калькулятора суммирующий данные
Sum_2Plus5_Returned7

4)Каждый тестирующий класс или метод должен тестировать только одну сущность. Если процесс слишком сложен (например, покупка в интернет-магазине), разделите его на несколько частей и протестируйте их отдельно.

5)Не делайте ненужных утверждений (assertion). Какое конкретное поведение вы тестируете? Если это не основное поведение, то оно и не нуждается в тестировании! Помните, что модульные тесты — это спецификация дизайна того, как должно срабатывать определенное поведение, а не список наблюдений за всем кодом.

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

7)Не стоит писать велосипеды. Пользуйтесь готовыми тестовыми фреймворками. Подберите тот, который подходит вам в данном, конкретном случае.

8)Боритесь с зависимостями. Тесты не должны зависеть от окружения, в котором они выполняются. Например, не проверяйте настройки конфигурации устройства. По необходимости используйте стабы и моки, а также готовые фреймворки для их написания.

9)Не относитесь к своим тестам как к второсортному коду. Все принципы, применяемые в разработке продакшн-кода могут и должны применяться при написании тестов. (DRY, KISS)

10)Тест должен легко поддерживаться. Есть всего три причины, почему тест перестал проходить:

  • Ошибка в продакшн-коде: это баг, его нужно завести в баг-трекере и починить.
  • Баг в тесте: видимо, продакшн-код изменился, а тест написан с ошибкой (например, тестирует слишком много или не то, что было нужно).
  • Смена требований. Если требования изменились слишком сильно — тест должен упасть. Это правильно и нормально. Вам нужно разобраться с новыми требованиями и исправить тест. Или удалить, если он больше не актуален.

11)Тесты должны запускаться регулярно в автоматическом режиме.

Определить успешность вашей системы тестирования на проекте можно двумя способами:

  1. Количество багов в новых релизах (в т.ч. и регрессии).
  2. Покрытие кода.

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

Тестовое покрытие — полезный инструмент для поиска непроверенных частей кодовой базы. Тестовый охват мало полезен в качестве числового заявления о том, насколько хороши ваши тесты. “Нормальным” считается покрытие в пределах 80%.

Наиболее популярные инструменты для измерения покрытия кода, написанного на JavaScript istanbul.js blanket.js JSCover

Выбор фреймворка для создания тестов

На сегодняшний день доступна целая масса фреймворков для тестирования JavaScript-кода (overview).

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

Например, поддерживаемая структура тестов. Если мы говорим о поддержке BDD, то следует выбирать среди Mocha, Jasmine, Jest, Сucumber.

Также доступны на выбор несколько assertion библиотек: Chai, Jasmine, Jest, Unexpected.

Если для вас важную роль играет представление и отображение результатов ваших проверок, то наибольший функционал в данной области предоставляют Mocha, Jasmine, Jest, Karma.

Для использования snap-shots в вашем тестировании следует обратить внимание на Jest или Ava.

Для борьбы с зависимостями и использования mocks и stubs следует обратить внимание на специализированные фреймворки типа Sinon.js, Jasmine, enzyme, Jest, testdouble.js.

Как уже упоминалось выше для измерения охвата и покрытия кода тестами возможно использование Istanbul, Jest.

Для функциональных тестов, для создания пользовательских сценариев поведения, необходимо использование браузерной среды или браузерной среды с программируемым API, что доступно в рамках Protractor, Nightwatch, Phantom.js, Сasper.

Описание некоторых фреймворков

JSDOM является реализацией JavaScript-стандартов WHATWG DOM и HTML. Другими словами, JSDom имитирует среду браузера, не запуская ничего, кроме простого JS. В этой моделируемой среде браузера тесты могут выполняться очень быстро. Недостатком JSDom является то, что не все может быть смоделировано вне реального браузера (например, вы не можете сделать снимок экрана), поэтому его использование ограничивает доступность ваших тестов.

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

Phantom.js — реализует «headless» браузер Webkit, который находится между реальным браузером и JSDom в скорости и стабильности. Достаточно популярен.

Karma — позволяет запускать тесты в браузерах, включая настоящие браузеры, Phantom, JSdom и даже устаревшие браузеры. Karma размещает тестовый сервер со специальной веб-страницей для запуска тестов в среде страницы. Эта страница может быть запущена во многих браузерах. Это также означает, что тесты можно запускать удаленно с помощью таких служб, как BrowserStack.

Chai — самая популярная assertion библиотека.

Unexpected — это также assertion библиотека с немного отличающимся синтаксисом от Chai.

Sinon.js — это набор очень мощных тестовых шпионов, заглушек и макетов (mocks) для модульного тестирования.

testdouble.js — представляет собой новую библиотеку, похожую на Sinon, с несколькими отличиями в дизайне, философии и особенностях, которые могли бы пригодиться во многих случаях.

Jasmine — представляет собой платформу тестирования, обеспечивающую все, что вам требуется для ваших тестов: работающая среда, структура, отчетность, assertion и mocks инструменты.

Mocha — в настоящее время является наиболее часто используемой библиотекой. В отличие от Jasmine, она используется со сторонними библиотеками mocks и assertions (обычно Enzyme и Chai). Это означает, что Mocha немного сложнее настроить, но она более гибкая и открыта для расширений.

Jest — это платформа тестирования, рекомендованная Facebook. Он использует функционал Jasmine и добавляет функции поверх него, поэтому все упоминания о Jasmine относится и к нему.

Ava — минималистическая библиотека, которая имеет возможность запускать тесты параллельно.

Selenium — автоматизирует браузер для имитации поведения пользователя. Он не написан специально для тестов и может управлять браузером для многих целей, предоставляя сервер, который имитирует поведение пользователя в браузере с использованием API. Selenium можно контролировать разными способами и использовать различные языки программирования, а также с некоторыми инструментами даже без реального программирования.

Protractor — это библиотека, которая использует Selenium, но добавляет улучшенный синтаксис и специально встроенные хуки для Angular.

Nightwatch — имеет собственную реализацию selenium WebDriver. И обеспечивает собственную среду тестирования, тестовый сервер, assertion и другие инструменты.

Сasper — написан поверх Phantom и Slimer (так же, как Phantom, но в Gecko FireFox), чтобы при помощи специальных утилиты более просто создавать Phantom и Slimer скрипты. Каспер предоставляет нам более быстрый, но менее стабильный способ запуска функциональных тестов в браузерах с интерфейсом UI.

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

Литература:

Книги

Статьи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *