Моделирование условий гонки в юнит-тестах RSpec - PullRequest
27 голосов
/ 07 января 2010

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

1 Ответ

27 голосов
/ 10 января 2010

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

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

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

...