Моделирование условий гонки в модульных тестах RSpec
-
19-09-2019 - |
Вопрос
У нас есть асинхронная задача, которая выполняет потенциально длительные вычисления для объекта.Результат затем кэшируется на объекте.Чтобы предотвратить повторение одной и той же работы несколькими задачами, мы добавили блокировку с помощью атомарного обновления SQL:
UPDATE objects SET locked = 1 WHERE id = 1234 AND locked = 0
Блокировка предназначена только для асинхронной задачи.Сам объект по-прежнему может обновляться пользователем.Если это произойдет, любая незавершенная задача для старой версии объекта должна отбросить ее результаты, поскольку они, вероятно, устарели.Это также довольно легко сделать с помощью атомарного обновления SQL:
UPDATE objects SET results = '...' WHERE id = 1234 AND version = 1
Если объект был обновлен, его версия не будет совпадать, и результаты будут отброшены.
Эти два атомарных обновления должны обрабатывать любые возможные состояния гонки.Вопрос в том, как проверить это в модульных тестах.
Первый семафор легко протестировать, поскольку нужно просто настроить два разных теста с двумя возможными сценариями:(1) когда объект заблокирован и (2) когда объект не заблокирован.(Нам не нужно проверять атомарность SQL-запроса, поскольку за это должен отвечать поставщик базы данных.)
Как проверить второй семафор?Объект должен быть изменен третьей стороной через некоторое время после первого семафора, но до второго.Это потребует паузы в выполнении, чтобы обновление могло выполняться надежно и последовательно, но я не знаю поддержки внедрения точек останова с помощью RSpec.Есть ли способ сделать это?Или есть какой-то другой метод, который я упускаю из виду для моделирования таких условий гонки?
Решение
Вы можете позаимствовать идею у производства электроники и внедрить тестовые крючки прямо в производственный код.Точно так же, как печатная плата может быть изготовлена со специальными местами для испытательного оборудования для контроля и проверки схемы, мы можем сделать то же самое с кодом.
Предположим, у нас есть код, вставляющий строку в базу данных:
class TestSubject
def insert_unless_exists
if !row_exists?
insert_row
end
end
end
Но этот код выполняется на нескольких компьютерах.Таким образом, возникает состояние гонки, поскольку другие процессы могут вставить строку между нашим тестом и нашей вставкой, вызывая исключение DuplicationKey.Мы хотим проверить, обрабатывает ли наш код исключение, возникающее в результате этого состояния гонки.Для этого нашему тесту необходимо вставить строку после вызова row_exists?
но перед вызовом insert_row
.Итак, давайте добавим тестовый хук прямо здесь:
class TestSubject
def insert_unless_exists
if !row_exists?
before_insert_row_hook
insert_row
end
end
def before_insert_row_hook
end
end
При запуске в реальном режиме перехват ничего не делает, а только съедает небольшую часть процессорного времени.Но когда код тестируется на состояние гонки, тестовая обезьяна исправляет before_insert_row_hook:
class TestSubject
def before_insert_row_hook
insert_row
end
end
Разве это не хитро?Подобно личинке осы-паразита, захватившей тело ничего не подозревающей гусеницы, тест похитил тестируемый код, чтобы создать именно те условия, которые нам нужно протестировать.
Эта идея так же проста, как и курсор XOR, поэтому я подозреваю, что многие программисты изобрели ее независимо.Я обнаружил, что это в целом полезно для тестирования кода в условиях гонки.Я надеюсь, что это помогает.