По теме, сугубо из желания сделать мир лучше, субъективное мнение. Статью закончил читать с некоторым разочарованием. Сам по себе пример вполне жизненный, валидация — добро, и не только для форм :) Но исполнение не очень радует.
К статье:
Зачем двойной листинг с комментариями? Под кат бы его, он только сбивает с толку свои появлением второй раз, когда уже собрался читать дальше :) Да и когда тебе показывают комментарии в духе "# вернет true, если число больше заданного", невольно начинаешь чувствовать себе идиотом.
Тема классов не раскрыта совсем. Даже конструктора нет, не говоря про полиморфизм и т.д., в вашем примере никакой разницы не было бы, даже если все переписать на глобальные функции переменные. Вы даже после создания инстанса сразу уничтожаете класс :)
validation = new validation
По коду:
Чем вам так нравятся инверсные условия? Я лично в таким нагромождениях уже не могу с ходу их разобрать:
unless result in [true,false]
...
# если не просили пустую строку, но ничего не получили
else unless 'required' not in rules and str.length is 0
if result is no
Что, простите? О_о Это не конкурс головоломок, это же код. К слову, комментарий тут под стать коду и тоже ломает мозг(не просили, но не получили...?!). Мне лично кажется, что в этом условии даже есть ошибка. (Правило внесения отрицания под скобки: !(a && b) раскрывается как !a || !b)
Зачем ошибки отделены от класса в отдельный внешний объект? Это же часть логики класса, хранили бы их в статическом поле класса.
Почтой валидатор не пропустит мой ящик :) Объект класса назван с маленькой буквы, да и правильнее назвать «Validator» — вроде бы и мелочь, но неопрятно. Еще всякие мелочи бросаются в глаза. Нормальный прототип, но в продакшн в такой виде не пустил бы :)
Если вы хотите опубликовать цикл примеров — заведите репу на гитхабе, выложите их там рядом со всеми необходимым зависимостями и страничкой-примером, вам спасибо большое только скажут :)
Конкретно для CLI более развесистая либа, автоаннотации, поддержка сокращений и т.д., плюс некоторые инструменты для общения с пользователем. В целом — рекомендую, очень качественная штука.
В прочем, это-то пример мелковат, полчается всего 9998 знаков, но я что-то предложил, что калькулятор виндовый на плавающей точке сделан, там бы мог дать какой попало ответ.
Интересно, а оно делится на 16127?:) Это я к тому, что такой ответ совершенно бесполезен для вычислительной математики, запись через мантису дает условную точность, позволяя понять лишь порядок числа, но не точное значение.
В общем-то раскладывать на делители надо только основания степеней. Оно и понятно, у степени числа точно такой же набор делителей, как и у самого числа. Но складывать числа с несколькими тысячами знаков — тоже та еще проблема.
Вообще говоря, даже бранчей как таковых нет, по сути все, что есть — это ссылки на коммиты. :) Есть лишь одно здоровенное дерево коммитов и садовый инвентарь для его подстригания и причесывания :)
Каждый коммит имеет цепь предков, которая сходится к init-коммиту. В итоге образуется дерево коммитов, которым мы манипулируем. Имена бранчей (и не только) — это всего лишь указатели на листья этого дерева. Не даром все «master», «develop», «origin/master», «HEAD», «HEAD~2», «07abc92f» абсолютно взаимозаменяемы :)
Во-первых ветка origin/master — локальная, всего лишь копия серверной, ее легко вернуть на место используя git fetch. Во-вторых она не изменяется. Команда означает «установить HEAD в состояние origin/master», origin/master остается при своих.
Простите за банальность, но традиционно, про html и регекспы
Вас спасает надежда на то, что пользователь не будет злоупотреблять, но вообще когда-нибудь оно обязательно сломается ;)
В целом согласен, но мне кажется, что если это решение будет использоваться в коде регулярно, а не 1-2 раза, то почему бы и нет — и человек рано или поздно увидит и этот код с переопределением контекста, или спросит «почему везде так, а у нас не так», или заметит потом, что написанные им на другом проекте работают не так, как надо (хотя потратит час на дебаг — но всего лишь один раз :) )
У меня был схожий случай, когда сам дописал нетривиальный метод в RSpec, для вызова оригинального метода при использовании should_receive в сложных тестах, но тут вышла версия 2.12, где появился метод and_call_original :) Там, конечно, понятнее было, но сам факт, что мы расширяли стандартное поведение — на лицо, и нам это было только на руку.
Ну в любом случае, что ж вы как не родной-то! Это ж руби, а спеки — ваш локальный код, всегда можно подкрутить чего-нибудь по желанию ;)
require 'rspec'
class RSpec::Core::ExampleGroup
class << self
alias_method :old_context, :context
end
def self.context *args, &block
args[0].is_a?(Symbol) ? old_context(args[0].to_s, *args, &block) : old_context(*args, &block)
end
end
RSpec.configure do |config|
config.treat_symbols_as_metadata_keys_with_true_values = true
end
shared_context "shared_context", :key do
let(:val) { 2 }
end
describe 'shared context' do
let(:val) { 1 }
context :key do
specify { val.should == 2 }
end
context :other_key do
specify { val.should == 1 }
end
end
Ну мы не тестировали саму EM, мы используем очень ограниченный её функционал — она обеспечивает нам на сервере глобальную очередь c таймерами, собственно всё что нам надо — запустить эвентмашину и поднять эту самую очередь. А потому всё те же stub и should_receive и should change, никакой особой специфики. Как протестировать написанный на ней вебсервер я до сих пор толком не знаю :)
Агитирую вас не использовать пустые контексты, пусть всегда будут именованые — ведь эта группировка не с потолка же берется, должна быть за ней какая-то логика.
Да, действительно опечатка. А вот с неявным инклюдом shared_context — это очень крутая фича, о которой, собственно, и речь, а автор поста не обратил на неё особого внимания.
Смотрите, вот рабочий тест, специально написал и проверил, и вам заодно покажу :)
require 'rspec'
shared_context "shared_context", state: :a do
let(:val) { 2 }
end
describe 'shared context' do
let(:val) { 1 }
context 'included by metadata', state: :a do
specify { val.should == 2 }
end
context 'not included with distinct metadata', state: :b do
specify { val.should == 1 }
end
end
У shared_context явно прописана метадата {state => :a}. Соответственно, в те контексты, которые имеют такую метадату (а соответственно и во все контексты-потомки) подключится этот shared_context. Тут есть сложные вопросы, как работает матч (полное совпадение, частичное? в каком порядке инклюдятся эти контексты, и т.д., но суть ясна.
Как это использовать — довольно очевидно. Например, у нас есть код, работающий с логикой, и часть этого кода активно использует EventMachine. Для таких тестов требуется отдельная инициализация этой самой эвент-машины, что вы и вынесли в shared_context.
К слову, есть такой отличный ключ в конфиге rspec'а:
RSpec.configure do |config|
config.treat_symbols_as_metadata_keys_with_true_values = true
end
Можете сами догадаться, что он делает :D
Теперь тесты, подключающие эвентмашину, стали выглядеть так:
context 'some logic state', :eventmachine do
..
end
Соответствующий код перекочевал в shared_context, обосновался в своем файлике с прозрачным именем, и мы выкосили его из конфига в спек-хелпере (раньше ручками проверяли метадату и хачили окружение теста как надо)
1. Про «примеры не очень» сказали, имхо имело смысл начать с let, subject и one-liner'ов, и везде ниже их использовать
2. Про shared_context не слышал, звучит интересно, но из описания тут не очень понял, как конкретно работает фича, пришлось лезть в доки, больше внимания деталям :)
3. Макросы можно было, наверное, не описывать — велика вероятность, что они скоро попадут в deprecated, а shared_example действительно целиком замещают их функционал.
4. В форматировании однострочников ошибка, там лишний отступ для It'ов и лишний end в конце :)
От себя, про важные мелочи:
Про describe: В сэмплах выше использовался describe со стрингой, но он умеет больше. В качестве параметра может принять класс klass, тогда он сам заинит subject как klass.new и будет поддерживать нужный неймспейс внутри тестов. Причем его можно дернуть внутри контекста существующих модулей, например. Очень удобно:
module Project
module Models
describe User do # subject стал User.new
its(:value) { should be_nil }
specify { expect { subject.test }.to raise_error Errors::UserError }
# Не Project::Models::Errors::UserError
end
end
end
Про context: контекст — примерно то же самое, что и describe, кроме того что не изменяет subject. Хорошая практика их использовани — когда у нас контекст целиком состоит из атомарных спек (те самые one-liner'ы), а конкретное описание теста складывается из имен вложенных контекстов. См. примеры ниже про let и subject
Про let и subject: собственно отличаются они лишь тем, что subject указывает на тестируемый объект и в однострочниках expectation'ы автоматически связываются именно с ним, если вызваны не на каком-то конкретном объекте. Есть важные замечание про их природу.
let — это лямбда, которая лениво вычисляется при первом обращении внутри теста, и в let сохраняется результат вычисления. Так что если у вас есть какое-то тяжелое вычисление, которое может просадить тесты — не бойтесь запихать его в let, оно выполнится только если необходимо в тесте.
let'ы можно вызывать внутри друг друг, переопределять внутри контекста и т.д.
context "#request" do
let(:type) { 'correct_type' }
let(:params) { {type: type} }
subject { Interface.new.request(params) }
it { should be_ok }
context "with incorrect type" do
let(:type) { 'madness' }
it { should_not be_ok }
end
end
Можно хитрить и заставить let возвращать лямбду, тогда можно сократить на синтаксисе еще немного :) Не всегда это хорошо, но иногда уместно.
context "#request" do
let(:interface_call) { ->{ subject.some_call(type) } }
context "with incorrect type" do
let(:type) { 'Invalid type' }
specify { interface_call.should raise_error Errors::InvalidTypeError }
end
end
Только стоит помнить, что let вычисляется ровно один раз. Потому если вы решите его использовать как шорт-кат для измерения какого-то значения, то вас ждет провал :)
context "#add" do
let(:value) { subject.some_hardly_accessible_value.to_i }
specify { subject.add(20).should chage { value }.by(20) }
# Провалится, value вычислится один раз и больше не изменится
end
Важно помнить, что let вычисляется лениво, и если его выполнение имеет какие-то сайд-эффекты, то ленивое вычисление может привести к наступанию на грабли. Пример не очень хорош, но пояснит идею.
context "#collect_ids" do #Например, метод собирает из базы айдишники всех пользователей.
let(:user) { Fabricate(:user) } # Фабрикатором cоздается запись про юзера в базке
specify { User.all.collect_ids.should include user.id }
end
Эта спека провалится. Связано, очевидно, с тем, что список всех айдишников составляется до того, как будет вызвал let(:user), и на базке будет создан юзер. Соответственно делать let'ы надо без сайд-эффектов, что б на такие грабли не наступать. Ну или помнить о них. Чувствительные сайд-эффекты лучше явно указать в before.
В общем, let'ы очень удобны и довольно мощны, рекомендую их щедро использовать.
Пояснение про shared_context: Помимо того, что это переносной контекст, обращу внимание на одну фичу, которая из текста не очень ясна. Существует два метода его использования в спеках.
shared_context "shared_context", state: :a do # в метаданных указан state: :a - это важно
# some context
end
describe "#direct include" do
include_context "shared stuff" # прямо инклюдим shared_context в наш текущий контекст
context "subcontext_with_a_state", state: :a do
#some specs
end
context "subcontext_with_b_state", state: :b do
#other specs
end
#оба контекста будут включать в себя shared_context
end
describe "#metadata include" do
context "subcontext_with_a_state", state: :a do
#some specs
end
context "subcontext_with_b_state", state: :b do
#other specs
end
#только первый контекст включит в себя shared_context, так как совпадет по метадате
end
Я не 100% уверен в точности интерпретации, нет возможности потестить, но из док получается так
Из кратких замечаний наверное все %)
Еще хотел бы прочитать чужое мнение про тестирование методов с помощью should_receive — там вечные проблемы с лаконичностью и все не очень просто. Может и сам напишу вариант, попозже :)
Ну, кстати, falcon patch дал хороший прирост. Я замерял по спекам от рабочего проекта, у нас тут 600+ спек, которые выполняются 4m 30s, стало в районе 3m20s, одобряю.
И, собственно, это главная фича, -O3 тут мало при чем, а в этой версии статьи патч спрятан куда-то вниз в одну строчку. Его надо было поднять на самый верх и выделить большими буквами :)
К статье:
Зачем двойной листинг с комментариями? Под кат бы его, он только сбивает с толку свои появлением второй раз, когда уже собрался читать дальше :) Да и когда тебе показывают комментарии в духе "# вернет true, если число больше заданного", невольно начинаешь чувствовать себе идиотом.
Тема классов не раскрыта совсем. Даже конструктора нет, не говоря про полиморфизм и т.д., в вашем примере никакой разницы не было бы, даже если все переписать на глобальные функции переменные. Вы даже после создания инстанса сразу уничтожаете класс :)
По коду:
Чем вам так нравятся инверсные условия? Я лично в таким нагромождениях уже не могу с ходу их разобрать:
Что, простите? О_о Это не конкурс головоломок, это же код. К слову, комментарий тут под стать коду и тоже ломает мозг(не просили, но не получили...?!). Мне лично кажется, что в этом условии даже есть ошибка. (Правило внесения отрицания под скобки: !(a && b) раскрывается как !a || !b)
Зачем ошибки отделены от класса в отдельный внешний объект? Это же часть логики класса, хранили бы их в статическом поле класса.
Почтой валидатор не пропустит мой ящик :) Объект класса назван с маленькой буквы, да и правильнее назвать «Validator» — вроде бы и мелочь, но неопрятно. Еще всякие мелочи бросаются в глаза. Нормальный прототип, но в продакшн в такой виде не пустил бы :)
Если вы хотите опубликовать цикл примеров — заведите репу на гитхабе, выложите их там рядом со всеми необходимым зависимостями и страничкой-примером, вам спасибо большое только скажут :)
Воу-воу-воу, меньше наворотов! Если они есть — это не значит, что надо применить все сразу :) «if data»
Отличные оператор есть
||=Кстати слово list тут лишнее, да еще и неправда, у вас хэш, а не список :)
Конкретно для CLI более развесистая либа, автоаннотации, поддержка сокращений и т.д., плюс некоторые инструменты для общения с пользователем. В целом — рекомендую, очень качественная штука.
Каждый коммит имеет цепь предков, которая сходится к init-коммиту. В итоге образуется дерево коммитов, которым мы манипулируем. Имена бранчей (и не только) — это всего лишь указатели на листья этого дерева. Не даром все «master», «develop», «origin/master», «HEAD», «HEAD~2», «07abc92f» абсолютно взаимозаменяемы :)
Вас спасает надежда на то, что пользователь не будет злоупотреблять, но вообще когда-нибудь оно обязательно сломается ;)
У меня был схожий случай, когда сам дописал нетривиальный метод в RSpec, для вызова оригинального метода при использовании should_receive в сложных тестах, но тут вышла версия 2.12, где появился метод and_call_original :) Там, конечно, понятнее было, но сам факт, что мы расширяли стандартное поведение — на лицо, и нам это было только на руку.
Правильный ответ: ничем :)
Агитирую вас не использовать пустые контексты, пусть всегда будут именованые — ведь эта группировка не с потолка же берется, должна быть за ней какая-то логика.
Смотрите, вот рабочий тест, специально написал и проверил, и вам заодно покажу :)
У shared_context явно прописана метадата {state => :a}. Соответственно, в те контексты, которые имеют такую метадату (а соответственно и во все контексты-потомки) подключится этот shared_context. Тут есть сложные вопросы, как работает матч (полное совпадение, частичное? в каком порядке инклюдятся эти контексты, и т.д., но суть ясна.
Как это использовать — довольно очевидно. Например, у нас есть код, работающий с логикой, и часть этого кода активно использует EventMachine. Для таких тестов требуется отдельная инициализация этой самой эвент-машины, что вы и вынесли в shared_context.
К слову, есть такой отличный ключ в конфиге rspec'а:
Можете сами догадаться, что он делает :D
Теперь тесты, подключающие эвентмашину, стали выглядеть так:
Соответствующий код перекочевал в shared_context, обосновался в своем файлике с прозрачным именем, и мы выкосили его из конфига в спек-хелпере (раньше ручками проверяли метадату и хачили окружение теста как надо)
Из замечаний:
1. Про «примеры не очень» сказали, имхо имело смысл начать с let, subject и one-liner'ов, и везде ниже их использовать
2. Про shared_context не слышал, звучит интересно, но из описания тут не очень понял, как конкретно работает фича, пришлось лезть в доки, больше внимания деталям :)
3. Макросы можно было, наверное, не описывать — велика вероятность, что они скоро попадут в deprecated, а shared_example действительно целиком замещают их функционал.
4. В форматировании однострочников ошибка, там лишний отступ для It'ов и лишний end в конце :)
От себя, про важные мелочи:
Про describe: В сэмплах выше использовался describe со стрингой, но он умеет больше. В качестве параметра может принять класс klass, тогда он сам заинит subject как klass.new и будет поддерживать нужный неймспейс внутри тестов. Причем его можно дернуть внутри контекста существующих модулей, например. Очень удобно:
Про context: контекст — примерно то же самое, что и describe, кроме того что не изменяет subject. Хорошая практика их использовани — когда у нас контекст целиком состоит из атомарных спек (те самые one-liner'ы), а конкретное описание теста складывается из имен вложенных контекстов. См. примеры ниже про let и subject
Про let и subject: собственно отличаются они лишь тем, что subject указывает на тестируемый объект и в однострочниках expectation'ы автоматически связываются именно с ним, если вызваны не на каком-то конкретном объекте. Есть важные замечание про их природу.
let — это лямбда, которая лениво вычисляется при первом обращении внутри теста, и в let сохраняется результат вычисления. Так что если у вас есть какое-то тяжелое вычисление, которое может просадить тесты — не бойтесь запихать его в let, оно выполнится только если необходимо в тесте.
let'ы можно вызывать внутри друг друг, переопределять внутри контекста и т.д.
Можно хитрить и заставить let возвращать лямбду, тогда можно сократить на синтаксисе еще немного :) Не всегда это хорошо, но иногда уместно.
Только стоит помнить, что let вычисляется ровно один раз. Потому если вы решите его использовать как шорт-кат для измерения какого-то значения, то вас ждет провал :)
Важно помнить, что let вычисляется лениво, и если его выполнение имеет какие-то сайд-эффекты, то ленивое вычисление может привести к наступанию на грабли. Пример не очень хорош, но пояснит идею.
Эта спека провалится. Связано, очевидно, с тем, что список всех айдишников составляется до того, как будет вызвал let(:user), и на базке будет создан юзер. Соответственно делать let'ы надо без сайд-эффектов, что б на такие грабли не наступать. Ну или помнить о них. Чувствительные сайд-эффекты лучше явно указать в before.
В общем, let'ы очень удобны и довольно мощны, рекомендую их щедро использовать.
Пояснение про shared_context: Помимо того, что это переносной контекст, обращу внимание на одну фичу, которая из текста не очень ясна. Существует два метода его использования в спеках.
Я не 100% уверен в точности интерпретации, нет возможности потестить, но из док получается так
Из кратких замечаний наверное все %)
Еще хотел бы прочитать чужое мнение про тестирование методов с помощью should_receive — там вечные проблемы с лаконичностью и все не очень просто. Может и сам напишу вариант, попозже :)
И, собственно, это главная фича, -O3 тут мало при чем, а в этой версии статьи патч спрятан куда-то вниз в одну строчку. Его надо было поднять на самый верх и выделить большими буквами :)