


Структура проекта
Показаны только затронутые части:

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
Рассмотрим Spring контейнер с бинами, необходимыми для наших тестов (в частности и бин создания MariaDB):
@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 — определение бина базы данных
12 — задание имени создаваемой Базы данных
17 — вытягиваем конфигурации для нашего случая
19 — строим базу данных с помощью паттерна Builder (неплохой обзор на паттерн)
И наконец то, ради чего весь сыр-бор — это бин JdbcTemplate для связи с поднимаемой базой.
Идея в том, что у нас будет основной класс для тестов дао, от которого будут наследоваться все классы-тесты дао, в чьи задачи входит:
- запуск некоторых используемых в основной БД скриптов (скриптов создания таблиц, изменения колонок и прочих);
- запуск тестовых скриптов, заполняющих таблицы тестовыми данными;
- удаление таблиц.
@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 — инициализируем наш дао
15 — метод, который будет запускаться после каждого теста, чистя нашу базу
19 — реализация нашего RowMapper, аналог с класса дао
Мы используем @Before и @After, которые юзатся до и после одного метода-теста, а могли бы взять какую-нибудь либу, позволяющую использовать аннотации, привязанные к началу выполнений тестов данного класса и концу. Например, вот эту, что значительно ускорило бы тесты, так как создавать таблицы и полностью их удалять нужно было бы гн каждый раз, а один раз на класс.
Но мы так не делаем. Почему, спросите вы? А что если один из методов будет менять структуру таблицы? Например, удалять одну колонку. В таком случае остальные методы могут либо не выполниться, либо должны отреагировать должным образом (например, создать колонку назад). Приходится признать, что это даёт ненужную нам связанность (зависимость) тестов друг от друга, что нам ни к чему.
Но я отвлёкся, продолжаем….
Тестирование метода 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 — сверяем полученные данные с ожидаемыми
Тест метода update
@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 — сверяем полученные данные с ожидаемыми
Тестирование метода update схоже с create. По крайней мере у меня.
Извращаться со сверками можно как душе угодно: проверок много не бывает.
Очень бы хотелось заметить и то, что тесты не гарантируют полной работоспособности или отсутствия багов. Тесты всего лишь обеспечивают соответствие реального результата работы программы (её фрагмента) ожидаемому. При этом проверка происходит только тех частей, для которых были написаны тесты.
Запускаем класс с тестами…


Полезные ссылки
- Неплохая статья по нерассмотреной части с этим стеком технологий.
- Любопытный цикл статей о Maven, Spring, MySQL, Hibernate.
- Немного освежим память по юнит тестам.
- Интересный пример интеграционного тестирования, но с подменой PostgreSQL.
- Многие и не только в тестах используют MariaDB вместо MySql.

*эпичная музыка из Star Wars*
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ