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.

@Service
public 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.

Протестируем Rollback
Creating 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.

19

20.

#1. Антипаттерн.
DirtiesContext для очистки данных
20

21.

@BeforeEach
void beforeEach() {
robotRepository.deleteAll();
}
21

22.

#2. Антипаттерн.
Инстанцирование сущностей
напрямую
22

23.

@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());
}
23

24.

Конструктор
final var robot = new Robot("some_name", false, DRIVER);
24

25.

Object Mother
public class RobotFactory {
public static Robot createWithName(String name) {

}
public static Robot createWithType(Type type) {

}
}
https://bit.ly/3GHMWZc
25

26.

Object Mother
public 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 Builder
27

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 Builder
final var robot = Robot.builder()
.switched(false)
.type(DRIVER)
.name("some_name")
.build();
29

30.

Test Data Builder
public interface TestBuilder<T> {
T build();
}
https://bit.ly/3532gSo
30

31.

Test Data Builder
public 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 Builder
with 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 Random
testImplementation '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 Random
var 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 Random
Robot –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.

40

41.

Варианты
• 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.

TestDbFacade
public class TestDBFacade {
@Autowired
private TestEntityManager testEntityManager;
@Autowired
private TransactionTemplate
transactionTemplate;
@Autowired
private JdbcTemplate jdbcTemplate;

}
https://bit.ly/3KpsKOf
47

48.

TestDbFacade
public void cleanDatabase() {
transactionTemplate.execute(status -> {
JdbcTestUtils.deleteFromTables(jdbcTemplate,
"robot");
return null;
});
}
https://bit.ly/3KpsKOf
48

49.

TestDbFacade
public <T> T find(Object id, Class<T>
entityClass) {
return transactionTemplate.execute(
status ->
testEntityManager.find(entityClass, id)
);
}
https://bit.ly/3KpsKOf
49

50.

TestDbFacade
public <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.

@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());
@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 тоже поднимает Spring
Context
Но не весь
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.

@TestConfiguration
static class Config {
@Bean
public RobotUpdateService service(
RobotRepository robotRepository,
RobotRestrictions robotRestrictions
){
return new RobotUpdateService(robotRepository,
robotRestrictions);
}
}
64

65.

@BeforeEach
void beforeEach() {
db.cleanDatabase();
}
65

66.

@Test
void shouldSwitchOnSuccessfully() { … }
@Test
void shouldRollbackIfCannotSwitchOn() { … }
66

67.

assertFalse(savedRobot.isSwitched())
;
expected: <false> but was: <true>
Expected :false
Actual :true
67

68.

68

69.

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.

74

75.

75

76.

Пойдем дальше
@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.

@Transactional
public 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.

@BeforeEach
void beforeEach() {
db.cleanDatabase();
}
80

81.

81

82.

assertFalse(savedRobot.isSwitched());
82

83.

Вывод
• Транзакционные тесты допустимы в readonly-операциях
83

84.

@Transactional(readOnly = true)
#5. Антипаттерн. Rollback readonly
транзакций
84

85.

@Transactional
public 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.

@Test
void 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-only
org.springframework.transaction.UnexpectedRollbackException:
92

93.

93

94.

@Transactional(readOnly = true, propagation =
REQUIRES_NEW)
public void checkSwitchOn(Long robotId) {

}
94

95.

95

96.

96

97.

Проблемы
• N + 1 транзакций
• Не работает кэш первого уровня
97

98.

@Transactional(readOnly = true, noRollbackFor =
Exception.class)
public void checkSwitchOn(Long serverId) {

}
98

99.

99

100.

100

101.

@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
English     Русский Правила