2010-01-07 7 views
25

Nous avons une tâche asynchrone qui effectue un calcul potentiellement long pour un objet. Le résultat est ensuite mis en cache sur l'objet. Pour éviter des tâches multiples de répéter le même travail, nous avons ajouté de verrouillage avec une mise à jour de SQL atomique:Simulation des conditions de concurrence dans les tests unitaires RSpec

UPDATE objects SET locked = 1 WHERE id = 1234 AND locked = 0 

Le verrouillage est uniquement pour la tâche asynchrone. L'objet lui-même peut encore être mis à jour par l'utilisateur. Si cela se produit, toute tâche inachevée pour une ancienne version de l'objet doit ignorer ses résultats car ils sont probablement périmés. Ceci est également assez facile à faire avec une mise à jour de SQL atomique:

UPDATE objects SET results = '...' WHERE id = 1234 AND version = 1 

Si l'objet a été mis à jour, sa version ne correspond pas à si bien que les résultats seront mis au rebut.

Ces deux mises à jour atomiques doivent gérer toutes les conditions de concurrence possibles. La question est de savoir comment vérifier cela dans les tests unitaires. Le premier sémaphore est facile à tester, puisqu'il suffit de configurer deux tests différents avec les deux scénarios possibles: (1) où l'objet est verrouillé et (2) où l'objet n'est pas verrouillé. (Nous n'avons pas besoin de tester l'atomicité de la requête SQL car cela devrait être la responsabilité du fournisseur de la base de données.)

Comment tester le second sémaphore? L'objet doit être changé par un tiers quelque temps après le premier sémaphore mais avant le second. Cela nécessiterait une pause dans l'exécution afin que la mise à jour puisse être effectuée de manière fiable et cohérente, mais je ne connais aucun support pour l'injection de points d'arrêt avec RSpec. Y a-t-il un moyen de faire cela? Ou y a-t-il une autre technique que je néglige pour simuler de telles conditions de course?

Répondre

26

Vous pouvez emprunter une idée de la fabrication électronique et mettre des crochets de test directement dans le code de production. Tout comme une carte de circuit imprimé peut être fabriquée avec des emplacements spéciaux pour tester et sonder le circuit, nous pouvons faire la même chose avec le code.

Supposons que nous avons un code d'insérer une ligne dans la base de données:

class TestSubject 

    def insert_unless_exists 
    if !row_exists? 
     insert_row 
    end 
    end 

end 

Mais ce code est en cours d'exécution sur plusieurs ordinateurs. Il y a une condition de concurrence, puisqu'un autre processus peut insérer la ligne entre notre test et notre insert, provoquant une exception DuplicateKey. Nous voulons tester que notre code gère l'exception qui résulte de cette condition de concurrence. Pour ce faire, notre test doit insérer la ligne après l'appel à row_exists? mais avant l'appel à insert_row. Donc, nous allons ajouter un crochet de test là:

class TestSubject 

    def insert_unless_exists 
    if !row_exists? 
     before_insert_row_hook 
     insert_row 
    end 
    end 

    def before_insert_row_hook 
    end 

end 

Lorsqu'il est exécuté dans la nature, le crochet ne fait rien à part manger un petit peu de temps CPU. Mais lorsque le code est mis à l'essai pour la condition de course, le before_insert_row_hook singe-patches test:

class TestSubject 
    def before_insert_row_hook 
    insert_row 
    end 
end 

est-ce pas rusé? Comme une larve de guêpe parasite qui a détourné le corps d'une chenille sans méfiance, le test a détourné le code sous test afin qu'il crée la condition exacte dont nous avons besoin testé.

Cette idée est aussi simple que le curseur XOR, donc je suppose que beaucoup de programmeurs l'ont inventé indépendamment. Je l'ai trouvé généralement utile pour tester le code avec les conditions de course. J'espère que ça aide.

+1

A-ha. Cela le ferait.Bien que plutôt que d'ajouter un hook explicite, je pourrais juste utiliser 'alias_method_chain' pour étendre la fonctionnalité d'une méthode qui doit être appelée entre les deux sémaphores de toute façon - la tâche de longue durée. – Ian

+0

Ian, ça le ferait. –

+4

+1 pour l'utilisation de la larve de guêpe parasite dans votre comparaison. – aronchick

Questions connexes