Comment utiliser junit pour exécuter le test de concurrence?
Disons que j'ai une classe
public class MessageBoard
{
public synchronized void postMessage(String message)
{
....
}
public void updateMessage(Long id, String message)
{
....
}
}
Je veux tester plusieurs accès à ce postMessage simultanément. Un conseil à ce sujet? Je souhaite exécuter ce type de test de concurrence par rapport à toutes mes fonctions de définition (ou toute méthode impliquant une opération de création/mise à jour/suppression).
Malheureusement, je ne pense pas que vous puissiez définitivement prouver que votre code est thread-safe en utilisant des tests d'exécution. Vous pouvez lancer autant de threads que vous le souhaitez contre, et cela peut/peut ne pas passer selon la planification.
Vous devriez peut-être regarder certains outils d'analyse statique, tels que PMD , qui peuvent déterminer comment vous utilisez la synchronisation et identifier les problèmes d'utilisation.
Je recommanderais d'utiliser MultithreadedTC - Écrit par le maître de la concurrence lui-même Bill Pugh (et Nat Ayewah ). Citation de leur aperç :
MultithreadedTC est un cadre pour tester des applications simultanées. Il dispose d'un métronome qui est utilisé pour fournir un contrôle précis sur la séquence d'activités dans plusieurs threads.
Ce framework vous permet de tester de manière déterministe chaque entrelacement de threads dans des tests séparés
Vous ne pouvez prouver que la présence de bugs simultanés, pas leur absence.
Cependant, vous pouvez écrire un exécuteur de test spécialisé qui génère plusieurs threads simultanés, puis appelle vos méthodes annotées @Test.
Dans .NET, il existe des outils tels que TypeMock Racer ou Microsoft CHESS qui sont conçus spécifiquement pour les tests unitaires simultanés. Ces outils non seulement trouvent des bogues multithreads comme les blocages, mais vous fournissent également l'ensemble d'entrelacements de threads qui reproduisent les erreurs.
J'imagine qu'il y a quelque chose de similaire pour le monde Java.
L'exécution simultanée peut entraîner des résultats inattendus. Par exemple, je viens de découvrir que, bien que ma suite de tests avec 200 tests réussisse lorsqu'elle est exécutée une par une, elle échoue pour une exécution simultanée, je l'ai creusée et ce n'était pas un problème de sécurité des threads, mais un test dépendant d'un autre, qui est une mauvaise chose et je pourrais résoudre le problème.
Le travail de Mycila sur JUnit ConcurrentJunitRunner et ConcurrentSuite est très intéressant. L'article semble un peu dépassé par rapport à la dernière version GA, dans mes exemples, je vais montrer l'utilisation mise à jour.
L'annotation d'une classe de test comme la suivante entraînera l'exécution simultanée de méthodes de test, avec un niveau de concurrence de 6:
import com.mycila.junit.concurrent.ConcurrentJunitRunner;
import com.mycila.junit.concurrent.Concurrency;
@RunWith(ConcurrentJunitRunner.class)
@Concurrency(6)
public final class ATest {
...
Vous pouvez également exécuter toutes les classes de test simultanément:
import com.mycila.junit.concurrent.ConcurrentSuiteRunner;
@RunWith(ConcurrentSuiteRunner.class)
@Suite.SuiteClasses({ATest.class, ATest2.class, ATest3.class})
public class MySuite {
}
dépendance Maven est:
<dependency>
<groupId>com.mycila</groupId>
<artifactId>mycila-junit</artifactId>
<version>1.4.ga</version>
</dependency>
J'étudie actuellement comment exécuter les méthodes plusieurs fois et simultanément avec ce package. Il est peut-être déjà possible, si quelqu'un a un exemple de me le faire savoir, ci-dessous ma solution homebrewed.
@Test
public final void runConcurrentMethod() throws InterruptedException {
ExecutorService exec = Executors.newFixedThreadPool(16);
for (int i = 0; i < 10000; i++) {
exec.execute(new Runnable() {
@Override
public void run() {
concurrentMethod();
}
});
}
exec.shutdown();
exec.awaitTermination(50, TimeUnit.SECONDS);
}
private void concurrentMethod() {
//do and assert something
}
Comme d'autres l'ont noté, il est vrai que vous ne pouvez jamais être sûr qu'un bogue de concurrence s'affiche ou non, mais avec des dizaines de milliers ou des centaines de milliers d'exécutions avec une concurrence de, disons 16, les statistiques sont de votre côté.
Essayez de regarder ActiveTestSuite qui est livré avec JUnit. Il peut lancer simultanément plusieurs tests JUnit:
public static Test suite()
{
TestSuite suite = new ActiveTestSuite();
suite.addTestSuite(PostMessageTest.class);
suite.addTestSuite(PostMessageTest.class);
suite.addTestSuite(PostMessageTest.class);
suite.addTestSuite(PostMessageTest.class);
suite.addTestSuite(PostMessageTest.class);
return suite;
}
Ce qui précède exécutera la même classe de test JUnit 5 fois en parallèle. Si vous vouliez des variations dans vos tests parallèles, créez simplement une classe différente.
Dans votre exemple, la méthode postMessage () est synchronized, vous ne verrez donc aucun effet de concurrence à l'intérieur d'un seul VM mais vous pourriez être en mesure d'évaluer la performances de la version synchronisée.
Vous devrez exécuter plusieurs copies du programme de test en même temps sur différentes machines virtuelles. Vous pouvez utiliser
Si vous ne pouvez pas obtenir votre framework de test pour le faire, vous pouvez lancer certaines machines virtuelles vous-même. Le truc du générateur de processus est pénible avec les chemins et ainsi de suite, mais voici l'esquisse générale:
Process running[] = new Process[5];
for (int i = 0; i < 5; i++) {
ProcessBuilder b = new ProcessBuilder("Java -cp " + getCP() + " MyTestRunner");
running[i] = b.start();
}
for(int i = 0; i < 5; i++) {
running[i].waitFor();
}
Je fais habituellement quelque chose comme ça pour de simples tests filetés, comme d'autres l'ont posté, les tests ne sont pas une preuve d'exactitude, mais ils secouent généralement les bugs idiots dans la pratique. Cela aide à tester pendant longtemps dans une variété de conditions différentes - parfois, les bogues de concurrence prennent un certain temps à se manifester dans un test.
public void testMesageBoard() {
final MessageBoard b = new MessageBoard();
int n = 5;
Thread T[] = new Thread[n];
for (int i = 0; i < n; i++) {
T[i] = new Thread(new Runnable() {
public void run() {
for (int j = 0; j < maxIterations; j++) {
Thread.sleep( random.nextInt(50) );
b.postMessage(generateMessage(j));
verifyContent(j); // put some assertions here
}
}
});
PerfTimer.start();
for (Thread t : T) {
t.start();
}
for (Thread t : T) {
t.join();
}
PerfTimer.stop();
log("took: " + PerfTimer.elapsed());
}
}**strong text**
Il est impossible de tester les bogues de concurrence; vous n'avez pas seulement à valider les paires d'entrée/sortie, mais vous devez valider l'état dans des situations qui peuvent ou non se produire pendant vos tests. Malheureusement, JUnit n'est pas équipé pour cela.
Vous pouvez utiliser la bibliothèque tempus-fugit pour exécuter des méthodes de test en parallèle et plusieurs fois pour simuler un environnement de type test de charge. Bien qu'un commentaire précédent souligne que la méthode post est synchronisée et donc protégée, des membres ou des méthodes associés peuvent être impliqués qui eux-mêmes ne sont pas protégés, donc son possible qu'un test de type charge/trempage pourrait les attraper. Je vous suggère de configurer un test de type assez grossier/de bout en bout pour vous donner les meilleures chances d'attraper les trous de boucle.
Voir la section Intégration JUnit de la documentation .
BTW, je suis développeur sur ledit projet :)
Vous pouvez également essayer HavaRunner . Il exécute les tests en parallèle par défaut.
La meilleure approche ici consiste à utiliser des tests système pour dynamiser votre code avec des demandes pour voir s'il tombe. Utilisez ensuite le test unitaire pour vérifier l'exactitude logique. La façon dont j'aborderais cela est de créer un proxy pour l'appel asynchrone et de le faire synchroniquement sous test.
Cependant, si vous souhaitez le faire à un niveau autre qu'une installation complète et un environnement complet, vous pouvez le faire en un instant en créant votre objet dans un thread séparé, puis créez de nombreux threads qui envoient des requêtes à votre objet et bloquent le thread principal jusqu'à ce qu'il se termine. Cette approche peut faire échouer les tests par intermittence si vous ne le faites pas correctement.