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

Семён Киреков — Spring Data JPA. Антипаттерны тестирования

Семён Киреков — Spring Data JPA. Антипаттерны тестирования

За свою карьеру спикер столкнулся с рядом (а некоторые даже попробовал) антипаттернов тестирования при использовании Spring Data JPA. Они не только не помогают, но и усложняют поддержку кода и вызывают раздражение.

В рамках доклада Семен расскажет вам о таких антипаттернах, как избыточный coupling на декларацию сущностей, лишние зависимости, best practices для создания тестовых данных и транзакционные сценарии. А также покажет паттерны, на которые следует их заменить, чтобы упростить жизнь при написании тестов.

Moscow JUG

July 14, 2022
Tweet

More Decks by Moscow JUG

Other Decks in Programming

Transcript

  1. Spring Data JPA. Антипаттерны
    тестирования
    1
    Семен Киреков

    View Slide

  2. О себе
    •Киреков Семен
    •Java Dev и Team Lead в «МТС Диджитал»
    Центр Big Data
    •Java-декан в МТС.Тета
    2

    View Slide

  3. План доклада
    • Антипаттерны
    • Паттерны, на которые стоит заменить
    • Неочевидные моменты
    • SimonHarmonicMinor/spring-data-jpa-efficient-testing
    3

    View Slide

  4. Бизнес-область
    Система по управлению роботами.
    Включение, выключение и так далее.
    4
    Photo by Jason Leung on Unsplash

    View Slide

  5. Домен
    5
    @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
    }
    }
    с

    View Slide

  6. Task #1
    Требуется реализовать включение.
    Необходимо проверять, можно ли включить данного робота.
    6

    View Slide

  7. 7
    @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.flush();
    robotRestrictions.checkSwitchOn(robotId);
    }

    View Slide

  8. 8
    testRuntimeOnly 'com.h2database:h2'
    spring.jpa.hibernate.ddl-auto=create

    View Slide

  9. Disclaimer
    Далее в коде будет много антипаттернов!
    9

    View Slide

  10. 10
    @SpringBootTest
    @AutoConfigureTestDatabase
    class RobotUpdateServiceTestH2DirtiesContext {
    @Autowired
    private RobotUpdateService service;
    @Autowired
    private RobotRepository robotRepository;
    @MockBean
    private RobotRestrictions robotRestrictions;

    }

    View Slide

  11. 11
    @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
    }
    }

    View Slide

  12. 12
    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:"));
    }

    View Slide

  13. 13
    public static EmbeddedDatabaseConnection get(ClassLoader classLoader) {
    for (EmbeddedDatabaseConnection candidate : EmbeddedDatabaseConnection.values()) {
    if (candidate != NONE && ClassUtils.isPresent(candidate.getDriverClassName(), classLoader)
    return candidate;
    }
    }
    return NONE;
    }

    View Slide

  14. 14
    @SpringBootTest
    @AutoConfigureTestDatabase
    class RobotUpdateServiceTestH2DirtiesContext {
    @Autowired
    private RobotUpdateService service;
    @Autowired
    private RobotRepository robotRepository;
    @MockBean
    private RobotRestrictions robotRestrictions;

    }

    View Slide

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

    View Slide

  16. Логирование транзакций и SQL
    16
    logging.level.org.springframework.orm.jpa.JpaTransactionManager=D
    EBUG
    logging.level.org.hibernate.SQL=DEBUG
    logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE

    View Slide

  17. 17
    @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());
    }
    Протестируем Commit
    Creating new transaction with name
    [SimpleJpaRepository.save]:
    PROPAGATION_REQUIRED,ISOLATION_DEFAULT
    Hibernate: insert into robot (robot_id, name, switched, type)
    values (null, ?, ?, ?)
    Committing JPA transaction on EntityManager
    [SessionImpl(115584215)]
    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=?
    Committing JPA transaction on EntityManager
    [SessionImpl(93418194)]
    Creating new transaction with name
    [SimpleJpaRepository.findById]:
    PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly
    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=?
    Committing JPA transaction on EntityManager
    [SessionImpl(31874125)]

    View Slide

  18. Протестируем Rollback
    18
    @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());
    }
    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)]

    View Slide

  19. 19

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide

  23. Object Mother
    23
    https://bit.ly/3GHMWZc
    public class RobotFactory {
    public static Robot createWithName(String name) {

    }
    public static Robot createWithType(Type type) {

    }
    }

    View Slide

  24. Object Mother
    24
    https://bit.ly/3GHMWZc
    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) {

    }
    }

    View Slide

  25. 25
    Lombok Builder

    View Slide

  26. Lombok Builder
    26
    @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
    }

    View Slide

  27. Пример использования Lombok Builder
    27
    final var robot = Robot.builder()
    .switched(false)
    .type(DRIVER)
    .name("some_name")
    .build();

    View Slide

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

    View Slide

  29. Test Data Builder
    with Lombok
    29
    https://bit.ly/3532gSo
    @AllArgsConstructor
    @NoArgsConstructor(staticName = "aRobot")
    @With
    public class RobotTestBuilder implements
    TestBuilder {
    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;
    }
    }

    View Slide

  30. 30
    aRobot().switched(false).build()
    var robot = aRobot().name("my_robot");

    var switchedOn = robot.switched(true).build();
    var vacuum = robot.type(VACUUM).build();

    View Slide

  31. Easy Random
    31
    EasyRandom easyRandom = new
    EasyRandom();
    Robot robot =
    easyRandom.nextObject(Robot.class);
    testImplementation 'org.jeasy:easy-random-core:5.0.0'
    Robot(
    id=-5106534569952410475,
    name=eOMtThyhVNLWUZNRcBaQKxI,
    switched=true,
    type=VACUUM
    )

    View Slide

  32. Easy Random
    32
    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
    )

    View Slide

  33. Easy Random
    33
    Robot –one-to-many-> Detail

    View Slide

  34. Вывод. Инстанцирование сущностей
    напрямую
    • Инстанцирование через конструктор – позиционные аргументы
    • Сеттеры – ошибки в рантайме, verbose
    • Object Mother – простые сущности
    • Test Data Builder – универсальный
    • Object Mother is OK with Kotlin
    34

    View Slide

  35. 35
    #2. Антипаттерн.
    Зависимость на репозитории

    View Slide

  36. Причины
    • Сущностей может быть много
    • Затрудняет понимание
    • Порядок удаления
    36

    View Slide

  37. 37

    View Slide

  38. Варианты
    • TestEntityManager
    38

    View Slide

  39. TestEntityManager под капотом
    39
    public final EntityManager getEntityManager() {
    EntityManager manager =
    EntityManagerFactoryUtils.getTransactionalEntityManager(this.entityManagerFact
    ory);
    Assert.state(manager != null, "No transactional EntityManager found");
    return manager;
    }

    View Slide

  40. 40
    testEntityManager.persistAndFlush(
    aRobot().switched(true).build()
    );
    transactionTemplate.execute(status ->
    testEntityManager.persistAndFlush(
    aRobot().switched(true).build()
    )
    );
    No transactional EntityManager found

    View Slide

  41. Варианты
    • TestEntityManager + TransactionTemplate
    • TestDbFacade
    41

    View Slide

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

    }
    https://bit.ly/3KpsKOf

    View Slide

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

    View Slide

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

    View Slide

  45. TestDbFacade
    45
    https://bit.ly/3KpsKOf
    public T save(TestBuilder builder) {
    return transactionTemplate.execute(
    status ->
    testEntityManager.persistAndFlush(builder.build())
    );
    }

    View Slide

  46. Посты/комменты
    46
    TestBuilder user = db.persistedOnce(aUser().login("login"));
    TestBuilder 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")
    ))
    );
    }

    View Slide

  47. TestDbFacade
    47
    https://bit.ly/3KpsKOf
    @TestConfiguration
    public static class Config {
    @Bean
    public TestDBFacade
    testDBFacade() {
    return new TestDBFacade();
    }
    }

    View Slide

  48. 48
    @SpringBootTest
    @AutoConfigureTestEntityManager
    @AutoConfigureTestDatabase
    @Import(TestDBFacade.Config.class)
    class
    RobotUpdateServiceTestH2TestDataBuilder

    View Slide

  49. 49
    @Test
    void shouldSwitchOnSuccessfully() {
    final var id = db.save(aRobot().switched(false)).getId();
    doNothing().when(robotRestrictions).checkSwitchOn(id);
    service.switchOnRobot(id);
    final var savedServer = db.find(id, Robot.class);
    assertTrue(savedServer.isSwitched());
    }
    @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());
    }

    View Slide

  50. Дополнительные материалы
    50
    https://habr.com/ru/post/312248/
    Spring Boot + JPA — Clear Tes

    View Slide

  51. SQL Annotation
    51
    @Test
    @Sql(value = "/insert_data.sql", executionPhase =
    BEFORE_TEST_METHOD)
    @Sql(value = "/delete_data.sql", executionPhase =
    AFTER_TEST_METHOD)
    void shouldSwitchOnSuccessfully() {

    }

    View Slide

  52. Проблемы SQL Annotation
    • Нет статической типизации
    • Проблемы со сложными объектами (JSON to Java Object)
    • Каскадные изменения при ALTER TABLE
    • Как найти сущность с Sequence ID?
    52

    View Slide

  53. Вывод. Зависимость на репозитории
    • Затрудняет понимание теста
    • Лишние зависимости
    • Альтернативы: @SQL, TestEntityManager, Test DB Facade
    53

    View Slide

  54. 54
    #3. Антипаттерн.
    Поднятие всего контекста

    View Slide

  55. 55
    @DataJpaTest тоже поднимает Spring
    Context
    Но не весь

    View Slide

  56. 56
    @DataJpaTest
    @Import(TestDBFacade.Config.class)
    class RobotUpdateServiceTestH2DataJpa {
    @Autowired
    private RobotUpdateService service;
    @Autowired
    private TestDBFacade db;
    @MockBean
    private RobotRestrictions
    robotRestrictions;

    }

    View Slide

  57. 57
    @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

    View Slide

  58. 58
    @DataJpaTest
    @Import(TestDBFacade.Config.class)
    class RobotUpdateServiceTestH2DataJpa {
    @Autowired
    private RobotUpdateService service;
    @Autowired
    private TestDBFacade db;
    @MockBean
    private RobotRestrictions
    robotRestrictions;

    }

    View Slide

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

    View Slide

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

    View Slide

  61. 61
    @Test
    void shouldRollbackIfCannotSwitchOn() { … }
    @Test
    void shouldSwitchOnSuccessfully() { … }

    View Slide

  62. 62
    assertFalse(savedRobot.isSwitched())
    ;
    expected: but was:
    Expected :false
    Actual :true

    View Slide

  63. 63

    View Slide

  64. Propagation
    64
    @Transactional
    public void switchOnRobot(Long robotId) {
    final var robot =
    robotRepository.findById(robotId)
    .orElseThrow();
    robot.setSwitched(true);
    robotRepository.saveAndFlush(robot);
    robotRestrictions.checkSwitchOn(robotId);
    }

    View Slide

  65. 65
    @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

    View Slide

  66. 66
    assertFalse(savedRobot.isSwitched());

    View Slide

  67. Что делать?
    67
    @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);
    }

    View Slide

  68. 68
    assertThrows(OperationRestrictedException.class, () ->
    service.switchOnRobot(id));

    View Slide

  69. 69

    View Slide

  70. 70

    View Slide

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

    View Slide

  72. 72
    Hibernate: update robot set name=?, switched=?, type=? where robot_id=?
    SQL Error: 50200, SQLState: HYT00
    Время ожидания блокировки таблицы {0} истекло
    Timeout trying to lock table {0}; SQL statement:
    Dirty Write:

    View Slide

  73. 73
    Транзакционные тесты – это опасно
    https://bit.ly/3IhAbFc

    View Slide

  74. 74
    @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

    View Slide

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

    View Slide

  76. 76

    View Slide

  77. 77
    assertFalse(savedRobot.isSwitched());

    View Slide

  78. Mandatory Propagation
    78
    @Transactional(propagation = MANDATORY)
    public void mandatoryTransactionOperation(…) {

    }
    No existing transaction found for transaction marked with
    propagation 'mandatory’
    org.springframework.transaction.IllegalTransactionStateException:
    No existing transaction found for transaction marked with
    propagation 'mandatory'

    View Slide

  79. Программные транзакции
    79
    @Test
    void test() {
    // given
    ...
    // when
    transactionTemplate.execute(status ->
    service.mandatoryTransactionOperation()
    );
    // then
    ...
    }

    View Slide

  80. Вывод
    • Транзакционные тесты допустимы в readonly-операциях
    • Если транзакция неизбежна, используем TransactionTemplate
    80

    View Slide

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

    View Slide

  82. 82
    @Transactional
    public void switchOnRobot(Long robotId) {
    final var robot =
    robotRepository.findById(robotId)
    .orElseThrow();
    robot.setSwitched(true);
    robotRepository.flush();
    robotRestrictions.checkSwitchOn(robotId);
    }

    View Slide

  83. Task
    Необходимо получать список статусов: допустимо ли включение
    робота.
    83

    View Slide

  84. 1. Если робот уже включен, нельзя отправлять запрос повторно.
    2. Не более трех включенных роботов каждого типа
    84

    View Slide

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

    View Slide

  86. 86
    @Transactional(readOnly = true)
    public Map getRobotsSwitchOnStatus(Collection robotIds) {
    final var result = new HashMap();
    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;
    }
    }

    View Slide

  87. 87
    Есть три робота, которые пытаемся включить:
    1. DRIVER – включен
    2. LOADER – выключен
    3. VACUUM – выключен.
    В системе есть три других VACUUM робота, которые включены.
    Ожидаем:
    1. DRIVER – RESTRICTED
    2. LOADER – ALLOWED
    3. VACUUM - RESTRICTED

    View Slide

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

    View Slide

  89. 89
    Transaction silently rolled back because it has been marked as rollback-only
    org.springframework.transaction.UnexpectedRollbackException:

    View Slide

  90. 90

    View Slide

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

    }

    View Slide

  92. 92

    View Slide

  93. 93

    View Slide

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

    View Slide

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

    }

    View Slide

  96. 96

    View Slide

  97. 97

    View Slide

  98. 98
    @Transactional(readOnly = true, noRollbackFor =
    Exception.class)
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Transactional(readOnly = true, noRollbackFor = Exception.class)
    @Documented
    public @interface ReadTransactional {
    }

    View Slide

  99. 99
    @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 "";
    }

    View Slide

  100. 100
    @ReadTransactional(isolation = REPEATABLE_READ, propagation
    = NESTED)

    View Slide

  101. Выводы по Readonly
    • Не откатывайте readonly-транзакции
    • Интеграционные тесты важны
    • Между бинами
    101

    View Slide

  102. Бонусный антипаттерн
    102
    @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

    View Slide

  103. Бонусный антипаттерн
    103
    @Retention(RetentionPolicy.RUNTIME)
    @DataJpaTest
    @Import(TestDBFacade.Config.class)
    @Transactional(propagation = NOT_SUPPORTED)
    public @interface DBTest {
    @AliasFor(annotation = DataJpaTest.class, attribute = "properties")
    String[] properties() default {};
    }

    View Slide

  104. Общие выводы
    • Пишем интеграционные тесты (Embedded DB/Testcontainers)
    • Убираем coupling на декларацию сущностей (builder/object
    mother)
    • Избегаем transactional tests
    • Не боимся внедрять кастомные утилиты (TestDBFacade)
    • Acceptance tests/End-to-End tests необходимы
    104

    View Slide

  105. Спасибо за внимание!
    105
    Telegram: @kirekov
    Twitter: @simon_kirekov
    Репозиторий:
    https://github.com/SimonHarmonicMinor/spring-data-jpa-efficient-testing

    View Slide