使用Docker+Spring Boot+MySQL+Flyway+Spock搭建全容器基础的本地开发环境
KINTO Technologies葵タの2021年のアドベントカレンダー- Qiitaの14日目の記事です。
写这篇文章的原因
最初的目标是让后端开发人员了解并体验Spock数据驱动测试的优势。
然而,在建立撰写该文章所需的环境时,我开始认为前面的应用程序开发环境建立过程也是有需求的,甚至更多人需要作为知识来学习。
由于我们公司的系统平台基本上都是基于云端容器服务的,因此即使是在本地开发,也不需要在Mac上安装MySQL,而是最好从一开始就采用容器化构建的方式,这样不仅在DevOps方面更容易进行迁移。
基于以上的原因,我们决定最终将文章的主题设定为所述标题。
希望有人愿意阅读
-
- うっかりクラウドベースの会社に転職しちゃったオンプレ系エンジニア
-
- 開発はコンテナベースでできてるけどテストまでコンテナベースで動かしたい
- テストコードを書けと言われるけどJUnit書きづらいし読みづらいしなんかイヤ
环境
MacBook Pro
Docker Desktop for Mac 4.9.1 (已经付费啦…)
Java 17
Gradle 7.3.1
Spring Boot 2.6.1
Groovy 3.0
Spock 2.0
Testcontainers 1.16.2
示例源代码
行动概述
REST API -> REST 应用程序接口
http://localhost:8080/v1/cars/{price}
您可以获取到KINTO最受欢迎车型(截至2021年12月)中,指定价格或更低月租费的车辆数据。
【KINTO】是从丰田推出的一种汽车订阅服务。
在MySQL容器的表中,存放着以下的汽车数据。

考试内容
-
- HTTPステータス200でレスポンスが返るか
- 価格パターンによるデータ駆動テスト
建造过程
开发REST API
Spring Boot的类结构看起来是这样的。
.
├── java
│ └── com
│ └── example
│ └── restapitestbyspock
│ ├── RestApiTestBySpockApplication.java
│ ├── application
│ │ └── CarController.java
│ ├── domain
│ │ ├── model
│ │ │ └── Car.java
│ │ ├── repository
│ │ │ └── CarRepository.java
│ │ └── service
│ │ └── CarService.java
│ └── infrastructure
│ ├── entity
│ │ └── CarEntity.java
│ └── repository
│ ├── CarJpaRepository.java
│ └── CarRepositoryImpl.java
└── resources
├── application.yml
├── static
└── templates
使用Spring Data JPA的查询生成功能,获取“按照价格升序排列的月租车辆数据”,这些车辆的价格小于等于指定的价格或更低。
由于没有其他特别棘手的事情,所以我只会简单地贴上代码。
域名层
@Data
@Builder
public class Car {
@Id
private Integer id;
private String name;
private Integer price;
}
public interface CarRepository {
List<Car> findByPriceLessThanEqualOrderByPriceAsc(Integer price);
}
@Service
@RequiredArgsConstructor
public class CarService {
@NonNull
private final CarRepository carRepository;
public List<Car> findByPriceLessThanEqualOrderByPriceAsc(Integer price) {
return this.carRepository.findByPriceLessThanEqualOrderByPriceAsc(price);
}
}
基礎設施層
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
@Table(name = "cars")
public class CarEntity {
@Id
private Integer id;
private String name;
private Integer price;
public Car toDomainCar() {
return Car.builder()
.id(this.id)
.name(this.name)
.price(this.price)
.build();
}
}
public interface CarJpaRepository extends JpaRepository<CarEntity, Integer> {
List<CarEntity> findByPriceLessThanEqualOrderByPriceAsc(Integer price);
}
@Repository
@RequiredArgsConstructor
public class CarRepositoryImpl implements CarRepository {
@NonNull
private final CarJpaRepository carJpaRepository;
@Override
public List<Car> findByPriceLessThanEqualOrderByPriceAsc(Integer price) {
return this.carJpaRepository.findByPriceLessThanEqualOrderByPriceAsc(price)
.stream().map(CarEntity::toDomainCar)
.collect(Collectors.toList());
}
}
应用层
@RestController
@RequiredArgsConstructor
@RequestMapping(path = "/v1/cars")
public class CarController {
@NonNull
private final CarService carService;
@GetMapping("/{price}")
@ResponseStatus(HttpStatus.OK)
public List<Car> findByPriceLessThanEqualOrderByPriceAsc(@PathVariable("price") Integer price) {
return this.carService.findByPriceLessThanEqualOrderByPriceAsc(price);
}
}
安装Docker Desktop
现在让我们开始准备容器化的工作吧。
Docker桌面版适用于Mac和Windows系统
请根据上述内容选择并下载、安装到您的设备中。
Docker容器的配置。
配置文件的结构如下所示。
.
├── docker
│ ├── flyway
│ │ ├── conf
│ │ │ └── flyway.conf
│ │ └── sql
│ │ ├── V1.0.0__schema.sql
│ │ └── V1.1.0__data.sql
│ ├── log
│ │ └── mysql
│ │ └── mysqld.log
│ ├── mysql
│ │ ├── Dockerfile
│ │ └── conf.d
│ │ └── my.cnf
│ └── spring
│ └── Dockerfile
├── docker-compose.yml
我們將以Docker Compose為基礎進行配置文件的實現。我們想要建立一個結構,從Spring Boot連接到MySQL並使用Flyway進行遷移管理,因此我們需要以下3個容器映像。
-
- MySQL
-
- Flyway
- Spring Boot
version: "3.7"
services:
dbserver:
container_name: mysql-db
build:
context: ./docker/mysql
dockerfile: Dockerfile
image: chig1215/mysql:latest
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_USER: chig1215
MYSQL_PASSWORD: chig1215
MYSQL_DATABASE: kinto
restart: always
command: --default-authentication-plugin=mysql_native_password
ports:
- "3306:3306"
volumes:
- ./docker/mysql/conf.d:/etc/mysql/conf.d
- ./docker/log/mysql:/var/log/mysql
- mysql_db:/var/lib/mysql
flyway-repair:
container_name: flyway-repair
image: flyway/flyway
command: repair
volumes:
- ./docker/flyway/conf:/flyway/conf
depends_on:
- dbserver
flyway-migration:
container_name: flyway-migration
image: flyway/flyway
command: -url=jdbc:mysql://dbserver -schemas=kinto -user=chig1215 -password=chig1215 -connectRetries=60 migrate
volumes:
- ./docker/flyway/conf:/flyway/conf
- ./docker/flyway/sql:/flyway/sql
depends_on:
- flyway-repair
spring:
container_name: spring-app
build: ./docker/spring
depends_on:
- flyway-migration
ports:
- "8080:8080"
volumes:
- .:/app
environment:
spring.datasource.driverClassName: "com.mysql.cj.jdbc.Driver"
spring.datasource.url: "jdbc:mysql://dbserver/kinto"
spring.datasource.username: "chig1215"
spring.datasource.password: "chig1215"
working_dir: /app
command: sh -c "java -jar ./build/libs/rest-api-test-by-spock-0.0.1-SNAPSHOT.jar"
volumes:
mysql_db:
driver: local
我們將通過註解來解釋每個容器的設置。
MySQL:MySQL is a relational database management system. MySQL是一种关系型数据库管理系统。
dbserver:
container_name: mysql-db # コンテナ名(Docker Desktop上はこの名前で表示される)
build:
context: ./docker/mysql # Dockerfileを含むディレクトリへのパス
dockerfile: Dockerfile # Dockerfile名
image: chig1215/mysql:latest # イメージ名
environment:
MYSQL_ROOT_PASSWORD: root # 環境変数(rootユーザのパスワード)
MYSQL_USER: chig1215 # 環境変数(ユーザ)
MYSQL_PASSWORD: chig1215 # 環境変数(パスワード)
MYSQL_DATABASE: kinto # 環境変数(データベース名)
restart: always # 再起動ポリシー
command: --default-authentication-plugin=mysql_native_password # mysql_native_password を使用したネイティブ認証
ports:
- "3306:3306" # ポートマッピング
volumes:
- ./docker/mysql/conf.d:/etc/mysql/conf.d # mysql.confディレクトリのマッピング
- ./docker/log/mysql:/var/log/mysql # mysqld.logディレクトリのマッピング
- mysql_db:/var/lib/mysql # データ永続化ボリュームのマッピング
# DBの永続化先
volumes:
mysql_db:
driver: local
飞越
flyway-repair:
container_name: flyway-repair # コンテナ名(Docker Desktop上はこの名前で表示される)
image: flyway/flyway # イメージ名
command: repair # 前回のSQLエラー解消(サンプルコンテンツのため。本番稼働アプリでは不要)
volumes:
- ./docker/flyway/conf:/flyway/conf # flyway.confディレクトリのマッピング
depends_on:
- dbserver # MySQLコンテナが起動した後に起動させる
flyway-migration:
container_name: flyway-migration # コンテナ名(Docker Desktop上はこの名前で表示される)
image: flyway/flyway # イメージ名
# MySQLの接続先を指定してマイグレーションを実行する(host:port部分はコンテナ名を指定する)
command: -url=jdbc:mysql://dbserver -schemas=kinto -user=chig1215 -password=chig1215 -connectRetries=60 migrate
volumes:
- ./docker/flyway/conf:/flyway/conf # flyway.confディレクトリのマッピング
- ./docker/flyway/sql:/flyway/sql # マイグレーションSQLファイルディレクトリのマッピング
depends_on:
- flyway-repair # repairが完了した後に起動させる
春季启动
spring:
container_name: spring-app # コンテナ名(Docker Desktop上はこの名前で表示される)
build: ./docker/spring # Dockerfileを含むディレクトリへのパス
depends_on:
- flyway-migration # マイグレーションが完了した後に起動させる
ports:
- "8080:8080" # ポートマッピング
volumes:
- .:/app # ボリュームマッピング
environment:
# MySQLの接続設定
spring.datasource.driverClassName: "com.mysql.cj.jdbc.Driver"
spring.datasource.url: "jdbc:mysql://dbserver/kinto" # host:port部分はコンテナ名を指定する
spring.datasource.username: "chig1215"
spring.datasource.password: "chig1215"
working_dir: /app # 作業ディレクトリ
# jarから起動
command: sh -c "java -jar ./build/libs/rest-api-test-by-spock-0.0.1-SNAPSHOT.jar"
请参考每个容器的Dockerfile和conf文件中的示例源代码。
确认行动
应用程序的构建
$ ./gradlew clean build
让我们确认在build文件夹中创建了一个jar文件。
.
├── build
│ ├── libs
│ │ ├── rest-api-test-by-spock-0.0.1-SNAPSHOT-plain.jar
│ │ └── rest-api-test-by-spock-0.0.1-SNAPSHOT.jar
启动容器
$ docker-compose up --build
如果启动成功,Docker Desktop的Containers/Apps应该以以下方式显示。

如果成功启动,通过GET请求REST API并返回如下结果。
http://localhost:8080/v1/cars/20000
[{
"id": 3,
"name": "ルーミー",
"price": 14630
}, {
"id": 7,
"name": "ヤリス2WD",
"price": 14960
}, {
"id": 2,
"name": "RAIZE",
"price": 16170
}, {
"id": 10,
"name": "プリウス",
"price": 18700
}, {
"id": 4,
"name": "アクア",
"price": 19580
}]
停止容器
$ docker-compose down
小休一下
咖啡休息
考试开发
好,終於到了最初的主題,Spock的登場。
由于本次我们想要以容器为基础进行测试,因此我们将同时使用Testcontainers。
班级构成是这样的。
.
└── src
└── test
├── groovy
│ └── com
│ └── example
│ └── restapitestbyspock
│ └── application
│ └── CarControllerTest.groovy
├── java
│ └── com
│ └── example
│ └── restapitestbyspock
│ └── helper
│ └── test
│ └── MySQLContainerContextInitializer.java
└── resources
添加必要的库来构建测试版本
我会在build.gradle文件中按照以下方式进行追加。
plugins {
id 'org.springframework.boot' version '2.6.1'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
id 'groovy' // 追記(SpockはGroovyで記述するため)
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
maven { url 'https://repo.spring.io/release' }
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-rest'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'javax.persistence:javax.persistence-api'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'mysql:mysql-connector-java'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
annotationProcessor 'org.projectlombok:lombok'
// ここから追記
testImplementation 'org.springframework.boot:spring-boot-test:2.6.2'
testImplementation 'org.springframework.boot:spring-boot-starter-test:2.6.2'
testImplementation 'org.spockframework:spock-core:2.0-groovy-3.0'
testImplementation 'org.spockframework:spock-spring:2.0-groovy-3.0'
testImplementation 'org.testcontainers:testcontainers:1.16.2'
testImplementation 'org.testcontainers:mysql:1.16.2'
testImplementation 'com.jayway.jsonpath:json-path:2.6.0'
}
test {
useJUnitPlatform()
}
bootBuildImage {
builder = 'paketobuildpacks/builder:tiny'
environment = ['BP_NATIVE_IMAGE': 'true']
}
// 追記(FlywayのマイグレーションSQLをテストリソースとして使いたいため)
sourceSets.test {
resources.srcDirs = ["src/test/resources", "docker"]
}
用于测试数据库容器配置的辅助类
@SuppressWarnings({"rawtypes", "unchecked"})
public class MySQLContainerContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
private static final Logger LOGGER = LoggerFactory.getLogger(MySQLContainerContextInitializer.class);
private static final MySQLContainer MYSQL =
new MySQLContainer("mysql:latest") {
{
withDatabaseName("kinto");
withUsername("chig1215");
withPassword("chig1215");
withExposedPorts(3306);
withLogConsumer(new Slf4jLogConsumer(LOGGER));
withClasspathResourceMapping(
"mysql/conf.d",
"/etc/mysql/conf.d", BindMode.READ_ONLY);
withClasspathResourceMapping(
"flyway/sql",
"/docker-entrypoint-initdb.d", BindMode.READ_ONLY);
}
};
static {
MYSQL.start();
}
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
TestPropertyValues.of("spring.datasource.url=" + MYSQL.getJdbcUrl())
.applyTo(applicationContext.getEnvironment());
}
}
为了在Spring启动时执行操作,我们将创建一个实现ApplicationContextInitializer接口的类。
接続信息与docker-compose.yml文件中的设置相同。
mysql.conf目录和迁移SQL目录在层次上是一样的。
.
├── docker
│ ├── flyway
│ │ ├── conf
│ │ │ └── flyway.conf
│ │ └── sql
│ │ ├── V1.0.0__schema.sql
│ │ └── V1.1.0__data.sql
│ ├── mysql
│ │ ├── Dockerfile
│ │ └── conf.d
│ │ └── my.cnf
我在这里的话,只能将它复制到src/test/resources吗?我一直在考虑这个问题,但过了一会儿。
// 追記(FlywayのマイグレーションSQLをテストリソースとして使いたいため)
sourceSets.test {
resources.srcDirs = ["src/test/resources", "docker"]
}
这个意识到了一个好处,顺利地将文件通过应用和测试集中管理了起来。
写试卷
@SpringBootTest
@AutoConfigureMockMvc
@ContextConfiguration(initializers = [MySQLContainerContextInitializer.class])
class CarControllerTest extends Specification {
@Autowired
MockMvc mockMvc
@Unroll
def "FindByPriceLessThanEqualOrderByPriceAsc HttpStatus"() {
when:
def result =
mockMvc.perform(MockMvcRequestBuilders.get("/v1/cars/30000"))
.andReturn().getResponse()
then:
result.getStatus() == HttpStatus.OK.value
}
@Unroll
def "FindByPriceLessThanEqualOrderByPriceAsc Data Pattern"() {
expect:
mockMvc.perform(MockMvcRequestBuilders.get("/v1/cars/" + price))
.andExpect(MockMvcResultMatchers.jsonPath("\$.*", Matchers.hasSize(size)))
where:
price || size
"50000" || 10
"40000" || 10
"30000" || 9
"20000" || 5
"15000" || 2
"10000" || 0
}
}
@ContextConfiguration(initializers = [MySQLContainerContextInitializer.class]) – 采用 @ContextConfiguration 注解,并指定初始化器为 MySQLContainerContextInitializer.class。
通过指定先前创建的Helper类,启动并设置容器的连接信息。
这样,就可以从测试代码中访问用于测试的MySQL容器了。
考试的内容包括以下两项。
-
- HTTPステータス200でレスポンスが返るか(FindByPriceLessThanEqualOrderByPriceAsc HttpStatus)
- 価格パターンによるデータ駆動テスト(FindByPriceLessThanEqualOrderByPriceAsc Data Pattern)
我支持Spock的最大理由无疑是其“易读性”。
expect:
mockMvc.perform(MockMvcRequestBuilders.get("/v1/cars/" + price))
.andExpect(MockMvcResultMatchers.jsonPath("\$.*", Matchers.hasSize(size)))
where:
price || size
"50000" || 10
"40000" || 10
"30000" || 9
"20000" || 5
"15000" || 2
"10000" || 0
就算没有评论,你也知道在测试什么吧。我们将传递给REST API的价格数据模式定义为price,并将应返回的汽车对象数定义为每个数据模式的size。
通过考试
$ ./gradlew clean test
测试结果将显示在控制台上,并且还会生成报告,所以让我们在浏览器中查看一下。
.
├── build
│ ├── reports
│ │ └── tests
│ │ └── test
│ │ ├── classes
│ │ │ └── com.example.restapitestbyspock.application.CarControllerTest.html
│ │ ├── css
│ │ │ ├── base-style.css
│ │ │ └── style.css
│ │ ├── index.html
│ │ ├── js
│ │ │ └── report.js
│ │ └── packages
│ │ └── com.example.restapitestbyspock.application.html
所有的测试都取得了成功。

测试失败有

请查阅相关资料
https://matsuand.github.io/docs.docker.jp.onthefly/reference/ 的中文内容如下:
https://www.testcontainers.org/ 的中文内容如下:
最后
您对此有何看法?
如果能对大家稍有帮助就非常荣幸了。
我们公司目前正在进行丰田汽车的订阅服务“KINTO”等项目的企划和开发,并正在招聘工程师。
KINTO Technologies 公司网站