skip to content

How would you distribute one shared k6 helpers.js across several test scripts and repositories?

level: principalimportance: nice to knowfreq 27%

answer

  1. no package manager to lean on
  2. path, URL, or bundle
  3. the URL is the whole pin
  4. archive is the nearest thing to a lockfile

basics

~20 s

k6 offers exactly three routes: a relative path inside one repository, a version-pinned https URL, or a bundler artefact imported as a local file. Since k6 has no node_modules lookup and no lockfile, the choice is really about how the version is pinned.

solid answer

~40 s

Because k6 resolves nothing but literal paths and https URLs, the distribution options are limited and each has a distinct pinning story. Inside one repository, a relative path such as `../lib/helpers.js` is simplest — no network, no version skew, but it cannot cross repositories. Across repositories, publishing the helper at a versioned URL and importing `https://.../helpers/2.1.0/index.js` works, at the cost of executing fetched code and depending on that host at load time. If the helper itself needs npm dependencies, a bundler is the only route, since k6 will not look in `node_modules`; you publish or commit the emitted file and import that. Whichever you pick, `k6 archive` freezes the resolved graph so a CI run is reproducible.

code

bash · 3 lines
bash
# resolve every local and remote module once, then run the frozen graph
k6 archive -O suite-2.1.0.tar tests/smoke.js
k6 run suite-2.1.0.tar

go deeper

for a junior

Know the three shapes a shared helper can take in k6: a file next to the tests, a file at an https URL, or a bundle produced by a build tool. Bare package names are never one of them.

for a middle

Explain what pins each option: the git commit for a relative path, the URL string for a remote import, the artefact version for a bundle. Say why npm alone does not help a k6 script.

for a senior

Show that you would archive a suite whose result has to be reproducible, mount the right directory for container runs, and treat a remote import as a load-time dependency that CI egress rules can break.

for a principal

Own the tradeoff: reach across repositories against supply-chain exposure and reproducibility. Standardise the directory layout and the pinning rule, decide explicitly whether runtime fetching is acceptable, and make the archive the artefact of record.

## Why the options are so few Most JavaScript sharing questions are answered with a package manager. k6 removes that answer: its loader accepts a relative or absolute file path, an `https://` URL, or a `k6`-prefixed built-in, and refuses everything else. There is no `node_modules` lookup, no `package.json` resolution and no lockfile. So the real question is not "how do I publish a library" but "how does each consuming script name the file, and what pins its version". Three routes exist, and they are not interchangeable. ## Route 1 — a relative path inside one repository Keep `lib/helpers.js` beside the tests and let each script import it: ```javascript import { login } from '../lib/helpers.js'; ``` - **Pinning:** the git commit. The helper and its callers move together, so a change is reviewed in the same pull request as the tests it affects. - **Cost:** it cannot cross a repository boundary, and the specifier depends on the importing file's depth, so an inconsistent directory layout turns into a scattering of `../../..` prefixes. - **Docker:** the official k6 image contains none of your files, so a container run has to mount the tree, for example `docker run -v "$PWD:/src" grafana/k6 run /src/tests/smoke.js`. ## Route 2 — a version-pinned https URL Publish the helper to a static host, a CDN or a release asset and import it by URL, the way Grafana's own jslib is consumed: ```javascript import { randomItem } from 'https://jslib.k6.io/k6-utils/1.5.0/index.js'; ``` - **Pinning:** the URL, and nothing else. There is no lockfile to record what was actually fetched, so an unversioned or floating path silently changes the test. - **Cost:** the module is downloaded and executed at load time with the same reach as your own script, the host becomes a load-time dependency, and CI runners without egress cannot run the suite. k6 accepts only `https` here, so an internal host must terminate TLS. - **Reach:** this is the only route that spans repositories without copying files. ## Route 3 — a bundler artefact If the shared code depends on npm packages, no amount of path discipline helps, because k6 will not resolve a bare specifier. Run webpack or rollup, and import the single self-contained file it emits — either committed into each repository or published under route 2. - **Pinning:** the bundle's own version, plus the lockfile of the project that built it. - **Cost:** a build step that has to stay in step with the tests, and stack traces that point into generated code. ## Comparing them | | Relative path | https URL | Bundler artefact | |---|---|---|---| | Crosses repositories | No | Yes | Yes, once published | | Version pin | Git commit | The URL string | Build artefact version | | Needs network at load time | No | Yes | Only if fetched by URL | | Can use npm dependencies | No | No | Yes | | Executes third-party code | No | Yes | Depends on the bundle's contents | ## What to standardise, and what to leave open 1. **One directory layout.** Put every entry script at the same depth so they all share one specifier for the shared library. This is worth more than it sounds: it makes a new test a copy, and it keeps people away from absolute paths. 2. **Exact versions in every remote URL, reviewed like code.** A bump is a diff someone approves, not something that happens to the pipeline overnight. 3. **A decision about runtime fetching.** Whether the organisation accepts executing code downloaded at test time is a security posture question, not a preference. If the answer is no, vendor the file or ship an archive. 4. **`k6 archive` for anything that must be reproducible.** It resolves the whole graph once and writes `metadata.json`, the main script, and the local and remote file trees into one tar that `k6 run` executes without touching the network. That is the closest thing k6 has to a lockfile, and it is the artefact worth keeping when a run has to be explained months later. ## The trap to avoid The failure mode is a suite that imports a floating URL from three repositories with no archive anywhere. Nothing is broken until the library changes; then three teams see different numbers, the scripts are byte-identical in git, and there is no record of what was executed. Pinning and archiving cost almost nothing at setup time and are close to unrecoverable afterwards.

  • Why can a team not simply publish the k6 helper to an internal npm registry?
    Because k6 never resolves a bare specifier and never reads `node_modules`, so an installed package is invisible to the loader. npm is still useful for building the helper, but what k6 consumes is a file: either the package's contents committed into the repository, or a bundle emitted by webpack or rollup and imported by a relative path or an https URL.
  • What does k6 archive record beyond the JavaScript sources?
    The tar carries `metadata.json` with the consolidated options, the environment variables, the k6 version and the working directory, alongside the main script and the resolved `file` and `https` trees. `--exclude-env-vars` drops the environment if it holds secrets, and `-O` names the output, which defaults to `archive.tar`.
  • How do you keep three test scripts from drifting onto different helper versions?
    Either keep the helper in the same repository so a git commit versions all three at once, or pin the same exact URL in all three and treat a bump as a single reviewed change. Where drift must be provable, archive each suite and keep the tar, since it is the only artefact that records the bytes a run actually executed.

saying these in an interview costs you the question

  • publish the helper to npm and import it by name
  • point every script at a latest URL, it rarely changes
  • absolute paths make the helper reusable everywhere
  • the Docker image can see the repository without a mount
  • an archive is only for uploading to the cloud