[JAVA] Tirez parti de la barrière cyclique Spring AOP + pour garantir des conditions de test de verrouillage optimistes sur l'application Spring Boot

Cette fois, je voudrais vous présenter comment m'assurer que les conditions de test pour le verrouillage optimiste sont définies sur Spring Boot.

Opération version confirmée

Exemple d'application à tester

Jetons un coup d'œil au test du code de verrouillage optimiste comme suit.

package com.example.demo;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.stereotype.Repository;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.concurrent.atomic.AtomicInteger;

@SpringBootApplication
public class OptimisticLockDemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(OptimisticLockDemoApplication.class, args);
	}

	@RestController
	static class MyController {
		private static final Logger logger = LoggerFactory.getLogger(MyController.class);

		private final MyRepository repository;

		public MyController(MyRepository repository) {
			this.repository = repository;
		}

		@PostMapping("/version/increment")
		boolean incrementVersion() {
			int currentVersion = repository.getVersion(); // (1)Obtenir le numéro de version pour le verrouillage optimiste
			logger.info("current version : {}", currentVersion);

			boolean result = repository.updateVersion(currentVersion); // (2)Mis à jour à l'aide du verrouillage optimiste
			logger.info("updating result : {}", result);

			return result;
		}

	}

	@Repository
	static class MyRepository {
		private final AtomicInteger version = new AtomicInteger(1);

		public int getVersion() {
			return version.get();
		}

		public boolean updateVersion(int currentVersion) {
			return version.compareAndSet(currentVersion, currentVersion + 1);
		}
	}

}

Le verrouillage optimiste est-il correctement effectué? Lors du test (= le cas où le résultat de (2) est «faux»), il est nécessaire d'effectuer l'opération pour que le même numéro de version soit acquis par plusieurs threads. Dans l'exemple de code ci-dessus, il n'y a pas de traitement entre (1) et (2), il est donc assez difficile pour une personne de "définir le résultat de (2) sur" faux "" lors de l'opération.

Testé avec JUnit

Écrivons donc un code de test qui utilise JUnit pour envoyer des requêtes à peu près au même moment.

package com.example.demo;

import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.concurrent.atomic.AtomicInteger;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class OptimisticLockDemoApplicationTests {

	@Autowired TestRestTemplate restTemplate;

	@Test
	public void contextLoads() throws InterruptedException {
		AtomicInteger successCounter = new AtomicInteger(0);

		Thread client1 = new Thread(runnable(successCounter));
		Thread client2 = new Thread(runnable(successCounter));

		client1.start();
		client2.start();

		client1.join(10000);
		client2.join(10000);

		Assertions.assertThat(successCounter.get()).isEqualTo(1);
	}

	private Runnable runnable(AtomicInteger successCounter) {
		return () -> {
			boolean result = restTemplate.postForObject("/version/increment", null, boolean.class);
			if (result) {
				successCounter.incrementAndGet();
			}
		};
	}

}

Quand j'ai exécuté ce code de test ... il y avait de bonnes chances que le test soit réussi (en fait 100% réussi sur ma machine). Alors est-ce que ça va? Cela dit, il est toujours possible que le test échoue dans ce code de test.

C'est parce que ... je l'ai implémenté pour que les demandes soient envoyées au serveur presque en même temps en utilisant deux threads, mais il n'y a aucune garantie que le moment réel des demandes atteignant le serveur sera le même.

À titre d'essai ... Si vous essayez de décaler le timing lorsque la demande arrive de manière pseudo ...

@Test
public void contextLoads() throws InterruptedException {
	AtomicInteger successCounter = new AtomicInteger(0);

	Thread client1 = new Thread(runnable(successCounter));
	Thread client2 = new Thread(runnable(successCounter));

	client1.start();
	Thread.sleep(200); //Décaler intentionnellement le moment de l'arrivée de la demande
	client2.start();

	client1.join(10000);
	client2.join(10000);

	Assertions.assertThat(successCounter.get()).isEqualTo(1);
}

Le test échouera. Donc ... le code de test ci-dessus peut échouer en fonction du timing.

Alors, que pouvons-nous faire pour nous assurer que les conditions de test sont remplies? En parlant de ... Le processus de mise à jour ne doit pas être effectué tant que deux demandes n'ont pas obtenu le même numéro de version.

Testé avec Spring AOP + Cyclic Barrier

Afin de satisfaire cette condition de test sans modifier le code source à tester, je voudrais utiliser Spring AOP et CyclicBarrier cette fois.

pom.xml


<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId> <!--Ajout du démarreur AOP-->
</dependency>
package com.example.demo;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestComponent;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;

@EnableAspectJAutoProxy //Activer la fonction AOP à l'aide des annotations AspectJ
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class OptimisticLockDemoApplicationTests {

	@Autowired TestRestTemplate restTemplate;

	@Test
	public void contextLoads() throws InterruptedException {
		AtomicInteger successCounter = new AtomicInteger(0);

		Thread client1 = new Thread(runnable(successCounter));
		Thread client2 = new Thread(runnable(successCounter));

		client1.start();
		Thread.sleep(1000); //Réglez le temps de sommeil sur 0 pour assurer le décalage de synchronisation.Passer de 2 secondes à 1 seconde
		client2.start();

		client1.join(10000);
		client2.join(10000);

		Assertions.assertThat(successCounter.get()).isEqualTo(1);
	}

	private Runnable runnable(AtomicInteger successCounter) {
		return () -> {
			boolean result = restTemplate.postForObject("/version/increment", null, boolean.class);
			if (result) {
				successCounter.incrementAndGet();
			}
		};
	}

	//Aspect à attendre que la méthode de mise à jour s'exécute jusqu'à ce que les conditions de test soient remplies
	//conditions d'essai:Appelez la méthode de mise à jour après que deux threads se réfèrent au même numéro de version
	@TestComponent
	@Aspect
	static class UpdateAwaitAspect {
		private final CyclicBarrier barrier = new CyclicBarrier(2);

		@Before("execution(* com.example.demo.OptimisticLockDemoApplication.MyRepository.updateVersion(..))")
		public void awaitUpdating() throws BrokenBarrierException, InterruptedException, TimeoutException {
			barrier.await(10, TimeUnit.SECONDS); //Timeout pour éviter une attente infinie lorsque la deuxième requête ne vient pas(10 secondes dans l'exemple)Installer
		}
	}

}

Résumé

Avec Spring AOP + CyclicBarrier, vous pouvez être sûr de définir les conditions de test pour les tests qui dépendent du moment d'exécution (par exemple, test de verrouillage optimiste). Dans cette entrée, nous faisons une requête au Tomcat intégré et le testons, mais si vous appliquez le même mécanisme au test (test d'intégration de composant?) Qui connecte "Classe de service ⇄ Référentiel", cela dépend du mécanisme de Spring Boot. Vous pouvez faire un test similaire sans le faire. Arrêtons d'envoyer des demandes en même temps avec plusieurs personnes et de tester ~ ou de définir un point d'arrêt sur l'EDI ~ et ainsi de suite: wink:

Recommended Posts

Tirez parti de la barrière cyclique Spring AOP + pour garantir des conditions de test de verrouillage optimistes sur l'application Spring Boot
Déployer le projet Spring Boot sur Tomcat dans XAMPP
Introduction à Spring Boot ② ~ AOP ~
[Java] Déployer l'application Spring Boot sur Azure App Service
Connectez-vous aux applications Web Spring Boot sur la plate-forme Microsoft ID
L'histoire de la montée de la série Spring Boot 1.5 à la série 2.1
Connexion SSH au serveur d'applications avec heroku
Correspond aux annotations sur l'interface avec Spring AOP
[Spring Boot] Comment se référer au fichier de propriétés
05. J'ai essayé de supprimer la source de Spring Boot
J'ai essayé de réduire la capacité de Spring Boot