Upgrade to Pro — share decks privately, control downloads, hide ads and more …

JUGNsk Meetup#6. Дмитрий Максименко: "DAOсские практики в unit тестировании"

jugnsk
February 27, 2019

JUGNsk Meetup#6. Дмитрий Максименко: "DAOсские практики в unit тестировании"

Доклад посвящен проблеме покрытия DAO слоя unit тестами (в том числе, вопросу проверки синтаксической и семантической корректности скриптов). А точнее, одному из вариантов решения данной проблемы: с помощью самописной утилиты. На мой взгляд, это актуально для всех, кто пишет на Spring Boot и использует Spring JDBC + Oracle, PostgreSQL и другие не in-memory БД.

jugnsk

February 27, 2019
Tweet

More Decks by jugnsk

Other Decks in Programming

Transcript

  1. Перечень освещаемых вопросов aka оглавление  О какой проблеме пойдет

    речь?  Что делать, чтобы проблему решить?  Как работает предложенное решение?  Что можно и чего пока нельзя?
  2. О чём презентация? О DAO тестах!  Что такое DAO

    тесты? DAO – часть прикладного кода, отвечающая за работу с БД. DAO тесты – Unit тесты, проверяющие, что взаимодействие с БД происходит так как ожидалось.  Зачем нужны DAO тесты? Затем же, зачем любые Unit тесты.  И в чём, собственно, проблема? В Oracle :’(
  3. Исходные данные Что есть в проекте?  Микросервисы на Spring

    Boot  Oracle  С недавних пор добавился PostgreSQL  Spring JDBC для выполнения скриптов  Отдельная папочка со скриптами (для инициализации БД) Чего нет?  Нет JPA  Нет Spring Data  Нет возможности тестировать DAO! :(
  4. Что же делать? 1. Найти СУБД, которую можно поднимать in-memory

    (например, H2). 2. Поднять в unit тесте. 3. Взять инициализационные скрипты и создать таблицы. 4. Прогнать тестируемые скрипты. 5. … 6. Profit! Естественно, скрипты разных СУБД далеко не 100% совместимы, поэтому нужно их как-то конвертировать. Так появилась идея написать утилиту, которая обеспечит: 1. Подъем in-memory H2. 2. Конвертацию инициализационных скриптов из Oracle SQL в H2 SQL и их исполнение на H2. 3. Исполнение тестируемых скриптов на H2.  Не велосипед ли это?
  5. Основные компоненты  Парсер Oracle скриптов, отвечающий за превращение описаний

    таблиц в Java объекты  Построитель скриптов на H2  DatabaseInitializer – публичный компонент, предоставляющий методы для создания и наполнения БД  AbstractDaoTest – «прародитель» всех DAO тестов, предоставляющий им всё необходимое
  6. Чем и зачем можно пользоваться? AbstractDaoTest:  Использование данного класса

    как родителя DAO тестов позволяет поднять все необходимые бины, не задумываясь о том, как правильно настраивать контекст для unit теста. DatabaseInitializer:  initDb (принимает на вход путь до SQL скрипта): ◦ считывает скрипт; ◦ парсит SQL скрипт в Java объекты; ◦ формирует скрипты drop + create для каждой таблицы; ◦ выполняет скрипты.  insertRows (принимает на вход список строк с данными): ◦ сериализует объекты в строки. Всё через toString(), кроме: UUID, LocalDate, LocalDateTime, String, Enum; ◦ формирует insert скрипт; ◦ выполняет скрипт.
  7. CREATE TABLE SOME_TABLE ( VIRTUAL_COL VARCHAR2(256) AS (DECODE(VARCHAR_COL1, 'TEST', VARCHAR_COL1,

    NULL), UUID_COL RAW(16), NUMBER_COL1 NUMBER(10, 0) DEFAULT 0, NUMBER_COL2 NUMBER(19, 2), NUMBER_COL3 NUMBER(19), VARCHAR_COL1 VARCHAR2(255 CHAR), VARCHAR_COL2 VARCHAR2, DATE_COL TIMESTAMP DEFAULT SYSDATE, SEQ_COL NUMBER(19, 0) DEFAULT 1104 ); CREATE TABLE SOME_TABLE ( UUID_COL RAW(16), NUMBER_COL1 NUMBER(10, 0) DEFAULT 0, NUMBER_COL2 NUMBER(19, 2), NUMBER_COL3 NUMBER(19), VARCHAR_COL1 VARCHAR2(255 CHAR), VARCHAR_COL2 VARCHAR2, DATE_COL DATE DEFAULT SYSDATE, SEQ_COL NUMBER(19, 0) DEFAULT SOME_SEQ.nextval, VIRTUAL_COL GENERATED ALWAYS AS (DECODE(VARCHAR_COL1, 'TEST', VARCHAR_COL1, NULL)) VIRTUAL ) PARTITION BY RANGE (CREATED) INTERVAL (numtoyminterval(1, 'MONTH')) ( PARTITION p0701 VALUES LESS THAN (to_date('2017-09-01', 'yyyy-mm-dd')) ) TABLESPACE SOME_SPACE; Пример конвертации Oracle H2
  8. Как написать свой DAO тест? Предварительная подготовка.  Добавить библиотеку

    в проект.  Обеспечить тестам доступ к скриптам.  Прописать конфиги для тестов: ◦ spring.datasource.url=jdbc:h2:mem:test (значение по умолчанию) Написание тестов.  Создать тестовый класс, наследующий AbstractDaoTest.  Вызвать databaseInitializer.initDb(«createTables.sql») (перед каждым тестом).  Для выполнения скриптов нужно использовать namedParameterJdbcTemplate поставляемый AbstractDaoTest.  При необходимости, можно подготовить данные с помощью предлагаемого DSL (класс Row) и заполнить таблицы данными databaseInitializer.insertRows(List<Row>)).
  9. Основа DAO теста CREATE TABLE TEMPLATE ( TEMPLATE_CODE VARCHAR2(50), DESCRIPTION

    VARCHAR2(2048) ); @Repository @RequiredArgsConstructor(onConstructor = @__(@Autowired)) public class TemplateDao { private final NamedParameterJdbcTemplate jdbcTemplate; public List<Template> findAll() { String sql = "SELECT TEMPLATE_CODE, DESCRIPTION " + " FROM TEMPLATE"; return jdbcTemplate.query( sql, emptyMap(), (rs, rowNum) -> new Template( rs.getString("TEMPLATE_CODE"), rs.getString("DESCRIPTION"))); } }
  10. Класс с DAO тестом class TemplateDaoTest extends AbstractDaoTest { private

    static final String TEMPLATE_CODE = "payment"; private static final String ANOTHER_TEMPLATE_CODE = "anotherPayment"; private static final String DESCRIPTION = "Оплата"; private static final String ANOTHER_DESCRIPTION = "Другая оплата"; private TemplateDao sut; @BeforeEach void setUp() throws Exception { databaseInitializer.initDb("/create_tables.sql"); sut = new TemplateDao(namedParameterJdbcTemplate); }
  11. Использование insert-DSL private void insertRows() { String tableName = "TEMPLATE";

    Row row = new Row(tableName) .withColumn("TEMPLATE_CODE", TEMPLATE_CODE) .withColumn("DESCRIPTION", DESCRIPTION); Row row2 = new Row(tableName) .withColumn("TEMPLATE_CODE", ANOTHER_TEMPLATE_CODE) .withColumn("DESCRIPTION", ANOTHER_DESCRIPTION); List<Row> rows = asList(row, row2); databaseInitializer.insertRows(rows); }
  12. Пример DAO теста @Test @DisplayName("Поиск всех шаблонов") void selectAllTemplates() {

    insertRows(); List<Template> templates = sut.findAll(); assertThat(templates, hasSize(2)); Template template1 = templates.get(0); assertEquals(TEMPLATE_CODE, template1.getCode()); assertEquals(DESCRIPTION, template1.getDescription()); Template template2 = templates.get(1); assertEquals(ANOTHER_TEMPLATE_CODE, template2.getCode()); assertEquals(ANOTHER_DESCRIPTION, template2.getDescription()); }
  13. Ограничения (постоянные и нет) Постоянные ограничения:  Только чистый SQL,

    только DML.  Нельзя задавать алиасы для таблиц при insert. H2 не поддерживает. Временные ограничения:  Только Oracle.  Нельзя протестировать merge запросы.  Ограничения (constraints), партиции, индексы, сиквенсы игнорируются. Только таблицы.
  14. Вместо заключения  На текущий момент есть некоторые ограничения. 

    В любой момент может внезапно вылезти какая-нибудь особенность H2 и всё упадёт.  Ошибки не человекочитабельные. Но!  Статистика покрытия говорит «всё огонь!»: Микросервис Без DAO тестов С DAO тестами Stark 81 98 Compass 95 98 Informer 63 93