skip to content

How would you test the order several middleware run in, using a chain whose terminal element does nothing?

level: middleimportance: should knowfreq 46%

answer

  1. ordering is a property of assembly
  2. use the real registration path
  3. no-op element ends the chain
  4. label before and after delegating
  5. compare sequences, not sets

basics

~20 s

Assemble the chain through the application's own registration code, ending it with an element that only returns an empty success, and have each hook append a label before and after delegating. Compare the recorded sequence with the expected one.

solid answer

~40 s

Build the chain through the **same registration code the application uses**, so a mis-registration can actually show up, and terminate it with a no-op element that returns an empty success. Wrap each hook in a recorder — or, when the subject is the assembly rather than the hooks' work, use label-only test hooks — that appends `name:in` before delegating and `name:out` after the call returns. Send one synthetic request through it and assert the recorded list equals the expected sequence exactly. In the usual wrapping model that is all the `in` labels in chain order, the terminal element once, then the `out` labels in the mirror of that order. Assert sequence equality, not set membership: 'all five ran' is true of every permutation.

go deeper

for a junior

Give every element a label it appends before and after delegating, end the chain with something that does nothing, send one request, and compare the recorded list to the expected one.

for a middle

Explain the mirrored tail: in a wrapping chain the inbound phase runs outermost-first and the outbound phase unwinds innermost-first, so the second half reverses the first.

for a senior

Stress building the chain through the real registration path, and name the faults the sequence catches: a hook registered twice, in the wrong place, or not at all.

for a principal

Treat the expected sequence as a declared contract the codebase owns, so reordering hooks becomes a deliberate edit rather than a change nobody noticed.

Hook bugs are frequently ordering bugs, and ordering is a property of the *assembled* chain rather than of any one hook. A test for it is still a small, in-memory test: no server, no real work at the end of the chain, one synthetic request, one recorded sequence. ## The fixture 1. **Assemble the chain through the application's own registration path.** If the test hand-lists the hooks in the order it expects, it asserts that a list it just wrote equals itself. The value is in exercising the real assembly — the configuration, the registration calls, the priority values, whatever the application actually uses — so that a hook registered in the wrong place makes the test go red. 2. **Terminate the chain with a no-op element.** Something that takes the request, records that it was reached, and returns an empty success. It exists purely so the chain has an end. 3. **Give every element a recorder.** A shared, ordered list that each element appends to: one label immediately before it delegates, another immediately after the delegation returns. 4. **Send one synthetic request.** Ordering is a property of one traversal; a loop of requests only makes the expected sequence harder to write down. ## The assertion Assert **list equality** against the full expected sequence, not membership. `assert recorded == ["a:in", "b:in", "c:in", "terminal", "c:out", "b:out", "a:out"]` fails on a swap, on a duplicate, on a missing element, and on an unexpected extra one. `assert recorded.contains("b:in")` fails on none of those. The mirrored tail is the part candidates most often get wrong. In the common model where a hook *wraps* the rest of the chain — it calls the continuation and then continues executing when that call returns — the inbound phase runs outermost-first and the outbound phase unwinds innermost-first, so the second half of the sequence is the reverse of the first. Frameworks differ in how that is expressed: some have the hook wrap the continuation directly, others register separate before and after callbacks, and a few let a hook schedule outbound work as a callback registered during the inbound phase. The nesting story is the same in each; only the syntax and the vocabulary change. ## What this test actually catches | Fault | How it shows up in the recorded sequence | | --- | --- | | A hook registered in the wrong position | two labels swapped in the inbound half | | A hook registered twice | its labels appear twice | | A hook silently not registered at all | its labels are missing entirely | | A hook that returns without delegating | the sequence stops early and the terminal label is absent | | A hook whose outbound half never runs | its `:out` label is missing while its `:in` is present | | A hook that delegates twice | the inner labels and the terminal label appear twice | ## Why the terminal element does nothing Because the subject is the chain, not the work. A no-op end keeps the test's failure message about ordering: if a real handler is involved, then a change to that handler's dependencies, its serialization, or its validation can turn an ordering test red for reasons that have nothing to do with order. It also keeps the fixture free of the setup that real work drags in, and it makes the terminal label meaningful — reaching the end of the chain becomes a recorded fact rather than something inferred from a response body. ## Variants worth having - **Ordering under a short-circuit.** Make one middle element refuse instead of delegating, and assert the recorded sequence stops there: the inner elements' labels and the terminal label are absent, and the outer elements' outbound labels still appear because those calls were already on the way in. - **Ordering per route group.** Where hooks can be attached to a subtree rather than globally, run the same recorded request against two paths and assert the two sequences differ in exactly the expected element. - **Ordering as a stated contract.** Some teams keep the expected sequence as a single named constant that the test compares against, so that changing the order is a deliberate edit to a declared list rather than an accident that nothing noticed. ## Common ways this test gets written badly - **Hand-assembling the chain in the test.** The most common flaw: it converts a test of the application's registration into a test of the test's own literal. - **Asserting the set instead of the sequence.** 'All the expected hooks ran' is true of every permutation, which is precisely the thing under test. - **Expecting the outbound labels in the same order as the inbound ones.** In a wrapping chain they mirror; writing the expectation as a straight repeat produces a test that fails against correct code, and then gets 'fixed' by weakening it. - **Letting one recorder leak across tests.** A shared mutable list must be fresh per test, or the second test reads the first one's labels and the sequence assertion becomes noise.

  • Why assemble the chain through the application's registration code rather than listing the hooks in the test?
    Because a hand-written list turns the test into a comparison of the literal with itself. Registration order, configuration, and priority values are exactly where ordering faults live, so the test must run through them to be able to fail.
  • One middle hook refuses instead of delegating. What sequence do you now expect?
    The inbound labels up to and including that hook, no terminal label, none of the inner hooks' labels, then the outbound labels of the hooks that had already delegated, in mirrored order. It is the same assertion with a shorter expected list.
  • What makes 'the recorded list contains every expected label' a weak assertion?
    It is satisfied by every permutation of the chain, which is the whole property under test. Only equality against the full ordered sequence fails on a swap, a duplicate, or a missing element.

saying these in an interview costs you the question

  • Hand-assembling the chain in the test, so a registration fault cannot surface.
  • Asserting which hooks ran instead of the order they ran in.
  • Expecting a wrapping chain's outbound phase in the same order as its inbound phase.
  • Insisting an ordering test needs a real handler at the end of the chain.
  • Reusing one recorder list across tests, so earlier labels leak into later assertions.
  • Checking only that the first hook ran before the last one.