Hibernate 隔离集成测试

Posted

技术标签:

【中文标题】Hibernate 隔离集成测试【英文标题】:Hibernate isolated integration tests 【发布时间】:2019-05-06 08:42:46 【问题描述】:

我对hibernate有点陌生,所以我从简单的事情开始。

根据 F.I.R.S.T 测试原则,单元测试必须是 I - 隔离的。 我正在尝试使用@Transactional 注释将其应用于存储库层(Hibernate\JPA)的集成测试:

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = RepositoryConfig.class)
@Transactional
public class EmployeeRepositoryTest extends AbstractRepositoryTest     
    @Autowired
    private IEmployeeRepository employeeRepository;    
    @Test
    public void saveTest() 
        Employee expectedEmployee = buildEmployee(1, "Parker");
        employeeRepository.save(expectedEmployee);
        Employee actualEmployee = employeeRepository.findById(1);
        assertEquals(expectedEmployee, actualEmployee);
    
    private Employee buildEmployee(long id, String name) 
        Employee employee = new Employee();
        employee.setId(id);
        employee.setName(name);
        return employee;
    

但是,就事务中执行的两种方法而言,hibernate 并没有实际执行它们(据我了解)- 至少 日志中没有与 insert 的行

如果我通过将脚本添加到嵌入式数据源来运行数据插入,例如:

INSERT INTO employee (employee_id, employee_name) VALUES (1, 'name');

并尝试使用相同的 id 但新名称保存员工,测试将成功。这对我来说是最令人困惑的事情。

我看到了一个自动装配EntityManager 的解决方案并将其称为flush() 方法。但我不喜欢它,因为我尝试编写测试而不绑定到 Hibernate\JPA。

我也尝试了不同的flushMode,但也没有用。

Q1:有没有办法让 Hibernate 在调用存储库的方法后立即运行查询?

Q2:在保存/更新/删除存储库方法中显式调用EntityManager#flush 是一种好习惯吗?

我的员工:

@Entity
@Table(name = "employee")
public class Employee     
    @Id
    @Column(name = "employee_id")
    private long id;        
    @Column(name = "employee_name")
    private String name;
    // the rest required things (constructor, getters/setters and etc)

和 RepositoryConfig:

@Configuration
@EnableTransactionManagement
@ComponentScan("org.my.package")
public class RepositoryConfig     
    @Bean
    public DataSource getDataSource() 
        return new EmbeddedDatabaseBuilder()
                .setType(EmbeddedDatabaseType.H2)
                .build();
        
    @Bean
    public JpaTransactionManager transactionManager() 
        return new JpaTransactionManager();
        
    @Bean
    @Autowired
    public HibernateTemplate getHibernateTemplate(SessionFactory sessionFactory) 
        return new HibernateTemplate(sessionFactory);
        
    @Bean
    public LocalSessionFactoryBean getSessionFactory() 
        LocalSessionFactoryBean sessionFactory = new LocalSessionFactoryBean();
        sessionFactory.setDataSource(getDataSource());
        sessionFactory.setPackagesToScan("org.my.package.model");
        sessionFactory.setHibernateProperties(getHibernateProperties());
        return sessionFactory;
        
    private Properties getHibernateProperties() 
        Properties properties = new Properties();
        properties.put("hibernate.dialect", "H2Dialect");
        properties.put("hibernate.show_sql", true);
        return properties;
    

【问题讨论】:

【参考方案1】:

您别无选择,只能与实体管理器交互以使这些测试按您的预期工作 - 不要触发刷新(这可以通过在您的存储库上调用 saveAndFlush(..) 方法来完成,而不仅仅是 save(...))但是要清除一级缓存:

https://docs.spring.io/spring-data/jpa/docs/current/api/org/springframework/data/jpa/repository/JpaRepository.html#saveAndFlush-S-

@Test
public void saveTest() 
    Employee expectedEmployee = buildEmployee(1, "Parker");

    //call save and flush for immediate flush.
    employeeRepository.saveAndFlush(expectedEmployee);

    //now you will need to clear the persistence context to actually
    //trigger a load from the database as employee with id 1 is already
    //in the persistence context. 

    //without the below you will not see a db select
    entityManager.clear();

    Employee actualEmployee = employeeRepository.findById(1);
    assertEquals(expectedEmployee, actualEmployee);

清除持久性上下文的另一种方法是回退到使用原始 JDBC 来读取更新的行。

但我不喜欢它,因为我尝试编写测试而不绑定到 Hibernate\JPA。 您正在测试在 Hibernate\JPA 中实现的持久性机制,而您的存储库只是一个抽象这允许您避免直接调用它,所以这似乎是一个有点荒谬的声明。

【讨论】:

通过编写测试而不绑定到 Hibernate\JPA 我的意思是我不想在我的测试中包含任何属于 Hibernate\JPA 特定事物的东西。因此,假设我迁移到 iBatis,例如,我不需要更改测试。 您正在测试一个 JPA 存储库。如果您想测试 iBatis 存储库,那么您将编写一个新测试。您在当前测试中可以做的最好的事情是调用 saveAndFlush() 然后 - 如答案中所述,回退到使用原始 JDBC 读取更新,这将防止您不得不在 em 上调用 clear() 恐怕我没明白你的意思。澄清一下,我正在编写集成测试(使用 H2),从测试/行为驱动的角度来看,幕后运行的引擎并不重要。那么,更改框架的事实是否会改变我的接口方法的预期结果? 你不测试接口。你测试实现。现在被测试的单元是一个 JPA 存储库实现——employeeRepository——所以测试一下。将来您可能会拥有一个 iBatis,因此请在拥有它时进行测试。也许你需要澄清你的问题....... 是的,你是对的 - 我测试接口的实现。但它改变了逻辑吗?也许,我不清楚,你可以解释下一件事。我正在尝试与内存数据库一起测试存储库逻辑。我假设它是集成测试。我为什么要关心底层引擎?

以上是关于Hibernate 隔离集成测试的主要内容,如果未能解决你的问题,请参考以下文章

在spring boot集成测试中获取org.hibernate.LazyInitializationException

使用 H2 DB 进行 Spring/Hibernate 集成测试

系统级集成测试的断舍离|洞见

挑战:微服务集成测试

《Spring 5官方文档》11集成测试

依赖于数据库视图的集成测试