はじめに
この記事ではSpringBootでAPIを開発する際の開発環境構築を行います
以下筆者の実行環境と作成する開発環境です
実行環境
OS:Ventura 13.6.1
Java:21.0.3
作成する開発環境
- API実行環境
アプリケーションはSpringBootで作成し、ORMにはDoma2、DBはMySQL 8.0を使います
またアプリケーション・DBコンテナをDockerで作成します - テスト実行環境
ユニットテストはJUnit・インテグレーションテストはREST ASSUREDを利用し検証します
またテスト実行にはTestcontainer、テストデータはDatabase Riderを利用します
作成後にできること
http://localhost:8080へアクセスすると、DBからHello World
というメッセージを取得できます
目次
DockerでAPI実行環境を用意する
SpringInitializrでアプリケーションを作成します
DependenciesにはLombok・MySQL Driver・Testcontainers・Spring Webを追加し、テンプレートを作成します
他ライブラリはSpringInitializrでは追加できないので、適宜追加していきます
Docker環境を作成します
Dockerfileを作成
アプリケーションコードをビルド後にdockerを実行するので、ビルドしたjarファイルをコピー・実行しています
FROM openjdk:21
RUN mkdir /api
WORKDIR /api
COPY ./gradlew /api
COPY ./build.gradle /api
COPY ./settings.gradle /api
COPY ./src /api/src
COPY ./gradle /api/gradle
COPY ./build/libs/spring-project-template-0.0.1-SNAPSHOT.jar /api/build/libs/spring-project-template-0.0.1-SNAPSHOT.jar
CMD java -jar build/libs/spring-project-template-0.0.1-SNAPSHOT.jar
docker-compose.ymlを作成
networksにてコンテナ同士を接続しています
MySQL環境作成時にdocker-entrypoint-initdb.d/init.sql
が実行されるので、初期データとしてmigration/init.sql
を作成し、init.sqlをマウントしています
services:
api:
build: ./
container_name: api
ports:
- "8080:8080"
environment:
- DATASOURCE_URL=jdbc:mysql://db:3306/spring_project_template
- DATASOURCE_USER=user
- DATASOURCE_PASSWORD=password
depends_on:
- db
networks:
- app-net
db:
image: mysql:8.0
volumes:
- ./migration/init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "3306:3306"
environment:
MYSQL_DATABASE: spring_project_template
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: user
MYSQL_PASSWORD: password
TZ: Asia/Tokyo
networks:
- app-net
networks:
app-net:
driver: bridge
DB初期化のmigrationファイルを作成
MESSAGESスキーマの作成、初期データをinsertしています
SET CHARSET UTF8;
create table MESSAGES
(
id int unsigned AUTO_INCREMENT PRIMARY KEY,
text varchar(100) not null
);
insert into MESSAGES (id, text) values (1, 'HelloWorld');
DBからデータを取得できるようにする
ライブラリを導入
pluginsで記載しているorg.domaframework.doma.compile"
を導入しないと、ビルド時にSQLファイル置換エラーが出力されてしまいます
plugins {
id "org.domaframework.doma.compile" version "2.0.0"
}
dependencies {
implementation 'org.seasar.doma:doma-core:2.60.0'
implementation 'org.seasar.doma.boot:doma-spring-boot-starter:1.8.0'
annotationProcessor 'org.seasar.doma:doma-processor:2.60.0'
}
propertiesを設定
環境によってDBの接続URL・username・passwordを変更したいので、環境変数から取得できるようにします
開発環境ではdocker-compose.yml
で指定されています
こうすることでCDKでデプロイした際に、環境変数を設定するだけでDBの接続情報を変更できます
spring.application.name=spring-project-template
spring.datasource.url=${DATASOURCE_URL}
spring.datasource.username=${DATASOURCE_USER}
spring.datasource.password=${DATASOURCE_PASSWORD}
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
server.servlet.encoding.charset=UTF-8
Entityを作成
MESSAGESテーブルの定義に従いEntityを作成します
@Columnでnameを指定することで、アプリケーション側で扱う名前とDB側のカラム名を別で扱うことができます
詳しくは公式ドキュメント:エンティティクラスをご覧ください
@Getter
@Entity(immutable = true)
@AllArgsConstructor
@EqualsAndHashCode
public class Message {
@Id
private final long id;
@Column(name = "text")
private final String text;
}
Daoの作成
@Selectを追加することで、src/main/resources/META-INF/プロジェクト名/dao/Dao名/メソッド名.sql
のSQLファイルを実行することができます
また、SQLファイル内で/* */
を囲むことで、Daoで受け取った引数をSQLファイルに渡すことが可能です
詳しくは公式ドキュメント:SQLをご覧ください
@Dao
@ConfigAutowireable
public interface MessageDao {
@Select
Message selectMessage(int id);
}
select * from MESSAGES where id = /* id */1;
Controllerの作成
今回はDBからの値を取得できれば良いので、Daoからの値を直接返しています
実際のプロダクトの場合だとServiceやRepositoryが間に経由しますが、簡略化のため省略しています
@RestController
@RequestMapping("/")
public class HelloWorldController {
@Resource
MessageDao messageDao;
@RequestMapping
public String getData() {
return messageDao.selectMessage(1).getText();
}
}
テストを作成する
ライブラリを導入
初期データの用意やDBの期待値を確認するDatabase Riderですが、JUnit4用で作られています
今回は最新版のJUnit5を利用したいので、拡張されたrider-junit5
を導入します
dependencies {
testImplementation "com.github.database-rider:rider-junit5:1.44.0"
testImplementation 'io.rest-assured:rest-assured'
}
Testcontainersの設定
Daoのテスト・ControllerのインテグレーションテストのためにDBを用意する必要があります
しかし、わざわざテスト用のDBを用意するのは手間なので、動的にDBコンテナを立ち上げテストをできるようにTestcontainersを設定します
Testcontainersの設定はDaoのテスト・Controllerのテスト両方で使いたいので、TestBaseクラスを作成し、テストはそれを継承して使います
基本的に公式ドキュメントに沿って作成しています
気を付ける点としてregistry.add("spring.sql.init.mode", () -> "always");
を設定しています
この設定がないと、DBのスキーマを初期化してくれないので初期データの投入でエラーになってしまいます
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Testcontainers
@DBRider
public class TestBase {
@LocalServerPort
private Integer port;
static MySQLContainer<?> mysql = new MySQLContainer<>("mysql:8.0.33")
.withDatabaseName("spring_project_template")
.withUsername("user")
.withPassword("password");
@BeforeAll
static void beforeAll() {
mysql.start();
}
@AfterAll
static void afterAll() {
mysql.stop();
}
@DynamicPropertySource
static void configureProperties(DynamicPropertyRegistry registry) {
registry.add("spring.datasource.url", mysql::getJdbcUrl);
registry.add("spring.datasource.username", mysql::getUsername);
registry.add("spring.datasource.password", mysql::getPassword);
registry.add("spring.sql.init.mode", () -> "always");
}
@BeforeEach
void setUp() {
RestAssured.baseURI = "http://localhost:" + port;
}
}
スキーマの定義ファイルを作成
Testcontainersはデフォルトでmain/resources/schema.sql
を読みにいってくれるので、スキーマ定義ファイルを作成します
SET CHARSET UTF8;
create table MESSAGES
(
id int unsigned AUTO_INCREMENT PRIMARY KEY,
text varchar(100) not null
);
Controllerのテストを作成
@Datasetを使って初期データを用意します
valueはtest/resources
以下からのパスを記述します
実行の検証にはREST Assuredを利用しています
get
の引数に確認したいAPIのパスを指定し、then
以下で返り値の値を検証します
詳しくは公式ドキュメントを確認してください
class HelloWorldControllerTest extends TestBase {
@Test
@DataSet(value = "controller/HelloWorldControllerTest/case1.given.yaml")
void case1() throws Exception {
given()
.contentType(ContentType.JSON)
.when()
.get("/")
.then()
.statusCode(200);
}
}
MESSAGES:
- id: 1
text: "Hello World"
- id: 2
text: "Goodbye World"
Daoテストを作成
Controllerのテストと同様に、@Datasetを使い初期データをセットします
検証にはJUnitのassertを利用しています
class MessageDaoTest extends TestBase {
@Autowired
private MessageDao messageDao;
@Test
@DataSet(value = "dao/MessageDaoTest/selectMessage.given.yaml")
void selectMessage() {
Message expected = new Message(1, "Hello World");
Message actual = messageDao.selectMessage(1);
assertEquals(expected, actual);
}
}
MESSAGES:
- id: 1
text: "Hello World"
- id: 2
text: "Goodbye World"
まとめ
今回はSpringBootでAPIを作成するときの開発環境構築を行いました
テスト環境を含めると大変でしたが、1から構築したためすごく勉強になりました
まだテストの同時実行ができず、ビルドする際にテストが落ちてしまう問題があるため、Testcontainersの設定をもう少し詰めたいと思います