Mock A File Server With Karate
In this blog post, I will teach you an undocumented Karate Mock feature on how to deal with HTTP-Multipart/Form-Data requests.
I recently faced the problem of mocking a file server with Karate Test Framework at one of my clients and that wasn’t easy.
The File Server API uses HTTP Multipart/Form-Data requests to upload files and I couldn’t find documentation on how to handle Multipart/Form-Data in Karate Mocks.
If you’re new to Karate Mock Check out my previous blog post about Karate mock development.
As always, you can download the code from my Github repository and:
Let’s dive right into it.
1 The File Server API
The File Server has three endpoints:
POST:/files
– Post a new file to thefiles
resource. Returns the file object with its newly generatedfileId
.GET:/files/{fileId}
– Get the specified file and its meta data in jsonGET:/files/{fileId}/content
– Get the binary content of the file
2. Setup Test Case and Karate File Mock Feature
Let’s first create FileServerMock.feature
file with a catch-all rule.
Feature: Mock for File Store Api
Scenario:
* print `catch all rule matched: ${requestMethod}:${requestUrlBase}/${requestUri}`
* print 'With Headers:'
* print requestHeaders
* print 'With Request Parameters'
* print requestParams
* print 'And Request:'
* print request
* def responseStatus = 200
* def response = {status:'catch all'}
And a test FileServerMockTest.
feature
for our catch-all rule.
Feature: Testing File Server Mock
Background:
* def startMockServer = () => karate.start('FileServerMock.feature').port
* def port = callonce startMockServer
* url 'http://localhost:' + port
Scenario: Testing Catch All Rule
Given path "path", "does", "not", "exist"
* param url = "parameter"
* request {request:"content"}
When method put
Then status 200
* match response.status == "catch all"
It’s always a good idea to create your Mock along with a test to get fast feedback.
I turned this into a habit.
3. Test Case: Posting A New File
We are good developers and good developers do Test First – sometimes… at least ;-).
This test case creates a simple multipart/form-data request, posts that request to our mock server, and asserts the response:
Scenario: Post a new file and get it file meta data
Given path 'files'
* def expectedFileContent = "Content of our test file."
* multipart file file = {value: #(expectedFileContent), filename: "test.txt", contentType: 'text/plain' }
When method post
Then status 200
* match response == {id:"#number", name : "test.txt", content: "#string", contentType: "text/plain"}
* def decode = (base64Str) => {return new java.lang.String(java.util.Base64.getDecoder().decode(base64Str))}
* def content = decode(response.content)
* match content == expectedFileContent
We define the path files path, declare a variable with our expected test content, create a multipart request, and post that to our File Server Mock.
We expect a 200
status code and match the response. The response is in JSON, contains a unique identifier (id
), the name
, content type, and the content.
The content
of the file is a Base64 encoded string and that’s a common way to put binary data into a JSON format. We need to decode the content before we can match that against our original content.
I declared the decode function in the background section for that purpose. As you can see, I used the Karates Java-Interop functionality to access the Java Base64 class.
4. Implementation: Handle Multipart/Form-Data Request in Karate Mock
Background:
* def files = {}
* def idSequence = 1
* def nextId = () => { return idSequence++}
* def encode = (byteArray) => {return java.util.Base64.getEncoder().encodeToString(byteArray)}
* def decode = (base64Str) => {return java.util.Base64.getDecoder().decode(base64Str)}
Scenario: pathMatches('files') && methodIs('post')
* match requestParts contains {'file':'#[1]'}
* def filePart = requestParts['file'][0]
* def fileId = nextId()
* def base64EncodedContent = encode(filePart.value);
* files[fileId] = {id: fileId, name: filePart.filename, contentType: filePart.contentType, content: base64EncodedContent}
* def response = files[fileId]
That’s quite a piece of dense code. So let’s go through this line by line:
Background Section
The first line in the Background section defines our files collection. We have to store our files somewhere, right?
* def files = {}
The sequence declares the start of the sequence for your file Id and increments the sequence.
* def idSequence = 1
* def nextId = () => { return idSequence++}
We need the encode
and decode
functions to handle base64 data. Base64 is a binary-to-text encoding that helps us to transfer binary data within JSON.
* def encode = (byteArray) => {return java.util.Base64.getEncoder().encodeToString(byteArray)}
* def decode = (base64Str) => {return java.util.Base64.getDecoder().decode(base64Str)}
Scenario Section
The Scenario matches every HTTP post request on our files
endpoint. We archive that by using pathMatches
and methods
:
Scenario: pathMatches('files') && methodIs('post')
The next two lines are the most important. These two lines were the hard part.
The requestParts
is an undocumented variable and is a map. The keys of the map are the control names of the multipart/form-data request and the values are arrays. Each element of the array represents a part with its own headers and form data.
This line ensures that we have a multipart/form-data request with a so-called control named file
with exactly one part.
* match requestParts contains {'file':'[1]'}
And the next line assigns the part to the file part
variable.
* def filePart = requestParts['file'][0]
Retrieve the next file identifier:
* def fileId = nextId()
The file
Part. value is a java byte array containing binary data. If we want to transfer that data in JSON, we need to encode it. Base64 is a popular way to do that.
* def base64EncodedContent = encode(filePart.value);
Store the new file with the new file id in our files collection:
* files[fileId] = {id: fileId, name: filePart.filename, contentType: filePart.contentType, content: base64EncodedContent}
The nextId function gives us the next file id, and we use the encode function to encode
The binary data stored in filePart.value
to a Base64 text.
This line creates the file object and stores it in our files
collection. We use the fileId
as an identifier:
* files[fileId] = {id: fileId, name: filePart.filename, contentType: filePart.contentType, content: base64EncodedContent}
Define the response:
* def response = files[fileId]
5. Create Scenario for Getting Files
Endpoint to Access File Json Object
This part is straightforward. The fileId
is part of the URL-Path and we have to return the file with the requested file Id from our files collection.
Scenario: pathMatches('files/{fileId}') && methodIs('get')
* def response = files[pathParams.fileId]
PS: I did not consider the 404
case.
Endpoint to Access Binary File Content
Many file servers provide an endpoint to get the binary content of the file.
This Scenario shows how to do that:
Scenario: pathMatches('files/{fileId}/content') && methodIs('get')
* def file = files[pathParams.fileId]
* def response = decode(file.content)
* def responseHeaders = { 'Content-Type': #(file.contentType) }
The important part is: We have to decode the Base64 file content:
* def response = decode(file.content)
And we have to define the content-type:
* def responseHeaders = { 'Content-Type': #(file.contentType) }
That’s it.
Conclusion
It’s very easy to create API mocks with the Karate Test Framework, but some tasks can get tricky.
If you don’t know how to handle a multipart/form-data HTTP-Request for example.
Do you face a particular problem with Karate Mocks or do have a question?
Either way, let me know your thoughts in the comments section below.
github repository the repo link is not working. Kindly share the working link.
Thanks for pointing out Saurabh.
Fixed it!