Cucumber with Multipart File uploads
Recently I had to test an API contract involving a Multipart File form submission.
The testing framework of choice in this case was Cucumber.
I wanted to create two steps:
- A step to set the file content and metadata in the form data
@And("the Multipart File: {string} with name: {string} and content: {string}")
- A step to submit the form data to the API endpoint
@Then("the Multipart File is submitted to the API")
Multipart Formdata
For some background I had to refresh my memory of what a Multipart form body looks like when it is constructed, for this I went to Mozilla’s excellent developer docs.
Each file uploaded requires:
- A
Content-Dispositionheader with the following fields (see Mozilla Developer Network for more info):formdata;name=<parameter_name>filename=<filename>- e.g
Content-Disposition: formdata; name=myxml; filename=myxml.xml
- A
Content-Typeheader for your file content type e.g.application/jsonorapplication/xml
Cucumber test background
- Our test fixtures have a “context” that is a ThreadLocal declared like so:
public static final ThreadLocal<TestContext> context = ThreadLocal.withInitial(TestContext::new); - This uses the class
TestContextto hold any test fixtures between our@Given,@Whenand@Thensteps so we can share context.- The TestContext is a lombok’d class for holding data with the
@Dataannotation.
- The TestContext is a lombok’d class for holding data with the
- To this I added the
formDatafield in the form of a MultiValueMap which is keyed on string values and holds objects.- This represents the
key=valuepairs that is the main payload of our form data
- This represents the
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.testcontainers.shaded.org.checkerframework.checker.nullness.qual.Nullable;
import java.util.LinkedHashMap;
@Data
@NoArgsConstructor
public class TestContext {
private ResponseEntity<?> response;
public void setResponse(ResponseEntity<?> response) {
this.response = response;
if (this.response.getBody() != null) {
if (this.response.getBody() instanceof LinkedHashMap<?, ?> b) {
this.body = b;
}
}
}
@Nullable
private LinkedHashMap<?, ?> body;
@Nullable
private MultiValueMap<String, Object> formData;
}
Using Spring’s MockMultipartFile
To put this together we used Spring’s MockMultipartFile to hold file information.
The steps to combine this into a request we can use in our Cucumber steps are:
- Retrieve our existing form data from our context, this represents the
bodyof the form we’re going to be submitting. - Construct the
MockMultipartFilewith our filename, parameter name, content-type and fileContent. - Create the headers for our file upload
Content-Dispositionbased on the composition outlined priornameis the parameter name we’re storing the file underfilenameis the original name of the file prior to upload
Content-Typeto describe the content type of our file.
- Finally, we add the form content as a Spring HttpEntity to the form data map under our parameter name (same as our
namein the content disposition header)- Both our headers are added as a HttpHeaders instance
- The file contents are added as the
bodyof the entity by retrieving the bytes from ourMockMultipartFile
- We then put the form data back into our context to be used by our RestTemplate later on.
The full declared step looks as follows:
@And("the Multipart File: {string} with name: {string} and content: {string}")
public void setFormParameter(String parameterName, String filename, String fileContent) throws IOException {
var formData = Optional.ofNullable(context.get().getFormData())
.orElseGet(LinkedMultiValueMap::new);
MockMultipartFile metadataXml = new MockMultipartFile(
filename,
filename, // The original filename
"application/xml", // The content type
fileContent.getBytes() // The content of the file
);
// Create entity and add to body
var headers = new HttpHeaders();
headers.set("Content-Disposition", String.format("form-data; name=%s; filename=%s", parameterName, metadataXml.getOriginalFilename()));
headers.set("Content-Type", metadataXml.getContentType());
formData.set(parameterName, new HttpEntity<>(metadataXml.getBytes(),headers));
context.get().setFormData(formData);
}
Making the request
We then have another step for submitting the data:
MultiValueMap<String, Object> body = Optional.ofNullable(context.get().getFormData())
.orElseGet(LinkedMultiValueMap::new);
var httpEntity = new HttpEntity<>(body, getFileUploadHeaders());
var response = restTemplate.exchange("/saml/profile", HttpMethod.PUT, httpEntity, Object.class);
getFileUploadHeaders()simply sets theContent-Typeheader for the whole request tomultipart/form-data- The RestTemplate is a Spring web client we’re using in our tests to fire off requests.
Notes
It is worth noting when using the MockMultipartFile that MockMultipartFile.getName() is the name of the parameter not the name of the file, use MockMultipartFile.getOriginalFilename() instead.