This time, I would like to introduce how to make sure that the test conditions for optimistic locking are set on Spring Boot.
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.
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.
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
}
}
}
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