skip to content

Ports and Parallel Workers

Several test workers on one machine: giving each stub server a port nobody else holds, then handing the resolved port to the code under test. Fixed ports and one shared instance both fail here.

on this pageshow

explore

questions

4

In mountebank, how does a test worker learn the port its marina-berth imposter was given?

level: juniorimportance: must knowfreq 66%

answer

  1. the port is returned, not chosen
  2. the reply is the created imposter
  3. port field on the creation response
  4. GET /imposters lists what is bound
  5. base URL travels as configuration

basics

~20 s

The POST /imposters response is the created imposter, and its port field holds the number mountebank bound. Read it from that response, or list running imposters with GET /imposters, then build the worker's base URL from it.

solid answer

~40 s

Mountebank hands the port back in its own reply. `POST /imposters` returns the created imposter as JSON, and when the request omitted `port`, that document carries the number mountebank actually bound — the authoritative value, not anything the worker guessed. `GET /imposters` lists every imposter the running `mb` process holds, each with its `port`, which is the same read-back after the fact and a useful diagnostic. The worker composes the base URL from that number and passes it into the code under test as configuration: an environment variable, a constructor argument, or a property the application resolves at start-up. What it must never be is a constant baked into the fixture, because every worker resolves a different number. In MockServer the equivalent read-back is `getPort()` on `MockServerClient`; in WireMock it is `getHttpBaseUrl()`.

code

bash · 9 lines
bash
MB="http://localhost:${MB_PORT}"

PORT=$(curl -s -X POST "$MB/imposters" \
  -H 'Content-Type: application/json' \
  --data-binary @marina-berths-imposter.json \
  | jq -r '.port')

export MARINA_API_BASE_URL="http://localhost:${PORT}"
curl -s "${MARINA_API_BASE_URL}/berths?marina=falmouth" | jq '.berths | length'

go deeper

for a junior

Know that POST /imposters returns the imposter it created and that its port field carries the bound number. Be able to say where you would read it and what you would do with it next.

for a middle

Explain why the base URL must travel into the application as configuration rather than a constant, and what GET /imposters adds once several workers are running at once.

for a senior

Be ready to trace a suite where the resolved port never reaches the application, and to say which layer should own the read-back so a retried worker cannot use a stale number.

for a principal

Set the convention for how a stub's address reaches the code under test across many suites, and say why one shared mechanism beats each team inventing its own.

## The port is a value you read, not a value you pick A parallel run gives each worker its own stub of the marina berth-booking API, and each stub listens on its own TCP port. Because that number differs per worker — and differs again on the next run — it cannot be written down anywhere in advance. The only correct source is the thing that actually bound it. In mountebank that thing is the `mb` process, and it tells you. This is the half of port handling that teams get wrong more often than allocation itself. Getting mountebank to choose a free port is one line of omission; getting the chosen number all the way into the client that will call `GET /berths` is where suites quietly break. ## Where mountebank puts the number An imposter is created by posting a document to mountebank's control API. If that document omits the `port` field, mountebank binds a free port itself. Two places then carry the answer: - The **response to `POST /imposters`** is the created imposter, and its `port` field holds the number that is now bound. This is the value the worker should use; nothing else is authoritative. - **`GET /imposters`** lists every imposter the running `mb` process currently holds, each with its `port` and `name`. That is the same information after the fact, across every worker, and it is the first thing to look at when something collides. There is a quieter fact worth carrying with it: in mountebank an imposter is addressed *by its port*, so the number you read back is also the identifier you will use later — when you remove it, or when you post further stubs to it. ## Getting it into the code under test Reading the port is half the job. The application under test has to send its marina-berth requests there, which means the number must cross from the harness into the application: 1. Create the imposter and read its `port` from the reply. 2. Compose the base URL from scheme, host and that port. 3. Pass it in as configuration — an environment variable, a start-up property, or a constructor argument on the client the test drives. 4. Start the application or client *after* that, so it resolves an address that exists. Step four is where suites go wrong more often than the read-back itself. An application that resolves its base URL once at boot cannot pick up a port created afterwards, and the resulting failure looks like a connection error rather than an ordering mistake. ## The read-back surface of each product The three products in this space all answer this question, and their answers are easy to attribute to the wrong owner: | product | where the resolved port comes from | what you read | |---|---|---| | Mountebank | mountebank binds it when `port` is omitted | the `port` field of the returned imposter, or `GET /imposters` | | WireMock | `wireMockConfig().dynamicPort()` | `getHttpBaseUrl()`, or `getHttpsBaseUrl()` for TLS | | MockServer | `PortFactory.findFreePort()` supplies a candidate | `getPort()` on `MockServerClient` | The attribution trap here is worth stating plainly, because both halves of it are real identifiers. `PortFactory` is **MockServer's** — `org.mockserver.socket.PortFactory`. WireMock has no such class; its own test suite reaches a free port through `Network.findFreePort()`, and its DSL answer is `dynamicPort()` with the value read back through `getHttpBaseUrl()`. Claiming `PortFactory` for WireMock rather than MockServer is false even though every token in the claim exists somewhere. ## What a hardcoded base URL costs - With one worker it works, which is exactly why it survives review. - With N workers, N minus one of them point at a port they do not own. - If nothing is listening there, the failure is a connection error, which at least names itself. - If **another worker's imposter** is listening there, the request is answered — by the wrong stub set, with a plausible marina-berth body — and the test passes or fails for a reason unrelated to the code. - The symptom moves with worker count, which is why it gets blamed on the runner rather than on the address. ## A short checklist - Does anything in the suite name a port literally, in a fixture, a config file or a container definition? - Does every worker read its own number from its own creation response, not from a shared value? - Does the application receive that number before it resolves its base URL? - If a worker is retried in a fresh process, does it create a new imposter and read the new number?

  • What does GET /imposters give you that the creation response does not?
    The whole set currently bound in that `mb` process, not only yours — every imposter with its `port` and `name`. That is what lets you see another run's imposter, a leak from a crashed worker, or two suites sharing one machine. The creation response answers only for the imposter you just made.
  • Why should the resolved port reach the application as configuration rather than a constant?
    Because it differs per worker and per run. A constant compiles one worker's number into every worker's fixture, so the others either fail to connect or reach an imposter they do not own. Configuration also lets a retried worker pick up its new number without a code change.

saying these in an interview costs you the question

  • Hardcodes the imposter base URL into the fixture
  • Assumes every worker's imposter is on the same port
  • Scrapes the port from a log line instead of the reply
  • Thinks getHttpBaseUrl() is mountebank's read-back
  • Cannot say how the port reaches the application
open as a page

In mountebank, why is a base-port-plus-worker-index scheme worse than letting mb assign the port?

level: middleimportance: should knowfreq 46%

basics

~20 s

Nothing on the machine promises that block is yours. A second suite, a leftover process or another service can already hold one, and the worst outcome is not a bind failure but a worker quietly reaching another run's imposter.

open as a page

In mountebank, what happens to an imposter's port when a parallel worker finishes or dies?

level: middleimportance: should knowfreq 48%

basics

~20 s

Nothing releases it automatically. One mb process holds every imposter's listening socket for as long as the imposter exists, so a worker that dies without deleting its imposter leaves the port bound on that machine until mb itself exits.

open as a page

In mountebank, how do you stop two parallel workers creating marina-berth imposters on the same port?

level: seniorimportance: should knowfreq 52%

basics

~20 s

Stop computing the port yourself. Post the imposter to mountebank with no port field, and mountebank binds a free one and returns it in the created imposter. A pre-computed number is only free until someone else binds it.

open as a page