[JAVA] Leverage Spring AOP + CyclicBarrier to ensure optimistic lock testing conditions on the Spring Boot app

This time, I would like to introduce how to make sure that the test conditions for optimistic locking are set on Spring Boot.

Operation confirmed version

Sample app to be tested

Let's take a look at the test for optimistic locking code as follows.

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)Get version number for optimistic lock
			logger.info("current version : {}", currentVersion);

			boolean result = repository.updateVersion(currentVersion); // (2)Update using optimistic lock
			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);
		}
	}

}

Is optimistic locking done correctly? When testing (= the case where the result of (2) is false), it is necessary to perform the operation so that the same version number is acquired by multiple threads. In the above sample code, there is no processing between (1) and (2), so it is quite difficult to "set the result of (2) to false "when a person operates it.

Tested with JUnit

So let's write test code that uses JUnit to send requests at about the same time.

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();
			}
		};
	}

}

When I ran this test code ... there was a good chance the test was successful (actually 100% successful on my machine). So is this okay? That said, there is still the possibility that the test will fail in this test code.

This is because ... I have implemented it so that requests are sent to the server at almost the same time using two threads, but there is no guarantee that the actual timing of the requests reaching the server will be the same.

As a trial ... If you try to shift the timing when the request arrives in a pseudo manner ...

@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); //Intentionally shift the timing of request arrival
	client2.start();

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

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

The test will fail. So ... the above test code can fail depending on the timing.

So what can we do to ensure that the test conditions are met? Speaking of ... The update process should not be performed until two requests get the same version number.

Tested with Spring AOP + CyclicBarrier

In order to satisfy this test condition without modifying the source code to be tested, I would like to utilize Spring AOP and CyclicBarrier this time.

pom.xml


<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId> <!--Added AOP starter-->
</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 //Enable AOP functionality using AspectJ annotations
@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); //Set sleep time to 0 to ensure timing shift.Change from 2 seconds to 1 second
		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 to wait for the update method to execute until the test conditions are met
	//test conditions:Call the update method after two threads refer to the same version number
	@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); //Time out to avoid infinite wait when the second request does not come(10 seconds in the example)To set up
		}
	}

}

Summary

With Spring AOP + CyclicBarrier, you can be sure to set the test conditions for tests that depend on the timing of execution (eg optimistic lock test). In this entry, a request is made to the embedded Tomcat for testing, but if the same mechanism is applied to the test (component integration test?) That connects "Service class ⇄ Repository", it depends on the mechanism of Spring Boot. You can do a similar test without doing it. Let's stop sending requests at the same time with multiple people and testing ~ or setting breakpoints on the IDE ~ and so on: wink:

Recommended Posts

Leverage Spring AOP + CyclicBarrier to ensure optimistic lock testing conditions on the Spring Boot app
Deploy the Spring Boot project to Tomcat on XAMPP
Introduction to Spring Boot ② ~ AOP ~
[Java] Deploy the Spring Boot application to Azure App Service
Sign in to a Spring Boot web application on the Microsoft ID platform
The story of raising Spring Boot 1.5 series to 2.1 series
Ssh login to the app server on heroku
Matches annotations on the interface with Spring AOP
[Spring Boot] How to refer to the property file
05. I tried to stub the source of Spring Boot
I tried to reduce the capacity of Spring Boot