enum_cases
необходимо поддерживать в актуальном состоянии при изменении логики производства c, нарушающей принцип DRY . Это делает больше вероятной ошибкой. Кроме того, это тестовый код, работающий в производстве, еще один красный флаг.
Мы можем решить эту проблему путем рефакторинга дела в поиске Ha sh, сделав его управляемым данными . А также присвоение ему имени, описывающего, с чем оно связано и что оно делает, это «обработчики». Я также превратил его в вызов метода, который облегчает доступ и который принесет плоды позже.
def foo_handlers
{
a: 1,
b: 2,
c: 3
}.freeze
end
def foo(input)
foo_handlers.fetch(input)
rescue KeyError
raise NotImplementedError, "Unhandled new case: #{input}"
end
Hash#fetch
используется для поднятия KeyError
, если ввод не найден.
Затем мы можем написать управляемый данными тест, выполнив цикл, не foo_handlers
, а кажущийся избыточным expected
Ха sh, определенный в тестах.
class FooHelperTests < ActionView::TestCase
test "foo handles all expected inputs" do
expected = {
a: 1,
b: 2,
c: 3
}.freeze
# Verify expect has all the cases.
assert_equal expect.keys.sort, foo_handlers.keys.sort
# Drive the test with the expected results, not with the production data.
expected.keys do |key|
# Again, using `fetch` to get a clear KeyError rather than nil.
assert_equal foo(key), expected.fetch(value)
end
end
# Simplify the tests by separating happy path from error path.
test "foo raises NotImplementedError if the input is not handled" do
assert_raises NotImplementedError do
# Use something that obviously does not exist to future proof the test.
foo(:does_not_exist)
end
end
end
Избыточность между expected
и foo_handlers
это дизайн. Вам все еще нужно поменять пары в обоих местах, этого не избежать, но теперь вы всегда получите ошибку, когда foo_handlers
изменится, а тесты - нет.
- Когда новый ключ Пара / значение добавляется к
foo_handlers
, тест не пройден. - Если в
expected
отсутствует ключ, тест не пройден. - Если кто-то случайно уничтожит
foo_handlers
тест потерпит неудачу. - Если значения в
foo_handlers
неверны, тест не пройден. - Если лог c из
foo
сломан, тест не пройден.
Сначала вы просто скопируете foo_handlers
в expected
. После этого это становится регрессионным тестом , проверяющим, что код все еще работает даже после рефакторинга. Будущие изменения будут постепенно меняться foo_handlers
и expected
.
Но подождите, это еще не все! Код, который сложно протестировать, вероятно, трудно использовать. И наоборот, код, который легко протестировать, прост в использовании. С помощью нескольких дополнительных настроек мы можем использовать этот подход, основанный на данных, чтобы сделать производственный код более гибким.
Если мы сделаем foo_handlers
аксессором со значением по умолчанию, исходящим от метода, а не от константы, теперь мы можем изменить поведение foo
для отдельных объектов. Это может или не может быть желательным для вашей конкретной реализации, но оно есть в вашей панели инструментов.
class Thing
attr_accessor :foo_handlers
# This can use a constant, as long as the method call is canonical.
def default_foo_handlers
{
a: 1,
b: 2,
c: 3
}.freeze
end
def initialize
@foo_handlers = default_foo_handlers
end
def foo(input)
foo_handlers.fetch(input)
rescue KeyError
raise NotImplementedError, "Unhandled new case: #{input}"
end
end
Теперь отдельные объекты могут определять свои собственные обработчики или изменять значения.
thing = Thing.new
puts thing.foo(:a) # 1
puts thing.foo(:b) # 2
thing.foo_handlers = { a: 23 }
puts thing.foo(:a) # 23
puts thing.foo(:b) # NotImplementedError
И что более важно, подкласс может изменить свои обработчики. Здесь мы добавляем обработчики, используя Hash#merge
.
class Thing::More < Thing
def default_foo_handlers
super.merge(
d: 4,
e: 5
)
end
end
thing = Thing.new
more = Thing::More.new
puts more.foo(:d) # 4
puts thing.foo(:d) # NotImplementedError
Если ключ требует более простого значения, используйте имена методов и вызывайте их с помощью Object#public_send
. Затем эти методы могут быть проверены модулем.
def foo_handlers
{
a: :handle_a,
b: :handle_b,
c: :handle_c
}.freeze
end
def foo(input)
public_send(foo_handlers.fetch(input), input)
rescue KeyError
raise NotImplementedError, "Unhandled new case: #{input}"
end
def handle_a(input)
...
end
def handle_b(input)
...
end
def handle_c(input)
...
end