Я использую первый подход, но немного другой, который позволяет решать упомянутые проблемы.
Все, что необходимо для запуска тестов для DAO, находится в системе контроля версий. Он включает в себя схему и сценарии для создания БД (докер очень хорош для этого). Если можно использовать встроенную БД - я использую ее для скорости.
Важным отличием от других описанных подходов является то, что данные, необходимые для тестирования, не загружаются из сценариев SQL или файлов XML. Все (кроме некоторых словарных данных, которые фактически являются постоянными) создается приложением с использованием служебных функций / классов.
Основная цель - сделать данные, используемые тестом
- очень близко к тесту
- явный (использование данных SQL для данных делает очень проблематичным определение того, какой фрагмент данных используется в каком тесте)
- изолировать тесты от несвязанных изменений.
Это в основном означает, что эти утилиты позволяют декларативно указывать только те вещи, которые важны для теста, в самом тесте и пропускают ненужные вещи.
Чтобы дать некоторое представление о том, что это означает на практике, рассмотрим тест для некоторого DAO, который работает с Comment
с Post
с, записанным Authors
. Чтобы протестировать CRUD-операции для таких DAO, в БД должны быть созданы некоторые данные. Тест будет выглядеть так:
@Test
public void savedCommentCanBeRead() {
// Builder is needed to declaratively specify the entity with all attributes relevant
// for this specific test
// Missing attributes are generated with reasonable values
// factory's responsibility is to create entity (and all entities required by it
// in our example Author) in the DB
Post post = factory.create(PostBuilder.post());
Comment comment = CommentBuilder.comment().forPost(post).build();
sut.save(comment);
Comment savedComment = sut.get(comment.getId());
// this checks fields that are directly stored
assertThat(saveComment, fieldwiseEqualTo(comment));
// if there are some fields that are generated during save check them separately
assertThat(saveComment.getGeneratedField(), equalTo(expectedValue));
}
Это имеет несколько преимуществ перед сценариями SQL или файлами XML с тестовыми данными:
- Ведение кода намного проще (добавление обязательного столбца, например, в некоторый объект, на который ссылаются во многих тестах, например, в Author, не требует изменения большого количества файлов / записей, а только изменения в сборщике и / или фабрике)
- Данные, необходимые для конкретного теста, описаны в самом тесте, а не в каком-то другом файле. Эта близость очень важна для понимания теста.
Откат против фиксации
Мне удобнее, когда тесты фиксируются при выполнении. Во-первых, некоторые эффекты (например, DEFERRED CONSTRAINTS
) не могут быть проверены, если коммит никогда не происходит. Во-вторых, если тест не пройден, данные могут быть проверены в БД, поскольку они не возвращаются при откате.
Из-за этого есть и обратная сторона: тест может выдавать неверные данные, что приведет к сбоям в других тестах. Чтобы справиться с этим, я пытаюсь выделить тесты. В приведенном выше примере каждый тест может создавать новый Author
, и все другие объекты, связанные с ним, создаются, поэтому столкновения случаются редко. Чтобы справиться с остальными инвариантами, которые могут быть потенциально нарушены, но не могут быть выражены как ограничение уровня БД, я использую некоторые программные проверки для ошибочных условий, которые могут выполняться после каждого отдельного теста (и они запускаются в CI, но обычно отключаются локально для производительности причины).