Spring WebFlux 中 reactor.core.publisher.MonoWhen$WhenCoordinator 中 FilePart 上的 NPE 模拟

NPE on FilePart mock in Spring WebFlux at reactor.core.publisher.MonoWhen$WhenCoordinator

对于我遇到的以下问题的任何提示,我将不胜感激。在 Spring Reactive WebFlux 应用程序中对多部分文件上传服务方法进行单元测试时,我得到 reactor.core.publisher.MonoWhen$WhenCoordinator 的 NPE 如下

java.lang.NullPointerException
    at reactor.core.publisher.MonoWhen$WhenCoordinator.subscribe(MonoWhen.java:149) 

下面也列出了完整的日志。

测试:

@RunWith(SpringRunner.class)
@SpringBootTest
public class FileServiceTest2 {


@MockBean
private UploadedImageRepository uploadedImageRepository;

...

       @Test
        public void assembleImageTest() {
            UploadedImage ui1 = new UploadedImage("1", "ui1.png");
            UploadedImage ui2 = new UploadedImage("2", "ui2.png");

            FilePart filePart1 = mock(FilePart.class);
            FilePart filePart2 = mock(FilePart.class);

            given(this.uploadedImageRepository.save(ui1))
                    .willReturn(Mono.just(ui1));
            given(this.uploadedImageRepository.save(ui2))
                    .willReturn(Mono.just(ui2));
            given(this.uploadedImageRepository.findAll())
                    .willReturn(Flux.just(ui1, ui2));

            given(filePart1.filename())
                    .willReturn(ui1.getImageName());
            given(filePart1.transferTo(any()))
                    .willReturn(Mono.empty());
            given(filePart2.filename())
                    .willReturn(ui2.getImageName());
            given(filePart2.transferTo(any()))
                    .willReturn(Mono.empty());

            Flux<FilePart> files = Flux.just(filePart1, filePart2);

            StepVerifier.create(this.uploadService.createFile(files))
                    .verifyComplete();
        }

正在测试中:

@Service
public class UploadService {

Mono<Void> createFile(Flux<FilePart> fileParts) {
        return fileParts.flatMap(part -> {
            Mono<UploadedImage> savedToDBImage = this.uploadedImageRepository.save(
                    new UploadedImage(UUID.randomUUID().toString(), part.filename()))
                    .log("createFile-fileSavedToDB"); // NPE!

            Mono<Void> copiedFile = Mono.just(Paths.get(UPLOAD_URL, part.filename()).toFile())
                    .log("createFile-pathAssembled")
                    .doOnNext(destinationFile -> {
                        try {
                            destinationFile.createNewFile();
                        } catch (IOException e) {
                            throw new RuntimeException(e);
                        }
                    })
                    .log("createFile-fileAssembled")
                    .flatMap(part::transferTo)
                    .log("createFile-fileCopied");

            return Mono.when(savedToDBImage, copiedFile)
                    .log("createFile-monoWhen");
        })
                .log("createFile-flatMap")
                .then()
                .log("createFile-done");
    }

UploadedImage class(龙目岛):

@Data
@RequiredArgsConstructor
@NoArgsConstructor
public class UploadedImage {

        @NonNull private String id;
        @NonNull private String imageName;

}

SpringData 反应存储库:

@Repository
public interface UploadedImageRepository extends ReactiveCrudRepository<UploadedImage, String> {

}

日志如下:

java.lang.NullPointerException
    at reactor.core.publisher.MonoWhen$WhenCoordinator.subscribe(MonoWhen.java:149)
    at reactor.core.publisher.MonoWhen.subscribe(MonoWhen.java:99)
    at reactor.core.publisher.MonoOnAssembly.subscribe(MonoOnAssembly.java:76)
    at reactor.core.publisher.MonoLogFuseable.subscribe(MonoLogFuseable.java:53)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3080)
    at reactor.core.publisher.FluxFlatMap$FlatMapMain.onNext(FluxFlatMap.java:372)
    at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onNext(FluxPeekFuseable.java:198)
    at reactor.core.publisher.FluxArray$ArraySubscription.slowPath(FluxArray.java:118)
    at reactor.core.publisher.FluxArray$ArraySubscription.request(FluxArray.java:91)
    at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.request(FluxPeekFuseable.java:138)
    at reactor.core.publisher.FluxFlatMap$FlatMapMain.onSubscribe(FluxFlatMap.java:332)
    at reactor.core.publisher.FluxPeekFuseable$PeekFuseableSubscriber.onSubscribe(FluxPeekFuseable.java:172)
    at reactor.core.publisher.FluxArray.subscribe(FluxArray.java:53)
    at reactor.core.publisher.FluxArray.subscribe(FluxArray.java:59)
    at reactor.core.publisher.FluxLogFuseable.subscribe(FluxLogFuseable.java:53)
    at reactor.core.publisher.FluxFlatMap.subscribe(FluxFlatMap.java:97)
    at reactor.core.publisher.FluxLog.subscribe(FluxLog.java:50)
    at reactor.core.publisher.MonoIgnoreElements.subscribe(MonoIgnoreElements.java:37)
    at reactor.core.publisher.MonoLog.subscribe(MonoLog.java:51)
    at reactor.core.publisher.Mono.subscribe(Mono.java:3080)
    at reactor.test.DefaultStepVerifierBuilder$DefaultStepVerifier.verify(DefaultStepVerifierBuilder.java:728)
    at reactor.test.DefaultStepVerifierBuilder$DefaultStepVerifier.verify(DefaultStepVerifierBuilder.java:700)
    at reactor.test.DefaultStepVerifierBuilder.verifyComplete(DefaultStepVerifierBuilder.java:566)
    at pb.sl.UploadService.createFile(FileServiceTest2.java:112)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.base/java.lang.reflect.Method.invoke(Method.java:564)
    at org.junit.runners.model.FrameworkMethod.runReflectiveCall(FrameworkMethod.java:50)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:73)
    at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:83)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:26)
    at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75)
    at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86)
    at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:290)
    at org.junit.runners.ParentRunner.schedule(ParentRunner.java:71)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288)
    at org.junit.runners.ParentRunner.access[=17=]0(ParentRunner.java:58)
    at org.junit.runners.ParentRunner.evaluate(ParentRunner.java:268)
    at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61)
    at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:363)
    at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68)
    at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70)

仔细观察,我认为您没有按预期模拟您的存储库,并且在您的测试中存储库调用 returns null。

您使用的不是 Mockito 参数匹配器,而是参数的具体实例。稍后在您的服务实现中,调用此方法,但使用不同的实例。我的猜测是 Mockito 正在使用 equals 来检查给定的是否匹配那个模拟调用 - 也许你的 UploadedImage 在这方面可以改进?

given(this.uploadedImageRepository.save(ui1)) 应该是:

given(this.uploadedImageRepository.save(any()))
.willAnswer(invocation -> Mono.just(invocation.getArgument(0)))