У нас есть асинхронная задача, которая выполняет потенциально продолжительное вычисление для объекта. Результат тогда кэшируется на объекте. Для препятствования нескольким задачам повторить ту же работу мы добавили блокировку с атомарным обновлением 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
, но этот код работает на нескольких компьютерах. Затем есть состояние гонки, поскольку другие процессы могут вставлять строку между нашим тестом и нашей вставкой, вызывая исключение дубликатов. Мы хотим проверить, что наш код обрабатывает исключение, которое является результатом этого состояния гонки. Для этого наш тест должен вставлять строку после вызова на ROW_EXISS?
, но перед вызовом 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
при запуске в дикой природе, крючок ничего не делает, кроме как съесть крошечное количество процессоров. Но когда код протестирован на условие гонки, тестовые обезьяны-патча до_insert_Row_Hook:
class TestSubject
def before_insert_row_hook
insert_row
end
end
Разве это не так sly? Как паразитарная личинка, которая угонала тело ничего не подозреваемого гусеницы, тест угнал код в тесте, чтобы он создал точное состояние, которое нам нужно проверить.
Эта идея так же просто, как курсор XOR, поэтому я подозреваю, что многие программисты независимо изобрели его. Я нашел его, как правило, полезно для тестирования кода с условиями гонок. Я надеюсь, что это помогает.