如何干净地测试使用 DomainClassConverter 检索参数的 Spring Controller?

2022-01-14 00:00:00 unit-testing spring mockito java spring-mvc

我非常喜欢干净且隔离良好的单元测试.但是我在这里的干净"部分绊倒了测试一个控制器,该控制器使用 DomainClassConverter 功能来获取实体作为其映射方法的参数.

I am big on clean well-isolated unit tests. But I am stumbling on the "clean" part here for testings a controller that uses DomainClassConverter feature to get entities as parameters for its mapped methods.

@Entity
class MyEntity {
    @Id
    private Integer id;
    // rest of properties goes here.
}

控制器是这样定义的

@RequestMapping("/api/v1/myentities")
class MyEntitiesController {
    @Autowired
    private DoSomethingService aService;

    @PostMapping("/{id}")
    public ResponseEntity<MyEntity> update(@PathVariable("id")Optional<MyEntity> myEntity) {
        // do what is needed here
    }
}

所以从 DomainClassConverter 小 documentation 我知道它使用 CrudRepository#findById 来查找实体.我想知道的是如何在测试中干净地模拟它.通过执行以下步骤,我取得了一些成功:

So from the DomainClassConverter small documentation I know that it uses CrudRepository#findById to find entities. What I would like to know is how can I mock that cleanly in a test. I have had some success by doing this steps:

  1. 创建一个我可以模拟的自定义转换器/格式化程序
  2. 用上面的转换器实例化我自己的 MockMvc
  3. 在每次测试时重置模拟并更改行为.

问题在于设置代码很复杂,因此很难调试和解释(我的团队 99% 都是来自 rails 或 uni 的初级人员,所以我们必须保持简单).我想知道是否有办法从我的单元测试中注入所需的 MyEntity 实例,同时继续使用 @Autowired MockMvc 进行测试.

The problem is that the setup code is complex and thus hard to debug and explain (my team is 99% junior guys coming from rails or uni so we have to keep things simple). I was wondering if there is a way to inject the desired MyEntity instances from my unit test while keep on testing using the @Autowired MockMvc.

目前我正在尝试查看是否可以为 MyEntity 注入 CrudRepository 的模拟,但没有成功.我已经有几年没有在 Spring/Java 中工作了 (4),所以我对可用工具的了解可能不是最新的.

Currently I am trying to see if I can inject a mock of the CrudRepository for MyEntity but no success. I have not worked in Spring/Java in a few years (4) so my knowledge of the tools available might not be up to date.

推荐答案

所以从 DomainClassConverter 小文档中我知道它使用 CrudRepository#findById 来查找实体.我想知道的是如何在测试中干净利落地模拟它.

So from the DomainClassConverter small documentation I know that it uses CrudRepository#findById to find entities. What I would like to know is how can I mock that cleanly in a test.

您需要模拟在 CrudRepository#findById 之前调用的 2 个方法,以便返回您想要的实体.下面的示例使用 RestAssuredMockMvc,但如果您也注入 WebApplicationContext,您也可以使用 MockMvc 做同样的事情.

You will need to mock 2 methods that are called prior the CrudRepository#findById in order to return the entity you want. The example below is using RestAssuredMockMvc, but you can do the same thing with MockMvc if you inject the WebApplicationContext as well.

@RunWith(SpringRunner.class)
@SpringBootTest(classes = SomeApplication.class)
public class SomeControllerTest {

    @Autowired
    private WebApplicationContext context;

    @MockBean(name = "mvcConversionService")
    private WebConversionService webConversionService;

    @Before
    public void setup() {
        RestAssuredMockMvc.webAppContextSetup(context);

        SomeEntity someEntity = new SomeEntity();

        when(webConversionService.canConvert(any(TypeDescriptor.class), any(TypeDescriptor.class)))
                .thenReturn(true);

        when(webConversionService.convert(eq("1"), any(TypeDescriptor.class), any(TypeDescriptor.class)))
                .thenReturn(someEntity);
    }
}

在某些时候 Spring Boot 会执行 WebConversionService::convert,稍后会调用 DomainClassConverter::convert 然后像 invoker.invokeFindByIdcode>,它将使用实体存储库来查找实体.

At some point Spring Boot will execute the WebConversionService::convert, which will later call DomainClassConverter::convert and then something like invoker.invokeFindById, which will use the entity repository to find the entity.

那么为什么要模拟 WebConversionService 而不是 DomainClassConverter?因为DomainClassConverter是在应用启动时实例化的,没有注入:

So why mock WebConversionService instead of DomainClassConverter? Because DomainClassConverter is instantiated during application startup without injection:

DomainClassConverter<FormattingConversionService> converter =
        new DomainClassConverter<>(conversionService);

同时,WebConversionService 是一个允许我们模拟它的 bean:

Meanwhile, WebConversionService is a bean which will allow us to mock it:

@Bean
@Override
public FormattingConversionService mvcConversionService() {
    WebConversionService conversionService = new WebConversionService(this.mvcProperties.getDateFormat());
    addFormatters(conversionService);
    return conversionService;
}

将mock bean命名为mvcConversionService很重要,否则它不会替换原来的bean.

It is important to name the mock bean as mvcConversionService, otherwise it won't replace the original bean.

关于存根,您需要模拟 2 个方法.首先你必须告诉你你的模拟可以转换任何东西:

Regarding the stubs, you will need to mock 2 methods. First you must tell that your mock can convert anything:

when(webConversionService.canConvert(any(TypeDescriptor.class), any(TypeDescriptor.class)))
        .thenReturn(true);

然后是main方法,它将匹配URL路径中定义的所需实体ID:

And then the main method, which will match the desired entity ID defined in the URL path:

when(webConversionService.convert(eq("1"), any(TypeDescriptor.class), any(TypeDescriptor.class)))
        .thenReturn(someEntity);

到目前为止一切顺利.但是匹配目标类型不是更好吗?eq(TypeDescriptor.valueOf(SomeEntity.class)) 之类的东西?会,但这会创建一个 TypeDescriptor 的新实例,在域转换期间调用此存根时,该实例将不匹配.

So far so good. But wouldn't be better to match the destination type as well? Something like eq(TypeDescriptor.valueOf(SomeEntity.class))? It would, but this creates a new instance of a TypeDescriptor, which will not match when this stub is called during the domain conversion.

这是我投入工作的最干净的解决方案,但我知道如果 Spring 允许它可能会好很多.

This was the cleanest solution I've put to work, but I know that it could be a lot better if Spring would allow it.

相关文章