Spring BootでRDBやRedisを使うWebアプリのユニットテスト自動化について書く。とりあえず、H2DBとRedisをUnitテスト実行時に自動的に立ち上げるようにしてテストの自動化が出来るようにする。

WebアプリのUnitテスト

Webアプリを作るとき、MySQLやRedisなどのミドルウェアにデータを保存する。開発時にもMySQLやRedisが必要になる。当然、書いたプログラムのUnitテストを書くことになるが、これらのミドルウェアにデータが保存されるとなると開発時だけでなくUnitテストのときにも同等の役割をするミドルウェアが必要になる。また、DBの中に保存されているデータの管理もしなければならず意外とメンドウ。

次の項目があるとテストが楽。

  • 環境の自動構築 (DBを立ち上げたり、停止したり)
  • テストデータの用意 (DBへのmigrationを含む)

という、無理矢理な説明だけど、まぁテストとか開発する環境とかの構築では自動化は必須だしJavaだけで解決できると嬉しいよね。という話。

とりあえず、MySQLやRedisはTest時に自動的に立ち上げるようにして環境を意識せずUnitテストできるようにする。CIでの自動化は書きません。

環境

  • Java 8
  • CentOS 7
  • Dependency

RDBは組み込みDBのh2を使い、Unitテスト実行時にflyway-coreでDDLを流し込む。テストデータはSQLで用意するのはメンドウなのでUnitテストのコードからjpaを使ってテストコードに記述する感じにしてみる。基本的にインメモリDBにしてテストが終わったら全てのデータを捨てる。

Redisはembedded-redisという良さげなものがあるのでそれを使う。他はSpring initializrでポチポチすると追加出来る。

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.kstyrc</groupId>
<artifactId>embedded-redis</artifactId>
<version>0.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

テスト用の設定の作成

src/test/resources以下に次の設定を用意する。

テスト用のプロファイルを作成する。ここで指定したファイル名のunitの部分がProfile名になる。profile毎に設定ファイルを用意しておけば設定ファイルの差し替えが簡単に出来る。

application-unit.yml

spring:
datasource:
driver-class-name: org.h2.Driver
url: jdbc:h2:mem:test;MODE=MySQL;DB_CLOSE_ON_EXIT=FALSE
username: sa
password:
flyway.schemas: PUBLIC

spring.redis:
host: localhost
port: 6379
database: 0

インメモリDBで設定してテスト終了後のデータは捨てる。DB_CLOSE_ON_EXIT=FALSEを指定しないとH2に怒られるので設定しとく。

flyway.schemasはflywayが期待するスキーマとH2のデフォルトのスキーマが異なるので設定しないと実行できない。

Unitテスト時はSentinelいらないと思うのでRedisServerの設定をすることにする。

Redisは以下の様に設定する。

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootAutomationUnitTestSampleApplicationTests {

private static RedisServer redisServer;

@BeforeClass
public static void startRedis() throws IOException {
redisServer = new RedisServer(6379);
redisServer.start();
}
@AfterClass
public static void stopRedis() {
redisServer.stop();
}
}

設定はコレくらい。だが、本物のRedisが動くのでLinuxで動かす場合は以下のカーネルパラメータを設定しないと動かなかった。

$ sysctl -w vm.overcommit_memory=1
$ sysctl -w net.core.somaxconn=1024

テストの準備

適当なDDLとEntityとControllerなどを用意する。

まず、DDLから。パスとファイル名は意味があるので、詳しくはflywayのドキュメントを読んでください。

src/test/resources/db/migration/V1__init.sql

create schema if not exists "public";
create table user (
id bigint generated by default as identity,
name varchar(255) not null,
email varchar(255) not null,
password varchar(255) not null
);

対応するEntity

@Data
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class User {
@Id
@GeneratedValue
private Long id;

@Column(nullable = false)
private String name;

@Column(nullable = false)
private String email;

@Column(nullable = false)
private String password;
}

適当なRepository。

@Repository
public interface UserRepository extends JpaRepository<User,Long> {
User findByName(String name);
}

ココまで作ったら適当なテストをするためのコントローラを作る。

@Controller
public class UserController {

@Autowired
private UserRepository userRepository;

@RequestMapping(value = "/", method = RequestMethod.GET)
@ResponseBody
public String getUser() {
return userRepository.findByName("alice").getEmail();
}
}

テストを書いてみる

setUpでコントローラで取得するデータをDBに書き込む。

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SpringBootAutomationUnitTestSampleApplication.class)
@AutoConfigureMockMvc
public class UserControllerTest {

@Autowired
private MockMvc mockMvc;

@Autowired
private UserRepository userRepository;

private User testUser;

@Before
public void setUp() {
testUser = userRepository.saveAndFlush(
new User(0L, "alice", "alice@example.com", "password"));
}

@After
public void tearDown() {
userRepository.delete(testUser);
}

@Test
public void testGetUser() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.get("/"))
.andExpect(MockMvcResultMatchers.status().is2xxSuccessful())
.andExpect(MockMvcResultMatchers.content()
.string("alice@example.com"));
}
}

実行してみる。

$ ./mvnw clean test --define spring.profiles.active=unit

テストが正常に実行されることが確認できる。unitテストのときだけ実行したいテストケースは以下の様にアノテートするとprofileが一致するときだけテストが実行されるように出来る。

@IfProfileValue(name = "spring.profiles.active", values = "unit")

おまけ

Redisにつながっているか確認してみる。

@Autowired
private StringRedisTemplate redisTemplate;

@Test
public void redisTest() {
redisTemplate.opsForValue().set("testKey", "testValue");
Assert.assertNotNull(redisTemplate.opsForValue().get("testKey"));
Assert.assertEquals("testValue",
redisTemplate.opsForValue().get("testKey"));
}

おわり。

参考

  1. http://docs.spring.io/spring-boot/docs/current/reference/html/howto-database-initialization.html
  2. https://github.com/spring-projects/spring-boot/tree/v1.4.2.RELEASE/spring-boot-samples/spring-boot-sample-flyway