Scenario

Scenario entity explained


Overview

The following sections will describe in details how to declare a Scenario, and for this example, we will create a mock for an HTTP POST endpoint that receives a body and creates a new user from it.

Check other examples at the Scenarios examples section.

Also, some fields are present only for certain protocols, please check the Supported Protocols section to see when an specific field is present and required/not required for the protocol.

Key

The Key object is used as an unique identifier to a group of Scenarios, it is composed by a protocol and its specific properties.

  "key": {
    "protocol": "HTTP",
    "method": "POST",
    "pathTemplate": "/users"
  },

Every request received by Copycat will be parsed to a Scenario Key, and this key will be used to find the available Scenarios.

If no Scenario Key matches the given request Copycat will respond, based on the received protocol, with an error message informing that no Scenario was found for that request.

The key field may vary according to the protocol, check protocol specificities at Supported Protocols section.

Default Response

When no declared Scenario matches the received request, the defaultResponse declared for that group of Scenarios will be returned. The defaultResponse is a mandatory field.

  "defaultResponse": {
    "status": "400",
    "headers": { "Content-Type": "application/json" },
    "body": {  "message": "Bad Request" }
  },

Fallback Response

The Fallback Response will be returned if an error occurs while processing the request. This is an optional field designed to customize Copycat's behavior if it could not act as expected.

  "fallbackResponse": {
    "status": "500",
    "headers": { "Content-Type": "application/json" },
    "body": { "message": "Unexpected error creating a new user" }
  },

Global Variables

The globalVariables field is used to store values that will be available to all Scenarios. These variables are evaluated only one time, before the analysis of the declared Scenarios and are useful to store repeated values (specially if they are generated by a time consuming operation, like accessing the database) or values that must be evaluated before the Scenario matching system kicks in.

  "globalVariables":{
    "validations": {
        "invalidAge": "${request.body.age < 18 ? 'Age must be > 18' : null}",
        "invalidType": "${request.body.type != 1 ? 'Invalid user type!' : null}"
    }
  },

Scenarios

All desired Scenarios should be declared as an array on the scenarios field. Copycat will iterate over this array, on the declared order, until it finds a match, and once found, the matching scenario will be used to generate the response. This means that the Scenarios must be on the correct order to work properly. Scenarios with a very generic filter, should probably be placed at the end of the array.

  "scenarios": [
    {
      "name": "Use real system",
      "preConditions": [],
      "request": {
        "headers": {
          "x-real-system": true
        }
      },
      "redirect": {
        "passThrough": {
          "host": "http://real-app/users"
        }
      }
    },
    {
      "name": "Bad request",
      "preConditions": [ "${globalVariables.validations.values().stream().anyMatch(predicate((v)-> {v != null})) }" ],
      "request": {},
      "response": {
        "status": "400",
        "delay": 0,
        "headers": { "Content-Type": "application/json" },
        "body": {
          "message": "${globalVariables.validations.values().stream().filter(predicate((v)-> {v != null})).collect(Collectors.toList())}"
        }
      },
      "commands": []
    },
    {
      "name": "Fail to create user - username not available",
      "preConditions": [
        "${db.getBy('users', {'username': request.body.username}) != null}"
      ],
      "request": {},
      "response": {
        "status": "409",
        "body": {
          "message": "${format.string('username %s is not available',request.body.username)}"
        }
      }
    },
    {
      "name": "Create new user - Success",
      "request": {
        "headers": {"Content-Type": "application/json"},
        "queryParams": {},
        "pathParams": {},
        "body": {
          "name": "#{[a-zA-Z]*}",
          "username": "#{.*}",
          "telephone": "#{[0-9]*}",
          "age": "${request.body.age > 18}",
          "type": "regular"
        }
      },
      "response": {
        "status": "201",
        "delay": 0,
        "headers": {},
        "body": {
          "id": "${idGenerator.seqId('users')}"
        }
      },
      "callback": {
        "type": "HTTP",
        "typeProperties": {
          "headers": {"Content-Type": "application/json"},
          "url": "http://www.my-awesome-app.com/listener/users",
          "method": "POST"
        },
        "payload": {
          "id": "${response.body.id}"
        },
        "delay": 100
      },
      "commands": [
        "${db.save('users', response.body.id, request.body)}"
      ]
    }
  ]

Name

The Scenario's name field should be used to tell what the Scenario is supposed to represent. Think about it as a unit test method name, it should be clear and descriptive, to make easier to find Scenarios, specially on long arrays. This is a mandatory field, and it will not be used directly by Copycat's match logic.

      "name": "Fail to create user - username not available",

Pre Conditions

preConditions is an optional field that contains expressions that should be evaluated to booleans and is the first thing that is checked when Copycat tries to match the received request with a Scenario. All expressions on the array must evaluate to true in order to proceed with the matching process, otherwise the current Scenario will be discarded, and the matching process will follow to check the next Scenario

      "preConditions": [
        "${db.getBy('users', {'username': request.body.username}) != null}"
      ],

Request

The request field is used as a filter to check if the received request matches the Scenario's declared request.

This field accepts Regular expressions, Code expressions and raw values.

      "request": {
        "headers": {"Content-Type": "application/json"},
        "queryParams": {},
        "pathParams": {},
        "body": {
          "name": "#{[a-zA-Z]*}",
          "username": "#{.*}",
          "telephone": "#{[0-9]*}",
          "age": "${request.body.age > 18}",
          "type": "regular"
        }
      },

It may vary according to the protocol, check protocol specificities at Supported Protocols section

Response

If a Scenario matches the received request, this is the field that will be used to generate the response that will be returned by Copycat.

This field accepts Code expressions and raw values.

      "response": {
        "status": "201",
        "delay": 0,
        "headers": {},
        "body": {
          "id": "${idGenerator.seqId('users')}"
        }
      },

The response field may vary according to the protocol, check protocol specificities at Supported Protocols section.

Callback

After the response has been sent, a callback can be scheduled. This optional field helps the developer to mock applications that will trigger some events on a received request.

The scheduled callback does not have to be of the same protocol of the received request, but it is important to notice that the target of the callback must be accessible by Copycat, otherwise the execution will fail.

This field accepts Code expressions and raw values.

      "callback": {
        "type": "HTTP",
        "typeProperties": {
          "headers": {"Content-Type": "application/json"},
          "url": "http://www.my-awesome-app.com/listener/users",
          "method": "POST"
        },
        "payload": {
          "id": "${response.body.id}"
        },
        "delay": 100
      },

Variables

When some value is used many times, it can be replaced by a variable, this is specially useful if the repeated value is generated by a time-consuming operation (like accessing database).

For instance, on the example that is being used so far, a variable could be created for the generated id, as follows:

    "variables": {"userId": "${idGenerator.seqId('users'))}"},

After that, the variable could be accessed this way:

    "body": {
      "id": "${variables.userId}"
    }

The declared variables will be available to use on response, commands and callback.


IMPORTANT: The variables field is very similar to globalVariables but it is available only to the enclosing Scenario and it is evaluated after the matching system.


PassThrough

A PassThrough is used to redirect the received request to the real system, the request and response will be recorded and can be used to create a customized scenario.

      "redirect": {
        "passThrough": {
          "host": "http://real-app/users"
        }
      }

It is important to notice that the Pass Through target must be accessible by Copycat, otherwise the execution will fail.

Commands

It is possible to execute Code expressions through the commands field after a Scenario is elected. This is specially useful to change the storage state based on the elected Scenario (check Data Storage for more details).

      "commands": [
        "${db.save('users', response.body.id, request.body)}"
      ]