Service Models

A service model defines behavior of a virtual service. Unlike defining the interface or contract described by service interface, it specifies scenarios of usage of the service by defining sequences of calls with sample data issued on the service interface.

Defining a service model

To make service interface available inside a service model, pass it as a constructor argument as shown in the following service model skeleton code.

import * as sv from "sv-vsl.js";

export class MyServiceModel extends sv.ServiceModel {

    constructor(s: sv.RestServiceInterface) {
        super(s);
        this.service = s;
    }

    @sv.scenario
    myScenario() {
        //...
        // Sequence of calls issued on 'this.service'
        //...
    }
}

When using a custom service interface (not a built-in one like RestServiceInterface), must also import the service interface's class:

import {MyCustomServiceInterface} from "./MyCustomServiceInterface.js";

Your Service Model also needs to inherit from the ServiceModel class. In addition, each service scenario method needs to be annotated with the @sv .scenario decorator.

Tip

Always make sure the imported sv-vsl.js file exists in the respective location to enable auto-completion in your IDE.

Defining service scenarios

A service model can contain one or more service scenarios, each of which describes a message exchange between the service and its caller. The expected message flow is described as a sequence of calls invoked on a service interface using withRequest and withResponse constructs as shown below.

Data for both the requests and responses are present in each service scenario. This allows the virtual service to either simulate the server side, responding to client requests, or simulate the client behavior, sending virtual requests to a real service and validating the received responses.

The following example demonstrates a basic scenario:

@sv.scenario
myScenario() {
    let greetingMessage = sv.svVar("Hello, world!");
    this.service.POST("/hello")
        .withRequest({"requestGreeting": greetingMessage}, sv.JSON)
        .withResponse({"responseGreeting": greetingMessage}, sv.JSON)
            .withHeaders({"Content-Type": "application/json"})
            .withStatusCode(200);
}     

The service scenario above describes a simple message exchange (using the built-in REST Service Interface), in which the virtual service accepts a greeting JSON message via the HTTP POST method.

For example:

{
    "requestGreeting": "...Some greeting message here..."
}

It responds by producing the same greeting message in the output:

{
    "responseGreeting": "...The same greeting message repeated here..."
}

Using SV variables to capture internal state

To capture the internal state of simulated service or subsystem, use a variable. You create variables, referred to as SV variables, using sv.svVar( ...) construct. Their purpose is to capture data relations.

In this example, the greeting message issued with the request overwrites the default value of Hello, world! and the new value, obtained from the request, is sent to the output as a response.

When creating an SV variable, you must always provide a default value. This is required, because when you configure a virtual service to simulate a caller in invocation mode, the SV Lab server needs to know the value to use in the requestGreeting field of the generated request.

SV Variables can have their default values imported from external files.

Note

SV Variables that contain complex values must be defined with all possible default values. For details, see Variable types and default values.

Value generators

To generate a new value, optionally based on values of some existing SV variables, a value generator must be used.

A value generator is a function, optionally taking SV variables as input arguments. It is evaluated at runtime (in contrast with the templating functions). Value generators must be annotated using the @include decorator, which instruct the compiler to include respective generator functions in the resulting model and evaluate them at runtime—not at compile time.

The following is an example of a simple value generator:

@sv.include
makeGreeting(name) {
    return "Hello, " + name + "!";
}

The following example shows an SV variable with an assigned value generator, which takes another SV variable as input:

let yourName = sv.svVar("Karel");
let greeting = sv.svVar("Hello, somebody!")
                 .setJsGenerator(this.makeGreeting, yourName);

Note

For SV variables with complex content storing the result of a value generator function, the default value assigned to the variables must contain all parts of the structure that the value generator may possibly generate (all the JSON fields, even the optional ones). Since the VSL compiler creates a schema for all SV variables at compile time, it needs to know all options of the generated value.

External value generators

Generator functions may be placed in a separate file and imported to the service model source code using the standard import keyword. The imported file must define export of the generator function.

ECMA6 example (JavaScript 6)

The following example shows an ECMA6 generator source code import:

import {priceGenerator} from "VarGeneratorImports.vsinc"
//...
let basePrice = sv.svVar(10);
let price = sv.svVar(1).setJsGenerator(priceGenerator, basePrice);

And the related VarGeneratorImports.vsinc content:

"use strict";

Object.defineProperty(exports, "__esModule", {
    value: true
});
exports.priceGenerator = priceGenerator;
function priceGenerator(basePrice) {
    return basePrice * 1.21;
}

ECMA5 example (JavaScript 5)

The following example shows an ECMA5 generator source code import:

import mm from "VarGeneratorImportsEcma5.vsinc"
//...
let basePrice = sv.svVar(10);
let price = sv.svVar(1).setJsGenerator(mm.priceGenerator, basePrice);

And the related VarGeneratorImports.vsinc content:

var myModule = { 
    priceGenerator: function priceGenerator(basePrice) {
        return basePrice * 1.21;
    }
};
module.exports = myModule;

Note

  • During VSL compilation, in order to distinguish between VSL source files and imported generator files, the imported files must have an extension other than .js. We recommend using the .vsinc extension.
  • In the current SV Lab release, all the source code, including libraries, must be contained in a single source file. The Babel.js compiler can be used to merge libraries of more complex projects.

Copying complex data from requests to responses using value generators

For copying of simple-type values, see the Defining service scenarios.

Note

Currently there is a limitation that prevents 1:1 copying of SV variables with )complex_ content between request and response, without
using a value generator.Internally the SV Lab handles them as different data types.

The following syntax shows how to copy complex values from requests to response leveraging a trivial copyMyRequestValue value generator.

@sv.include
copyMyRequestValue(complexValue) {
    return complexValue; // This will serialize the value to JSON and back.
}

@sv.scenario
myScenario() {
    let requestValue = sv.svVar({"requestGreeting": "Hello, world!"}).
    let responseValue = sv.svVar({"requestGreeting": "Hello, world!"}).
        setJsGenerator(this.copyMyRequestValue, requestValue);
    this.service.POST("/hello")
        .withRequest(requestValue, sv.JSON)
        .withResponse(responseValue, sv.JSON)
            .withHeaders({"Content-Type": "application/json"})
            .withStatusCode(200);
}

Using scenario parameters

SV variables used inside service scenarios can be passed using method parameters, instead of being defined inside a service scenario.

In this way you can:

  • share values captured by SV variables across multiple service scenarios,
  • call the same method multiple times with different parameters.

Parametrized scenario definition

    @sv.scenario
    myScenario(
        requestGreeting = sv.svVar("Hello, world!"), 
        responseGreeting = sv.svVar("Bye, world!")
    ) {
        this.service.POST("/hello")
            .withRequest({"requestGreeting": requestGreeting}, sv.JSON)
            .withResponse({"responseGreeting": responseGreeting}, sv.JSON)
                .withHeaders({"Content-Type": "application/json"})
                .withStatusCode(200);
    }     

In the code above, the myScenario method has two SV variable input parameters: requestGreeting and responseGreeting. For both, a default value is defined. Defining all default values is a best practice.

Calling a parametrized scenario

Tis example shows calling of the same parametrized scenario twice, using different data:

    @sv.scenario
    myComposedScenario() {
        this.myScenario(sv.svVar("Hello!"), sv.svVar("Goodbye!"));
        this.myScenario(sv.svVar("Hi!"), sv.svVar("Bye!"))
    }     

Sharing data between scenarios

This example illustrates the sharing of data between two service scenarios forked to run in parallel in application scenario. For details, see Application models.

    @sv.applicationScenario
    myApplicationScenario() {
        let sharedGreeting = sv.svVar("Hello!")
        sv.forkScenario(() => service1.myScenario(sharedGreeting));
        sv.forkScenario(() => service2.anotherScenario(sharedGreeting));
    }     

SV variable types and default values

The schema for all the used SV variables is generated at compile time by the VSL compiler, based on the default values specified in the SV variable definitions. Therefore, it is essential that you define default values for complex SV variables, containing as complete structure as possible including parts that are not mandatory. The SV Lab server must know the structure variants for all variables it may encounter during future model simulations, at compile time.

In VSL, the following literal types may be used as default values of SV variables:

  • Simple type: number, string, boolean
  • JSON
    sv.svVar({
      "data": {
        "rain_dif": {
          "max": 10.0,
          "values": [0.0, 0.0036],
          "unit": {"cz": "mm/h", "gb": "mm/h"},
          "gridStep": 1,
          "min": 0
        }
      }
    });
  • XML
    sv.svVar(
      <data>
        <rain_dif max="10.0" min="0" gridStep="1">
          <values>
            <value>0.0</value>
            <value>0.0036</value>
          </values>
          <units>
            <unit country="cz">mm/h</unit>
            <unit country="gb">mm/h</unit>
          </units>
        </rain_dif>
      </data>);
  • Binary
    • When using external resources, raw bytes represent the content.
    • In the VSL source code, binary literals are base-64 encoded strings.
      let binaryVariable = svVar("SGVsbG8gYmluYXJ5");
      this.service.binaryOperation()
          .withRequest(binaryVariable, sv.Binary)
          .withResponse()
          .withStatus(200);

Note

According to ECMA-6 standard, the number type corresponds to a IEEE 754-2008 standard 64-bit double precision floating point number with 53-bit mantissa and 11-bit exponent. As a result, integers larger than 2^53 = 9,007,199,254,740,992 (more than 15 decimal digits) suffer from rounding, for example the number 123456789012345678 gets rounded to 123456789012345680. If you ever needed to work with messages with larger integer values, you could leverage a workaround using Java BigDecimal type:

var BigInteger = Java.type("java.math.BigDecimal");
sv.svVar({
  "data": {
    "car_id": new BigInteger("123456789012345678"),
    ...
  }
});     

Final SV variables

Final SV variables help simulate scenarios where a sequence of calls relates to the same ID, usually a session or object ID. Make an SV Variable final when its value will not change over the scenario run. Alternatively, make it final if you need to explicitly enforce the value, to ensure that it remains constant. For example:

  • to ensure that the session ID stays the same throughout the simulation,
  • to only allow the scenario to use a single sample object, with a single object identifier.

The following code shows how to set an SV Variable to final:

let id = sv.svVar("abc123").setFinal();

Making an SV variable final keeps its value unchanged over its lifetime. A final SV Variable can only be written to once. After it receives an initial value from input message, or the default value if it was initially used in an output message, it can never be modified.

Delaying service scenario calls

The example above calling a parametrized scenario used two subsequent calls to myScenario issued from myComposedScenario.

@sv.scenario
myComposedScenario() {
    this.myScenario(sv.svVar("Hello!"), sv.svVar("Goodbye!"));
    this.myScenario(sv.svVar("Hi!"), sv.svVar("Bye!"))
}     

In order to specify a delay, wrap the scenario call in the following construct:

sv.callScenario(() => ...);

and only then we can add .withDelay(...) where the argument is a delay in milliseconds:

sv.callScenario(() => ...).withDelay(...);

The following example shows the same myComposedScenario but defined with a 5 seconds delay between the two scenario calls.

@sv.scenario
myComposedScenario() {
    sv.callScenario(() => this.myScenario(sv.svVar("Hello!"), sv.svVar("Goodbye!")));
    sv.callScenario(() => this.myScenario(sv.svVar("Hi!"), sv.svVar("Bye!")))
        .withDelay(5000);
}     

The .withDelay(...) construct can also be applied to .withRequest(...) and .withResponse(...) calls as shown below. The specified delay is always interpreted relative to the previous call, or the scenario beginning (if there was no previous call within the scenario):

this.service.POST("/article")
    .withRequest({"action": "create", "id": id }, sv.JSON)
    .withResponse({"result": "ok"}, sv.JSON)
        .withHeaders({"Content-Type": "application/json"})
        .withStatusCode(200)
        .withDelay(5000);

Message correlation

The simpliest form of message correlation is a response generated immediately after receiving a request, illustrated in the following example:

@sv.scenario
simpleRequestResponse() {
    this.service.GET("/hello")
        .withRequest()
        .withResponse({"greeting": "Hello, world!"}, sv.JSON)
        .withStatusCode(200);
}     

In some cases, if you need to process the request and response separately, it may be convenient to split them. For this, you can create a correlation variable (different from an SV variable).

Below is the same scenario correlated using a correlation variable:

@sv.scenario
simpleRequestResponse() {
    let corr = this.service.GET("/hello").withRequest();
    corr.withResponse({"greeting": "Hello, world!"}, sv.JSON)
        .withStatusCode(200);
}     

Correlation variables are useful when the requests and responses are interleaved and you expect a delay between message calls, as shown below:

@sv.scenario
simpleRequestResponse() {
    let helloCorrelation = this.service.GET("/hello").withRequest();
    let byeCorrelation = this.service.GET("/bye").withRequest();
    helloCorrelation.withResponse({"greeting": "Hello, world!"}, sv.JSON)
        .withStatusCode(200);
    byeCorrelation.withResponse({"greeting": "Good bye!"}, sv.JSON)
        .withStatusCode(200);
}     

In the example above, requests are expected to arrive before the responses are generated.

Calling another service scenario

One service scenario can call another. However, since it has only visibility of scenarios of the same class, it can only call a service scenario of the same virtual service.

  • Sequential call: Below is a scenario with a simple request and response message, followed by a scenario call:

    @sv.scenario
    myScenario() {
        this.service.GET("/status").withRequest()
            .withResponse({"status":"OK"}, sv.JSON)
            .withStatusCode(200);
        this.simpleRequestResponse();
    }     
  • Sequential delayed call: The following example shows the same scenario as above, but with a 5 second delay, running sequentially as a part of myScenario:

    @sv.scenario
    myScenario() {
        this.service.GET("/status").withRequest()
            .withResponse({"status":"OK"}, sv.JSON)
            .withStatusCode(200);
        this.callScenario(() => this.simpleRequestResponse())
            .withDelay(5000);
    }     
  • Parallel call: The following example shows the same scenario as above, but with a 5 second delay in running the called scenario in parallel, independently on myScenario, so that it may finish before the simpleRequestResponse scenario:

    @sv.scenario
    myScenario() {
        this.service.GET("/status").withRequest()
            .withResponse({"status":"OK"}, sv.JSON).withStatusCode(200);
        this.forkScenario(() => this.simpleRequestResponse())
            .withDelay(5000);
    }     

Scenario synchronization

In certain environments, you may have multiple service scenarios running in parallel, that must wait for each other at a given point before proceeding. You can achieve this with VSL lock and unlock functions.

The lock function takes a single argument, specifying the name of the lock. When you call the lock function in a service scenario, it suspends its execution until another scenario calls the unlock function with the same lock name.

In the following example, lockedScenario suspends its execution using myLock before generating a "GoodBye!" response. It only resumes its execution after calling the unlockingScenario, which internally unlocks the myLock lock.

Using this approach, when we run the following scenarios in parallel, we can be sure the "Hello, world!" response will always be generated before the "GoodBye!" response.

@sv.scenario
lockedScenario() {
    let corr = this.service.GET("/bye").withRequest();
    this.lock("myLock")
    corr.withResponse({"greeting": "GoodBye!"}, sv.JSON)
        .withStatusCode(200);
}

@sv.scenario
unlockingScenario() {
    this.service.GET("/hello").withRequest()
        .withResponse({"greeting": "Hello, world!"}, sv.JSON)
        .withStatusCode(200);
    this.unlock("myLock")
}     

Templating functions

Templating functions can reduce duplicity in the VSL code.

The following section shows how to create a virtual service that returns numbers 1 through 5 in five subsequent calls, without templating functions:

@sv.scenario
myServiceScenario() {
    this.service.GET("/counter")
        .withRequest()
        .withResponse({"value":"1"}, sv.JSON)
        .withStatusCode(200);
    this.service.GET("/counter")
        .withRequest()
        .withResponse({"value":"2"}, sv.JSON)
        .withStatusCode(200);
    this.service.GET("/counter")
        .withRequest()
        .withResponse({"value":"3"}, sv.JSON)
        .withStatusCode(200);
    this.service.GET("/counter")
        .withRequest()
        .withResponse({"value":"4"}, sv.JSON)
        .withStatusCode(200);
    this.service.GET("/counter")
        .withRequest()
        .withResponse({"value":"5"}, sv.JSON)
        .withStatusCode(200);
}

For a few numbers, this approach may be acceptable. However, for multiple calls, duplicating the code is not a practical solution. You can simplify your code significantly using templating functions, which are basically any JavaScript functions created to reduce duplicity in VSL source code.

Tip

The VSL compiler interprets JavaScript constructs during the compilation of VSL source code. This can be utilized to create templating functions that generate scenarios and scenario data during VSL compile time.

The following example uses a templating function, createCall, to reduce code duplicity in the VSL source code above:

@sv.scenario
myServiceScenario() {
    for (i = 1; i <= 5; i++) {
        createCall(i);
    }
}

createCall(val) {
    this.service.GET("/counter")
        .withRequest()
        .withResponse({"value":val}, sv.JSON)
        .withStatusCode(200);
}

Loading SV variables from external files

Large data structures, or binary data in service scenarios could hinder VSL source code readability. To prevent this, load the default values of SV variables from external files using the importExternal function.

The following example loads the default values of the request and response variables from external data files:

@sv.scenario
myScenario() {
    let request = this.importExternal("request.xml");
    let response = this.importExternal("response.json");
    this.service.POST("/hello")
        .withRequest(request, sv.XML)
        .withResponse(response, sv.JSON)
            .withStatusCode(200);
}