Pull to refresh
0
0
Антон Сидельников @Meredian

Highload backend developer

Send message
Not bad! Хотя я не помню, честно говоря, когда использовал уже эту запись для создания чего-то другого кроме массивов и объектов.
По теме, сугубо из желания сделать мир лучше, субъективное мнение. Статью закончил читать с некоторым разочарованием. Сам по себе пример вполне жизненный, валидация — добро, и не только для форм :) Но исполнение не очень радует.

К статье:

Зачем двойной листинг с комментариями? Под кат бы его, он только сбивает с толку свои появлением второй раз, когда уже собрался читать дальше :) Да и когда тебе показывают комментарии в духе "# вернет 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» — вроде бы и мелочь, но неопрятно. Еще всякие мелочи бросаются в глаза. Нормальный прототип, но в продакшн в такой виде не пустил бы :)

Если вы хотите опубликовать цикл примеров — заведите репу на гитхабе, выложите их там рядом со всеми необходимым зависимостями и страничкой-примером, вам спасибо большое только скажут :)

if data isnt off

Воу-воу-воу, меньше наворотов! Если они есть — это не значит, что надо применить все сразу :) «if data»

if @errors_list is undefined
    @errors_list = {}
if @errors_list[field] is undefined
    @errors_list[field] = []

Отличные оператор есть ||=
@errors_list ||= {}
@errors_list[field] ||= []

Кстати слово list тут лишнее, да еще и неправда, у вас хэш, а не список :)
gem install commander

Конкретно для 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 :) Там, конечно, понятнее было, но сам факт, что мы расширяли стандартное поведение — на лицо, и нам это было только на руку.
Кстати, пока писал этот сэмпл, полез в код RSpec'а, узнал чем отличаются describe и context.
Правильный ответ: ничем :)
# rspec-core / lib / rspec / core / example_group.rb
class << self
  alias_method :context, :describe
end
Ну в любом случае, что ж вы как не родной-то! Это ж руби, а спеки — ваш локальный код, всегда можно подкрутить чего-нибудь по желанию ;)

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 тут мало при чем, а в этой версии статьи патч спрятан куда-то вниз в одну строчку. Его надо было поднять на самый верх и выделить большими буквами :)

Information

Rating
Does not participate
Location
Россия
Date of birth
Registered
Activity