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 client 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.

Rack of Hard Disk Representing a File Server

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:

  1. POST:/files – Post a new file to the files resource. Returns the file object with its newly generated fileId.
  2. GET:/files/{fileId} – Get the specified file and its meta data in json
  3. GET:/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 an unique identifier (id), the name, contentType, 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 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 lets 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 idSequence declares the start of the sequence for your file IDs, and the nextId is a function that returns the next id and increments the idSequence.

    * 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 methodIs:

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 filePart variable.

     * def filePart = requestParts['file'][0]

Retrieve the next file identifier:

     * def fileId = nextId()

The filePart.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 request fileId 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 task 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.