Какова наилучшая практика, когда дело доходит до тестирования "бесконечных циклов"?
Моя основная логика заключается в том, чтобы иметь бесконечный цикл, где-то работать, и тестировать его как можно лучше. Причина наличия бесконечного цикла не важна (основной цикл для игр, демона-подобная логика...), и я больше спрашиваю о лучших практиках в отношении такой ситуации.
Возьмем этот код, например:
module Blah
extend self
def run
some_initializer_method
loop do
some_other_method
yet_another_method
end
end
end
Я хочу протестировать метод Blah.run
с помощью Rspec (также я использую RR, но простой rspec будет приемлемым ответом).
Я считаю, что лучший способ сделать это - это разложить бит больше, например, отделить цикл от другого метода или что-то еще:
module Blah
extend self
def run
some_initializer_method
do_some_looping
end
def do_some_looping
loop do
some_other_method
yet_another_method
end
end
end
... это позволяет нам протестировать run
и высмеять цикл... но в какой-то момент необходимо проверить код внутри цикла.
Итак, что бы вы сделали в такой ситуации?
Просто не проверяйте эту логику, то есть тест some_other_method
и yet_another_method
, но не do_some_looping
?
Перерыв петли в какой-то момент через макет?
... что-то еще?
Ответы
Ответ 1
Что может быть более практичным, так это выполнить цикл в отдельном потоке, утверждать, что все работает правильно, а затем завершать поток, когда он больше не требуется.
thread = Thread.new do
Blah.run
end
assert_equal 0, Blah.foo
thread.kill
Ответ 2
Как насчет того, чтобы тело цикла было в отдельном методе, например calculateOneWorldIteration
? Таким образом, вы можете вращать цикл в тесте по мере необходимости. И это не повредит API, его вполне естественный метод для публичного интерфейса.
Ответ 3
в rspec 3.3, добавьте эту строку
allow(subject).to receive(:loop).and_yield
до вашего крючка будет простой выход в блок без каких-либо циклов
Ответ 4
Вы не можете проверить что-то, что работает навсегда.
Когда вы сталкиваетесь с частью кода, который трудно (или невозможно) проверить, вы должны: -
- Рефлятор для изоляции сложной тестовой части кода. Сделайте непроверенные части крошечными и тривиальными. Комментарий, чтобы они не были расширены, чтобы стать нетривиальными.
- Unit test другие части, которые теперь отделены от труднодоступной секции
- Трудная для тестирования часть будет покрыта интеграционным или приемочным тестом
Если основной цикл в вашей игре проходит только один раз, это будет сразу же очевидно при запуске.
Ответ 5
Как насчет насмешивания цикла, чтобы он выполнялся только количество раз, которое вы указали?
Module Object
private
def loop
3.times { yield }
end
end
Конечно, вы издеваетесь над этим только в своих спецификациях.
Ответ 6
Я знаю, что это немного старо, но вы также можете использовать метод yields для подделки блока и передать одну итерацию методу цикла. Это должно позволить вам протестировать методы, которые вы вызываете в своем цикле, не вставляя их в бесконечный цикл.
require 'test/unit'
require 'mocha'
class Something
def test_method
puts "test_method"
loop do
puts String.new("frederick")
end
end
end
class LoopTest < Test::Unit::TestCase
def test_loop_yields
something = Something.new
something.expects(:loop).yields.with() do
String.expects(:new).returns("samantha")
end
something.test_method
end
end
# Started
# test_method
# samantha
# .
# Finished in 0.005 seconds.
#
# 1 tests, 2 assertions, 0 failures, 0 errors
Ответ 7
Я почти всегда использую конструкцию catch/throw для тестирования бесконечных циклов.
Возникновение ошибки также может работать, но это не идеально, особенно если ваш цикл цикла блокирует все ошибки, включая Исключения. Если ваш блок не освобождает Exception (или какой-либо другой класс ошибок), вы можете подклассифицировать Exception (или другой невосстановленный класс) и спасти свой подкласс:
Пример исключения
Настройка
class RspecLoopStop < Exception; end
Тест
blah.stub!(:some_initializer_method)
blah.should_receive(:some_other_method)
blah.should_receive(:yet_another_method)
# make sure it repeats
blah.should_receive(:some_other_method).and_raise RspecLoopStop
begin
blah.run
rescue RspecLoopStop
# all done
end
Пример Catch/throw:
blah.stub!(:some_initializer_method)
blah.should_receive(:some_other_method)
blah.should_receive(:yet_another_method)
blah.should_receive(:some_other_method).and_throw :rspec_loop_stop
catch :rspec_loop_stop
blah.run
end
Когда я впервые попробовал это, я был обеспокоен тем, что использование should_receive
во второй раз на :some_other_method
будет "перезаписывать" первую, но это не так. Если вы хотите сами убедиться, добавьте блоки в should_receive
, чтобы узнать, вызвало ли оно ожидаемое количество раз:
blah.should_receive(:some_other_method) { puts 'received some_other_method' }
Ответ 8
:) Несколько месяцев назад у меня был этот запрос.
Короткий ответ - нет простого способа проверить это. Вы проверяете внутренность петли. Затем вы примените метод петли и выполните ручной тест, чтобы цикл работал до завершения условия завершения.
Ответ 9
Самое легкое решение, которое я нашел, - это дать цикл один раз и вернуть. Я использовал мокко здесь.
require 'spec_helper'
require 'blah'
describe Blah do
it 'loops' do
Blah.stubs(:some_initializer_method)
Blah.stubs(:some_other_method)
Blah.stubs(:yet_another_method)
Blah.expects(:loop).yields().then().returns()
Blah.run
end
end
Мы ожидаем, что цикл будет фактически выполнен, и он обеспечит его выход после одной итерации.
Тем не менее, как указано выше, хорошей практикой является сохранение метода петли как можно более малым и глупым.
Надеюсь, это поможет!
Ответ 10
Наше решение для тестирования цикла, которое выходит только из сигналов, заключалось в том, чтобы заглушить метод условия выхода, чтобы вернуть false первый раз, но true второй раз, гарантируя, что цикл выполняется только один раз.
Класс с бесконечным циклом:
class Scheduling::Daemon
def run
loop do
if daemon_received_stop_signal?
break
end
# do stuff
end
end
end
spec, проверяющий поведение цикла:
describe Scheduling::Daemon do
describe "#run" do
before do
Scheduling::Daemon.should_receive(:daemon_received_stop_signal?).
and_return(false, true) # execute loop once then exit
end
it "does stuff" do
Scheduling::Daemon.run
# assert stuff was done
end
end
end