JavaRush /Java блог /Random UA /Інтеграційне тестування БД за допомогою MariaDB для замін...
Константин
36 рівень

Інтеграційне тестування БД за допомогою MariaDB для заміни MySql

Стаття з групи Random UA
Інтеграційне тестування БД за допомогою MariaDB для заміни MySql - 1Сьогодні хотілося б поговорити про тестування, бо чим більше код покритий тестами, тим він вважається якіснішим та надійнішим. Торкнемося не модульне тестування, а інтеграційне тестування баз даних. У чому, власне, різниця між модульними тестами та інтеграційними? Інтеграційне тестування БД за допомогою MariaDB для заміни MySql - 2Модульне (юніт) - це тестування програми на рівні окремо взятих модулів, методів або класів, тобто тести швидкі та легкі, що стосуються максимально ділимих частин функціоналу. Про них говорять «один тест на один метод». Інтеграційні - більш повільні та великовагові, можуть складатися з кількох модулів та підйому додаткового функціоналу. Інтеграційне тестування БД за допомогою MariaDB для заміни MySql - 3Чому тести для шару dao (Data Access Object) є інтеграційними?Тому що для тестування методів із запитами до БД нам потрібно піднімати окрему БД в оперативній пам'яті, яка підміняє основну. Ідея в тому, що ми створюємо потрібні нам таблички, заповнюємо їх тестовими даними та перевіряємо коректність відпрацювання методів класу-репозиторію (адже ми знаємо, яким має бути кінцевий результат у тому чи іншому випадку). Тож почнемо. Теми щодо підключення БД вже давно виїжджені вздовж і впоперек, і тому сьогодні на цьому зупинятися не хотілося б, і розглянемо лише частини програми, які нас цікавлять. За замовчуванням будемо відштовхуватися від того, що додаток у нас на Spring Boot, для шару дао Spring JDBC (для більшої наочності), основна БД у нас MySQL, а підміняти ми будемо за допомогою MariaDB (вони максимально сумісні, і відповідно у скриптів MySQL ніколи не буде конфліктів з діалектом MariaDB, як буде у H2).

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

Показані тільки торкнуті частини: Інтеграційне тестування БД за допомогою 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 — звіряємо отримані дані з очікуваними тестування. Принаймні в мене. Перекручуватися зі звірками можна як душі завгодно: перевірок багато не буває. Дуже хотілося б помітити і те, що тести не гарантують повної працездатності або відсутності багів. Тести лише забезпечують відповідність реального результату роботи програми (її фрагмента) очікуваному. У цьому перевірка відбувається лише частин, котрим були написані тести.

Запускаємо клас із тестами…

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

Корисні посилання

Хто дочитав — дякую за увагу та… Інтеграційне тестування БД за допомогою MariaDB для заміни MySql - 9

*епічна музика зі Star Wars*

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ