今日はテストについて話したいと思います。テストでカバーされるコードが多ければ多いほど、コードはより良く、より信頼できるとみなされるからです。単体テストについてではなく、データベースの統合テストについて話しましょう。単体テストと結合テストの違いは何ですか? モジュラー (ユニット) は、個々のモジュール、メソッド、またはクラスのレベルでプログラムをテストします。つまり、テストは高速かつ簡単で、機能の最も分割可能な部分に影響を与えます。これらは「メソッドごとに 1 つのテスト」とも呼ばれます。統合のものは遅くて重く、複数のモジュールと追加機能で構成される場合があります。 dao (データ アクセス オブジェクト) レイヤーのテストが統合テストであるのはなぜですか? データベースへのクエリを使用してメソッドをテストするには、メインのデータベースを置き換えて、RAM 内に別のデータベースを作成する必要があるためです。考え方としては、必要なテーブルを作成し、そこにテスト データを入力し、リポジトリ クラス メソッドの正確性をチェックするということです (結局のところ、特定のケースで最終結果がどうなるかはわかっています)。それでは、始めましょう。データベースの接続に関するトピックは長い間広範囲にわたって取り上げられてきたため、今日はこれについては触れず、プログラムの中で興味のある部分だけを取り上げます。デフォルトでは、アプリケーションが Spring Boot に基づいているという事実から開始し、Spring JDBC dao レイヤーについては (より明確にするため)、メインデータベースは MySQL であり、それを MariaDB を使用して置き換えます (これらは最大限の互換性があり、したがって、MySQL スクリプトは、H2 と同様に、MariaDB ダイアレクトと競合することはありません)。また、条件付きで、プログラムがデータベース スキーマの変更を管理および適用するために Liquibase を使用し、それに応じて、適用されたすべてのスクリプトがアプリケーションに保存されると仮定します。
プロジェクトの構造
影響を受ける部分のみが表示されます: そして、はい、今日はロボットを作成します)) テーブルのスクリプト、今日テストするメソッド (create_table_robots.sql):CREATE TABLE `robots`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`name` CHAR(255) CHARACTER SET utf8 NOT NULL,
`cpu` CHAR(255) CHARACTER SET utf8 NOT NULL,
`producer` CHAR(255) CHARACTER SET utf8 NOT NULL,
PRIMARY KEY (`id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
このテーブルを表すエンティティ:
@Builder
@Data
public class Robot {
private Long id;
private String name;
private String cpu;
private String producer;
}
テストされたリポジトリのインターフェイス:
public interface RobotDAO {
Robot findById(Long id);
Robot create(Robot robot);
List<Robot> findAll();
Robot update(Robot robot);
void delete(Long id);
}
実際、ここでは特殊なものを除いた標準的な CRUD 操作を示します。そのため、すべてのメソッドではなく (これは誰も驚かないでしょう)、簡潔にするために一部のメソッドの実装を検討します。
@Repository
@AllArgsConstructor
public class RobotDAOImpl implements RobotDAO {
private static final String FIND_BY_ID = "SELECT id, name, cpu, producer FROM robots WHERE id = ?";
private static final String UPDATE_BY_ID = "UPDATE robots SET name = ?, cpu = ?, producer = ? WHERE id = ?";
@Autowired
private final JdbcTemplate jdbcTemplate;
@Override
public Robot findById(Long id) {
return jdbcTemplate.queryForObject(FIND_BY_ID, robotMapper(), id);
}
@Override
public Robot update(Robot robot) {
jdbcTemplate.update(UPDATE_BY_ID,
robot.getName(),
robot.getCpu(),
robot.getProducer(),
robot.getId());
return robot;
}
private RowMapper<Robot> robotMapper() {
return (rs, rowNum) ->
Robot.builder()
.id(rs.getLong("id"))
.name(rs.getString("name"))
.cpu(rs.getString("cpu"))
.producer(rs.getString("producer"))
.build();
}
少し脱線して、依存関係で何が起こっているのかを見てみましょう (アプリケーションのデモ部分に使用される依存関係のみが示されています)。
<dependencies>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>2.5.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.craftercms.mariaDB4j</groupId>
<artifactId>mariaDB4j-springboot</artifactId>
<version>2.4.2.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.10</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<version>2.2.1.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
</dependencies>
4 - MariaDb データベース自体の依存関係 10 - SpringBoot と接続するための依存関係 16 - Lombok (まあ、これがどのような種類のライブラリであるかは誰もが知っていると思います) 22 - テスト用のスターター (必要な JUnit が埋め込まれている場所) 28 - スターターspringJdbc の操作 テストに必要な Bean (特に MariaDB 作成 Bean) を含む Spring コンテナを見てみましょう。
@Configuration
public class TestConfigDB {
@Bean
public MariaDB4jSpringService mariaDB4jSpringService() {
return new MariaDB4jSpringService();
}
@Bean
public DataSource dataSource(MariaDB4jSpringService mariaDB4jSpringService) {
try {
mariaDB4jSpringService.getDB().createDB("testDB");
} catch (ManagedProcessException e) {
e.printStackTrace();
}
DBConfigurationBuilder config = mariaDB4jSpringService.getConfiguration();
return DataSourceBuilder
.create()
.username("root")
.password("root")
.url(config.getURL("testDB"))
.driverClassName("org.mariadb.jdbc.Driver")
.build();
}
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource) {
return new JdbcTemplate(dataSource);
}
}
5 - MariaDB を起動するためのメインコンポーネント (Spring Framework に基づくアプリケーション用) 10 - データベース Bean の定義 12 - 作成されたデータベースの名前の設定 17 - このケースの構成の抽出 19 - Builder パターンを使用したデータベースの構築(パターンの概要) そして最後に、大騒ぎしているのは、データベースと通信するための JdbcTemplate Bean が生成されることです。考え方としては、Tao テスト用のメイン クラスを用意し、そこからすべての Tao テスト クラスを継承し、そのタスクには以下が含まれるということです。
- メインデータベースで使用されるいくつかのスクリプト (テーブルの作成、列の変更などのためのスクリプト) を起動します。
- テーブルにテスト データを埋めるテスト スクリプトを起動します。
- テーブルを削除しています。
@SpringBootTest(classes = TestConfigDB.class)
public abstract class DataBaseIT {
@Autowired
private JdbcTemplate jdbcTemplate;
public JdbcTemplate getJdbcTemplate() {
return jdbcTemplate;
}
public void fillDataBase(String[] initList) {
for (String x : initList) {
try {
jdbcTemplate.update(IOUtils.resourceToString("/db.migrations/" + x, StandardCharsets.UTF_8));
} catch (IOException e) {
e.printStackTrace();
}
}
}
public void cleanDataBase() {
getJdbcTemplate().update("DROP database testDB");
getJdbcTemplate().update("CREATE database testDB");
getJdbcTemplate().update("USE testDB");
}
public void fillTables(String[] fillList) {
for (String x : fillList) {
try {
Stream.of(
IOUtils.resourceToString("/fill_scripts/" + x, StandardCharsets.UTF_8))
.forEach(jdbcTemplate::update);
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
1 - @SpringBootTest アノテーションを使用して、テスト構成を設定します。 11 - このメソッドの引数として、必要なテーブルの名前を渡します。責任あるハードワーカーとして、テーブルがロードされます (これにより、私たちに機会が与えられます)。 21 - このメソッドはクリーニング、つまりデータベースからすべてのテーブル (およびそのデータ) を削除するために使用します。 27 - このメソッドの引数は、テスト データを持つスクリプトの名前の配列です。特定のメソッドをテストするためにロードされるテスト データを含むスクリプト:
INSERT INTO robots(name, cpu, producer)
VALUES ('Rex', 'Intel Core i5-9400F', 'Vietnam'),
('Molly', 'AMD Ryzen 7 2700X', 'China'),
('Ross', 'Intel Core i7-9700K', 'Malaysia')
そして今日私たちが集まったもの。
タオテストクラス
@RunWith(SpringRunner.class)
public class RobotDataBaseIT extends DataBaseIT {
private static RobotDAO countryDAO;
@Before
public void fillData() {
fillDataBase(new String[]{
"create_table_robots.sql"
});
countryDAO = new RobotDAOImpl(getJdbcTemplate());
}
@After
public void clean() {
cleanDataBase();
}
private RowMapper<Robot> robotMapper() {
return (rs, rowNum) ->
Robot.builder()
.id(rs.getLong("id"))
.name(rs.getString("name"))
.cpu(rs.getString("cpu"))
.producer(rs.getString("producer"))
.build();
}
2 - テストのメインクラスから継承します 4 - テストされるリポジトリ 7 - 各テストの前に起動されるメソッド 8 - 親クラスのメソッドを使用して必要なテーブルをロードします 11 - dao を初期化します 15 - メソッド各テストの後に起動され、データベースがクリーンアップされます。 19 - Tao クラスに似た RowMapper の実装です。 @Before と @After を使用します。これらは 1 つのテスト メソッドの前後に使用されますが、これを可能にするいくつかのライブラリを使用することもできます。このクラスの実行テストの開始と終了に関連付けられたアノテーションを使用します。たとえば、これは、毎回、クラスごとに 1 回テーブルを作成して完全に削除する必要があるため、テストが大幅に高速化されます。しかし、私たちはそんなことはしません。なぜ聞くの?いずれかのメソッドがテーブルの構造を変更した場合はどうなるでしょうか? たとえば、1 つの列を削除します。この場合、残りのメソッドは失敗するか、期待どおりに応答する必要があります (たとえば、バックカラムの作成)。これにより、テスト間の不必要な接続 (依存) が生じ、役に立たないことを認めなければなりません。話がそれましたが、続けましょう...
findById メソッドのテスト
@Test
public void findByIdTest() {
fillTables(new String[]{"fill_table_robots.sql"});
Long id = getJdbcTemplate().queryForObject("SELECT id FROM robots WHERE name = 'Molly'", Long.class);
Robot robot = countryDAO.findById(id);
assertThat(robot).isNotNull();
assertThat(robot.getId()).isEqualTo(id);
assertThat(robot.getName()).isEqualTo("Molly");
assertThat(robot.getCpu()).isEqualTo("AMD Ryzen 7 2700X");
assertThat(robot.getProducer()).isEqualTo("China");
}
3 - テーブルにテスト データを入力します。 5 - 必要なエンティティの ID を取得します。 6 - テスト対象のメソッドを使用します。 8...12 - 受信したデータと予想されるデータを比較します。
更新メソッドのテスト
@Test
public void updateTest() {
fillTables(new String[]{"fill_table_robots.sql"});
Long robotId = getJdbcTemplate().queryForObject("SELECT id FROM robots WHERE name = 'Rex'", Long.class);
Robot updateRobot = Robot.builder()
.id(robotId)
.name("Aslan")
.cpu("Intel Core i5-3470")
.producer("Narnia")
.build();
Robot responseRobot = countryDAO.update(updateRobot);
Robot updatedRobot = getJdbcTemplate().queryForObject(
"SELECT id, name, cpu, producer FROM robots WHERE id = ?",
robotMapper(),
robotId);
assertThat(updatedRobot).isNotNull();
assertThat(updateRobot.getName()).isEqualTo(responseRobot.getName());
assertThat(updateRobot.getName()).isEqualTo(updatedRobot.getName());
assertThat(updateRobot.getCpu()).isEqualTo(responseRobot.getCpu());
assertThat(updateRobot.getCpu()).isEqualTo(updatedRobot.getCpu());
assertThat(updateRobot.getProducer()).isEqualTo(responseRobot.getProducer());
assertThat(updateRobot.getProducer()).isEqualTo(updatedRobot.getProducer());
assertThat(responseRobot.getId()).isEqualTo(updatedRobot.getId());
assertThat(updateRobot.getId()).isEqualTo(updatedRobot.getId());
}
3 - テーブルにテスト データを入力します 5 - 更新されるエンティティの ID を取得します 7 - 更新されたエンティティを構築します 14 - テストされるメソッドを使用します 15 - 検証のために更新されたエンティティを取得します 20...28 - 受信したデータを比較します期待されるもの 更新メソッドのテストは作成と似ています。少なくとも私にとっては。調整は好きなだけ変更できます。チェックが多すぎることはありません。また、テストは完全な機能やバグがないことを保証するものではないことにも注意してください。テストは、プログラムの実際の結果 (そのフラグメント) が期待された結果と一致することを確認するだけです。この場合、テストが記述された部分のみがチェックされます。
テストのある授業を始めましょう...
勝利)) お茶を入れてクッキーを買いに行きましょう: 私たちはそれに値する))役立つリンク
- このテクノロジースタックの未調査の部分に関する優れた記事。
- Maven、Spring、MySQL、Hibernate に関する興味深い記事シリーズ。
- 単体テストについて少し思い出してみましょう。
- 統合テストの興味深い例ですが、PostgreSQL を置き換えています。
- テストに限らず、多くの人が MySql の代わりに MariaDB を使用します。
*壮大なスターウォーズ音楽*
GO TO FULL VERSION