Подтвердить что ты не робот

Моделирование условий гонки в модульных тестах RSpec

У нас есть асинхронная задача, которая выполняет потенциально длительный расчет для объекта. Результат затем кэшируется на объекте. Чтобы предотвратить повторение одной и той же работы несколькими задачами, мы добавили блокировку с помощью атомного обновления 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. Есть ли способ сделать это? Или есть какая-то другая техника, которую я пропускаю для моделирования таких условий гонки?

4b9b3361

Ответ 1

Вы можете взять идею из производства электроники и поставить тестовые крючки непосредственно в производственный код. Подобно тому, как монтажная плата может быть изготовлена ​​со специальными местами для испытательного оборудования для контроля и зондирования схемы, мы можем сделать то же самое с кодом.

Предположим, что у нас есть код, вставляющий строку в базу данных:

class TestSubject

  def insert_unless_exists
    if !row_exists?
      insert_row
    end
  end

end

Но этот код работает на нескольких компьютерах. Там есть условие гонки, так как другие процессы могут вставлять строку между нашим тестом и нашей вставкой, вызывая исключение DuplicateKey. Мы хотим проверить, что наш код обрабатывает исключение, которое возникает в результате этого состояния гонки. Чтобы это сделать, нашему тесту нужно вставить строку после вызова 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, поэтому я подозреваю, что многие программисты самостоятельно ее изобрели. Я нашел, что это вообще полезно для тестирования кода с условиями гонки. Надеюсь, это поможет.