0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Spring BootでAPI開発環境構築

Last updated at Posted at 2024-10-01

はじめに

この記事では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というメッセージを取得できます

目次

  1. DockerでAPI実行環境を用意する
  2. DBからデータを取得できるようにする
  3. テストを作成する
  4. まとめ
  5. 参考文献

DockerでAPI実行環境を用意する

SpringInitializrでアプリケーションを作成します

DependenciesにはLombok・MySQL Driver・Testcontainers・Spring Webを追加し、テンプレートを作成します
他ライブラリはSpringInitializrでは追加できないので、適宜追加していきます

Docker環境を作成します

Dockerfileを作成
アプリケーションコードをビルド後にdockerを実行するので、ビルドしたjarファイルをコピー・実行しています

Dockerfile
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をマウントしています

docker-compose.yml
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しています

migration/init.sql
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ファイル置換エラーが出力されてしまいます

build.gradle
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の接続情報を変更できます

application.properties
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側のカラム名を別で扱うことができます

詳しくは公式ドキュメント:エンティティクラスをご覧ください

Message.java
@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をご覧ください

MessageDao.java
@Dao
@ConfigAutowireable
public interface MessageDao {
    @Select
    Message selectMessage(int id);
}
selectMessage.sql
select * from MESSAGES where id = /* id */1;

Controllerの作成
今回はDBからの値を取得できれば良いので、Daoからの値を直接返しています
実際のプロダクトの場合だとServiceやRepositoryが間に経由しますが、簡略化のため省略しています

HelloWorldController.java
@RestController
@RequestMapping("/")
public class HelloWorldController {
    @Resource
    MessageDao messageDao;

    @RequestMapping
    public String getData() {
        return messageDao.selectMessage(1).getText();
    }
}

テストを作成する

ライブラリを導入
初期データの用意やDBの期待値を確認するDatabase Riderですが、JUnit4用で作られています
今回は最新版のJUnit5を利用したいので、拡張されたrider-junit5を導入します

build.gradle
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のスキーマを初期化してくれないので初期データの投入でエラーになってしまいます

TestBase.java

@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を読みにいってくれるので、スキーマ定義ファイルを作成します

src/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以下で返り値の値を検証します

詳しくは公式ドキュメントを確認してください

HelloWorldControllerTest.java
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);
    }
}
test/resources/controller/HelloWorldControllerTest/case1.given.yaml
MESSAGES:
  - id: 1
    text: "Hello World"
  - id: 2
    text: "Goodbye World"

Daoテストを作成
Controllerのテストと同様に、@Datasetを使い初期データをセットします
検証にはJUnitのassertを利用しています

MessageDaoTest.java
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);
    }
}
test/resources/dao/MessageDaoTest/selectMessage.given.yaml
MESSAGES:
  - id: 1
    text: "Hello World"
  - id: 2
    text: "Goodbye World"

まとめ

今回はSpringBootでAPIを作成するときの開発環境構築を行いました
テスト環境を含めると大変でしたが、1から構築したためすごく勉強になりました

まだテストの同時実行ができず、ビルドする際にテストが落ちてしまう問題があるため、Testcontainersの設定をもう少し詰めたいと思います

参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?