Недавно я тестировал класс сервиса в Ruby, и мне нужно было убедиться, что определенные комбинации условий приводят к желаемым эффектам.
Сначала я начал вставлять условия в контексты, как я это делаю обычно, поэтому мои тесты выглядели следующим образом:
context 'when A1' do
let(:a1) {}
it 'R1' do
# ...
end
context 'and B1' do
let(:b1) {}
it 'R1' do
# ...
end
end
context 'and B2' do
let(:b2) {}
it 'R1' do
# ...
end
end
end
context 'when A2' do
let(:a2) {}
it 'R2' do
# ...
end
context 'and B1' do
let(:b1) {}
it 'R3' do
# ...
end
end
context 'and B2' do
let(:b2) {}
it 'R2' do
# ...
end
end
end
В моем случае был еще один уровень вложенности, поэтому код был действительно нечитабельным и трудным для понимания. Здесь я удалил один уровень и очистил содержимое контекста, чтобы сохранить ваши глаза, дорогой читатель 🙂
Как вы можете видеть, здесь также было много повторений, поэтому первое, что я сделал, это извлек повторяющиеся контексты в блоки shared_context
и повторяющиеся тестовые примеры в shared_examples
, так что код стал похож на этот:
shared_context 'A1' do
let(:a1) {}
end
shared_context 'A2' do
let(:a2) {}
end
shared_context 'B1' do
let(:b1) {}
end
shared_context 'B2' do
let(:b2) {}
end
shared_examples 'R1' do
it 'R1' do
# ...
end
end
shared_examples 'R2' do
it 'R2' do
# ...
end
end
shared_examples 'R3' do
it 'R3' do
# ...
end
end
context 'A1' do
include_context 'A1'
include_examples 'R1'
context 'B1' do
include_context 'B1'
include_examples 'R1'
end
context 'B2' do
include_context 'B2'
include_examples 'R1'
end
end
context 'A2' do
include_context 'A2'
include_examples 'R2'
context 'B1' do
include_context 'B1'
include_examples 'R3'
end
context 'B2' do
include_context 'B2'
include_examples 'R2'
end
end
Хорошо, по крайней мере, теперь при изменении деталей теста или контекстного случая мне не придется обновлять все места.
Однако тесты все еще трудно читать из-за вложенности, поэтому я подумал, что было бы здорово определять каждый сценарий в одной строке вместо того, чтобы просматривать бесконечные вложенные блоки.
Представьте себе что-то вроде приведенного ниже фрагмента. Я перевел вложенные контексты и примеры из предыдущего блока кода так, чтобы в каждой строке последний элемент массива определял ожидаемую группу shared_examples
, а все предыдущие элементы были именами shared_context
, которые должны быть глубоко вложены один за другим:
[
['A1', 'R1'],
['A1', 'B1', 'R1'],
['A1', 'B2', 'R1'],
['A2', 'R2'],
['A2', 'B1', 'R3'],
['A2', 'B2', 'R2'],
].each do |scenario|
# ... do some Ruby + rspec magic here
end
Читается гораздо лучше, не так ли? Давайте посмотрим, как выглядит полное решение:
[
['A1', 'R1'],
['A1', 'B1', 'R1'],
['A1', 'B2', 'R1'],
['A2', 'R2'],
['A2', 'B1', 'R3'],
['A2', 'B2', 'R2'],
].each do |scenario|
# All but last element of the array
contexts = scenario[0..-2]
test_example = scenario.last
# Let's prepare a deeply nested set of Procs with all the required nesting to achieve the same result.
# A tricky thing to understand here is that we need to start from the most nested block and go up,
# until the most outer context.
contexts.reverse.inject(proc { include_examples(test_example) }) do |inner, ctx|
proc do
context ctx do
include_context(ctx, &inner)
end
end
end.call
end
Я понимаю, что это решение может быть очень специфичным для моей проблемы, но, возможно, оно вдохновит вас на рефакторинг ваших тестов и улучшение их читабельности.
Если вы знаете более простой или лучший метод (гем Ruby? встроенная функциональность RSpec?) решения подобных проблем, пожалуйста, сообщите мне об этом в комментарии или личном сообщении.