如何使用"Spring Data JPA"规范测试方法

18

我正在使用org.springframework.data.jpa.domain.Specifications进行操作,这是一个基本搜索:

 public Optional<List<Article>> rechercheArticle(String code, String libelle) {
    List<Article> result = null;

    if(StringUtils.isNotEmpty(code) && StringUtils.isNotEmpty(libelle)){
        result = articleRepository.findAll(Specifications.where(ArticleSpecifications.egaliteCode(code)).and(ArticleSpecifications.egaliteLibelle(libelle)));
    }else{
        if(StringUtils.isNotEmpty(code)){
            result= articleRepository.findAll(Specifications.where(ArticleSpecifications.egaliteCode(code)));
        }else{
            result = articleRepository.findAll(Specifications.where(ArticleSpecifications.egaliteLibelle(libelle)));
        }
    }

    if(result.isEmpty()){
        return Optional.empty();
    }else{
        return Optional.of(result);
    }
}

实际上这是有效的,但我想为这个方法编写单元测试,我无法弄清楚如何检查传递给articleRepository.findAll()的规范。

目前我的单元测试看起来像:

@Test
public void rechercheArticle_okTousCriteres() throws FacturationServiceException {
    String code = "code";
    String libelle = "libelle";
    List<Article> articles = new ArrayList<>();
    Article a1 = new Article();
    articles.add(a1);
    Mockito.when(articleRepository.findAll(Mockito.any(Specifications.class))).thenReturn(articles);


    Optional<List<Article>> result = articleManager.rechercheArticle(code, libelle);

    Assert.assertTrue(result.isPresent());
    //ArgumentCaptor<Specifications> argument = ArgumentCaptor.forClass(Specifications.class);
    Mockito.verify(articleRepository).findAll(Specifications.where(ArticleSpecifications.egaliteCode(code)).and(ArticleSpecifications.egaliteLibelle(libelle)));
    //argument.getValue().toPredicate(root, query, builder);


}

有什么主意吗?


你想不出来是因为你不知道如何模拟articleRepository,还是你不知道如何进行断言?你当前的测试看起来是什么样子? - Manu
我正在使用Mockito,因此我可以轻松地模拟articleRepository,但我无法弄清楚如何正确地进行断言。 - Seb
3个回答

7
我曾经遇到了和你类似的问题,我将包含规范的类改为一个对象,而不是只有静态方法的单个类。这样我就可以轻松地模拟它,使用依赖注入来传递它,并测试哪些方法被调用了(无需使用PowerMockito来模拟静态方法)。
如果你想像我一样做,我建议你使用集成测试来测试规范的正确性,对于其他部分,只需检查是否调用了正确的方法。
例如:
public class CdrSpecs {

public Specification<Cdr> calledBetween(LocalDateTime start, LocalDateTime end) {
    return (root, query, cb) -> cb.between(root.get(Cdr_.callDate), start, end);
}
}

然后您需要对该方法进行集成测试,以测试该方法是否正确:
@RunWith(SpringRunner.class)
@DataJpaTest
@Sql("/cdr-test-data.sql")
public class CdrIntegrationTest {

@Autowired
private CdrRepository cdrRepository;

private CdrSpecs specs = new CdrSpecs();

@Test
public void findByPeriod() throws Exception {
    LocalDateTime today = LocalDateTime.now();
    LocalDateTime firstDayOfMonth = today.with(TemporalAdjusters.firstDayOfMonth());
    LocalDateTime lastDayOfMonth = today.with(TemporalAdjusters.lastDayOfMonth());
    List<Cdr> cdrList = cdrRepository.findAll(specs.calledBetween(firstDayOfMonth, lastDayOfMonth));
    assertThat(cdrList).isNotEmpty().hasSize(2);
}

现在,当您想对其他组件进行单元测试时,您可以像这样进行测试,例如:

@RunWith(JUnit4.class)
public class CdrSearchServiceTest {

@Mock
private CdrSpecs specs;
@Mock
private CdrRepository repo;

private CdrSearchService searchService;

@Before
public void setUp() throws Exception {
    initMocks(this);
    searchService = new CdrSearchService(repo, specs);
}

@Test
public void testSearch() throws Exception {

    // some code here that interact with searchService

    verify(specs).calledBetween(any(LocalDateTime.class), any(LocalDateTime.class));
   // and you can verify any other method of specs that should have been called
}

当然,在Service内部,您仍然可以使用Specifications类的where和and静态方法。希望这能帮到您。

4
如果您正在编写单元测试,那么您应该使用类似于Mockito或PowerMock这样的模拟框架来模拟articleRepository类的findAll()方法的调用。
有一种名为verify()的方法,您可以使用它来检查模拟对象是否被调用了特定参数。
例如,如果您正在模拟articleRepository类的findAll()方法,并想知道此方法是否使用特定参数进行调用,则可以执行以下操作:
Mokito.verify(mymock, Mockito.times(1)).findAll(/* Provide Arguments */);

如果您提供的参数未被模拟调用,这将导致测试失败。

1
我并不建议他开始使用 PowerMock。我只是建议他开始使用他觉得舒适的任何一个工具。 - user2004685
Mockito.verify(articleRepository) 这应该是 Mockito.verify(articleRepository, Mockito.times(1)),对吗?模拟将只被调用一次。另外,当测试被执行时,您可以分享JUnit的输出内容吗? - user2004685
2
我不想检查它是否只执行了一次,我想确保使用哪些规格:Specifications.where(ArticleSpecifications.egaliteCode(code)).and(ArticleSpecifications.egaliteLibelle(libelle))或Specifications.where(ArticleSpecifications.egaliteCode(code))或Specifications.where(ArticleSpecifications.egaliteLibelle(libelle)) - Seb
3
将内容放入不同的方法中正是要达到的目的!不要将规范作为文字量写入代码中。相反,请编写一个生成规范的方法,测试该方法,然后模拟生成规范的结果;只需检查您的方法是否应用了该规范结果即可。这就是单元测试的整个理念:您必须解耦事物。如果您想检查某个特定的事物是否以某种特定的方式被使用,那么这个事物必须“来自外部”;并且您可以提供一个“模拟对象”来控制内部行为。 - GhostCat
2
与楼主意见一致。这个答案提出了使用模拟的建议,这是可以接受的。但问题仍然存在:如何断言传递给模拟的被测试方法创建的规范是否符合预期。我喜欢规范,但一旦构建完成,它们就变得难以理解。如果@Seb能够解决这个问题,那将是非常棒的。 - Nick Foote
显示剩余11条评论

0

你的问题在于你在一个方法中做了太多的事情。你应该有三个不同的方法来处理articleRepository。

然后你可以像其他人建议的那样使用mocking:

  • 设置你的mocks,以便你知道应该调用articleRepository上的哪个方法
  • 验证确切发生了预期的调用

请注意:这三个方法应该是内部的;主要的一点是:你不能通过外部的一个调用来测试这个方法;因为它做了不止一件事,取决于你提供的输入。因此,你需要为代码中每个潜在的路径创建至少一个测试方法。当你将代码分成不同的方法时,这变得更容易(从概念上讲)。


我实际上正在尝试避免有三种不同的方法,如果我有10个标准而不仅仅是两个呢? - Seb
那么你的代码中有10个路径。 忽略它并不能帮助解决问题。它更清楚地表明您可能需要重新设计整个系统。 把所有东西都塞到一个块中并不能消除复杂性。 - GhostCat
我不是只想做一次测试,实际上我需要做五次。 - Seb
我完全同意你所说的。 - Seb
3
我认为这种方法本身很好,它是规范设计的一个简单示例; 动态查询。正如原帖所说,如果有许多可选的搜索条件,则使用多个方法是不可能的,因为你需要为每个可能的查询参数组合都编写一个方法。动态构建规范的单个方法是正确的。在单元测试中使用模拟来尝试检查规范是否构建正确也是正确的。问题是规范 API 似乎没有便于在构建后检查规范的功能。 - Nick Foote
我仍然没有找到任何好的测试方法 :/ - Seb

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