Article
· 12 hr ago 11m read

Customizing API Test Code Generation with Mustache Templates

The previous article introduced IrisOASTestGen, a tool designed to generate REST API test code for InterSystems IRIS based on OpenAPI 2.0 specifications. It demonstrated how to scaffold test cases using the default templates bundled with OpenAPI Generator.

This follow-up focuses on the next natural step: customizing the generated test code.
By extending the code generation logic with Mustache templates, it becomes possible to express richer semantics, implement CRUD-aware tests, and create more meaningful test suites.

The example in this article will modify IrisOASTestGen to generate tests for the createPerson operation, including iterating through all expected responses. This will showcase how custom fields in the OpenAPI specification can drive conditional rendering inside Mustache templates.


OpenAPI Extensions for CRUD Operations

To enable CRUD-aware code generation, the OpenAPI specification can include custom fields inside vendorExtensions. These custom fields act as flags that the Mustache template can detect.

Below is the relevant snippet for the createPerson operation:

"post": {
  "tags": [ "Person" ],
  "x-crud-operation": "",
  "x-crud-operation-create": "",
  "summary": "Create a new Person record",
  "operationId": "createPerson",
  ...
  "responses": {
    "201": {
      "description": "Person created successfully",
      "schema": { "$ref": "#/definitions/Person" },
      "headers": {
        "Location": {
          "description": "The full URI of the newly created resource",
          "type": "string"
        }
      }
    },
    "400": {
      "description": "Invalid input",
      "schema": { "$ref": "#/definitions/Error" }
    }
  }
}

Two custom fields stand out:

  • x-crud-operation — Marks the operation as a CRUD action.
  • x-crud-operation-create — Specifies that the operation corresponds to the Create step of CRUD.

These custom fields will be used by the Mustache template to decide which code blocks should be generated for this particular operation.


How IrisOASTestGen Uses Mustache Templates

IrisOASTestGen relies on OpenAPI Generator under the hood, meaning that all generated files are ultimately produced through Mustache templates.

Three template files participate in code generation:

  • api.mustache — Produces the test code for API operations
  • model.mustache — Produces test model classes
  • HttpUtils.mustache — Produces a lightweight HTTP utility class

For the objective of this article, only api.mustache will be modified, because the goal is to customize operation-level test generation.
To keep things concise, only the createPerson operation will be fully implemented in the example.


Understanding the api.mustache Template

Below is the full Mustache template used to generate test cases.
Several regions inside this file respond to the custom vendor extension fields shown earlier.

Import {{modelPackage}}

Class {{apiPackage}}.Tests{{classname}} Extends %UnitTest.TestCase
{

/// The base URL of the API
Parameter BasePath = "{{host}}{{basePath}}";

/// Preparation method for the whole test
Method OnBeforeAllTests() As %Status
{
    Return $$$OK
}

/// Cleanup method for the whole test
Method OnAfterAllTests() As %Status
{
    Return $$$OK
}

/// Preparation method for each test
Method OnBeforeOneTests(testname As %String) As %Status
{
    Return $$$OK
}

/// Cleanup method for each test
Method OnAfterOneTest() As %Status
{
    Do ##Class(dc.musketeers.irisOasTestGenDemo.personApi.Person).%KillExtent()
    Return $$$OK
}

{{#operations}}
{{#operation}}
{{#vendorExtensions.x-crud-operation}}
{{#vendorExtensions.x-crud-operation-create}}
{{#responses}}
/// Test for {{operationId}} with response code {{code}}
Method Test{{#lambda.pascalcase}}{{operationId}}{{/lambda.pascalcase}}ResponseCode{{code}}()
{
    {{#bodyParams}}
    Set resource = ##Class({{dataType}}).%New()
    #; // Set here the resource properties to test the API
    #; Set resource.property = "some value"
    #; ...
    {{/bodyParams}}

    // Execute the operation
    Set httpResponse = ..{{#lambda.pascalcase}}{{operationId}}{{/lambda.pascalcase}}(
        resource
    )

    // Parse the JSON Response
    Set responseString = httpResponse.Data.Read()
    Set response = ##Class({{baseType}}).%New()
    Set sc = response.%JSONImport(responseString)

    // Response Body Type Check
    Do $$$AssertStatusOK(sc, "{{#lambda.pascalcase}}{{operationId}}{{/lambda.pascalcase}}: Response should be a valid {{dataType}} object")

    // Status Code Check
    Do $$$AssertEquals(httpResponse.StatusCode, {{code}}, "{{#lambda.pascalcase}}{{operationId}}{{/lambda.pascalcase}}: Should return {{code}}")
}

{{/responses}}
{{/vendorExtensions.x-crud-operation-create}}
{{/vendorExtensions.x-crud-operation}}
{{^vendorExtensions.x-crud-operation}}
/// Test for {{operationId}} (no custom spec property found, so renders only the scaffold)
Method Test{{#lambda.pascalcase}}{{operationId}}{{/lambda.pascalcase}}()
{
    // TODO:
    {{#allParams}}
    // Set p{{#lambda.pascalcase}}{{paramName}}{{/lambda.pascalcase}} = ""
    {{/allParams}}
    // Set httpResponse = ..{{#lambda.pascalcase}}{{operationId}}{{/lambda.pascalcase}}(
        {{#allParams}}
    //     p{{#lambda.pascalcase}}{{paramName}}{{/lambda.pascalcase}}{{^-last}},{{/-last}}
        {{/allParams}}
    // )
}
{{/vendorExtensions.x-crud-operation}}

{{/operation}}
{{/operations}}
/// Helper method to create parameter objects expected by SendRequest
Method MakeParamObject(pName As %String, pValue) As %RegisteredObject
{
    Set paramObj = ##class(%RegisteredObject).%New()
    Set paramObj.Name = pName
    Set paramObj.Value = pValue
    Return paramObj
}

{{#operations}}
{{#operation}}
/// {{summary}}
/// OperationId: {{operationId}}
Method {{#lambda.pascalcase}}{{operationId}}{{/lambda.pascalcase}}({{#allParams}}p{{#lambda.pascalcase}}{{paramName}}{{/lambda.pascalcase}} As {{dataType}}{{^-last}},{{/-last}}{{/allParams}}) As %Net.HttpResponse
{
    Set path = "{{path}}"
    Set queryParams = ""
    Set bodyStream = ""
    Set headers = ##class(%ListOfDataTypes).%New()
    Set formParams = ##class(%ListOfDataTypes).%New()
    Set multipartParams = ##class(%ListOfDataTypes).%New()

    {{#pathParams}}
    Set path = $REPLACE(path, "{{=<% %>=}}{<%paramName%>}<%={{ }}=%>", p{{#lambda.pascalcase}}{{paramName}}{{/lambda.pascalcase}})
    {{/pathParams}}
    {{#queryParams}}

    Set queryParams = queryParams _ "&" _ "{{name}}=" _ p{{#lambda.pascalcase}}{{paramName}}{{/lambda.pascalcase}}
    If $EXTRACT(queryParams) = "&" Set queryParams = "?" _ $EXTRACT(queryParams, 2, *)
    {{/queryParams}}

    {{#headerParams}}

    // Header parameter: {{name}}
    Do headers.Insert(..MakeParamObject("{{name}}", p{{#lambda.pascalcase}}{{paramName}}{{/lambda.pascalcase}}))
    {{/headerParams}}

    {{#formParams}}
        {{#isFile}}
        // Multipart/form-data file parameter: {{name}}
        Do multipartParams.Insert(..MakeParamObject("{{name}}", p{{#lambda.pascalcase}}{{paramName}}{{/lambda.pascalcase}}))
        {{/isFile}}
        {{^isFile}}
        // Form parameter (x-www-form-urlencoded): {{name}}
        Do formParams.Insert(..MakeParamObject("{{name}}", p{{#lambda.pascalcase}}{{paramName}}{{/lambda.pascalcase}}))
        {{/isFile}}
    {{/formParams}}

    {{#bodyParam}}

    // Handle body
    $$$ThrowOnError(p{{#lambda.pascalcase}}{{paramName}}{{/lambda.pascalcase}}.%JSONExportToStream(.bodyStream))
    {{/bodyParam}}

    Set request = ##class({{x-musketeers-package-name}}.utils.HttpUtils).%New()
    Set request.BasePath = ..#BasePath
    Set request.HttpRequest.Https = 1
    Set httpResponse = request.SendRequest("{{httpMethod}}", path, queryParams, bodyStream, headers, formParams, multipartParams)
    Return httpResponse
}

{{/operation}}
{{/operations}}
}

Significant sections inside this template include:

CRUD-specific conditional block

{{#vendorExtensions.x-crud-operation}}

This block is activated only when the OpenAPI spec marks the operation as CRUD-aware.

CREATE-specific conditional block

{{#vendorExtensions.x-crud-operation-create}}

This is triggered only for CRUD “Create” operations.

Fallback block when no CRUD extension is present

{{^vendorExtensions.x-crud-operation}}

This generates only the scaffold, avoiding test logic.

Together, these allow highly flexible generation logic tailored to each operation.


Iterating Over Expected Responses

One of the strengths of OpenAPI Generator is the ability to iterate over the structure of the entire OpenAPI spec using Mustache loops.

For example, in this portion of the template:

{{#responses}}
/// Test for {{operationId}} with response code {{code}}
Method Test{{#lambda.pascalcase}}{{operationId}}{{/lambda.pascalcase}}ResponseCode{{code}}()
{
    ...
}
{{/responses}}

The template loops over every response code defined for the operation.
For createPerson, this means generating two complete test methods:

  • TestCreatePersonResponseCode201()
  • TestCreatePersonResponseCode400()

The generated tests correctly instantiate the expected response types (Person or Error) depending on the response code, import the JSON response, assert the type, and check the HTTP status code.

Here is the result produced by IrisOASTestGen when using the custom template:

Import dc.musketeers.irisOasTestGenDemo.personApi.tests.model

Class dc.musketeers.irisOasTestGenDemo.personApi.tests.api.TestsPersonApi Extends %UnitTest.TestCase
{

/// The base URL of the API
Parameter BasePath = "http://localhost:52773/dc/musketeers/irisOasTestGenDemo/personApi";

/// Preparation method for the whole test
Method OnBeforeAllTests() As %Status
{
    Return $$$OK
}

/// Cleanup method for the whole test
Method OnAfterAllTests() As %Status
{
    Return $$$OK
}

/// Preparation method for each test
Method OnBeforeOneTests(testname As %String) As %Status
{
    Return $$$OK
}

/// Cleanup method for each test
Method OnAfterOneTest() As %Status
{
    Do ##Class(dc.musketeers.irisOasTestGenDemo.personApi.Person).%KillExtent()
    Return $$$OK
}

/// Test for createPerson with response code 201
Method TestCreatePersonResponseCode201()
{
    Set resource = ##Class(dc.musketeers.irisOasTestGenDemo.personApi.tests.model.Person).%New()
    #; // Set here the resource properties to test the API
    #; Set resource.property = "some value"
    #; ...

    // Execute the operation
    Set httpResponse = ..CreatePerson(
        resource
    )

    // Parse the JSON Response
    Set responseString = httpResponse.Data.Read()
    Set response = ##Class(Person).%New()
    Set sc = response.%JSONImport(responseString)

    // Response Body Type Check
    Do $$$AssertStatusOK(sc, "CreatePerson: Response should be a valid Person object")

    // Status Code Check
    Do $$$AssertEquals(httpResponse.StatusCode, 201, "CreatePerson: Should return 201")
}

/// Test for createPerson with response code 400
Method TestCreatePersonResponseCode400()
{
    Set resource = ##Class(dc.musketeers.irisOasTestGenDemo.personApi.tests.model.Person).%New()
    #; // Set here the resource properties to test the API
    #; Set resource.property = "some value"
    #; ...

    // Execute the operation
    Set httpResponse = ..CreatePerson(
        resource
    )

    // Parse the JSON Response
    Set responseString = httpResponse.Data.Read()
    Set response = ##Class(Error).%New()
    Set sc = response.%JSONImport(responseString)

    // Response Body Type Check
    Do $$$AssertStatusOK(sc, "CreatePerson: Response should be a valid Error object")

    // Status Code Check
    Do $$$AssertEquals(httpResponse.StatusCode, 400, "CreatePerson: Should return 400")
}


/// Test for deletePerson (no custom spec property found, so renders only the scaffold)
Method TestDeletePerson()
{
    // TODO:
    // Set pId = ""
    // Set httpResponse = ..DeletePerson(
    //     pId
    // )
}
...
}

Note that a third block is produced for operations without CRUD metadata - TestDeletePerson(), generating only a scaffold.

This clearly illustrates the effect of the conditional Mustache blocks.


Running IrisOASTestGen Using Custom Templates

To generate test code using a custom template directory, use the following ObjectScript code:

Set openapiFile = "/home/irisowner/dev/assets/person-api.json"
Set outputDir = "/home/irisowner/dev/tests"
Set packageName = "dc.musketeers.irisOasTestGenDemo.personApi.tests"
Set template = "/home/irisowner/dev/assets/mustache"

##class(dc.musketeers.irisOasTestGen.Main).BuildAndDeploy(
    openapiFile,
    outputDir,
    packageName,
    template
)

This command works exactly like the default generation, with the addition that a custom template directory is now supplied.


Automating the Process

To streamline template development, a small shell script can be used:

# from the project repository root:
cd java
sh build-demo-person-api.sh

This script:

  1. Invokes OpenAPI Generator with the custom template
  2. Builds a new JAR
  3. Replaces the existing JAR
  4. Executes the test code generation

This greatly accelerates experimentation and template iteration.


Debugging Mustache Templates and Inspecting the Model

One of the challenges when customizing OpenAPI Generator templates is understanding the structure of the template model — the JSON structure provided to Mustache.

Documentation for this model is limited, but IrisOASTestGen supports model debugging.

Use the following snippet to enable debug output:

Set openapiFile = "/home/irisowner/dev/assets/person-api.json"
Set outputDir = "/home/irisowner/dev/tests"
Set packageName = "dc.musketeers.irisOasTestGenDemo.personApi.tests"
Set template = "/home/irisowner/dev/assets/mustache"
Set debug = 1

##class(dc.musketeers.irisOasTestGen.Main).BuildAndDeploy(
    openapiFile,
    outputDir,
    packageName,
    template,
    debug
)

This mode prints the entire model structure, allowing exploration of fields such as:

  • all parameters
  • operations
  • responses
  • vendor extensions
  • types, imports, and filenames

For further details on debugging Mustache templates, refer to:
https://openapi-generator.tech/docs/debugging/#templates


Conclusion

Across this two-part series, we examined how IrisOASTestGen can be extended and adapted to support CRUD-oriented APIs. The first article introduced the idea of enriching OpenAPI documents with additional metadata through custom extensions, making it easier to express common CRUD semantics in a structured way. This second article showed how those extensions can drive code generation using Mustache templates, illustrating the process through a simplified example involving the createPerson operation.

Together, these articles outline a practical workflow: define meaningful CRUD hints in the OpenAPI specification, interpret them during generation, and incorporate them into template logic. Developers who rely on contract-based tooling may find this approach useful when trying to produce more consistent tests, demos, or scaffolding from their API specifications. The examples shown here are intentionally minimal, but they provide a pattern that can be expanded to support more complex operations and broader test generation strategies.

Discussion (0)1
Log in or sign up to continue