如何使用 Spring Boot 2 和 WebFlux 测试 @Controller
How To Test @Controller with Spring Boot 2 and WebFlux
我目前正在尝试在普通控制器中测试一个简单的 post 方法,其中 returns 一个 Mono 重定向到不同的页面或在本例中为主页。我已经尝试了各种不同的方法模拟组件,但我似乎总是在测试中返回一个空的 Mono,所有这些都通过表单提交正常工作。
@Controller
public class AddNewEntryController {
private final EntryService service;
@PostMapping("/add-new-entry")
public Mono<String> addNewEntrySubmit(@ModelAttribute("timeEntry") Entry entry) {
return service.addTimeKeepingEntry(Flux.just(entry)).then(Mono.just("redirect:/"));
}
}
和服务Class代码
public Mono<Void> addTimeKeepingEntry(Flux<Entry> entry) {
return entry.flatMap(entry -> Mono.when(repository.save(entry).log("Save to DB"))
.log("add entry when")).then().log("done");
}
和测试代码
@RunWith(SpringRunner.class)
@WebFluxTest(controllers = AddNewEntryController.class)
@Import({ThymeleafAutoConfiguration.class})
public class AddNewEntryControllerTest {
@Autowired
WebTestClient webTestClient;
@MockBean
EntryService service;
@Test
public void addNewEntrySubmit() {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("month", month);
formData.add("dateOfMonth", Integer.toString(21));
formData.add("startTime", "09:00");
when(service.addEntry(Flux.just(entry1))).thenReturn(Mono.empty());
webTestClient.post().uri("/add-new-entry").body(BodyInserters.fromFormData(formData)).exchange().expectStatus().isSeeOther().expectHeader().valueEquals(HttpHeaders.LOCATION, "/");
每当我 运行 测试时,我总是得到一个 Null 指针,并且在调试后它指向 Mono 为 Null。问题是我不确定哪个 Mono 或在哪个步骤。
我得到的StackTrace如下
java.lang.NullPointerException: null
at com.dbeer.timekeeping.UI.AddNewEntryController.addNewEntrySubmit(AddNewEntryController.java:47) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_181]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_181]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_181]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_181]
at org.springframework.web.reactive.result.method.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:243) ~[spring-webflux-5.0.9.RELEASE.jar:5.0.9.RELEASE]
at org.springframework.web.reactive.result.method.InvocableHandlerMethod.lambda$invoke[=14=](InvocableHandlerMethod.java:138) ~[spring-webflux-5.0.9.RELEASE.jar:5.0.9.RELEASE]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118) [reactor-core-3.1.9.RELEASE.jar:3.1.9.RELEASE]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1083) ~[reactor-core-3.1.9.RELEASE.jar:3.1.9.RELEASE]
at reactor.core.publisher.MonoZip$ZipCoordinator.signal(MonoZip.java:247) ~[reactor-core-3.1.9.RELEASE.jar:3.1.9.RELEASE]
at reactor.core.publisher.MonoZip$ZipInner.onNext(MonoZip.java:329) ~[reactor-core-3.1.9.RELEASE.jar:3.1.9.RELEASE]
at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:185) ~[reactor-core-3.1.9.RELEASE.jar:3.1.9.RELEASE]
查看您的项目后,问题似乎出在控制器命名和 html 页面的一致性上。
例如在 header.html 中你有一个 url 到 link add-entry
但是你的控制器有 add-new-entry
如果你把 header 中的 url 改成add-new-entry
有效。
作为清理,您应该使用 thmyeleaf 生成 URL 而不是正常的 href,就像您以后添加安全性一样,thymeleaf 会将 session id 添加到 URL等
***********编辑拉出分支可以重现******
行
given(service.addTimeKeepingEntry(Flux.just(new TimeKeepingEntry(month, 21, "Tuesday", "09:00", "30", "17:00", "7.5", false)))).willReturn(Mono.empty());
是问题所在,因为 Mockito 在此处匹配 Object.equals 而您尚未定义对 object.
意味着什么
另一种方法是捕获传递给 mock 的 object
例如
@Captor
private ArgumentCaptor<Flux<TimeKeepingEntry>> captor;
@Test
public void addNewEntrySubmit() {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("month", month);
formData.add("dateOfMonth", Integer.toString(21));
formData.add("day", "Tuesday");
formData.add("startTime", "09:00");
formData.add("endTime", "17:00");
formData.add("breakLength", "30");
given(service.addTimeKeepingEntry(any())).willReturn(Mono.empty());
webTestClient.post().uri("/add-new-entry")
.body(BodyInserters.fromFormData(formData)).exchange().expectStatus().isSeeOther().expectHeader().valueEquals(HttpHeaders.LOCATION, "/");
verify(service).addTimeKeepingEntry(captor.capture());
TimeKeepingEntry timeKeepingEntry = captor.getValue().blockFirst();
assertThat(timeKeepingEntry.getMonth()).isEqualTo(month);
//and whatever else you want to test
}
我目前正在尝试在普通控制器中测试一个简单的 post 方法,其中 returns 一个 Mono 重定向到不同的页面或在本例中为主页。我已经尝试了各种不同的方法模拟组件,但我似乎总是在测试中返回一个空的 Mono,所有这些都通过表单提交正常工作。
@Controller
public class AddNewEntryController {
private final EntryService service;
@PostMapping("/add-new-entry")
public Mono<String> addNewEntrySubmit(@ModelAttribute("timeEntry") Entry entry) {
return service.addTimeKeepingEntry(Flux.just(entry)).then(Mono.just("redirect:/"));
}
}
和服务Class代码
public Mono<Void> addTimeKeepingEntry(Flux<Entry> entry) {
return entry.flatMap(entry -> Mono.when(repository.save(entry).log("Save to DB"))
.log("add entry when")).then().log("done");
}
和测试代码
@RunWith(SpringRunner.class)
@WebFluxTest(controllers = AddNewEntryController.class)
@Import({ThymeleafAutoConfiguration.class})
public class AddNewEntryControllerTest {
@Autowired
WebTestClient webTestClient;
@MockBean
EntryService service;
@Test
public void addNewEntrySubmit() {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("month", month);
formData.add("dateOfMonth", Integer.toString(21));
formData.add("startTime", "09:00");
when(service.addEntry(Flux.just(entry1))).thenReturn(Mono.empty());
webTestClient.post().uri("/add-new-entry").body(BodyInserters.fromFormData(formData)).exchange().expectStatus().isSeeOther().expectHeader().valueEquals(HttpHeaders.LOCATION, "/");
每当我 运行 测试时,我总是得到一个 Null 指针,并且在调试后它指向 Mono 为 Null。问题是我不确定哪个 Mono 或在哪个步骤。
我得到的StackTrace如下
java.lang.NullPointerException: null
at com.dbeer.timekeeping.UI.AddNewEntryController.addNewEntrySubmit(AddNewEntryController.java:47) ~[classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_181]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_181]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_181]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_181]
at org.springframework.web.reactive.result.method.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:243) ~[spring-webflux-5.0.9.RELEASE.jar:5.0.9.RELEASE]
at org.springframework.web.reactive.result.method.InvocableHandlerMethod.lambda$invoke[=14=](InvocableHandlerMethod.java:138) ~[spring-webflux-5.0.9.RELEASE.jar:5.0.9.RELEASE]
at reactor.core.publisher.MonoFlatMap$FlatMapMain.onNext(MonoFlatMap.java:118) [reactor-core-3.1.9.RELEASE.jar:3.1.9.RELEASE]
at reactor.core.publisher.Operators$MonoSubscriber.complete(Operators.java:1083) ~[reactor-core-3.1.9.RELEASE.jar:3.1.9.RELEASE]
at reactor.core.publisher.MonoZip$ZipCoordinator.signal(MonoZip.java:247) ~[reactor-core-3.1.9.RELEASE.jar:3.1.9.RELEASE]
at reactor.core.publisher.MonoZip$ZipInner.onNext(MonoZip.java:329) ~[reactor-core-3.1.9.RELEASE.jar:3.1.9.RELEASE]
at reactor.core.publisher.FluxPeek$PeekSubscriber.onNext(FluxPeek.java:185) ~[reactor-core-3.1.9.RELEASE.jar:3.1.9.RELEASE]
查看您的项目后,问题似乎出在控制器命名和 html 页面的一致性上。
例如在 header.html 中你有一个 url 到 link add-entry
但是你的控制器有 add-new-entry
如果你把 header 中的 url 改成add-new-entry
有效。
作为清理,您应该使用 thmyeleaf 生成 URL 而不是正常的 href,就像您以后添加安全性一样,thymeleaf 会将 session id 添加到 URL等
***********编辑拉出分支可以重现******
行
given(service.addTimeKeepingEntry(Flux.just(new TimeKeepingEntry(month, 21, "Tuesday", "09:00", "30", "17:00", "7.5", false)))).willReturn(Mono.empty());
是问题所在,因为 Mockito 在此处匹配 Object.equals 而您尚未定义对 object.
意味着什么另一种方法是捕获传递给 mock 的 object 例如
@Captor
private ArgumentCaptor<Flux<TimeKeepingEntry>> captor;
@Test
public void addNewEntrySubmit() {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<>();
formData.add("month", month);
formData.add("dateOfMonth", Integer.toString(21));
formData.add("day", "Tuesday");
formData.add("startTime", "09:00");
formData.add("endTime", "17:00");
formData.add("breakLength", "30");
given(service.addTimeKeepingEntry(any())).willReturn(Mono.empty());
webTestClient.post().uri("/add-new-entry")
.body(BodyInserters.fromFormData(formData)).exchange().expectStatus().isSeeOther().expectHeader().valueEquals(HttpHeaders.LOCATION, "/");
verify(service).addTimeKeepingEntry(captor.capture());
TimeKeepingEntry timeKeepingEntry = captor.getValue().blockFirst();
assertThat(timeKeepingEntry.getMonth()).isEqualTo(month);
//and whatever else you want to test
}