ruby on rails - Simulación de condiciones de carrera en pruebas unitarias RSpec.
ruby-on-rails unit-testing (1)
Puede tomar prestada una idea de la fabricación de productos electrónicos y poner ganchos de prueba directamente en el código de producción. Al igual que una placa de circuito puede ser fabricada con lugares especiales para que el equipo de prueba controle y pruebe el circuito, podemos hacer lo mismo con el código.
A continuación, tenemos algunos códigos que insertan una fila en la base de datos:
class TestSubject
def insert_unless_exists
if !row_exists?
insert_row
end
end
end
Pero este código se ejecuta en varios equipos. Hay una condición de carrera, entonces, ya que otros procesos pueden insertar la fila entre nuestra prueba y nuestra inserción, causando una excepción DuplicateKey. Queremos probar que nuestro código maneja la excepción que resulta de esa condición de carrera. ¿Para hacer eso, nuestra prueba necesita insertar la fila después de la llamada a row_exists?
pero antes de la llamada a insert_row
. Así que vamos a agregar un gancho de prueba allí:
class TestSubject
def insert_unless_exists
if !row_exists?
before_insert_row_hook
insert_row
end
end
def before_insert_row_hook
end
end
Cuando se ejecuta en la naturaleza, el gancho no hace nada, excepto consumir un poco de tiempo de CPU. Pero cuando el código se está probando para la condición de carrera, la prueba monkey-patches before_insert_row_hook:
class TestSubject
def before_insert_row_hook
insert_row
end
end
¿No es eso astuto? Como una larva de avispa parasitaria que ha secuestrado el cuerpo de una oruga desprevenida, la prueba secuestró el código bajo prueba para que cree la condición exacta que necesitamos probar.
Esta idea es tan simple como el cursor XOR, así que sospecho que muchos programadores la han inventado de manera independiente. He encontrado que es generalmente útil para probar código con condiciones de carrera. Espero que ayude.
Tenemos una tarea asíncrona que realiza un cálculo potencialmente largo para un objeto. El resultado se almacena en caché en el objeto. Para evitar que varias tareas repitan el mismo trabajo, agregamos el bloqueo con una actualización de SQL atómico:
UPDATE objects SET locked = 1 WHERE id = 1234 AND locked = 0
El bloqueo es solo para la tarea asíncrona. El objeto todavía puede ser actualizado por el usuario. Si eso sucede, cualquier tarea no terminada para una versión antigua del objeto debería descartar sus resultados, ya que es probable que estén desactualizados. Esto también es bastante fácil de hacer con una actualización de SQL atómico:
UPDATE objects SET results = ''...'' WHERE id = 1234 AND version = 1
Si el objeto se ha actualizado, su versión no coincidirá y los resultados se descartarán.
Estas dos actualizaciones atómicas deben manejar cualquier posible condición de carrera. La pregunta es cómo verificar que en las pruebas unitarias.
El primer semáforo es fácil de probar, ya que es simplemente una cuestión de configurar dos pruebas diferentes con los dos escenarios posibles: (1) donde el objeto está bloqueado y (2) donde el objeto no está bloqueado. (No necesitamos probar la atomicidad de la consulta SQL, ya que eso debería ser responsabilidad del proveedor de la base de datos).
¿Cómo se prueba el segundo semáforo? El objeto debe ser cambiado por un tercero algún tiempo después del primer semáforo pero antes del segundo. Esto requeriría una pausa en la ejecución para que la actualización pueda realizarse de manera confiable y consistente, pero no conozco soporte para inyectar puntos de interrupción con RSpec. ¿Hay alguna forma de hacer esto? ¿O hay alguna otra técnica que estoy pasando por alto para simular tales condiciones de carrera?