Skip to content

A Groovy-based DSL for building and running JMeter test plans from command line and more.

License

Notifications You must be signed in to change notification settings

smicyk/groovy-jmeter

Repository files navigation

Groovy JMeter DSL

The Groovy-JMeter project is simple DSL to write JMeter test plans.

Build & Test

It has the following features:

  • keep JMeter test files (*.jmx) as a code (Groovy scripts)
  • run scripts as standalone scripts, JUnit tests or Gradle tasks
  • support of basic JMeter elements like controllers, groups, extractors and assertions
  • support of HTTP, JSR223 and JDBC JMeter samplers
  • support of JMeter listeners (includes a listener with backed systems like influxdb)
  • add modularization of the script with insert keyword (can insert part of the other test script)

Current version uses Apache JMeter 5.6.3 as runtime engine.

Note, that you don't have to download any components of JMeter to run the scripts, all necessary components are initialized at startup.

Checkout the project wiki for quick reference of all keywords and properties

Check Component Status page for supported JMeter features

Prerequisites

Before start, you should have:

Or you can use the Docker approach (check the section below):

How to start

Starting from version 0.11.0, all artifacts are available through maven repository. If you want the latest changes, go to Building from source.

First steps

To run your test script, it is enough to type in your favorite editor the following lines:

@GrabConfig(systemClassLoader=true)
@Grab('net.simonix.scripts:groovy-jmeter')

@groovy.transform.BaseScript net.simonix.dsl.jmeter.DefaultTestScript script

start {
    plan {
        group {
            http 'GET http://www.example.com'
        }

        // optional element, shows execution progress
        summary(file: 'log.jtl')
    }
}

This test plan will start one user and make call to http://www.example.com

Please remember that first execution of the script can take some time as Ivy must download all dependency to local cache

To run the script, execute the following command:

groovy yourscriptname.groovy

If you want to start the same script inside a docker, execute commands below:

# one time setup for script dependencies, it will speed up future executions
docker volume create --name grapes-cache

# in Linux
docker run --rm -u groovy -v "$(pwd)":"/home/groovy" -v "grapes-cache":"/home/groovy/.groovy/grapes" groovy:3.0.20-jdk11 groovy yourscriptname.groovy

# in Windows
docker run --rm -u groovy -v %CD%:"/home/groovy" -v "grapes-cache":"/home/groovy/.groovy/grapes" groovy:3.0.20-jdk11 groovy yourscriptname.groovy

Typical output on console should look like this:

+      1 in 00:00:01 =    1,7/s Avg:   419 Min:   419 Max:   419 Err:     0 (0,00%) Active: 1 Started: 1 Finished: 0
=      1 in 00:00:01 =    1,7/s Avg:   419 Min:   419 Max:   419 Err:     0 (0,00%)

Test plan generator

There is *.har converter to generate scripts from *.har files. It can greatly speed up scripts generation for your tests.

DefaultTestScript vs. TestScript

There are two implementation for the test scripts, the DefaultTestScript and TestScript. The DefaultTestScript is very basic and doesn't have any additional features. The TestScript comes with additional command line support.

When you change the line with @groovy.transform.BaseScript annotation to net.simonix.dsl.jmeter.TestScript, so your file would look like this:

@GrabConfig(systemClassLoader=true)
@Grab('net.simonix.scripts:groovy-jmeter')

@groovy.transform.BaseScript net.simonix.dsl.jmeter.TestScript script

start {
    plan {
        group {
            http 'GET http://www.example.com'
        }

        // optional element, shows execution progress
        summary(file: 'log.jtl')
    }
}

You can execute your script with help parameter to see all available options:

groovy yourscriptname.groovy --help
Usage: groovy [-h] [--no-run] [--jmx-out=<file>] [-V=<variable=value>
              [=<variable=value>]...]...
  -h, --help             Show help message
      --jmx-out=<file>   Generate .jmx file based on the script
      --no-run           Execute the script but don't run the test
  -V, --vars=<variable=value>[=<variable=value>]...
                         Define values for placeholders in the script

Some interesting usage might be to generate .jmx file with additional variables which comes from environment:

@GrabConfig(systemClassLoader=true)
@Grab('net.simonix.scripts:groovy-jmeter')

@groovy.transform.BaseScript net.simonix.dsl.jmeter.TestScript script

start {
    plan {
        // HTTP defaults test element
        defaults protocol: 'http', domain: "${var_host}", port: var_port

        group (users: 10) {
            http 'GET /books'
        }

        // optional element, shows execution progress
        summary(file: 'log.jtl')
    }
}

To generate .jmx file from this script you can execute the following in the command line:

groovy yourscriptname.groovy --jmx-out yourscriptname.jmx --no-run -Vvar_host=localhost -Vvar_port=8080

For more examples you should check the examples folder and unit tests.

To get more information about all available options you should check groovy docs page or generate it from the source.

gradlew groovydoc

cd ./build/docs/groovydoc && index.html

More advanced example

The example below shows more constructs related to testing simple REST API.

@GrabConfig(systemClassLoader=true)
@Grab('net.simonix.scripts:groovy-jmeter')

@groovy.transform.BaseScript net.simonix.dsl.jmeter.TestScript script

start {
    // define basic test plan
    plan {
        // add user define variables (this will be defined on Test Plan not as separate User Defined Variables test element)
        arguments {
            argument(name: 'var_host', value: 'prod.mycompany.com')
        }

        // define HTTP default values
        defaults(protocol: 'http', domain: '${var_host}', port: 1080)

        // define group of 1000 users with ramp up period 300 seconds
        group(users: 1000, rampUp: 300) {
            // add cookie manager for HTTP requests
            cookies(name: 'cookies manager')

            // load users data from file into variables
            csv(file: 'users.csv', variables: ['var_username', 'var_password'], delimiter: ';')

            // define POST request with parameters and extract tracking id for later use
            http('POST /login') {
                params {
                    param(name: 'username', value: '${var_username}')
                    param(name: 'password', value: '${var_password}')
                }

                extract_regex expression: '"trackId", content="([0-9]+)"', variable: 'var_trackId'
            }

            // define GET request and extract data from json response
            http('GET /api/books') {
                params values: [ limit: '10' ]

                extract_json expressions: '$..id', variables: 'var_bookId'
            }

            http('GET /api/books/${var_bookId}') {
                extract_json expressions: '$..author.id', variables: 'var_authorId'
            }

            // simplified form of HTTP request, no parenthesis
            http 'GET /api/authors/${var_authorId}'

            // define simple controller to make POST request
            simple {
                headers values: [ 'Content-Type': 'application/json' ]

                http('POST /api/books/${var_bookId}/comments') {
                    body '''\
                        {
                            "title": "Title",
                            "content": "Comment content"
                        }
                    '''

                    // check assertion about HTTP status code in the response
                    check_response {
                        status() eq 200
                    }
                }
            }
        }

        // output to .jtl file
        summary(file: 'script.jtl', enabled: true)
    }
}

The next example shows testing database with JMeter.

@GrabConfig(systemClassLoader=true)
@Grab('net.simonix.scripts:groovy-jmeter')
@Grab('org.postgresql:postgresql:42.2.20') // download JDBC driver for your database

@groovy.transform.BaseScript net.simonix.dsl.jmeter.TestScript script

start {
    plan {
        before {
            jdbc datasource: 'postgres', {
                pool connections: 10, wait: 1000, eviction: 60000, autocommit: true, isolation: 'DEFAULT', preinit: true
                connection url: 'jdbc:postgresql://database-db:5432/', driver: 'org.postgresql.Driver', username: 'postgres', password: 'postgres'
                init(['SET search_path TO public'])
                validation idle: true, timeout: 5000, query: '''SELECT 1'''
            }

            jdbc use: 'postgres', {
                jdbc_preprocessor use: 'postgres', {
                    execute('''
                        DROP TABLE employee
                    ''')
                }

                execute('''
                    CREATE TABLE employee (id INTEGER PRIMARY KEY, first_name VARCHAR(50), last_name VARCHAR(50), email VARCHAR(50), salary INTEGER)
                ''')
            }
        }

        group users: 100, rampUp: 60, loops: 1, {
            csv file: 'employees.csv', delimiter: ',', ignoreFirstLine: true, variables: ['var_id', 'var_first_name', 'var_last_name', 'var_email', 'var_salary']

            jdbc use: 'postgres', {
                execute('''
                    INSERT INTO employee (id, first_name, last_name, email, salary) VALUES(?, ?, ?, ?, ?)
                ''') {
                    params {
                        param value: '${var_id}', type: 'INTEGER'
                        param value: '${var_first_name}', type: 'VARCHAR'
                        param value: '${var_last_name}', type: 'VARCHAR'
                        param value: '${var_email}', type: 'VARCHAR'
                        param value: '${var_salary}', type: 'INTEGER'
                    }
                }
            }

            jdbc use: 'postgres', {
                query(limit: 1, result: 'var_employee_count', inline: '''
                    SELECT count(*) FROM employee
                ''')

                jsrpostprocessor(inline: '''
                    log.info('Number of employees: ' + vars.get('var_employee_count'))
                ''')
            }

            // output to .jtl file
            summary(file: 'script.jtl', enabled: true)
        }
    }
}

Building from source

Clone, build and publish jars to your local repository:

git clone https://github.com/smicyk/groovy-jmeter.git

# in Linux
./gradlew clean build publishIvyPublicationToIvyRepository

# in Windows
gradlew clean build publishIvyPublicationToIvyRepository

You can also try alternative approach and build everything on Docker without installing anything on your machine:

git clone https://github.com/smicyk/groovy-jmeter.git

docker volume create --name grapes-cache

# in Linux
docker run --rm -u gradle -w "/home/gradle/groovy-jmeter" -v "$(pwd)":"/home/gradle/groovy-jmeter" -v "grapes-cache":"/home/gradle/.groovy/grapes" gradle:8.5-jdk11 gradle -Dorg.gradle.project.buildDir=/tmp/gradle-build clean build publishIvyPublicationToIvyRepository

# in Windows
docker run --rm -u gradle -w "/home/gradle/groovy-jmeter" -v "%CD%":"/home/gradle/groovy-jmeter" -v "grapes-cache":"/home/gradle/.groovy/grapes" gradle:8.5-jdk11 gradle -Dorg.gradle.project.buildDir=/tmp/gradle-build clean build publishIvyPublicationToIvyRepository

Conventions

There are several conventions used in the design of the DSL.

Naming

All names in the DSL should make the script easy to read and concise. Most of the keywords and properties names are single words. The examples of such keywords might be: plan, group, loops. A similar rule applies to properties, e.g. name, comments, forever. However, some keywords must have two words like execute_if, extract_regx, assert_json. The longer names for properties are in camel case, e.g. rampUp, perUser.

Users vs. Threads

In JMeter world, the users and threads are used interchangeably (both means virtual concurrent users executing test plan). In the script, we use the users as a convention. Check the example below:

start {
    plan {
        group users: 10, {
            // define random variable 'var_random' for each user (in other words each user has its own random generator)
            random(minimum: 0, maximum: 100, variable: 'var_random', perUser: true)
        }
    }
}

Default values

All keywords in the DSL has predefined default values for its properties. For example, each keyword has name property with a default value defined. If there is no name property given for the keyword, the script will use the default value. You can check default values in Groovy docs. Below are more examples:

start {
    // would be same as plan (name: 'Test Plan')
    plan { 

    }

    plan {
        // would be same as group (users: 1, rampUp: 1) 
        group {

        }
    }
}

Required properties

Even though each property has a default value, sometimes there is no sense to have a test element without specific property value. Such properties are required and raise an exception if missing. Check the example below:

Please note that in JMeter documentation there are many properties which are required. Still, in the DSL we make them only required if they vital to execution, otherwise they have some reasonable default value.

start {
    plan {
        group {
            // condition property is required, otherwise using execute_if controller has no sense
            execute_if (condition: '''__jexl3(vars.get('var_random') mod 2 == 0)''') {

            }
        }
    }
}

Groovy as DSL

There are several things to keep in mind while writing the scripts; most of the stuff relates to Groovy language:

  • using different quotes around string, please refer to Groovy docs about string and quotes and check example below:
// test_1.groovy
start {
    // using single quotes (for Java plain String)
    plan(name: 'Test name')
}

// test_2.groovy
start {
    // using single quotes is recommended in most situation (should be used when you want use JMeter variable substitution in the script)
    plan(name: '${var_variable}')
}

// test_3.groovy
start {
    // using double quotes (for GString, interpolation available during test build but not execution by JMeter engine)
    plan (name: "${var_param}")
}
  • there are several ways to use keywords, check the example below:
// test_1.groovy
start {
    // keyword without properties, after keyword you can open closure without any properties or parenthesis
    plan {

    }
}

// test_2.groovy
start {
    // keyword with properties, the properties of the keyword can have properties defined as key/value pair 
    plan(name: 'test', comments: 'new test plan') {

    }
}

// test_3.groovy
start {
    // keyword with properties but without parenthesis, please note that after all properties you must put comma
    plan name: 'test', comments: 'new test plan', {

    }
}

// test_4.groovy
start {
    // keyword without properties and child test elements, note that the parenthesis must be used
    plan()
}

Shortcuts

There are many places where we can use shortcuts to define the same thing:

  • the first argument for the keyword can be simple value. In most cases it is treated as a name for test element:
// test_1.groovy
start {
    // long version
    plan name: 'Test plan111'
}

// test_2.groovy
start {
    // short version
    plan 'Test plan222'
}

// test_3.groovy
start {
    // short version with properties
    plan 'Test plan', enabled: true
}
  • some keywords treat first value as shortcut
start {
    plan {
        group {
            // long version
            loop count: 10
            // short version
            loop 10

            // long version
            execute_if condition: '''__jexl3(vars.get('var_random') mod 2 == 0)'''
            // short version
            execute_if '''__jexl3(vars.get('var_random') mod 2 == 0)'''
        }
    }
}
  • there is special syntax for HTTP sampler regarding first argument
start {
    plan {
        group {
            // long version for HTTP request
            http(protocol: 'http', domain: 'localhost', port: 8080, path: '/app/login', method: 'GET')
            
            // short version for HTTP request
            http 'GET http://localhost:8080/app/login'
        }

        // if used with defaults keyword, some elements can be ommitted
        group {
            defaults(protocol: 'http', domain: 'localhost', port: 8080)

            http 'GET /app/login'
        }
    }
}
  • simplified array like structures for samplers and config elements
// test_1.groovy
start {
    // long version to define user variables inside test plan
    plan {
        arguments {
            argument(name: 'var_variable', value: 'value')
            argument(name: 'var_other_variable', value: 'other_value')
        }
    }
}

// test_2.groovy
start {
    // short version to define user variables inside test plan
    plan {
        arguments values: [
                var_variable      : 'value',
                var_other_veriable: 'other_value'
        ]
    }
}

// test_3.groovy
start {
    // there are others test elements which has simplified behaviour e.g. params, variables, headers
    plan {
        group {
            // long version for params
            http 'GET http://www.example.com', {
                params {
                    param(name: 'param', value: 'value')
                    param(name: 'other', value: 'other')
                }
            }
            // short version for params
            http 'GET http://www.example.com', {
                params values: [
                        param: 'value',
                        other: 'other'
                ]
            }
        }
    }
}
  • by default samples have names after its name property, but in case of HTTP request there are some differences:
start {
    plan {
        arguments values: [ var_bookId: 'value' ]

        group {
            // the name of the sample will generated as 'GET /app/books/:var_bookId'
            // currently this is default behaviour only if short version is used
            http 'GET http://localhost/app/books/${var_bookId}'
            // to define own sample name you must use long version
            http name: 'Custom Name', protocol: 'http', domain: 'localhost', path: '/app/books/${var_bookId}', method: 'GET'
        }
    }
}