【问题标题】:How to test Spring JPA audit annotations?如何测试 Spring JPA 审计注释?
【发布时间】:2022-01-03 07:46:21
【问题描述】:

我在使用 H2 内存数据库测试 Spring JPA (2.5.4) 中的审计注释时遇到问题。我有一个用 @EnableJpaAuditing 注释的主类,以及我的实体的基类。

@Getter
@Setter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class AuditedEntity {
    @CreatedDate
    LocalDateTime createdDate;

    @LastModifiedDate
    LocalDateTime lastModifiedDate;
}

两个实体扩展了基类:父类和子类。

@Data
@Entity
@Table(name = "one2many")
class OneToManyEntity extends AuditedEntity {
    @Id
    @GeneratedValue(strategy = SEQUENCE)
    Integer id;

    @OneToMany(mappedBy = "parent", cascade = ALL, orphanRemoval = true)
    List<ManyToOneEntity> children;
}

@Data
@Entity
@Table(name = "many2one")
class ManyToOneEntity extends AuditedEntity {
    @Id
    @GeneratedValue(strategy = SEQUENCE)
    Integer id;

    @ManyToOne(optional = false, fetch = LAZY)
    OneToManyEntity parent;
}

父实体的存储库是一个简单的接口声明。

@Repository
interface OneToManyRepository extends CrudRepository<OneToManyEntity, Integer> {
}

我有几个 Spock 测试。

class OneToManyRepoSpec extends Specification {
    @Autowired
    OneToManyRepository repo

    def "test ID is assigned"() {
        given:
            def parent = new OneToManyEntity()
            parent.setChildren([new ManyToOneEntity()])
        expect:
            def persisted = repo.save(parent)
            persisted.getId() > 0
            persisted.getLastModifiedDate() != null
    }

    def "LastModifiedDate value is updated"() {
        given:
            def persisted1 = repo.save(new OneToManyEntity())
            sleep(1000)
            persisted1.setChildren([])
            def persisted2 = repo.save(persisted1)
        expect:
            persisted2.lastModifiedDate.isAfter(persisted1.lastModifiedDate)
    }
}

我可以通过这些测试中的任何一个,这取决于我如何注释测试类;但我不能让两个测试一起通过。

  • 如果我用 @DataJpaTest 注释测试类,第一个测试通过(分配了 ID 和审核值),但第二个测试失败(审核值未更新)。
  • 如果我用 @SpringBootTest(webEnvironment = NONE) 注释测试类,第一个测试失败(ConstraintViolationException: NULL not allowed for column "parent_id"; 因此未分配 ID),但第二个测试通过(更新审核值)。

我是否必须将这些测试拆分为具有不同注释的不同类,或者有没有办法将它们放在一起并通过?我也有兴趣进一步了解导致这些单独测试失败的原因。

【问题讨论】:

  • 我知道 Spock,但不知道 Spring 和/或 JPA。我认为在 GitHub 上发布 MCVE 对您会有所帮助,最好是一个 Maven 项目(Gradle,如果必须的话)。那么我可以看看,如果伦纳德不是更快。反正他知道的更多。顺便说一句,如果您实际上将 ManyToOneEntity 指向其父级以实现引用完整性,ConstraintViolationException 会消失吗?
  • 是的,手动分配child.setParent(parent)@SpringBootTest 方案的解决方法。假期过后,我将致力于发布 GitHub 存储库。感谢您的观看。
  • 我认为您的问题是,@DataJpaTest 被注释为 @Transactional 导致整个测试在单个事务中运行。
  • @LeonardBrünings,这是一个很好的观察:@Transactional 似乎是两个注释之间的区别。但是为什么一个事务会导致第二个测试失败呢?
  • @kriegaex,我在这里创建了一个 GitHub 项目:github.com/jaco0646/jpa-audit-test

标签: java spring-boot spring-data-jpa spock audit


【解决方案1】:

就像我说的,我不是 Spring 用户,但在玩你的 MCVE 时我注意到以下几点:

  • 对于@DataJpaTest,不仅persisted2 == persisted1 为真,甚至persisted2 === persisted1。即,对象就地更改,没有创建新实例。因此,检查persisted2.lastModifiedDate.isAfter(persisted1.lastModifiedDate) 永远无法工作。
  • 对于@DataJpaTestlastModifiedDate 永远不会更新。也许这种测试并不意味着检查时间戳。 IE。在第二次和以后的persisted2.lastModifiedDate.isAfter(lastModifiedDate) 保存之前,您甚至不能使用def lastModifiedDate = persisted1.lastModifiedDate。它也失败了。

所以你真的应该使用@SpringBootTest,如果你想检查这样的时间戳。但是,您需要满足父子关系的参照完整性。如果可以选择修改@DataJpaTest 行为以更新时间戳,我不知道。但这可能是在 JPA 测试中被模拟的数据库功能。有Spring经验的人或许可以回答这个问题。


更新:这样的东西应该适合你:

package spring.jpa

import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import spock.lang.Specification

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
class AuditingSpec extends Specification {
  @Autowired
  OneToManyRepository repo

  def "New parent and child are both assigned IDs and dates"() {
    given:
    def parent = new OneToManyEntity()
    parent.setChildren([createChild(parent)])
    when:
    def persisted = repo.save(parent)
    then:
    def persistedChild = persisted.children.first()
    persisted.createdDate
    persisted.createdDate == persisted.lastModifiedDate
    persistedChild.createdDate
    persistedChild.createdDate == persistedChild.lastModifiedDate
  }

  def "Appended child is assigned IDs and dates"() {
    given:
    def parent = new OneToManyEntity()
    parent.setChildren([createChild(parent)])
    def persisted = repo.save(parent)
    persisted.children.add(createChild(parent))
    when:
    def persisted2 = repo.save(persisted)
    then:
    persisted2.children.size() == 2
    def firstChild = persisted2.children.first()
    def secondChild = persisted2.children.last()
    secondChild.id > firstChild.id
    secondChild.createdDate
    secondChild.createdDate == secondChild.lastModifiedDate
  }

  def "LastModifiedDate value is updated"() {
    given:
    def persisted1 = repo.save(new OneToManyEntity())
    //sleep(1000)
    persisted1.setChildren([])
    def persisted2 = repo.save(persisted1)
    expect:
    persisted2.lastModifiedDate.isAfter(persisted1.lastModifiedDate)
  }

  static ManyToOneEntity createChild(OneToManyEntity parent) {
    def child = new ManyToOneEntity()
    child.setParent(parent)
    child
  }
}

【讨论】:

  • Leonard 上面的评论似乎是在正确的轨道上。 @DataJpaTest 是一个红鲱鱼:它改变测试行为只是因为它是用 @Transactional 进行元注释的。所以这两种不同的测试行为都可以通过@SpringBootTest@Transactional结合来观察,也可以不观察。我认为 @LastModifiedDate 的预期行为可能是仅通过单独事务中的更改进行更新,而单个事务中的多个更改不会记录为单独的修改。
【解决方案2】:

感谢 cmets 中的提示,我意识到这两个测试在相反的情况下会失败,因为第一个测试必须在事务中运行,而第二个测试必须在事务中运行。测试失败表现为不同的类级别注释,因为@DataJpaTest 是事务性的,而@SpringBootTest 不是。所以解决方案是使用@SpringBootTest,并且只将第一个测试注释为@Transactional。我已经相应地更新了 GitHub project

【讨论】:

  • "第一个测试必须在事务中运行,而第二个测试不得在事务中运行" - 如果你之前提到过,那就太好了。所以基本上你使用我的解决方案加上一个@Transactional 注释。这值得一个额外的答案,自我接受而不是我的吗?
  • 顺便说一句,即使 3 个特性方法中的最后一个不能在(单个)事务中运行,其他两个也可以通过而不是事务性的。它们应该是事务性的,但决不是必须的。如果我们想做到 100% 正确,那么两次测试保存也应该打开 2 个单独的事务,因为有零个孩子只是一对多的特例。
  • 在我提出这个问题时,我不明白交易对不同测试的影响。伦纳德指出来了,我还是不能百分百理解。但这就是我没有提到它的原因:这是我从那以后学到的东西。第一个测试必须是事务性的,如问题中所述。当 Spring 能够自动设置父引用时,修改测试以显式设置父引用不是可接受的解决方案。这个接受的答案允许测试以原始形式从 OP 中通过。
  • “第一个测试必须是问题中所写的事务性测试。”问题的任何地方都没有写。文本中没有术语交易。否则我不会打扰写作可能会以我的方式回答。但不管怎样,我很高兴你解决了你的问题,我可以忍受浪费一些时间克隆你的存储库并让你的测试以我不知道的方式运行,这会让你觉得次优或不可接受。我们都在这里学到了一些东西,所以很好。 ?
【解决方案3】:

对于任何对场景 f.e. 有问题的人。 @CreatedDate 在 junit 测试中存储到 DB 后不会生成。只需将@DataJpaTest 与@EnableJpaAuditing 一起使用,您的@CreatedDate 和其他生成的字段也将在junit 测试中开始工作。在我的案例中缺少的是 @EnableJpaAuditing 注释。 @DataJpaTest 默认未启用 jpa 审计,因此使用注释 @EnableJpaAuditing 您将启用它。祝你好运:)

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2019-01-27
    • 1970-01-01
    • 1970-01-01
    • 2016-08-22
    • 1970-01-01
    • 1970-01-01
    • 2016-06-02
    相关资源
    最近更新 更多