Содержание


Использование заглушек и фиктивных объектов в Ruby on Rails

Стратегии тестирования Ruby для RSpec, Mocha и Flex Mock

После выхода в свет моей последней статьи по RSpec некоторые читатели спрашивали меня о том, что могут представлять собой фиктивные объекты в RSpec. Должен признать, что в прошлый раз я преднамеренно уклонился от темы mock-сред (mocking frameworks).

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

Описание проблемы

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

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

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

Фиктивные объекты — это не заглушки

Почти четыре года назад Мартин Фаулер из ThoughtWorks разместил в своем блоге известный пост под названием "Фиктивные объекты — это не заглушки" (ссылка приведена в разделе Ресурсы). Спустя четыре года большинство разработчиков все еще продолжают путать эти понятия. Возможно, мы выбрали для них не самые лучшие названия, но они прижились. Поясним эти понятия на следующем примере.

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

Использование заглушек
Очевидно, что недостаточно по очереди заменять каждую лампочку в гирлянде на рабочую. Из строя может выйти несколько лампочек. Вместо этого я проверяю каждую лампочку в отдельности. Работают оба способа. Первая методика проверки заключается в том, чтобы выкрутить лампочку и вкрутить ее в маленький фонарик, питающийся от батарейки. Если лампочка загорается, значит, она рабочая, и я перехожу к следующей. А если лампочка не горит, я разбиваю ее вдребезги молотком и заменяю рабочей. Фактически я делаю следующее: заменяю сложную цепочку лампочек более простым устройством, а затем, включив ток, проверяю каждую лампочку и проверяю полученный результат, на основании чего делаю вывод о том, пройден тест или нет.

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

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

Заглушка базы данных может в ответ на запрос возвратить фиктивные записи. Конечно, то же самое может случиться и при использовании фиктивного объекта, однако фиктивный объект базы данных может также убедиться, что клиентский код вызвал определенные запросы, а затем вызвал запрос close, чтобы закрыть соединение. Фиктивные объекты следует использовать, когда тест зависит от того, как используется интерфейс, а заглушки — когда это не важно.

Недостатки

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

Основы создания заглушек

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

  1. Заменить часть реальной системы в пределах контрольного примера на заглушку.
  2. Воспроизвести необходимое реальное поведение заглушки.

Может возникнуть желание заменить целый объект или изменить результаты отдельного метода. Заменить целый объект легко — просто напишите новый объект и замените его в контрольном примере. В некоторых языках программирования методы могут задаваться более жестко, однако при использовании динамических языков, например Ruby, заглушками пользоваться легко, так как при этом метод просто переопределяется. Однако хватит теории, разберем конкретный пример.

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

Для экземпляра объекта document создадим заглушку метода print, возвращающую значение "ложь".

Оператор можно разбить на две части. В первой части будет определено, что необходимо заменить данной заглушкой, а во второй — что она должна делать. Теперь создадим заглушку в трех средах.

Mocha/Stubba

В настоящее время одну из лидирующих позиций среди сред, в которых применяются заглушки и фиктивные объекты, занимает Mocha. Ее можно легко установить с сайта gems.rubyforge.org:

Листинг 1. Установка Mocha
batate$ gem install mocha
Bulk updating Gem source index for: http://gems.rubyforge.org
Successfully installed mocha-0.5.5
Installing ri documentation for mocha-0.5.5...
Installing RDoc documentation for mocha-0.5.5...

Для иллюстрации мне понадобится приложение. Наше приложение, показанное в листинге 2, будет состоять из модели Document и представления View. Пользовательский интерфейс будет передавать процесс печати модели Document.

Листинг 2. Приложение в document.rb
class Document
  def print
    # не имеет значения — используем заглушку
  end
end

class View
  attr :document

  def initialize(document)
    @document = document
  end

  def print()
    if document.print
      puts "Excellent!"
      true
    else
      puts "Bummer."
      false
    end
  end
end

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

В листинге 3 показан наш контрольный пример.

Листинг 3. Тестирование с помощью Mocha
require 'test/unit'
require 'rubygems'
require 'mocha'
require 'document'

class ViewTest < Test::Unit::TestCase

  def test_should_return_false_for_failed_print
    document = stub("my document")
    document.stubs(:print).returns(false)

    ui = View.new(document)
    assert_equal false, ui.print
  end

end

Теперь видно, как это работает. С помощью оператора stub("my document") была создана простая заглушка. Затем мы определили в заглушке поведение для процесса печати с помощью строки document.stubs(:print).returns(false). Если посмотреть внимательно, то данная строка кода очень сильно напоминает псевдокод, о котором мы говорили выше:

Для экземпляра объекта document создадим заглушку метода print, возвращающую значение "ложь".

Можно сократить две первые строки метода тестирования до одной:

Листинг 4. Упрощение теста
    Document.any_instance.stubs(:print).returns(false)

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

Создание заглушек с помощью Flex Mock

Как и Mocha, Flex Mock является популярной средой с поддержкой фиктивных объектов. Джим Вайрих (Jim Weirich), автор таких популярных инструментов Ruby, как rake, написал Flex Mock для управления основными видами заглушек и фиктивных объектов в тестах Ruby. С точки зрения популярности и практичности Mocha и Flex Mock очень похожи. Установить Flex Mock очень шегко с помощью gems (это еще один проект Джима Вайриха):

Листинг 5. Установка Flex Mock
gem install flexmock

С помощью метода flexmock можно создать заглушки. Для этого необходимо добавить хэш, где ключом будет название метода, который необходимо заменить заглушкой, а значением — результат. Пример подобной заглушки показан в листинге 6:

Листинг 6. Создание заглушки с помощью Flex Mock
require 'rubygems'
require 'test/unit'
require 'flexmock/test_unit'
require 'document'

class ViewTest < Test::Unit::TestCase

  def test_should_return_false_for_failed_print
    document = flexmock(:print => false)

    ui = View.new(document)
    assert_equal false, ui.print
  end

end

Хотя синтаксис здесь немного другой, основные принципы те же. Мы заменяем document на упрощенную заглушку, которая возвращает значение false для метода print.

RSpec

Имейте в виду, что с RSpec можно использовать и Mocha, и Flex Mock. При этом синтаксис, который используется для создания заглушки в RSpec, похож на аналогичный синтаксис в Mocha. См. листинг 7.

Листинг 7. Создание заглушки с помощью RSpec
document.stub!(:print).and_return(false)

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

Основы создания фиктивных объектов

Как мы уже говорили, создание фиктивного объекта очень похоже на создание заглушки. Различия заключаются в том, что заглушка пассивна — она лишь имитирует реальное решение, которое будет вызываться вместо замененных заглушками методов. Фиктивный объект активен — он реально проверяет, каким образом используется фиктивный объект. Если он используется не так, как предполагается, то тест завершится неудачей. Вот основные шаги при использовании фиктивного объекта:

  1. В контрольном примере замените часть реальной системы заглушкой.
  2. Воспроизведите необходимое реальное поведение заглушки.
  3. Определите ожидания.
  4. После выполнения теста сравните ожидания с полученным результатом.

Вернемся к омметру и новогодним гирляндам. Мы выкрутили лампочку, которая представляет тестируемый объект. Затем заменили гирлянду из лампочек (реальное приложение) на омметр, который символизировал тестируемый объект. Здесь нужно выполнить еще два неочевидных шага. Нам нужен омметр, так как предполагается, что он зафиксирует наличие тока, если лампочка работает. После проведения испытания показания омметра проверяются. Это шаги 3 и 4. На практике среда тестирования выполняет шаг 4 после завершения контрольного примера. Строка псевдокода, описывающая фиктивный объект для нашего приложения печати документов может выглядеть следующим образом:

Для объекта document будет вызван метод print, который возвратит значение "ложь".

Создание фиктивных объектов с помощью Mocha, Flex Mock и RSpec

Часто, если какую-то идею можно сформулировать в одном предложении, ее просто реализовать с помощью одной или двух строк кода Ruby. В листинге 8 показан очень простой код Mocha, заменяющий заглушку из листинга 4 на фиктивный объект.

Листинг 8. Упрощение теста
    Document.any_instance.expects(:print).once.returns(false)

Чтобы отразить мои ожидания, я заменил метод stubs на expects, а также добавил метод once, чтобы показать, сколько раз клиентский код должен вызвать метод print. Данный контрольный пример завершится успешно только в том случае, если тестируемое приложение вызовет метод print для любого представителя класса Document только один раз.

Код Flex Mock для реализации фиктивных объектов практически идентичен коду Mocha, однако названия методов немного другие. Отличия показаны в листинге 9.

Листинг 9. Реализация фиктивного объекта в Flex Mock
    document = flexmock("my document")
    document.should_receive(:print).times(1).and_return(false)

В листинге 10 показано то же самое в RSpec:

Листинг 10. Реализация фиктивного объекта в RSpec
document = mock("my document")
document.should_receive(:print).and_return(false)

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

Заключение

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

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

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


Ресурсы для скачивания


Похожие темы

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Web-архитектура, SOA и web-сервисы
ArticleID=495483
ArticleTitle=Использование заглушек и фиктивных объектов в Ruby on Rails
publish-date=06112010