Spring AOP+CyclicBarrierを活用してSpring Bootアプリ上での楽観ロックのテスト条件を確実に整える

今回は、Spring Boot上で楽観ロックのテスト条件を確実に整える方法を紹介したいと思います。

# 動作確認バージョン

* Spring Boot 1.5.3.RELEASE

# テスト対象のサンプルアプリ


以下のように楽観ロックを行うコードに対するテストをみてみましょう。

```java
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) 楽観ロック用にバージョン番号を取得
			logger.info("current version : {}", currentVersion);

			boolean result = repository.updateVersion(currentVersion); // (2) 楽観ロックを使用して更新
			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);
		}
	}

}
```

楽観ロックが正しく行われているか？（＝(2)の結果が`false`になるケース）をテストする場合、複数のスレッドで同じバージョン番号が取得されるようにオペレーションを行う必要があります。
上記のサンプルコードだと(1)と(2)の間に何も処理がないので・・・人がオペレーションすると「(2)の結果を`false`にする」のは結構むずかしいです。

# JUnitを使用してテスト

ということで、JUnitを使ってほぼ同時にリクエストを送るテストコードを書いてみましょう。

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

}
```

このテストコードを実行すると・・・かなりの確率でテストは成功しました（私のマシンで試した範囲では実は１００％成功）。じゃ〜これで問題ないのか？というと話は別で、このテストコードにはテストが失敗する可能性が残っています。

というのも・・・２つのスレッドを使用してほぼ同時にリクエストをサーバに投げるように実装していますが・・・実際にサーバにリクエストが届くタイミングも同時になる保証はありません。

ためしに・・・擬似的にリクエストが届くタイミングをずらしてみると・・・


```java
@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); // 意図的にリクエストが届くタイミングをずらす
	client2.start();

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

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

テストは失敗します。つまり・・・上記のテストコードはタイミングによっては失敗する可能性があるのです。

では、確実にテスト条件を充すようにするためには、どうすればよいのか？というと・・・
２つのリクエストが同じバージョン番号を取得するまで更新処理が行われないようにすればよいのです。

# Spring AOP+`CyclicBarrier`を活用してテスト

テスト対象のソースコードに手を加えずにこのテスト条件を充すために、今回はSpring AOPと`CyclicBarrier`を活用してみたいと思います。

```xml:pom.xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-aop</artifactId> <!-- AOPのstarterを追加 -->
</dependency>
```

```java
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 // AspectJのアノテーションを利用したAOP機能を有効化する
@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); // 確実にタイミングをずらすためにsleep時間を0.2秒から1秒に変更
		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
	// テスト条件: ２つのスレッドが同じバージョン番号を参照した後に更新メソッドを呼び出す
	@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); // 2回目のリクエストが来ない時に無限待機にならないようにタイムアウト(例では10秒)を設けておく
		}
	}

}
```

# まとめ

Spring AOP + `CyclicBarrier`を活用すると、実施タイミングに左右されるテスト（例：楽観ロックのテストなど）のテスト条件を確実に整えることができます。本エントリーでは組み込みTomcatに対してリクエストを投げてテストしてますが、「Serviceクラス⇄Repository」をつないだテスト(コンポーネント結合テスト？)に対して同じ仕掛けを適用すれば、Spring Bootの仕組みに依存せずに同様のテストを行うこともできます。
複数の人で同時にリクエストを送ってテスト〜とか、IDE上でブレークポイント設けてテスト〜とかやめましょうね :wink: