如何使用Spring Boot + Spring Data JPA进行悲观锁的单元测试

9
我希望测试 FooRepository.lock() 是否有效,当某人调用 lock() 后,其他人呼叫它应该会等待。以下是我的最佳尝试,但并不起作用。我相信原因是 entityMangerfooRepository 都参与了同一个事务。如何从另一个事务中调用 lock()?或者有没有关于悲观锁单元测试的建议?谢谢!

FooRepositoryTest:

package com.example.demo;

import java.util.UUID;

import javax.persistence.LockModeType;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.boot.test.autoconfigure.orm.jpa.TestEntityManager;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@DataJpaTest
public class FooRepositoryTest {

    @Autowired
    private TestEntityManager entityManager;    

    @Autowired
    private FooRepository fooRepository;

    @Test
    public void lockTest() {
        // given
        Foo foo = new Foo();
        foo.setName("foo-name");

        UUID fooId = fooRepository.save(foo).getFooId();
        entityManager.flush();
        entityManager.clear();

        // when
        Foo savedFoo = fooRepository.findOne(fooId);
        fooRepository.lock(savedFoo);

        // then
        // I want something like this to be lock wait,
        // something to confirm the above fooRepository.lock() work
        entityManager.getEntityManager().lock(savedFoo, LockModeType.PESSIMISTIC_WRITE);
    }

}

类 Foo:

package com.example.demo;

import java.util.UUID;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Foo {  
    @Id
    @GeneratedValue
    private UUID fooId;

    private String name;

    public UUID getFooId() {
        return fooId;
    }

    public void setFooId(UUID fooId) {
        this.fooId = fooId;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

}

类 FooApplication:

package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class FooApplication {

    public static void main(String[] args) {
        SpringApplication.run(FooApplication.class, args);
    }
}

类 FooRepository:

package com.example.demo;

import java.util.UUID;

import org.springframework.data.jpa.repository.JpaRepository;

public interface FooRepository extends JpaRepository<Foo, UUID>, FooRepositoryCustom {  
}

类 FooRepositoryCustom:

package com.example.demo;

public interface FooRepositoryCustom {  
    public void lock(Foo foo);  
}

类 FooRepositoryImpl:

package com.example.demo;

import javax.persistence.EntityManager;
import javax.persistence.LockModeType;

import org.springframework.beans.factory.annotation.Autowired;

public class FooRepositoryImpl implements FooRepositoryCustom {

    @Autowired
    private EntityManager entityManager;

    @Override
    public void lock(Foo foo) {
        entityManager.lock(foo, LockModeType.PESSIMISTIC_WRITE);
    }   
}

1
为什么EntityManager.lock是JPA规范的一部分,因此JPA提供者有责任确保它按照规范工作(示例)。如果它不能(按照规范工作),那么无论如何我们作为最终用户都无能为力,直到供应商修复问题。 - manish
@manish,@GhostCat,我的测试目标是fooRepository.lock(savedFoo),而不是EntityManager.lock(),抱歉让你们感到困惑。我已经添加了//given//when//then来展示SUT是什么。 测试的原因是我想将所有与Foo相关的数据操作放在FooRepository上,并且在需要时将对FooRepository进行模拟。 - ysl
@GhostCat,谢谢提醒,我会在你的答案下添加评论。 - ysl
2个回答

1

通过集成测试测试悲观锁处理是很重要的(我可以说是强制性的)有两个原因:

  • 很有可能LockTimeout参数没有正确配置(在Oracle和PostgreSQL中默认为无限时间),这可能会对生产系统的稳定性/性能产生重大影响;
  • 大多数RDBMS的悲观锁JPA实现非常有限,因为6个内部LockTimeout参数不同;仅针对内存数据库进行测试可能不足,并且需要特殊考虑。

在博客文章Testing Pessimistic Locking Handling With SpringBoot and JPA中,您可以:

  • 了解更多关于悲观锁处理方面的内容;
  • 并且您可以找到针对Oracle、PostgreSQL、MySQL和Apache Derby进行测试的GitHub示例

Spring框架仅用作示例。您可以轻松地将测试适应于任何其他框架。


你能详细解释一下第一个选项吗?在运行Spring Boot应用程序的PostgreSQL数据库上,何时应定义LockTimeout参数,例如在微服务架构中?那个值应该是多少? - Ahmed Hasn.

1
你的单元测试做错了。
你不应该编写单元测试来验证第三方框架实现的功能。单元测试是为了测试你的代码单元!
换句话说:你不需要验证锁定是否按预期工作,你的单元需要这样做:
entityManager.lock(foo, LockModeType.PESSIMISTIC_WRITE);

因此,您唯一需要考虑测试的是:确保实体管理器lock()方法以预期的参数调用。

意思是:验证您的代码确实按照您认为应该使用框架的方式使用-但不要测试其他人的代码!您看,当您的单元测试显示框架错误时,您该怎么办...您无法更改它!(当然,您可以写一个错误报告)

声明:可能会有特殊情况,您会假设某个第三方产品存在bug-这时编写单元测试以验证此假设可能非常有用。这样,您稍后可以针对该产品的新版本运行单元测试,以查看该bug是否仍然存在。


谢谢你的回答,但EntityManager.lock()在这种情况下不是被测试的方法。当然,我并不是试图测试EntityManager.lock(),测试可以揭示的是我是否正确地调用了使用EntityManager.lock()的Foo。 - ysl
1
我仍在寻找一种测试我的代码的方法。是的,我正在调用第三方框架,但它现在已成为我的系统的一部分,在交付软件之前,我需要对整个系统进行测试。因此,无论我是手动测试还是自动化测试,我仍在寻找后者的解决方案。 我还编写测试代码来测试我的SQL语句,以查看它们是否按预期工作,当它们不能时,我将尝试解决问题。当一个SQL语句是100%正确的,但数据库未能按预期执行时,说什么都不能解决问题,对吧? - ysl
我不确定还有什么可以说的了:你的代码是 entityManager.lock()。你唯一可以合理验证的事情是将 entityManager 设为模拟对象,然后验证 lock 方法是否按照你的预期被调用了。 - GhostCat
我理解你的观点,是的,我确实在模拟FooRepository以验证系统是否调用了它。但我仍然希望自动测试它,也许代码没有使用PESSIMISTIC_WRITE来调用它,也可能使用了错误的实体,这部分是我的代码问题,而不是EntityManage.lock()的问题。 - ysl
很抱歉,我的Spring知识不够深入,无法给出适当的建议。如果您没有得到更好的答案,请下周随时给我留言。也许在某个好日子里,我会愿意为您的问题提供悬赏。除此之外,我添加了Java标签,或许能够吸引足够的关注,以获得更好的答案! - GhostCat
尊重地说,我不同意测试这个想法是错误的。这种类型的测试(无论你将其称为单元测试还是集成测试)应该存在于系统中。否则,你怎么知道功能是否符合你的预期?你测试的不是框架,而是你对框架的理解。 - Michal Krasny

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接