【问题标题】:Spring boot mockito mock method according to input parametersspring boot mockito根据输入参数的mock方法
【发布时间】:2022-01-10 12:14:25
【问题描述】:

我有一个特殊情况,我不知道如何解决。

我正在使用 spring boot、junit 和 mockito 测试服务类方法。在我正在测试的类方法中,它也调用了一个内部方法,如下所示:

@Service
public class ServiceFacadeImpl implements ServiceFacade {

  //Some autowire and stuffs

  @Override
  public List<Log> getObjectLog(String objectCode) {
    List<Log> objectLogs = objectRepository.findByObjectCode(objectCode);
    return objectLogs;
  }

  @Override
  public MyObjectWrapper getMyObjectsLogByExternalCode(String externalCode) {
    List<String> objectCodes = objectRepository.findobjectCodesByExternalCode(externalCode);

    if(CollectionUtils.isEmpty(objectCodes)){
      return null;
    }

    MyObjectWrapper wrapper = new MyObjectWrapper();
    wrapper.setExternalCode(externalCode);
    wrapper.setObjectLogs(new ArrayList<>());
 
    for (String objectCode : objectCodes ) {
      ObjectLogWrapper logWrapper = new ObjectLogWrapper();
      List<Log> logs = getObjectLog(opCode);   // Call to internal method of the class
      logWrapper.setObjectCode(objectCode);
      logWrapper.setObjectLog(logs);
      wrapper.getObjectLogs().add(logWrapper);
    }
    return MyObjectWrapper ;
  }
}

我试图在测试“getMyObjectsLogByExternalCode”方法时模拟这个 getObjectLog(opCode) 以根据收到的参数返回不同的日志列表

@SpringBootTest
@RunWith(SpringRunner.class)
class ObjectServiceFacadeImplTest {

  //Some mock beans

  @Autowired private ServiceFacade serviceFacade;

@Test
  @DisplayName("test")
  void testGetMyObjectsLogByExternalCode() {

    // Given
    List<String> mockObCodes = Arrays.asList( "obcode1","obcode2" );
    when(repository.findObjectCodesByexternalCode(any())).thenReturn(mockObCodes );

    List<OperationLogDTO> mockLogsOb1 = Arrays.asList( new Log(), new Log() );  //Size 2 for ob1
    when(serviceFacade.getObjectLog(eq( "obcode1"))).thenReturn( mockLogsOb1 );

    List<OperationLogDTO> mockLogsOb2 = Collections.singletonList( new Log() ); //Size 1 for ob1
    when(serviceFacade.getObjectLog(eq( "obcode2"))).thenReturn( mockLogsOb2 );

    // When
    MyObjectWrapper objectWrapper = serviceFacade.getMyObjectsLogByExternalCode("TESTCODE");

    // Then
    assertEquals( objectWrapper.getExternalCode(),"TESTCODE" );
    assertEquals( objectWrapper.getObjects().size(),2 );
    assertEquals( objectWrapper.getObjects().get( 0 ).getLogs().size(),2 );  //Fails because it gets the second mock return value
    assertEquals( objectWrapper.getObjects().get( 0 ).getCode(),"obcode1" ); //Fails because it gets the second mock return value
    assertEquals( objectWrapper.getObjects().get( 1 ).getLogs().size(),1 );
    assertEquals( objectWrapper.getObjects().get( 1 ).getCode(),"obcode2" );
  }

由于机密内容,代码已更改,但我认为我正在尝试做的事情的逻辑很清楚

逻辑基本上是我有包含次要对象的主要对象,而这个次要对象也包含日志对象。有一种方法可以获取每个辅助对象的日志,另一种方法可以提供主对象代码,它会获取与包装对象关联的所有辅助对象的所有日志以及所有信息。

这里的问题是,当 getObjectLogs 作为模拟调用时,总是不雅地返回接收到的参数的第二种情况。调用它时,我总是丢失大小为 1 的日志。

我不知道我做错了什么。

更新:

我也尝试使用@SpyBean 而不是@Autowired 来提供服务,但它总是返回相同的

@SpyBean private ServiceFacade serviceFacade;

【问题讨论】:

  • 您的测试中有serviceFacadeoperationServiceFacade,哪一个是正确的?此外,由于您是自动接线serviceFacade,因此您将无法模拟任何有关它的内容。
  • @JoãoDias 我编辑了失败。
  • @JoãoDias 我知道我正在自动装配它,但我还需要模拟一个额外的方法,那么我该怎么做呢?它似乎也在嘲笑它,但总是返回 obcode2 响应
  • 您可以尝试使用 Spy,但模拟 getObjectLog 方法中的任何内容听起来会更好。由于您没有提供它的代码,我们仍然无法为您提供帮助。
  • 能否请附上getMyObjectsLogByExternalCodegetObjectLog 的完整代码?谢谢。

标签: spring-boot unit-testing junit mockito arguments


【解决方案1】:

首先 - 尝试只模拟合作者,而不是你的被测对象。

话虽如此 - Spy 可用于模拟被测对象的某些部分。

我真的不认为有必要使用 @SpringBootTest 为您的服务,所以这里是简单的 Mockito 版本:

@ExtendWith(MockitoExtension.class)
class ObjectServiceFacadeImplTest {

    @Mock
    ObjectRepository repository;

    @Spy
    @InjectMocks
    private ServiceFacadeImpl serviceFacade;

    @Test
    void testGetMyObjectsLogByExternalCode() {

        // Given
        List<String> mockObCodes = List.of("obcode1", "obcode2");
        String externalCode = "TESTCODE";
        when(repository.findObjectCodesByExternalCode(externalCode)).thenReturn(mockObCodes);

        List<Log> mockLogsOb1 = List.of(new Log(), new Log());  //Size 2 for ob1
        when(serviceFacade.getObjectLog("obcode1")).thenReturn(mockLogsOb1);

        List<Log> mockLogsOb2 = List.of(new Log()); //Size 1 for ob1
        when(serviceFacade.getObjectLog("obcode2")).thenReturn(mockLogsOb2);

        // When
        MyObjectWrapper objectWrapper = serviceFacade.getMyObjectsLogByExternalCode(externalCode);

        // Then
        assertEquals(objectWrapper.getExternalCode(), externalCode);
        assertEquals(objectWrapper.getObjects().size(), 2);
        assertEquals(objectWrapper.getObjects().get(0).getLogs().size(), 2);  //Fails because it gets the second mock return value
        assertEquals(objectWrapper.getObjects().get(0).getCode(), "obcode1"); //Fails because it gets the second mock return value
        assertEquals(objectWrapper.getObjects().get(1).getLogs().size(), 1);
        assertEquals(objectWrapper.getObjects().get(1).getCode(), "obcode2");
    }
}

【讨论】:

    【解决方案2】:

    在使用 mock 进行测试时,您应该明确哪些类正在测试中,哪些其他类只是应该被模拟的依赖项。在您的情况下,您想在方法 getMyObjectsLogByExternalCode() 上测试 ServiceFacade

    所以请将整个 ServiceFacade 对象保留为自动装配的 Spring bean,不要使用 spys 或尝试模拟它的一部分。

    还请注意,您不能模拟自动装配服务的方法,也不能用间谍来欺骗它。间谍实际上是一个门面,只能重定向初始方法调用。内部方法调用始终引用真实对象。

    但正如您已经做过的那样,模拟 objectRepository 是有意义的,因为它应该在您的单元测试范围之外。在您的代码中,我可以看到该对象的两个方法称为:

      objectRepository.findobjectCodesByExternalCode(externalCode); // called once to get the list
      objectRepository.findByObjectCode(objectCode); // called twice, which each element of the list
    

    你都应该用标准的 Mockito 风格来模拟它们:

      List<String> mockObCodes = Arrays.asList("obcode1", "obcode2");
      when(objectRepository.findObjectCodesByexternalCode(any())).thenReturn(mockObCodes );
    
      List<OperationLogDTO> mockLogsOb1 = Arrays.asList( new Log(), new Log() );  //Size 2 for ob1
      when(objectRepository.findByObjectCode("obcode1")).thenReturn( mockLogsOb1 );
    
      List<OperationLogDTO> mockLogsOb2 = Collections.singletonList( new Log() ); //Size 1 for ob1
      when(objectRepository.findByObjectCode("obcode2")).thenReturn( mockLogsOb2 );
    

    我希望这将使您的测试运行,请告诉我,

    【讨论】:

    • 但是想象一下,我有一个带有私有方法的类,该方法在该类的其他方法中被内部调用了很多次,实际上我正在测试代码的同一部分并一直调用它,甚至更好的方法似乎很短,但它分为 15 个私有方法,以便更好地理解它。如果我不能独立地测试私有方法和使用私有模拟的主要方法,那么这种情况是一团糟,因为实际上一个方法分为 15 个较小的方法并且更容易测试,必须作为一个巨大的方法进行测试,这真的很难测试
    • 同样适用于在我的情况下被多次调用的公共方法
    • 在进行单元测试时,您应该将被测类视为一个黑盒。想象一下别人写的。代码拆分多少私有方法以及内部调用什么都无关紧要。对于您作为测试人员的角色,该单元应该只是工作并完成它的工作。如果你觉得应该测试私有方法,这是一个强有力的指标,表明你的类变得复杂了。也许应该将私有方法重构为新服务?您是否考虑过关注点分离? en.wikipedia.org/wiki/Separation_of_concerns
    • 是的,但我是测试人员,而不是开发人员 :(
    • 那么我只能说:专注于公共方法,并与您的开发人员讨论可测试性设计。 ;-)
    【解决方案3】:

    这里getObjectLogs 总是返回第二个值。这里的问题是当你第一次模拟 Mockito 存储 mockLogsOb1 作为响应时。当您第二次模拟而不是将其添加为响应时,它会将值替换为mockLogsOb2
    由于 Mockito 通过为对象创建代理来工作。您可以通过多种方式处理上述情况。

    1. 通过链接响应
    when(serviceFacade.getObjectLog(anyString()).thenReturn(mockLogsOb1).thenReturn(mockLogsOb2 );
    
    1. 以经典风格返回响应(检查模拟版本支持)
     when(serviceFacade.getObjectLog(anyString()).thenReturn(mockLogsOb1, mockLogsOb2);
    
    1. 使用 Answer 返回响应以提供多重响应
    when(mock.someMethod(anyString())).thenAnswer(
         new Answer() {
             public Object answer(InvocationOnMock invocation) {
                 Object[] args = invocation.getArguments();
                 Object mock = invocation.getMock();
                 return "called with arguments: " + Arrays.toString(args);
             }
     });
    
     //Following prints "called with arguments: [foo]"
     System.out.println(mock.someMethod("foo"));
    

    您可以检查传递的参数并相应地返回响应。

    【讨论】:

    • 如何获得 testValue1?谢谢
    • 我已根据您的查询更新了答案。我们可以获取从调用对象传递的参数。该对象的值可以在测试条件中使用,并且可以相应地返回来自答案的响应对象。但我总是更喜欢通过链接响应的第一种方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-02-08
    • 1970-01-01
    • 1970-01-01
    • 2012-01-06
    • 1970-01-01
    相关资源
    最近更新 更多