Updates & News 28 February 2019

Announcing: JMeter to k6 JS converter tool

Robin

If you’ve been in the load testing game for awhile or ever looked around for open source load testing tools, which you no doubt had at some point if you’re reading this, you’d likely have run into JMeter. It’s a well-known, featureful load testing tool but with quite a few years on its back, first seeing the light of day in 1998.

It’s no surprise that many of the users that find their way to k6 and Load Impact in search of a modern and developer-centric take on load testing, come to us with prior experience of JMeter and many also have an existing suite of tests. To make the transition to k6 smoother we set out to build a JMeter to k6 JavaScript conversion tool, that takes a JMeter .jmx file and converts it into a k6 JS test script, with the help of a few Node.js libraries to bridge the feature gap.

Before jumping into the details of what the converter can do and how it works, credit needs to be given where credit is due; major thanks to @bookmoons for doing an amazing job implementing this tool 👏🙌

Getting started

Let’s waste no more time, this is how you get the converter installed: npm install -g jmeter-to-k6

This will install the converter globally on your system so it can be run from anywhere (we recommend to use nvm to avoid permission issues with trying to use sudo on Unix/Linux systems). If you prefer to install it on a per-project basis, as a dependency, you can of course do that as well (run something like npm install --save-dev jmeter-to-k6 instead).

To convert a JMeter .jmx file you run a command like this: jmeter-to-k6 -o /conversion/output/directory MyTest.jmx

When you run this two things happen:

  1. The converter will parse the JMeter XML and output the equivalent k6 JS test script to a file at /conversion/output/directory/test.js.

As some JMeter features don’t exist in k6 yet, we need to bridge that feature gap by importing third-party Node.js libraries. A /conversion/output/directory/libs directory will be created including the necessary Node.js libraries. The JS libs in this directory will be imported by the main test.js file as needed.

Will it work on all JMeter files?

The short answer is no. The longer answer requires us to start digging into the feature set of the tool by looking at the supported JMeter features in this initial release.

Supported JMeter features/elements

This will only be a summary of the features that the converter is able to convert (and the equivalent k6 options or JS code), to see the full details of its support go here.

Threads/VUs

  • Stepping Thread Group: converts into the equivalent k6 options.stages settings.
  • Thread Group: converts into the equivalent k6 options.stages settings.
  • setUp Thread Group: converts to an export function setup() in k6 including all the child logic of this element converted into the body of the this function.
  • tearDown Thread Group: converts to an export function teardown() in k6 including all the child logic of this element converted into the body of this function.

Config

  • CSV Data Set: converts into an open() statement to read the CSV file data and implements CDV parsing via the third-party - PapaParse JS library.

  • DNS Cache Manager: converts to the equivalent k6 options.hosts settings.

  • HTTP Auth Manager: converts to the appropriate URL userinfo or Authorization header.

  • HTTP Cookie Manager: converts to the a global (per-VU) cookiejar merged with per-request cookies. Example:

    export default function (data) {
    const jar = http.cookieJar();
    jar.set("http://test.loadimpact.com", "session", "test", {"domain":"test.loadimpact.com","secure":false});
    ...
    }

  • HTTP Header Manager: converts to per-request headers. Example:

    url = 'https://test.loadimpact.com/';
    opts = {
    headers: {
    ['Custom-Header']: 'test',
    ['Content-Type']: 'application/json',
    },
    };
    r = http.request('GET', url, '', opts);

Assertions

  • JSON Path Assertion: converts to a k6 check using the third-party jsonpath JS library. Example:

    r = http.request('GET', 'https://httpbin.test.loadimpact.com/json', '', opts);
    check(r, {
    'Check Author': (r) => {
    const body = (() => {
    try {
    return JSON.parse(r.body);
    } catch (e) {
    return null;
    }
    })();
    if (!body) return false;
    const values = jsonpath.query(body, '$.slideshow.author');
    return !!values.find(
    (value) => (typeof value === 'object' ? JSON.stringify(value) : value) === 'Yours Truly'
    );
    },
    });

  • Duration Assertion: converts to a k6 check looking at Response.timings.duration. Example:

    r = http.request("GET", "https://test.loadimpact.com/", "", opts);
    check(r, {
    "Duration Assertion": r => {
    return r.timings.duration <= 1000;
    }
    });

  • Response Assertion: converts to a k6 check looking at Response.status, Response.body, Response.headers etc. Example:

    r = http.request('GET', 'https://test.loadimpact.com/', '', opts);
    check(r, {
    'Is status 200': (r) => {
    return r.status === 200;
    },
    });

  • XPath Assertion: converts to a comment recommending a pure JS library be used (see section below on future improvements).

Logic controllers

  • ForEach Controller: converts to a JS for-loop in k6. Example:

    for (let i = 0, first = true; i <= 3; i++) {
    vars['j'] = vars['i_' + i];
    // child element logic
    first = false;
    }

  • If Controller: converts to a JS if statement in k6. Example:

    if (eval(`${vars['i']}>0`)) {
    // child element logic
    }

  • Interleave Controller: converts to a JS switch statement in k6 that alternates between all child elements for each VU iteration. Example (with 2 child elements of the Interleave Controller):

    const index = __ITER - ((__ITER / 2) | 0) * 2;
    switch (index) {
    case 0:
    // child element 1 logic
    break;
    case 1:
    // child element 2 logic
    break;
    default:
    throw new Error('Unexpected interleave index: ' + index);
    }

  • Loop Controller: converts to either a JS while(true)-loop, if "Forever” has been checked, or a JS for-loop otherwise. Example (with a loop count of 5):

    for (let i = 0, first = true; i &lt;= 5; i++) {
    // child element logic
    first = false;
    }

  • Once Only Controller: converts to an if statement whose body is only run if the VU is in the first iteration. Example:

    if (__ITER === 0) {
    // child element logic
    }

  • Random Controller: converts to a JS switch statement selecting a case at random. Example (with 2 child elements):

    const index = Math.floor(Math.random() * 2);
    switch (index) {
    case 0:
    // child element 1 logic
    break;
    case 1:
    // child element 2 logic
    break;
    default:
    throw new Error('Unexpected interleave index: ' + index);
    }

  • Runtime Controller: converts to JS block that keeps track of a deadline time in the future that it checks at the end of each iteration of child element execution. Example (with a Loop Controller, looping forever, as a child element):

    const deadline = Date.now() + 5000;
    {
    let first = true;
    while (true) {
    // child element logic
    first = false;
    if (Date.now() &gt;= deadline) break;
    }
    }

  • Simple Controller: converts to a group() in k6, with the controller name as the group name. Example:

    group('Simple Controller Name', () => {
    // child element logic
    });

  • Transaction Controller: like the Simple Controller above, the Transaction Controller converts to a group() in k6, with the controller name as the group name. As k6 measures group duration the equivalent metric data will be available as for a Transaction Controller. Example:

    group('Transaction Controller Name', () => {
    // child element logic
    });

  • While Controller: converts to a JS while-loop in k6. Example:

    let first = true;
    while (`${vars[`usersLeft`]}` !== 'false') {
    // child element logic
    first = false;
    }

Timers

Pre-processors

Post-processors

  • BeanShell PostProcessor: converts to a comment with the code in k6.

  • Boundary Extractor: converts to JS code using a regex to extract the needed data in k6. Example (extracting the slideshow.author field from the JSON returned by https://httpbin.test.loadimpact.com/json):

    regex = new RegExp('"author": "' + '(.*)' + '",', 'g');
    matches = (() => {
    const matches = [];
    while ((match = regex.exec(r.body))) {
    matches.push(match[1]);
    }
    return matches;
    })();
    extract = matches.length === 0 ? null : matches[Math.floor(Math.random() * matches.length)];
    vars['author'] = extract || 'Default Author';

  • CSS Selector Extractor: converts to using the HTML parsing and CSS querying support built into k6. Example:

    output = 'pageTitle';
    const doc = html.parseHTML(r.body);
    matches = doc.find('body h1');
    match = matches.size() === 0 ? null : matches.eq(Math.floor(Math.random() * matches.size()));
    extract = match ? match.text() : null;
    vars[output] = extract || 'Default Title';

  • JSON Extractor: converts to JS code using the third-party jsonpath JS library. Example (extracting the slideshow.author field from the JSON returned by https://httpbin.test.loadimpact.com/json):

    const queries = ['slideshow.author'];
    const outputs = ['author'];
    const defaults = ['Default Author'];
    const body = (() => {
    try {
    return JSON.parse(r.body);
    } catch (e) {
    return null;
    }
    })();
    if (body) {
    for (let i = 0; i < queries.length; i++) {
    const query = queries[i];
    const output = outputs[i];
    const defaultValue = defaults[i];
    matches = jsonpath.query(body, query);
    extract = matches.length === 0 ? null : matches[Math.floor(Math.random() * matches.length)];
    vars[output] = extract || defaultValue;
    }
    } else {
    defaults.forEach((value, i) => {
    vars[outputs[i]] = value;
    });
    }

  • JSR223 PostProcessor: onverts to a comment with the code in k6.

  • Regex Extractor: converts to code using the standard JS RegExp API to extract the specified data. Example:

    regex = new RegExp('"author": "(*.)",');
    matches = (() => {
    const matches = [];
    while ((match = regex.exec(r.body))) {
    matches.push(match);
    }
    return matches;
    })();
    match = matches.length <= 1 ? null : matches[Math.floor(Math.random() * (matches.length - 1)) + 1];
    output = "author";
    if (match) {
    extract = "$0$".replace(/\$(\d*)\$/g, (match, digits) => {
    if (!digits) return "";
    const index = Number.parseInt(digits, 10);
    if (index > match.length - 1) return "";
    return match[index];
    });
    vars[output] = extract;
    vars[output + "_g"] = match.length - 1;
    for (let i = 0; i &lt; match.length; i++)
    vars[output + "_g" + i] = match[i];
    } else {
    vars[output] = "Default Author";
    delete vars[output + "_g"];
    delete vars[output + "_g0"];
    delete vars[output + "_g1"];
    }

  • Result Status Action Handler: converts to a JS if-statement checking the response status code and taking appropriate action. Example:

    if (Math.floor(r.status / 100) !== 2) {
    fail('Request failed: ' + r.status);
    }

  • XPath Extractor: converts to a comment recommending a pure JS library be used (see section below on future improvements).

A word about the future

We’re very excited about getting this initial release into the hands of our customers, users and the greater community. We’d love to hear what you think of the tool, especially what you think we should improve. Add a comment to this post, open an issue on Github or reach out through any of the other channels (k6 community forum, k6 Slack or Email). I’ll close off by sharing the next steps we’d like to take with the converter:

Multi-scenario support: In JMeter you can have several different thread groups with independent ramping configurations. This is something we’re working on adding in k6 as well. Once it’s implemented in k6 we’ll expand the converter to support this in a cleaner 1-to-1 way than what is currently implemented.

Arrival-rate support: Once arrival-rate execution support lands in k6 we’ll add support in the converter to convert Arrivals Thread Group and Free-form Arrivals Thread Group.

Popular thread groups: In this initial release we’re missing support for the Ultimate Thread Group and Concurrency Thread Group.

More logic controllers: There are some logic controllers that we left out of this initial release that we plan to add in a future release. They include:

Include Controller & Module Controller to convert modularized JMeter test plans Random Order Controller Switch Controller Throughput Controller

XPath assertions and extraction: As k6 currently doesn’t support XML parsing nor XPath querying this can’t be implemented without using a third-party JS library. We thus opted to drop it from the initial release, but something we want to add.

Reduce dependencies: The current converter has a dependency on a number of third-party Node.js libraries to bridge the current feature gap between JMeter and k6. As k6 gains these features natiely, we’ll move off of the dependencies.

< Back to all posts