Hey,
Sorry for late reply! Had trouble with my Grafana account so didn’t notice any replies.
Going into a bit of detail around the concepts:
For k6 we write javascript to invoke go sub routines
For nodejs we write javascript and invoke node sub routines
Effectively the only difference between both runtimes is therefore how we invoke subroutines and how our code is invoked by the runtime. The code in between is interopable.
Therefore if we sacrifice some of the invocation logic in k6 and replace the subroutines we invoke with node equivalents we can achieve a mostly consistent debug runtime. Whilst running in node we lose things like grafana integration and the execution model of k6 so this approach is predominately for debugging or low throughput testing. Importantly we don’t lose k6 functionality when running in k6
With the above in mind what I did in my codebase is as follows:
- Have a common entry point between k6 and node
This is where we export options and scenarios for k6 as normal
At the end of this file i have a dirty try catch block which references a protected variable exclusive to nodejs, in my case “process”
From here if process is defined (ie. the try block succeeds) my next operation is to invoke the function that we pass in on k6’s scenario. In pseudocode something like this:
try {process.env.scenario ? main(process.env.scenario) : null } catch(err){}
This gets us to the point where we can interop the entrypoint of our nodejs and k6 apps. Next obvious question is what about all the subroutines?
I create a file called k6PolyFill (for lack of a better name) and pop in a bunch of functions or singletons to interop between k6 native functions and node equivalents. The important thing to note here is that the functions we define in here should have the same invocation parameters and return data as the k6 equivalents for max capability. The functions I’ve interoped for my usage are:
- sleep
- SharedArray
- Trend
- Counter
- exec
- check
- open
This file will conditionally export the node functions we’ve created OR alias the k6 functions depending on whether the env is node or k6 (as defined by the psuedo code earlier). You’ll need to use module.exports as an FYI since the ECMA import/export syntax doesn’t support conditional export
Special mention goes to:
-
Group
At the time I wrote the codebase, group could not be awaited in k6 and made compatibility super ugly to deal with. In exchange I’ve defined a trigger function which computes the probability of a trigger based on the scenarios specified throughput
-
HTTP
For this one I use nodejs HTTP/HTTPS libs and the native HTTP function for k6. There are a few flags that need to be added to the node calls to make it behave more similarly to k6. In addition I put in a whole bunch of standardised logic before exporting this one out for logs that can be forwarded to log ingestion engines and a bunch of standardised assertions
From here if you refer to only functions in polyfil instead of k6 native directly you can easily interop between nodejs and k6 as your runtime.
In all honesty, I have no idea how I would maintain the k6 scenarios that I have (which consist of thousands of API’s often in complex sequences) without the ability to debug with line breaks in an IDE
When i started and ran using traces natively in k6 with just a segment of the tests we have now, it was becoming impossible to maintain
Feel free to let me know if you guys want more details or code