unit-testingspring-webfluxmultipartform-datamultipartwebtestclient

How to mock FilePart type field of entity using WebTestClient


While trying unit test for controller, i have used MultipartBodyBuilder. Also saw from here.

The MultipartBodyBuilder.part for file gives DefaultPart while the type of the field of the customer entity is FilePart which gives the below:

> POST /customers
> WebTestClient-Request-Id: [1]
> Content-Type: [multipart/form-data;boundary=x8QNWe6JiRUfEdfbvC6KSeF7RgKVGl6ughbQcs-t]
> Accept: [application/json]

1309 bytes of content.

< 400 BAD_REQUEST Bad Request
< Vary: [Origin, Access-Control-Request-Method, Access-Control-Request-Headers]
< Cache-Control: [no-cache, no-store, max-age=0, must-revalidate]
< Pragma: [no-cache]
< Expires: [0]
< X-Content-Type-Options: [nosniff]
< X-Frame-Options: [DENY]
< X-XSS-Protection: [1 ; mode=block]
< Referrer-Policy: [no-referrer]

0 bytes of content (unknown content-type).


java.lang.AssertionError: Status expected:<200 OK> but was:<400 BAD_REQUEST>
Expected :200 OK
Actual   :400 BAD_REQUEST

More deep inside error for customer request:

rg.springframework.web.bind.support.WebExchangeBindException: Validation failed for argument at index 0 in method: public reactor.core.publisher.Mono<org.springframework.http.ResponseEntity<nz.co.jware.domain.customer.responses.CustomerResponse>> nz.co.jware.controllers.CustomerController.create(reactor.core.publisher.Mono<nz.co.jware.domain.customer.requests.CustomerRequest>), with 1 error(s): [Field error in object 'customerRequestMono' on field 'logo': rejected value [name]; codes [typeMismatch.customerRequestMono.logo,typeMismatch.logo,typeMismatch.org.springframework.http.codec.multipart.FilePart,typeMismatch]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [customerRequestMono.logo,logo]; arguments []; default message [logo]]; default message [Failed to convert property value of type 'java.lang.String' to required type 'org.springframework.http.codec.multipart.FilePart' for property 'logo'; nested exception is java.lang.IllegalStateException: Cannot convert value of type 'java.lang.String' to required type 'org.springframework.http.codec.multipart.FilePart' for property 'logo': no matching editors or conversion strategy found]] 
    at org.springframework.web.reactive.result.method.annotation.ModelAttributeMethodArgumentResolver.lambda$null$3(ModelAttributeMethodArgumentResolver.java:134)
    Suppressed: reactor.core.publisher.FluxOnAssembly$OnAssemblyException: 

So far, my classes look like this.

Entity Customer:

public class Customer {
    @Id
    private String id;
    @NotNull
    private String company_id;
    @NotBlank
    @Size(max = 255)
    @UniqueElements
    private String name;
    @Size(max = 255)
    private String client_no;
    @Size(max = 255)
    private String phone_number;
    @Size(max = 255)
    private String logo;
    @NotNull
    @JsonProperty("is_activated")
    private Boolean is_activated;
    @NotNull
    private String created_by;
    @NotNull
    private String updated_by;
    @NotNull
    private LocalDateTime created_at;
    @NotNull
    private LocalDateTime updated_at;
    private List<Location> locations = List.of();
    private List<Charge> charges = List.of();
    private List<String> users = List.of();
}

CustomerRequest:

public class CustomerRequest {
    @NotNull(message = "Company id can not be null.")
    private String company_id;
    @NotBlank(message = "Name can not be blank.")
    @Size(max = 255, message = "Name can not be exceed 255 characters.")
    private String name;
    @Size(max = 255, message = "Client no. can not be exceed 255 characters.")
    private String client_no;
    @Size(max = 15, message = "Phone number can not be exceed 15 digits.")
    private String phone_number;
    private FilePart logo;
    private String logo_path;
    @NotNull(message = "is_activated cannot be null")
    @JsonProperty("is_activated")
    private Boolean is_activated;
    @NotNull(message = "Created_by cannot be null")
    private String created_by;
    @NotNull(message = "Updated_by cannot be null")
    private String updated_by;
}

CustomerController:

@PostMapping(value = "/customers", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = "application/json")
public Mono<ResponseEntity<CustomerResponse>> create(@ModelAttribute @Valid Mono<CustomerRequest> customerRequest) {
    return customerRequest
            .flatMap(customerRequestTemp -> customerUseCasePort.create(customerRequestTemp).map(savedCustomerResponse -> ResponseEntity.ok().body(savedCustomerResponse)))
            .onErrorResume(WebExchangeBindException.class,
                    ex -> Mono.just(ResponseEntity.status(HttpStatus.BAD_REQUEST)
                            .body(customerUseCasePort.getResponseFromWebExchangeBindException(ex))));
}

CustomerControllerTest:

@Test
void createTest(){
    FilePart file = new FilePart() {
        @Override
        public String filename() {
            return "example.jpg";
        }

        @Override
        public Mono<Void> transferTo(Path dest) {
            return Mono.empty();
        }

        @Override
        public String name() {
            return "example";
        }

        @Override
        public HttpHeaders headers() {
            return HttpHeaders.EMPTY;
        }

        @Override
        public Flux<DataBuffer> content() {
            return DataBufferUtils.read(
                    new ByteArrayResource("name".getBytes(StandardCharsets.UTF_8)), new DefaultDataBufferFactory(), 1024);
        }
    };

    CustomerRequest customerRequest = new CustomerRequest();
    customerRequest.setName("example");
    customerRequest.setLogo(file);
    customerRequest.setLogo_path("");

    CustomerResponse customerResponse = new CustomerResponse();
    customerResponse.setName("example");
    customerResponse.setLogo("example.jpg");
    customerResponse.setId("1");

    MultipartBodyBuilder builder = new MultipartBodyBuilder();

    builder.part("company_id", "1")
            .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"company_id\"")
            .contentType(MediaType.TEXT_PLAIN);
    builder.part("name", "example")
            .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"name\"")
            .contentType(MediaType.TEXT_PLAIN);
    builder.part("client_no", "1")
            .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"client_no\"")
            .contentType(MediaType.TEXT_PLAIN);
    builder.part("phone_number", "123456")
            .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"phone_number\"")
            .contentType(MediaType.TEXT_PLAIN);

    builder.part("logo", file);

    builder.part("logo_path", "")
            .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"logo_path\"")
            .contentType(MediaType.TEXT_PLAIN);
    builder.part("is_activated", "1")
            .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"is_activated\"")
            .contentType(MediaType.TEXT_PLAIN);
    builder.part("created_by", "1")
            .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"created_by\"")
            .contentType(MediaType.TEXT_PLAIN);
    builder.part("updated_by", "1")
            .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"updated_by\"")
            .contentType(MediaType.TEXT_PLAIN);

    Mockito.when(customerUseCasePort.create(customerRequest)).thenReturn(Mono.just(customerResponse));

    var test = customerUseCasePort.create(customerRequest).block(); //it will perfect stubbed result.

    webTestClient
            .mutateWith(csrf())
            .post().uri("/customers")
            .contentType(MediaType.MULTIPART_FORM_DATA)
            .accept(MediaType.APPLICATION_JSON)
            .bodyValue(builder.build())
            .exchange()
            .expectStatus()
            .isOk();
}

I checked the stubbing and it returns as expected but result of WebTestClient.post does not return as expected. Thanks in advance for any hint.

So far, my classes look like this.

Entity Customer:

public class Customer {
    @Id
    private String id;
    @NotNull
    private String company_id;
    @NotBlank
    @Size(max = 255)
    @UniqueElements
    private String name;
    @Size(max = 255)
    private String client_no;
    @Size(max = 255)
    private String phone_number;
    @Size(max = 255)
    private String logo;
    @NotNull
    @JsonProperty("is_activated")
    private Boolean is_activated;
    @NotNull
    private String created_by;
    @NotNull
    private String updated_by;
    @NotNull
    private LocalDateTime created_at;
    @NotNull
    private LocalDateTime updated_at;
    private List<Location> locations = List.of();
    private List<Charge> charges = List.of();
    private List<String> users = List.of();
}

CustomerRequest:

public class CustomerRequest {
    @NotNull(message = "Company id can not be null.")
    private String company_id;
    @NotBlank(message = "Name can not be blank.")
    @Size(max = 255, message = "Name can not be exceed 255 characters.")
    private String name;
    @Size(max = 255, message = "Client no. can not be exceed 255 characters.")
    private String client_no;
    @Size(max = 15, message = "Phone number can not be exceed 15 digits.")
    private String phone_number;
    private FilePart logo;
    private String logo_path;
    @NotNull(message = "is_activated cannot be null")
    @JsonProperty("is_activated")
    private Boolean is_activated;
    @NotNull(message = "Created_by cannot be null")
    private String created_by;
    @NotNull(message = "Updated_by cannot be null")
    private String updated_by;
}

CustomerController:

@PostMapping(value = "/customers", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = "application/json")
public Mono<ResponseEntity<CustomerResponse>> create(@ModelAttribute @Valid Mono<CustomerRequest> customerRequest) {
    return customerRequest
            .flatMap(customerRequestTemp -> customerUseCasePort.create(customerRequestTemp).map(savedCustomerResponse -> ResponseEntity.ok().body(savedCustomerResponse)))
            .onErrorResume(WebExchangeBindException.class,
                    ex -> Mono.just(ResponseEntity.status(HttpStatus.BAD_REQUEST)
                            .body(customerUseCasePort.getResponseFromWebExchangeBindException(ex))));
}

CustomerControllerTest:

@Test
void createTest(){
    FilePart file = new FilePart() {
        @Override
        public String filename() {
            return "example.jpg";
        }

        @Override
        public Mono<Void> transferTo(Path dest) {
            return Mono.empty();
        }

        @Override
        public String name() {
            return "example";
        }

        @Override
        public HttpHeaders headers() {
            return HttpHeaders.EMPTY;
        }

        @Override
        public Flux<DataBuffer> content() {
            return DataBufferUtils.read(
                    new ByteArrayResource("name".getBytes(StandardCharsets.UTF_8)), new DefaultDataBufferFactory(), 1024);
        }
    };

    CustomerRequest customerRequest = new CustomerRequest();
    customerRequest.setName("example");
    customerRequest.setLogo(file);
    customerRequest.setLogo_path("");

    CustomerResponse customerResponse = new CustomerResponse();
    customerResponse.setName("example");
    customerResponse.setLogo("example.jpg");
    customerResponse.setId("1");

    MultipartBodyBuilder builder = new MultipartBodyBuilder();

    builder.part("company_id", "1")
            .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"company_id\"")
            .contentType(MediaType.TEXT_PLAIN);
    builder.part("name", "example")
            .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"name\"")
            .contentType(MediaType.TEXT_PLAIN);
    builder.part("client_no", "1")
            .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"client_no\"")
            .contentType(MediaType.TEXT_PLAIN);
    builder.part("phone_number", "123456")
            .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"phone_number\"")
            .contentType(MediaType.TEXT_PLAIN);

    builder.part("logo", file);

    builder.part("logo_path", "")
            .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"logo_path\"")
            .contentType(MediaType.TEXT_PLAIN);
    builder.part("is_activated", "1")
            .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"is_activated\"")
            .contentType(MediaType.TEXT_PLAIN);
    builder.part("created_by", "1")
            .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"created_by\"")
            .contentType(MediaType.TEXT_PLAIN);
    builder.part("updated_by", "1")
            .header(HttpHeaders.CONTENT_DISPOSITION, "form-data; name=\"updated_by\"")
            .contentType(MediaType.TEXT_PLAIN);

    Mockito.when(customerUseCasePort.create(customerRequest)).thenReturn(Mono.just(customerResponse));

    var test = customerUseCasePort.create(customerRequest).block(); //it will perfect stubbed result.

    webTestClient
            .mutateWith(csrf())
            .post().uri("/customers")
            .contentType(MediaType.MULTIPART_FORM_DATA)
            .accept(MediaType.APPLICATION_JSON)
            .bodyValue(builder.build())
            .exchange()
            .expectStatus()
            .isOk();
}

I checked the stubbing and it returns as expected but result of WebTestClient.post does not return as expected. Thanks in advance for any hint.


Solution

  • This error indicates that there was a validation failure for an argument at index 0 in the create method of the CustomerController class. The error message indicates that there is a problem with the logo field of the customerRequestMono object.

    More specifically, the error message says that the value provided for the logo field (which is name in this case) could not be converted to the required type FilePart. The error message also indicates that there are no matching editors or conversion strategies available to convert the provided value to the required type.

    To fix this error, you should ensure that the value provided for the logo field is of the correct type (FilePart in this case).

    To Add a file part, you could do the following as mentioned on JavaDoc of MultipartBodyBuilder. e.g Resource image = new ClassPathResource("image.jpg");

    Hence, your builder.part for the logo file should be:

    builder.part("logo", new ClassPathResource("example.png"));

    Make sure example.png or your file is inside ~/src/test/resources.

    Hope it fixes the issue you're facing.