【问题标题】:Testing a service method by Unit Test? [closed]通过单元测试测试服务方法? [关闭]
【发布时间】:2021-06-27 18:42:15
【问题描述】:

我没有测试经验,并尝试通过单元测试来测试方法。我看过的所有示例都通过使用模拟值来执行操作。我知道,我还将在我的项目中使用带有mockito 的模拟值。这是我要测试的服务方法:

ProductServiceImpl:

public List<ProductDTO> findAllByCategoryUuid(UUID categoryUuid) {

    // code omitted

    return result;
}

这是我的单元测试类:

ProductServiceImplTest:

// ? @InjectMocks
@Autowired 
ProductServiceImpl productService;

@Mock
ProductRepository productRepository;
  

@Test
public void testFindAllByCategoryUuid() {

    UUID categoryUuid = UUID.randomUUID();

    final List<Product> productList = new ArrayList<>();
    for (int i = 0; i < size; i++) {
        // create product by setting "categoryUuid" and add to productList
    }
    productRepository.saveAll(productList);


    when(productService.findAllByCategoryUuid(categoryUuid)
                .thenReturn(productList);
}

我的问题:

1. 为了测试服务方法,上述方法是否正确?我认为我不应该在服务方法内部处理,只需通过categoryUuid 并检查该方法的结果进行测试?这是真的吗?

2. 在测试类中,我使用@Autowired 访问服务方法,但我不确定是否应该@Mock。有错吗?

任何帮助将不胜感激。


更新:我还使用 DiffBlue 插件创建单元测试,它会生成如下所示的测试方法。但我认为它似乎是作为测试存储库方法而不是服务方法。不是吗?

@Test
public void testFindAllByCategoryUuid() {
    when(this.productRepository.findAllByCategoryUuid((UUID) any()))
        .thenReturn(new ArrayList<Product>());
    assertTrue(this.productService.findAllByCategoryUuid(UUID.randomUUID())
        .isEmpty());
    verify(this.productRepository).findAllByCategoryUuid((UUID) any());
    // ...
}

【问题讨论】:

  • 我对 testFindAllByCategoryUuid 的评论 - 它使用模拟 productRepository 来测试 productService(productService 正在测试中,因为它在 assert 语句中),看起来对我有效
  • 理想情况下,JUnit 测试应该在实际代码之前编写,因此它可以作为开发人员的规范,据我在 TDD 书中记得。如今,当人们拥有高测试覆盖率时,他们似乎更快乐,但我不确定是否所有代码都需要所有这些测试。 CRUD 测试对我来说听起来像是浪费时间,恕我直言
  • 您似乎误解了什么是模拟。必须将模拟配置为返回某些内容。 DiffBlue 生成的测试确实设置了模拟以返回一个已知值(在这种情况下为空列表),但我不明白对 assertTrue(...)verify(...) 的调用应该测试什么。
  • @justice 如果它的格式正确,我可能会回答这个问题。

标签: java spring spring-boot unit-testing testing


【解决方案1】:

我不是专家,但我会尽力回答你的问题

对您的方法进行单元测试的一般方法应该是针对所有可能的输入集测试输出。 在您的具体情况下,您可以测试

  • 输入:现有 UUID 输出:NonNull 列表。
  • 输入:不存在的 UUID 输出:空列表。
  • 输入:null:空列表。

现在您在这里所做的是正确的,您需要自动装配您正在为其编写测试用例的类并模拟该类中的依赖项。 唯一的问题是

when(productService.findAllByCategoryUuid(categoryUuid)
                .thenReturn(productList);

应该是

when(productRepository.findAllByCategoryUuid(categoryUuid)
                .thenReturn(productList);   

您在这里模拟 productRepository.findAllByCategoryUuid,因为您的目标是测试服务类中的方法。

在此之后,只需为上述所有条件添加适当的断言语句。

此外,每当针对某些代码记录错误时,我通常都会遵循一条规则,我尝试在我的 Junit 中使用 assert 来覆盖输入和输出情况,以便每次我都会测试所有可能的输入和输出场景。

【讨论】:

  • 非常感谢您的精彩解释。但是,我想知道,如果我们测试服务方法,为什么我们要从存储库方法中检索数据?
  • 请回复?
  • 我们正在模拟对存储库的调用,因为您的服务正在调用存储库方法,我们需要一种方法来避免该调用并返回一个虚拟数据。因此,正如我之前所说,单元测试的目的只是现在在这里测试您的服务方法,如果您的 repo 方法有问题,将很难找到它,这也不是您需要单独编写的目的回购层的测试用例。这就是为什么只是模拟对 repo 的调用并返回之前创建的虚拟数据。希望这有助于我在不同的时区,所以答案会有一些延迟。
  • 非常感谢您的宝贵帮助,投了赞成票;)另一方面,您能否根据相关实体发布适当的单元测试场景?我的意思是,假设有 2 个实体,比如 Employee 和 City(有 City.Id == Employee.CityId 关系),我们想要测试排序。在这个场景中,(在测试方法中),我需要先模拟或创建员工,然后模拟或创建城市记录。然后将员工记录传递给服务进行测试。如何基于此场景构建简单的单元测试?我尝试了许多不同的场景,但我无法创建正确的测试:(((
【解决方案2】:

使用 Mockito 编写 Junit 测试时要记住的重要事项

  1. 所有类级别@Runwith()
  2. 测试类应该使用@InjectMocks
  3. 所有测试都应使用@Test 注释
  4. 应使用@Mock 模拟任何外部服务
  5. 应模拟任何对 DB 或其他服务的调用,并应相应地返回值。
  6. 您应该有断言来测试您的结果。

我会这样写:

@RunWith(MockitoJUnitRunner.class)
public class ProductServiceImplTest {

@InjectMocks
ProductServiceImpl productService;

@Mock
ProductRepository productRepository;

@Test
public void testFindAllByCategoryUuid() {

    UUID categoryUuid = UUID.randomUUID();

    final List<Product> productList = new ArrayList<>();
    for (int i = 0; i < size; i++) {
        // create product by setting "categoryUuid" and add to productList
    }
    
    when(productRepository.saveAll(ArgumentMatchers.any()).thenReturn(productList); 
// Or below might work for newer version of test cases when we get Null Pointer Exp using older version of Junit test cases

//doReturn(productList).when(productRepository).saveAll(any(List.class));
    
    List<ProductDTO> response = productService.findAllByCategoryUuid(categoryUuid);
    
    Assert.assertNotNull(response);
    Assert.assertEquals("Object Value", response.getXXX());
}

【讨论】:

  • 完美的解释,我现在正在申请,会尽快通知你。
  • 它抛出 NullPointer 异常,因为 ArgumentMatchers.any() 为空。 ArgumentMatchers.any() 是什么,我该如何解决这个问题?
  • ArgumentMatchers 信息:stackoverflow.com/questions/44287635/….
  • 有时由于 Mockito 版本,我们会遇到类似的问题。吹线固定空指针。您可以根据您的测试条件进行修改。 doReturn(result).when(repository).saveAll(any(List.class));
  • 用您的查询更新了测试用例。
【解决方案3】:

针对服务层编写单元测试有缺点:

  1. 您违反了被测方法的封装。如果它因为您开始调用不同的类/方法而改变 - 测试将中断。即使该方法可能工作正常。
  2. 因为您打算使用模拟,部分测试将只是检查您的模拟是否已设置为通过测试。所以基本上你将测试测试逻辑。

将逻辑向下移动通常更有效率,例如到模型。这些类可以在没有模拟的情况下进行单元测试。然后你可以编写一个更高级别的测试(包括 DB)来检查一切是否一起工作。

阅读:

【讨论】:

  • 非常感谢斯坦尼斯拉夫的精彩解释,投了赞成票。你是对的(我没有经验,但我看到你有单元测试的经验)。现在我必须为服务方法编写这个单元测试,但除此之外,我当然会遵循你提到的这种方法。现在,您能否就以下问题向我澄清一下?
  • #1 实际上,我需要将相关数据保存到 3 个具有 PK-FK 关系的表中(为简洁起见,我省略了该部分)。在这种情况下,我认为我需要通过List&lt;UUID&gt; productUuidList = productRepository.saveAll(productList) 获取保存的产品 ID。但是,productUuidList 的大小始终为 0。为什么?我应该使用whenthenReturn() 获取这些值吗?我也试试这个when(productRepository.saveAll(productList)).thenReturn(demoList); ? (我将 demoList 设置为一个空的产品列表),但 demoList 仍然是空的。
  • @justice,如果您不使用模拟,那么是的 - 您确实需要先保存这些产品。您可以使用例如用于这些测试的 H2 内存数据库或本地安装的真实数据库。如果您决定使用模拟(我个人认为这是一个错误),那么您需要模拟 productRepository.findAllByCategoryUuid() - 因为这是服务调用的方法。数据库中实际上不会存储任何内容,因此无需模拟 productRepository.saveAll(productList)(除非服务出于某种原因调用它)。
  • 感谢您的回复。但我当然更喜欢最合适和最简单的方法。另一方面,我想知道如何管理相关的实体记录?即使我使用mock,我也需要设置相关实体​​的相关PK&FK值。我想有些东西会在不模拟其他字段的情况下设置相关实体​​的 PK 和 FK 值。但是因为我真的太困惑了,你能根据我的例子发布一个正确的答案吗?您可以只举一个具有相关实体的示例(设置它们的相应字段并测试例如排序)。提前致谢。
  • 以上场景的最佳实践?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-01-18
相关资源
最近更新 更多