我注意到开箱即用的 JSON 序列化在生产和单元测试中的工作方式不同。当我使用
./gradlew bootRun
运行应用程序时,一切正常的事情在单元测试中失败了。我想了解为什么会这样,以及如何使事情在两个地方都以相同的方式工作,而不必编写自定义配置只是为了使测试模拟生产。
为了重现,我使用使用 Spring Initializr 创建的 Java 17 在 Spring Boot 3.1.2 中创建了一个简单的 HelloWorld API 服务器。我正在使用 Lombok 和 Guava (以及 WebFlux,这对于这个问题可能并不重要)。
我有一个这样定义的 API:
import lombok.*;
import com.google.common.collect.*;
@Builder
@Value
public class SearchResult {
@Singular
ImmutableList<String> results;
}
@RestController
@RequestMapping("/api/v1/search")
final class SearchController {
@GetMapping(produces = "application/json")
public Mono<SearchResult> search() {
return Mono.just(SearchResult.builder().result("result1").result("result2").build());
}
}
当我运行
./gradlew bootRun
时,效果非常好。然而,当我尝试测试这个时,事情变得很奇怪。我的测试看起来像这样:
@AutoConfigureWebTestClient
@SpringBootTest
final class SearchControllerTest {
@Autowired private WebTestClient webTestClient;
@Test
public void search() {
var response = webTestClient.get()
.uri("/api/v1/search")
.header("Content-Type", "application/json")
.exchange();
response.expectStatus().isOk();
response.expectBody(SearchResult.class).isEqualTo(...);
}
}
有两件事会导致此测试失败。首先,如果我将
ImmutableList
更改为 java.util.List
,一个模糊的错误就会消失。第二个问题是它说如果没有默认构造函数就无法反序列化对象。
我可以通过删除
@Value
并将其设为 Java record
(因为我希望该对象是不可变的)并仅在我的单元测试中创建一个自定义对象映射器来完成这项工作,这样显式注册 GuavaModule
:
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.datatype.guava.GuavaModule;
@Configuration
public class TestConfig {
@Bean
public ObjectMapper objectMapper() {
var mapper = new ObjectMapper();
mapper.registerModule(new GuavaModule());
return mapper;
}
}
但我不想做任何这些事情。我希望能够让我的测试使用已经注入到我的主要生产代码中的任何 ObjectMapper。这将使我的测试更加真实,并让我使用我显然已经能够在正常应用程序中使用的功能。我也很好奇为什么/如何在测试中使用不同的东西。
当您启动应用程序并在自己的应用程序中调用端点时,我怀疑您会遇到相同的错误?
您可以向 SearchResult 类添加默认构造函数,例如 lombok NoArgsConstructor、AllArgsConstructor 和/或RequiredArgsConstructor。
这应该可以解决您的反序列化问题。