JavaRush /Java блог /Java Developer /Интеграционное тестирование БД с помощью MariaDB для подм...
Константин
36 уровень

Интеграционное тестирование БД с помощью MariaDB для подмены MySql

Статья из группы Java Developer
Интеграционное тестирование БД с помощью MariaDB для подмены MySql - 1Сегодня хотелось бы поговорить о тестировании, ибо чем больше код покрыт тестами, тем он считается качественней и надёжней. Затронем не модульное тестирование, а интеграционное тестирование баз данных. В чём, собственно, разница между модульными тестами и интеграционными? Интеграционное тестирование БД с помощью MariaDB для подмены MySql - 2Модульное(юнит) — это тестирование программы на уровне отдельно взятых модулей, методов или классов, то есть тесты быстрые и легкие, затрагивающие максимально делимые части функционала. О них ещё говорят «один тест на один метод». Интеграционные — более медленные и тяжеловесные, могут состоять из нескольких модулей и подъема дополнительного функционала. Интеграционное тестирование БД с помощью MariaDB для подмены MySql - 3Почему тесты для слоя dao (Data Access Object) — интеграционные? Потому, что для тестирования методов с запросами к БД нам нужно поднимать отдельную БД в оперативной памяти, подменяющую основную. Идея в том, что мы создаем нужные нам таблички, заполняем их тестовыми данными и проверяем корректность отработки методов класса-репозитория (ведь мы знаем, каким должен быть конечный результат в том или ином случае). Итак начнём. Темы по подключению БД уже давно изъезжены вдоль и поперек, и поэтому сегодня на этом останавливаться не хотелось бы, и рассмотрим лишь интересующие нас части программы. По умолчанию отталкиваться будем от того, что приложение у нас на Spring Boot, для слоя дао Spring JDBC (для большей наглядности), основная БД у нас MySQL, а подменять мы будем с помощью MariaDB (они максимально совместимы, и соответственно у скриптов MySQL никогда не будет конфликтов с диалектом MariaDB, как будет у H2). Также условно примем, что наша программа для управления и применения изменений схемы базы данных использует Liquibase, и соответственно, все примененные скрипты хранятся у нас в приложении.

Структура проекта

Показаны только затронутые части: Интеграционное тестирование БД с помощью MariaDB для подмены MySql - 5И да, мы сегодня будем создавать роботов)) Интеграционное тестирование БД с помощью MariaDB для подмены MySql - 6Скрипт для таблицы, методы к которой бы будем тестить сегодня (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 Рассмотрим 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 для связи с поднимаемой базой. Идея в том, что у нас будет основной класс для тестов дао, от которого будут наследоваться все классы-тесты дао, в чьи задачи входит:
  1. запуск некоторых используемых в основной БД скриптов (скриптов создания таблиц, изменения колонок и прочих);
  2. запуск тестовых скриптов, заполняющих таблицы тестовыми данными;
  3. удаление таблиц.

@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. По крайней мере у меня. Извращаться со сверками можно как душе угодно: проверок много не бывает. Очень бы хотелось заметить и то, что тесты не гарантируют полной работоспособности или отсутствия багов. Тесты всего лишь обеспечивают соответствие реального результата работы программы (её фрагмента) ожидаемому. При этом проверка происходит только тех частей, для которых были написаны тесты.

Запускаем класс с тестами…

Интеграционное тестирование БД с помощью MariaDB для подмены MySql - 7Победа)) Идём заваривать чай и доставать печеньки: мы это заслужили)) Интеграционное тестирование БД с помощью MariaDB для подмены MySql - 8

Полезные ссылки

Кто дочитал — спасибо за внимание и… Интеграционное тестирование БД с помощью MariaDB для подмены MySql - 9

*эпичная музыка из Star Wars*

Комментарии (4)
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ
6 сентября 2021
Зачем нам юзать другую СУБД, если мы можем в том же мускуле поднять тестовую базу?
Roman Beekeeper Уровень 35
9 декабря 2019
Спасибо за статью! Не пробовал использовать @DataJpaTest вместо @SpringBootTest? Дело в том, что при запуске интеграционных тестов через SpringBootTest запускается весь контейнер бинов, а если использовать DataJpaTest, то запускается только те бины, которые относятся к слою репозитория.