Similar presentations:
Spring Data JPA. Антипаттерны тестирования
1.
Spring Data JPA. Антипаттернытестирования
Семен Киреков
1
2.
О себе• Киреков Семен
• Java Dev и Team Lead в «МТС Диджитал»
Центр Big Data
• Java-декан в МТС.Тета
2
3.
Немного статистикиhttps://bit.ly/3GFdQ3X
3
4.
План доклада• Антипаттерны
• Паттерны, на которые стоит заменить
• Неочевидные моменты
• SimonHarmonicMinor/spring-data-jpa-efficient-testing
4
5.
Бизнес-областьСистема по управлению роботами.
Включение, выключение и так далее.
Photo by Jason Leung on Unsplash
5
6.
Домен@Entity
@Table(name = "robot")
public class Robot {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "robot_id")
private Long id;
@NotNull
private String name;
@NotNull
private boolean switched;
@Enumerated(STRING)
@NotNull с
private Type type;
public enum Type {
DRIVER,
LOADER,
VACUUM
}
}
6
7.
Task #1Требуется реализовать включение.
Необходимо проверять, можно ли включить данного робота.
7
8.
@Servicepublic class RobotUpdateService {
private final RobotRepository robotRepository;
private final RobotRestrictions
robotRestrictions;
@Transactional
public void switchOnRobot(Long robotId) {
final var robot =
robotRepository.findById(robotId)
.orElseThrow();
robot.setSwitched(true);
robotRepository.saveAndFlush(robot);
robotRestrictions.checkSwitchOn(robotId);
}
8
9.
testRuntimeOnly 'com.h2database:h2'spring.jpa.hibernate.ddl-auto=create
9
10.
DisclaimerДалее в коде будет много антипаттернов!
10
11.
@SpringBootTest@AutoConfigureTestDatabase
@DirtiesContext(classMode =
AFTER_EACH_TEST_METHOD)
class RobotUpdateServiceTestH2DirtiesContext {
@Autowired
private RobotUpdateService service;
@Autowired
private RobotRepository robotRepository;
@MockBean
private RobotRestrictions robotRestrictions;
…
}
11
12.
@Target({ ElementType.TYPE, ElementType.METHOD })@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ImportAutoConfiguration
@PropertyMapping("spring.test.database")
public @interface AutoConfigureTestDatabase {
@PropertyMapping(skip = SkipPropertyMapping.ON_DEFAULT_VALUE)
Replace replace() default Replace.ANY;
EmbeddedDatabaseConnection connection() default
EmbeddedDatabaseConnection.NONE;
enum Replace {
ANY,
AUTO_CONFIGURED,
NONE
}
}
12
13.
public enum EmbeddedDatabaseConnection {NONE(null, null, null, (url) -> false),
H2(EmbeddedDatabaseType.H2,
DatabaseDriver.H2.getDriverClassName(),
"jdbc:h2:mem:%s;DB_CLOSE_DELAY=1;DB_CLOSE_ON_EXIT=FALSE", (url) -> url.contains(":h2:mem")),
DERBY(EmbeddedDatabaseType.DERBY,
DatabaseDriver.DERBY.getDriverClassName(),
"jdbc:derby:memory:%s;create=true",
(url) -> true),
HSQLDB(EmbeddedDatabaseType.HSQL,
DatabaseDriver.HSQLDB.getDriverClassName(), "org.hsqldb.jdbcDriver",
"jdbc:hsqldb:mem:%s", (url) -> url.contains(":hsqldb:mem:"));
}
13
14.
public static EmbeddedDatabaseConnection get(ClassLoader classLoader) {for (EmbeddedDatabaseConnection candidate : EmbeddedDatabaseConnection.values()) {
if (candidate != NONE && ClassUtils.isPresent(candidate.getDriverClassName(), classLoader
return candidate;
}
}
return NONE;
}
14
15.
@SpringBootTest@AutoConfigureTestDatabase
@DirtiesContext(classMode =
AFTER_EACH_TEST_METHOD)
class RobotUpdateServiceTestH2DirtiesContext {
@Autowired
private RobotUpdateService service;
@Autowired
private RobotRepository robotRepository;
@MockBean
private RobotRestrictions robotRestrictions;
…
}
15
16.
Логирование транзакцийlogging.level.org.springframework.orm.jpa.JpaTransactionManager=DE
BUG
16
17.
Creating new transaction with name[RobotUpdateService.switchOnRobot]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Creating
new
transaction
with namewith name
Creating
new
Hibernate:
selecttransaction
robot0_.robot_id
as robot_id1_1_0_,
[SimpleJpaRepository.save]:
[SimpleJpaRepository.findById]:
robot0_.name
as name2_1_0_, robot0_.switched as
PROPAGATION_REQUIRED,ISOLATION_DEFAULT
PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly
switched3_1_0_,
robot0_.type as type4_1_0_ from robot
Hibernate:
insert
intoselect
robot
(robot_id, name,as
switched,
type)
Hibernate:
robot0_.robot_id
robot_id1_1_0_,
robot0_
where
robot0_.robot_id=?
values (null,
?, ?, ?)updateasrobot
robot0_.name
name2_1_0_,
robot0_.switched
as
Hibernate:
set name=?,
switched=?, type=?
Committing
JPA
transactionrobot0_.type
on EntityManager
switched3_1_0_,
as type4_1_0_ from robot
where
robot_id=?
[SessionImpl(115584215<open>)]
robot0_ where
robot0_.robot_id=?
Committing
JPA transaction
on EntityManager
Committing JPA transaction on EntityManager
[SessionImpl(93418194<open>)]
[SessionImpl(31874125<open>)]
Протестируем Commit
@Test
void shouldSwitchOnSuccessfully() {
final var robot = new Robot();
robot.setSwitched(false);
robot.setType(DRIVER);
robot.setName("some_name");
robotRepository.save(robot);
doNothing().when(robotRestrictions).checkSwitchOn(robot.getId());
service.switchOnRobot(robot.getId());
final var savedRobot =
robotRepository.findById(robot.getId()).orElseThrow();
assertTrue(savedRobot.isSwitched());
}
17
18.
Протестируем RollbackCreating new transaction with name
[RobotUpdateService.switchOnRobot]:
PROPAGATION_REQUIRED,ISOLATION_DEFAULT
Hibernate: select robot0_.robot_id as robot_id1_1_0_, robot0_.name as
name2_1_0_, robot0_.switched as switched3_1_0_, robot0_.type as
type4_1_0_ from robot robot0_ where robot0_.robot_id=?
Hibernate: update robot set name=?, switched=?, type=? where
robot_id=?
Rolling back JPA transaction on EntityManager
[SessionImpl(1969238242<open>)]
@Test
void shouldRollbackIfCannotSwitchOn() {
final var robot = new Robot();
robot.setSwitched(false);
robot.setType(DRIVER);
robot.setName("some_name");
robotRepository.save(robot);
doThrow(new OperationRestrictedException("")).when(robotRestrictions)
.checkSwitchOn(robot.getId());
assertThrows(OperationRestrictedException.class, () ->
service.switchOnRobot(robot.getId()));
final var savedRobot = robotRepository.findById(robot.getId()).orElseThrow();
assertFalse(savedRobot.isSwitched());
}
18
19.
1920.
#1. Антипаттерн.DirtiesContext для очистки данных
20
21.
@BeforeEachvoid beforeEach() {
robotRepository.deleteAll();
}
21
22.
#2. Антипаттерн.Инстанцирование сущностей
напрямую
22
23.
@Testvoid shouldRollbackIfCannotSwitchOn() {
final var robot = new Robot();
robot.setSwitched(false);
robot.setType(DRIVER);
robot.setName("some_name");
robotRepository.save(robot);
doThrow(new
OperationRestrictedException("")).when(robotRestrictions).checkSwitchOn(robot.getId());
assertThrows(OperationRestrictedException.class, () ->
service.switchOnRobot(robot.getId()));
final var savedRobot = robotRepository.findById(robot.getId()).orElseThrow();
assertFalse(savedRobot.isSwitched());
}
23
24.
Конструкторfinal var robot = new Robot("some_name", false, DRIVER);
24
25.
Object Motherpublic class RobotFactory {
public static Robot createWithName(String name) {
…
}
public static Robot createWithType(Type type) {
…
}
}
https://bit.ly/3GHMWZc
25
26.
Object Motherpublic class RobotFactory {
public static Robot createWithName(String name) {
…
}
public static Robot createWithType(Type type) {
…
}
public static Robot createWithNameAndType(String name, Type type) {
…
}
public static Robot createWithTypeAndSwitched(Type type, boolean switched) {
…
}
public static Robot createWithNameAndTypeAndSwitched(String name, Type type, boolean
switched) {
…
}
}
https://bit.ly/3GHMWZc
26
27.
Lombok Builder27
28.
Lombok Builder@Entity
@Table(name = "robot")
@Builder
@NoArgsConstructor
public class Robot {
@Id
@GeneratedValue(strategy = IDENTITY)
@Column(name = "robot_id")
private Long id;
@NotNull
private String name;
@NotNull
private boolean switched;
@Enumerated(STRING)
@NotNull
private Type type;
public enum Type {
DRIVER,
LOADER,
VACUUM
}
28
29.
Пример использования Lombok Builderfinal var robot = Robot.builder()
.switched(false)
.type(DRIVER)
.name("some_name")
.build();
29
30.
Test Data Builderpublic interface TestBuilder<T> {
T build();
}
https://bit.ly/3532gSo
30
31.
Test Data Builderpublic class RobotTestBuilder implements
TestBuilder<Robot> {
private String name = "";
private boolean switched = false;
private Type type = DRIVER;
private RobotTestBuilder() {}
private RobotTestBuilder(RobotTestBuilder builder) {
this.name = builder.name;
this.switched = builder.switched;
this.type = builder.type;
}
public static RobotTestBuilder aRobot() {
return new RobotTestBuilder();
}
public RobotTestBuilder name(String name) {
return copyWith(b -> b.name = name);
}
}
@Override
public Robot build() {
final var server = new Robot();
server.setName(name);
server.setSwitched(switched);
server.setType(type);
return server;
} https://bit.ly/3532gSo
31
32.
Test Data Builderwith Lombok
@AllArgsConstructor
@NoArgsConstructor(staticName = "aRobot")
@With
public class RobotTestBuilder implements
TestBuilder<Robot> {
private String name = "";
private boolean switched = false;
private Type type = DRIVER;
@Override
public Robot build() {
final var server = new Robot();
server.setName(name);
server.setSwitched(switched);
server.setType(type);
return server;
}
}
https://bit.ly/3532gSo
32
33.
aRobot().switched(false).build()var robot = aRobot().name("my_robot");
…
var switchedOn = robot.switched(true).build();
var vacuum = robot.type(VACUUM).build();
33
34.
Easy RandomtestImplementation 'org.jeasy:easy-random-core:5.0.0'
EasyRandom easyRandom = new
EasyRandom();
Robot robot =
easyRandom.nextObject(Robot.class);
Robot(
id=-5106534569952410475,
name=eOMtThyhVNLWUZNRcBaQKxI,
switched=true,
type=VACUUM
)
34
35.
Easy Randomvar parameters =
new EasyRandomParameters()
.excludeField(field ->
field.getName().equals("id"));
var easyRandom = new EasyRandom(parameters);
var robot = easyRandom.nextObject(Robot.class);
Robot(
id=null,
name=FypEwUZ,
switched=false,
type=DRIVER
)
35
36.
Easy RandomRobot –one-to-many-> Detail
36
37.
Вывод. Инстанцирование сущностейнапрямую
• Инстанцирование через конструктор – ошибки компиляции
• Сеттеры – ошибки в рантайме, verbose
• Object Mother – простые сущности
• Test Data Builder – универсальный
• Object Mother is OK with Kotlin
37
38.
#3. Антипаттерн.Зависимость на репозитории
38
39.
Причины• Сущностей может быть много
• Затрудняет понимание
• Порядок удаления
39
40.
4041.
Варианты• JdbcTemplate
41
42.
jdbcTemplate.update("insert into robot(name, switched, type) values(?, ?,
?)",
"robot_name",
false,
"VACUUM"
);
Хватит писать тесты, пора писать спецификации!
https://bit.ly/3AHqayp
42
43.
Варианты• JdbcTemplate
• TestEntityManager
43
44.
TestEntityManager под капотомpublic final EntityManager getEntityManager() {
EntityManager manager =
EntityManagerFactoryUtils.getTransactionalEntityManager(this.entityManagerFact
ory);
Assert.state(manager != null, "No transactional EntityManager found");
return manager;
}
44
45.
testEntityManager.persistAndFlush(aRobot().switched(true).build()
);
No transactional EntityManager found
transactionTemplate.execute(status ->
testEntityManager.persistAndFlush(
aRobot().switched(true).build()
)
);
45
46.
Варианты• JdbcTemplate
• TestEntityManager + TransactionTemplate
• TestDbFacade
46
47.
TestDbFacadepublic class TestDBFacade {
@Autowired
private TestEntityManager testEntityManager;
@Autowired
private TransactionTemplate
transactionTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;
…
}
https://bit.ly/3KpsKOf
47
48.
TestDbFacadepublic void cleanDatabase() {
transactionTemplate.execute(status -> {
JdbcTestUtils.deleteFromTables(jdbcTemplate,
"robot");
return null;
});
}
https://bit.ly/3KpsKOf
48
49.
TestDbFacadepublic <T> T find(Object id, Class<T>
entityClass) {
return transactionTemplate.execute(
status ->
testEntityManager.find(entityClass, id)
);
}
https://bit.ly/3KpsKOf
49
50.
TestDbFacadepublic <T> T save(TestBuilder<T> builder) {
return transactionTemplate.execute(
status ->
testEntityManager.persistAndFlush(builder.build())
);
}
https://bit.ly/3KpsKOf
50
51.
Посты/комментыTestBuilder<User> user = db.persistedOnce(aUser().login("login"));
TestBuilder<Comment> comment = aComment().author(user);
for (int i = 0; i < 10; i++) {
db.save(
aPost()
.rating(i)
.author(user)
.comments(List.of(
comment.text("comment1"),
comment.text("comment2")
))
);
}
51
52.
TestDbFacade@TestConfiguration
public static class Config {
@Bean
public TestDBFacade
testDBFacade() {
return new TestDBFacade();
}
}
https://bit.ly/3KpsKOf
52
53.
@SpringBootTest@AutoConfigureTestEntityManager
@AutoConfigureTestDatabase
@Import(TestDBFacade.Config.class)
class
RobotUpdateServiceTestH2TestDataBuilder
53
54.
@Testvoid shouldSwitchOnSuccessfully() {
final var robot = new Robot();
robot.setSwitched(false);
robot.setType(DRIVER);
robot.setName("some_name");
robotRepository.save(robot);
doNothing().when(robotRestrictions)
.checkSwitchOn(robot.getId());
@Test
void shouldSwitchOnSuccessfully() {
final var id = db.save(aRobot().switched(false)).getId();
doNothing().when(robotRestrictions).checkSwitchOn(id);
service.switchOnRobot(robot.getId());
final var savedServer = db.find(id, Robot.class);
assertTrue(savedServer.isSwitched());
final var savedRobot =
robotRepository.findById(robot.getId()).orElseThrow();
assertTrue(savedRobot.isSwitched());
}
service.switchOnRobot(id);
}
54
55.
Дополнительные материалыSpring Boot + JPA — Clear Tes
https://habr.com/ru/post/312248/
55
56.
SQL Annotation@Test
@Sql("/delete_data.sql")
@Sql("/insert_data.sql")
void
shouldSwitchOnSuccessfully() {
…
}
56
57.
Проблемы SQL Annotation• Нет статической типизации
• Проблемы со сложными объектами (JSON to Java Object)
• Каскадные изменения при ALTER TABLE
• Как найти сущность с Sequence ID?
57
58.
Вывод. Зависимость на репозитории• Затрудняет понимание теста
• Лишние зависимости
• Альтернативы: JdbcTemplate / @SQL, TestEntityManager, Test DB
Facade
58
59.
#4. Антипаттерн.Поднятие всего контекста
59
60.
@DataJpaTest тоже поднимает SpringContext
Но не весь
60
61.
@DataJpaTest@Import(TestDBFacade.Config.class)
class RobotUpdateServiceTestH2DataJpa {
@Autowired
private RobotUpdateService service;
@Autowired
private TestDBFacade db;
@MockBean
private RobotRestrictions
robotRestrictions;
…
}
61
62.
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(DataJpaTestContextBoots
trapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled =
false)
@TypeExcludeFilters(DataJpaTypeExclude
Filter.class)
@Transactional
@AutoConfigureCache
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@ImportAutoConfiguration
public @interface DataJpaTest
62
63.
@DataJpaTest@Import(TestDBFacade.Config.class)
class RobotUpdateServiceTestH2DataJpa {
@Autowired
private RobotUpdateService service;
@Autowired
private TestDBFacade db;
@MockBean
private RobotRestrictions
robotRestrictions;
…
}
63
64.
@TestConfigurationstatic class Config {
@Bean
public RobotUpdateService service(
RobotRepository robotRepository,
RobotRestrictions robotRestrictions
){
return new RobotUpdateService(robotRepository,
robotRestrictions);
}
}
64
65.
@BeforeEachvoid beforeEach() {
db.cleanDatabase();
}
65
66.
@Testvoid shouldSwitchOnSuccessfully() { … }
@Test
void shouldRollbackIfCannotSwitchOn() { … }
66
67.
assertFalse(savedRobot.isSwitched());
expected: <false> but was: <true>
Expected :false
Actual :true
67
68.
6869.
Propagation@Transactional
public void switchOnRobot(Long robotId) {
final var robot =
robotRepository.findById(robotId)
.orElseThrow();
robot.setSwitched(true);
robotRepository.saveAndFlush(robot);
robotRestrictions.checkSwitchOn(robotId);
}
69
70.
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(DataJpaTestContextBoots
trapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled =
false)
@TypeExcludeFilters(DataJpaTypeExclude
Filter.class)
@Transactional
@AutoConfigureCache
@AutoConfigureDataJpa
@AutoConfigureTestDatabase
@AutoConfigureTestEntityManager
@ImportAutoConfiguration
public @interface DataJpaTest
70
71.
assertFalse(savedRobot.isSwitched());71
72.
Что делать?@Transactional(propagation =
REQUIRES_NEW)
public void switchOnRobot(Long robotId) {
final var robot =
robotRepository.findById(robotId)
.orElseThrow();
robot.setSwitched(true);
robotRepository.saveAndFlush(robot);
robotRestrictions.checkSwitchOn(robotId);
}
72
73.
assertThrows(OperationRestrictedException.class, () ->service.switchOnRobot(id));
73
74.
7475.
7576.
Пойдем дальше@Transactional(propagation = REQUIRES_NEW, isolation =
READ_UNCOMMITTED)
public void switchOnRobot(Long robotId) {
final var robot =
robotRepository.findById(robotId)
.orElseThrow();
robot.setSwitched(true);
robotRepository.saveAndFlush(robot);
robotRestrictions.checkSwitchOn(robotId);
}
76
77.
Dirty Write:Hibernate: update robot set name=?, switched=?, type=? where robot_id=?
SQL Error: 50200, SQLState: HYT00
Время ожидания блокировки таблицы {0} истекло
Timeout trying to lock table {0}; SQL statement:
77
78.
Транзакционные тесты – это опасноhttps://bit.ly/3IhAbFc
78
79.
@Transactionalpublic void switchOnRobot(Long robotId) {
final var robot =
robotRepository.findById(robotId)
.orElseThrow();
robot.setSwitched(true);
robotRepository.saveAndFlush(robot);
robotRestrictions.checkSwitchOn(robotId);
}
@DataJpaTest
@Import(TestDBFacade.Config.class)
@Transactional(propagation =
NOT_SUPPORTED)
class RobotUpdateServiceTestH2DataJpa
79
80.
@BeforeEachvoid beforeEach() {
db.cleanDatabase();
}
80
81.
8182.
assertFalse(savedRobot.isSwitched());82
83.
Вывод• Транзакционные тесты допустимы в readonly-операциях
83
84.
@Transactional(readOnly = true)#5. Антипаттерн. Rollback readonly
транзакций
84
85.
@Transactionalpublic void switchOnRobot(Long robotId) {
final var robot =
robotRepository.findById(robotId)
.orElseThrow();
robot.setSwitched(true);
robotRepository.saveAndFlush(robot);
robotRestrictions.checkSwitchOn(robotId);
}
85
86.
TaskНеобходимо получать список статусов: допустимо ли включение
робота.
86
87.
1. Если робот уже включен, нельзя отправлять запрос повторно.2. Не более трех включенных роботов каждого типа
87
88.
@Transactional(readOnly = true)public void checkSwitchOn(Long robotId) {
final var robot =
robotRepository.findById(robotId)
.orElseThrow();
if (robot.isSwitched()) {
throw new OperationRestrictedException(
format("Robot %s is already switched on", robot.getName())
);
}
final var count = robotRepository.countAllByTypeAndIdNot(robot.getType(),
robotId);
if (count >= 3) {
throw new OperationRestrictedException(
format("There is already 3 switched on robots of type %s", robot.getType())
);
}
}
88
89.
@Transactional(readOnly = true)public Map<Long, OperationStatus> getRobotsSwitchOnStatus(Collection<Long> robotIds) {
final var result = new HashMap<Long, OperationStatus>();
for (Long robotId : robotIds) {
result.put(robotId, getOperationStatus(robotId));
}
return result;
}
private OperationStatus getOperationStatus(Long robotId) {
try {
robotRestrictions.checkSwitchOn(robotId);
return ALLOWED;
} catch (NoSuchElementException e) {
LOG.debug(format(“Robot with id %s is absent", robotId), e);
return ROBOT_IS_ABSENT;
} catch (OperationRestrictedException e) {
LOG.debug(format(“Robot with id %s cannot be switched on", robotId), e);
return RESTRICTED;
}
}
89
90.
Есть три робота, которые пытаемся включить:1. DRIVER – включен
2. LOADER – выключен
3. VACUUM – выключен.
В системе есть три других VACUUM робота, которые включены.
Ожидаем:
1. DRIVER – RESTRICTED
2. LOADER – ALLOWED
3. VACUUM - RESTRICTED
90
91.
@Testvoid shouldNotAllowSomeRobotsToSwitchOn() {
final var driver = db.save(
aRobot().switched(true).type(DRIVER)
);
final var loader = db.save(
aRobot().switched(false).type(LOADER)
);
final var vacuumTemplate = aRobot().switched(false).type(VACUUM);
final var vacuum = db.save(vacuumTemplate);
db.saveAll(
vacuumTemplate.switched(true),
vacuumTemplate.switched(true),
vacuumTemplate.switched(true)
);
final var robotsIds = List.of(driver.getId(), loader.getId(), vacuum.getId());
final var operations = robotAllowedOperations.getRobotsSwitchOnStatus(
robotsIds
);
assertEquals(RESTRICTED, operations.get(driver.getId()));
assertEquals(ALLOWED, operations.get(loader.getId()));
assertEquals(RESTRICTED, operations.get(vacuu,.getId()));
}
91
92.
Transaction silently rolled back because it has been marked as rollback-onlyorg.springframework.transaction.UnexpectedRollbackException:
92
93.
9394.
@Transactional(readOnly = true, propagation =REQUIRES_NEW)
public void checkSwitchOn(Long robotId) {
…
}
94
95.
9596.
9697.
Проблемы• N + 1 транзакций
• Не работает кэш первого уровня
97
98.
@Transactional(readOnly = true, noRollbackFor =Exception.class)
public void checkSwitchOn(Long serverId) {
…
}
98
99.
99100.
100101.
@Transactional(readOnly = true, noRollbackFor =Exception.class)
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(readOnly = true, noRollbackFor = Exception.class)
@Documented
public @interface ReadTransactional {
}
101
102.
@Target({ElementType.TYPE, ElementType.METHOD})@Retention(RetentionPolicy.RUNTIME)
@Transactional(readOnly = true, noRollbackFor = Exception.class)
@Documented
public @interface ReadTransactional {
@AliasFor(annotation = Transactional.class, attribute = "value")
String value() default "";
@AliasFor(annotation = Transactional.class, attribute = "transactionManager")
String transactionManager() default "";
@AliasFor(annotation = Transactional.class, attribute = "label")
String[] label() default {};
@AliasFor(annotation = Transactional.class, attribute = "propagation")
Propagation propagation() default Propagation.REQUIRED;
@AliasFor(annotation = Transactional.class, attribute = "isolation")
Isolation isolation() default Isolation.DEFAULT;
@AliasFor(annotation = Transactional.class, attribute = "timeout")
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
@AliasFor(annotation = Transactional.class, attribute = "timeoutString")
String timeoutString() default "";
}
102
103.
@ReadTransactional(isolation = REPEATABLE_READ, propagation= NESTED)
103
104.
Выводы по Readonly• Не откатывайте readonly-транзакции
• Интеграционные тесты важны
• Между бинами
104
105.
Бонусный антипаттерн@DataJpaTest
@Import(TestDBFacade.Config.class)
@Transactional(propagation = NOT_SUPPORTED)
class RobotUpdateServiceTestH2DataJpaNonTransactional
@Retention(RetentionPolicy.RUNTIME)
@DataJpaTest
@Import(TestDBFacade.Config.class)
@Transactional(propagation = NOT_SUPPORTED)
public @interface DBTest {
}
@DBTest
class
RobotUpdateServiceTestH2DataJpaNonTransactional
105
106.
Бонусный антипаттерн@Retention(RetentionPolicy.RUNTIME)
@DataJpaTest
@Import(TestDBFacade.Config.class)
@Transactional(propagation = NOT_SUPPORTED)
public @interface DBTest {
@AliasFor(annotation = DataJpaTest.class, attribute = "properties")
String[] properties() default {};
}
106
107.
Общие выводы• Пишем интеграционные тесты (Embedded DB/Testcontainers)
• Убираем coupling на декларацию сущностей (builder/object
mother)
• Избегаем transactional tests
• Не боимся внедрять кастомные утилиты (TestDBFacade)
• Acceptance tests/End-to-End tests необходимы
107
108.
Спасибо за внимание!Telegram: @kirekov
Репозиторий:
https://github.com/SimonHarmonicMinor/spring-data-jpa-efficient-testing
108