Собственные проверки для RSpec 2
января 23, 2011 | Published in BDD, Тестирование | 7 Comments
Внимание: Это первый мой перевод посвященный теме тестирования и я только становлюсь на стезю изучения данной темы, поэтому перевод может быть несколько неадекватен. Я очень прошу извенить меня за неточности и незнание устоявшейся русскоязычной терминологии касательно тестирования и работы с RSpec в частности. Я также прошу поправлять меня в комментариях, я буду очень благодарен более опытным программистам читающим мой блог за помощь.
RSpec один из моих любимых инструментов. Я без преувеличений влюбился в эту фантастическую библиотеку для BDD тестирования, особенно во вторую версию. Используя RSpec, я понял, что эта библиотека учит меня тому, как правильно писать тесты. Изучая RSpec DSL , его синтаксис и структуру примеров spec’ов (далее по тексту просто спеки) вы фактически изучаете лучшую методику написания тестов. RSpec, несмотря на множество встроенных условий проверки, содержит еще и DSL для объявления ваших собственных проверок, которые заточены по ваши личные, специфичные нужды. Возможно все это звучит несколько запутывающе, однако все достаточно просто, так просто, что в это сложно поверить, пока сам не попробуешь.
Основы
В RSpec проверки (matchers) ничто иное как методы доступные в контексте примера. Вы используете их, чтобы убедиться в том, что результат работы тестируемого кода соответствует переданому ожиданию (проверке, условию при котором тест возвратит значение true, что свидетельствует, что данная проверка пройдена успешно и код работает так, как мы того ожидали). Существует множество проверок, которые уже входят в состав RSpec. В примере ниже показано использование одной из таких проверок respond_to:
describe String do it { should respond_to(:gsub) } end
Этот пример так чист и минималистичен, что мне сложно будет объяснить то, что он делает.
Когда вы вызываете describe с классом в качестве аргумента, RSpec автоматически создаст экземпляр этого класса и сделает его доступным через метод subject. Метод subject по умолчанию является вложенным в блок кода. Вот почему мы не пишем subject.should respond_to(:gsub), потому, что по умолчанию should или should_not вызываются внутри subject.Для того чтобы получить полный список доступных условий проверок посмотрите .
Теперь давайте сфокусируемся на написании собственных проверок. Если вы не понимаете, зачем вам необходимо делать это, позвольте мне показать простой пример спека для модели User:
describe User do before { subject.email = "foobar" } it "should have errors on email" do subject.errors.should have_key(:email) end it "should have correct error message" do subject.errors[:email].should include("Email is invalid") end end
Теперь вы, вероятно, думаете о том, что почти идентичный код мог бы использоваться во множестве других случаев для множества других классов моделей. Эти шесть строк кода могут быть переписаны как одна. Вам лишь необходимо создать собственную проверку — обертку для всего этого.
Объявить собственную проверку очень просто. Давайте начнем с простой проверки, которая проверяет, имеет ли переданный экземпляр модели ошибки в валидации:
RSpec::Matchers.define :have_errors_on do |attribute| match do |model| model.valid? # call it here so we don't have to write it in before blocks model.errors.key?(attribute) end end
И мы можем использовать эту проверку следующим образом:
describe User do before { subject.email = "foobar" } it { should have_errors_on(:email) } end
Пример покрывает только первое условие проверки, но и это уже хорошее начало.
Связывание
Второе условие в примере заключается в том, чтобы увидеть, установлено ли правильное сообщение об ошибки. (Далее следует различать сообщения об ошибках отправляемые валидатором из модели, и об ошибках, которые возвращают тесты в случае, когда тестируемый код возвращает не такой результат, как мы ожидали). Для этого мы можем запускать связку проверок, поэтому давайте рассмотрим то, как можно реализовать связку в нашей собственной проверке:
RSpec::Matchers.define :have_errors_on do |attribute| chain :with_message do |message| @message = message end match do |model| model.valid? @has_errors = model.errors.key?(attribute) if @message @has_errors && model.errors[attribute].include?(@message) else @has_errors end end end
Это реально просто. Обратите внимание на то, что проверка проверяет сразу две вещи и возвращает true только если обе эти проверяемые вещи соответствую ожиданиям: во-первых сообщение об ошибке валидации существует, и во-вторых сообщение об ошибке валидации соответствует ожидаемому.
Давайте, используем нашу собственную проверку:
describe User do before { subject.email = "foobar" } it { should have_errors_on(:email).with_message("Email has an invalid format") } end
Отлично! Одна строка вместо шести! Но это не все, сообщение об ошибке возвращаемое спеком должно иметь следующий вид:
F Failures: 1) User when email is not valid Failure/Error: it { should have_errors_on(:email).with_message("Email has an invali format") } expected # to have errors on :email Finished in 0.00047 seconds 1 examples, 1 failure
Оно автоматически генерируется RSpec на основе имени проверки. Все в порядке, однако но сообщение об ошибке не скажет нам о том, правильно ли сообщение об ошибке валидации. Вот почему нам необходимо создать собственное сообщение об ошибке.
Собственное сообщение об ошибке. Как его сделать.
Написание осмысленных сообщений об ошибках — это реально полезная практика. Мы должны установить два типа сообщений, первый для should и второй для should_not ожидаемых значений. Идея состоит в том, что если здесь ошибка и сообщение не правильное, то нам нужно показать эту информацию при выводе информации об ошибках в консоль.
Таким образом, мы имеем следующий окончательный код для нашей собственной проверки:
RSpec::Matchers.define :have_errors_on do |attribute| chain :with_message do |message| @message = message end match do |model| model.valid? @has_errors = model.errors.key?(attribute) if @message @has_errors && model.errors[attribute].include?(@message) else @has_errors end end failure_message_for_should do |model| if @message "Validation errors #{model.errors[attribute].inspect} should include #{@message.inspect}" else "#{model.class} should have errors on attribute #{attribute.inspect}" end end failure_message_for_should_not do |model| "#{model.class} should not have an error on attribute #{attribute.inspect}" end end
В этот раз, если мы запустим наш пример, и он вернет ошибку, потому, что сообщение об ошибке при валидации не соответствует ожидаемому, то мы получим следующее сообщение об ошибке на выводе:
F Failures: 1) User Failure/Error: it { should have_errors_on(:email).with_message("Email has an invalid format") } Validation errors ["Email is blah"] should include "Email has an invalid format" # ./_examples/rspec2_matchers.rb:55:in `block (2 levels) in ' Finished in 0.00053 seconds 1 example, 1 failure
Подводим итоги
Как вы видите реализация собственной проверки для RSpec проста тривиальна и это очень рекомендуемая практика. Существует много случаев, в которых програмисту необходимо использование собственных проверок. Это делает ваши спеки чистыми и более читабельными, и что самое важное это то, что код ваших спеков соответствует принципу DRY и может с легкостью расширяться и редактироваться.
Оригинал статьи на английском:
P.S. Не умеишь работать с тестами — не писай на них! (К теме о том, что многие люди говорят, о том, что тесты это лишнее) ;-)
Лучшая благодарность автору блога — ваши безценные комментарии!
января 23, 2011 at 23:58 (#)
Хорошая статья.
Думаю, что книга The Rspec Book должна очень сильно помочь людям, которые считают тесты необходимым моментом в своей работе. В последнее время пришёл к мысли, что быстро и качественно писать тесты должен уметь каждый программист. Фактически написание спеков — изложение требуемого поведения в виде кода.
января 24, 2011 at 00:32 (#)
А перевод удобоваримый?
Я пока изучаю тестирование только потому, что в вакансиях часто это одно из обязательных требований. Поднабравшись опытом смогу сказать действительно ли оно критично или без него можно обойтись.
января 24, 2011 at 20:22 (#)
Перевод у меня проблем не вызвал.
января 25, 2011 at 17:18 (#)
А RSpec можно поставить на Убунту?
января 25, 2011 at 23:56 (#)
А то. Это же обычный ruby gem.
января 26, 2011 at 00:14 (#)
Как уже ответил alvir, rspec — это ruby gem. RubyGems — расширения, библиотеки языка Ruby, которые за очень редким исключением являются кроссплатформенными. Т.е. вы можете использовать RSpec 2 на любой платформе, на которой работает сам Ruby.
мая 9, 2011 at 09:11 (#)
to admin
> RubyGems – расширения, библиотеки языка Ruby, которые за очень редким исключением являются кроссплатформенными
ORLY? Может таки с точностью «до наоборот»? Редко НЕ являються кросплатформенными.
Насчет написания своих матчеров — это последнее, чем я буду заниматься при тестировании.
Гм… думаю такая практика будет востребована довольно редко ибо сам RSpec2 имеет довольно большой выбор матчеров. Свидетельство тому, что даже автор не смог придумать самостоятельного примера потому как уже есть
Почему эту практику я буду использовать последней? Потому, что ей будет предшествовать написание хелпера should_have_errors_on прямо в примере если его код используется более 2х раз(правило третьего удара) и последующее вынесение его в supports/helpers.rb если он используеться в других примерах.
Не стоит тратить свое время и силы на «правильные», я бы сказал «идеальные» практики. Это может стать самоцелью или даже навяжчевой идееей ;-)
Кстати, вопрос залу. Придумайте пример который бы действительно нуждался в написании _нового_ матчера которого нет в rspec.