Tutorials › Multi-Arm Multi-Stage (MAMS) Designs › Unit Tests for Interim Decision Logic

Simulation and Software · Tutorial 449 of 499

Unit Tests for Interim Decision Logic

Build small, transparent tests that confirm whether interim rules drop arms, declare efficacy, or continue exactly as specified.

Intermediate 10 min read

What You'll Learn

  • Specify an explicit contract for arm-dropping and trial-stopping functions.
  • Distinguish strict futility boundaries from inclusive efficacy boundaries.
  • Construct tests below, at, and above each threshold.
  • Check combined outcomes when several arms have different interim decisions.
  • Use logical properties to expose inconsistent or misapplied decision rules.
  • Recognize what unit tests establish and what they cannot establish.

Why Test Interim Decision Logic Directly?

A MAMS simulation repeatedly applies interim rules to statistics calculated for several treatment arms. A small coding error in a comparison, such as using the wrong inequality at a boundary, can alter which arms continue and whether the trial stops. If that error is embedded in a large simulation, its consequences may be hard to locate among random variation and other code. Unit tests address a narrower question: given a deliberately chosen input, does the decision function return the result required by its stated rule?

These tests use hand-constructed values rather than simulated trial data. They are deterministic: the same inputs should always produce the same outputs. The purpose is to verify the software contract for the interim logic, not to estimate error rates, power, or expected sample size. Those operating characteristics depend on the full design and on the data-generating assumptions, not just on whether a function implements a rule correctly.

State the Decision Contract First

For a concrete example, suppose an interim function receives a statistic \(Z_j\) for each currently active arm \(j\). Assume larger values indicate stronger evidence in favour of that treatment. Let \(c_F\) be a futility boundary and \(c_E\) an efficacy boundary, with \(c_F<c_E\). This direction and ordering are part of the example’s contract; another design may use a different statistic or different decision rules.

Definition: Under the illustrative contract used here, an active arm is dropped for futility if \(Z_j<c_F\), meets the efficacy criterion if \(Z_j\geq c_E\), and otherwise continues. For the trial-level rule, any arm meeting the efficacy criterion stops the trial for efficacy; if no arm meets efficacy and no arm remains active, the trial stops for futility; in all other cases, the trial continues. The inequalities are intentional: futility is strict, while efficacy includes equality.

To keep the example precise, take \(c_F=0.50\) and \(c_E=2.50\). The decision function should reject invalid configurations such as \(c_F\geq c_E\), and it should receive a statistic for every active arm. For these unit tests, assume the arm identifiers are distinct, the statistics are finite numbers, and the input lists active arms only. These are preconditions of this example’s function, not statistical assumptions about the distribution of the statistics.

The function can return three pieces of information: the set of arms dropped for futility, the set meeting efficacy, and the trial status. Making those outputs explicit helps a test pinpoint a failure. For example, a trial status of “stop for efficacy” does not by itself verify that the correct arm was identified as meeting the boundary.

A Boundary Partition Justifies the Test Cases

The threshold comparisons divide every possible real-valued statistic into three non-overlapping regions. This gives a short justification for why tests must include values on both sides of each boundary and the boundary values themselves.

$$ Z_j<c_F,\qquad c_F\leq Z_j<c_E,\qquad Z_j\geq c_E $$

Because \(c_F<c_E\), every finite \(Z_j\) belongs to exactly one of these regions. The first region means drop for futility; the middle region means continue; the final region means efficacy. At \(Z_j=c_F\), the strict futility comparison is false, so the arm is not dropped. At \(Z_j=c_E\), the inclusive efficacy comparison is true, so the arm meets efficacy. This partition also shows why a test only with values far from the boundaries is insufficient: it can pass even if the implementation uses the wrong equality convention.

A practical unit test pairs each input with an explicit expected output. The expected output should be worked out from the written rule before running the function; otherwise, a test can simply repeat the same mistaken logic as the implementation.

Worked Example: Values Below, At, and Above Both Boundaries

Use four hypothetical active arms, A through D, with \(c_F=0.50\) and \(c_E=2.50\). Give the function \(Z_A=0.49\), \(Z_B=0.50\), \(Z_C=2.50\), and \(Z_D=2.49\). Work out each arm’s result, then the trial status.

For arm A, \(0.49<0.50\), so A is dropped for futility. For arm B, \(0.50\not<0.50\) and \(0.50<2.50\), so B continues. For arm C, \(2.50\geq2.50\), so C meets the efficacy criterion. For arm D, \(2.49<2.50\) and \(2.49\not<0.50\), so D continues.

The expected dropped-arm set is \(\{A\}\), and the expected efficacy-arm set is \(\{C\}\). Since at least one arm meets efficacy under this contract, the expected trial status is “stop for efficacy.” The classification of D remains “continue”; it is not below the futility boundary. This test checks both exact equality cases and distinguishes a statistic just below efficacy from one that meets it.

Build Tests Around Expected Outputs

A useful test set is small enough to inspect by hand but varied enough to expose common logical errors. Include separate tests for each decision category and for trial-level combinations. A simple decision table can act as a checklist.

Input conditionExpected arm decisionBoundary convention checked
\(Z_j=0.49\)Drop for futilityBelow \(c_F=0.50\)
\(Z_j=0.50\)ContinueEquality is not below \(c_F\)
\(Z_j=2.49\)ContinueBelow \(c_E=2.50\)
\(Z_j=2.50\)Meet efficacyEquality meets \(c_E\)
Every arm below \(0.50\)Stop for futilityNo arm remains active; none meets efficacy
At least one arm at or above \(2.50\)Stop for efficacyAt least one arm meets efficacy

The first four rows test the arm-level classification. The last two test the trial-level rule, which depends on the combined decisions. Tests should assert the returned arm sets as well as the status. A function that returns the right status for the wrong reason is not implementing the complete contract.

Worked Example: Confirming a Futility Stop

Suppose three hypothetical active arms have interim statistics \(Z_A=0.20\), \(Z_B=0.49\), and \(Z_C=0.30\), with the same boundaries. Calculate their decisions and the expected status.

Each value is strictly below 0.50: \(0.20<0.50\), \(0.49<0.50\), and \(0.30<0.50\). None is at least 2.50. Therefore, the expected dropped-arm set is \(\{A,B,C\}\), the efficacy-arm set is empty, and no arm remains active. The trial status is “stop for futility.”

This case checks that the stop function recognizes the all-arms-dropped condition. A test that checks only the dropped-arm set would not catch a separate error in the trial-level status logic.

Worked Example: Continuing With Mixed Arm Decisions

Suppose the active-arm statistics are \(Z_A=0.30\), \(Z_B=0.50\), and \(Z_C=2.49\). Determine all expected outputs.

Arm A is dropped because \(0.30<0.50\). Arm B continues because \(0.50\not<0.50\) and \(0.50<2.50\). Arm C continues because \(2.49<2.50\) and \(2.49\not<0.50\). Thus the dropped-arm set is \(\{A\}\), the efficacy-arm set is empty, and B and C remain active. The trial status is “continue.”

The test is useful because it combines a futility decision with continuing arms. It can reveal code that incorrectly stops the entire trial as soon as any arm is dropped, even though the stated rule only stops for futility when no arm remains active.

Useful Properties and Failure Diagnostics

Alongside specific input-output tests, check simple logical properties implied by the contract. These do not replace hand-worked examples; they broaden the ways an inconsistency can be detected.

  • Mutual exclusivity: no arm can be both dropped for futility and meet efficacy, because \(c_F<c_E\).
  • Completeness: every valid statistic is classified as dropped, continuing, or meeting efficacy.
  • Boundary equality: \(Z_j=c_F\) is not a futility drop, while \(Z_j=c_E\) meets efficacy.
  • Arm-order invariance: reordering the input arms should reorder corresponding outputs, not change which named arms are selected or the trial status.
  • Monotonicity within this rule: increasing one arm’s statistic cannot turn an efficacy result into a non-efficacy result; decreasing it cannot turn a futility result into a non-futility result.

When a test fails, inspect the contract and the smallest failing input first. A wrong result at \(Z_j=0.50\) often indicates that the code used \(Z_j\leq c_F\) instead of \(Z_j<c_F\). A wrong result at \(Z_j=2.50\) often indicates that it used \(Z_j>c_E\) rather than \(Z_j\geq c_E\). If arm-level decisions are correct but the trial status is wrong, examine the aggregation rule: “any efficacy” and “no active arms remain” are distinct conditions in this example.

Use exactly representable boundary values such as 0.50 and 2.50 for direct equality tests. Do not add a tolerance to the decision comparison unless the design’s rule explicitly defines one: a tolerance changes the rule being tested. If statistics are calculated by preceding numerical code, separate tests can check those calculations; the unit tests here supply the statistic directly so that the decision function is tested in isolation.

What Unit Tests Do—and Do Not—Establish

A unit test checks a small function against a specified input-output contract. It is especially effective for branch logic, boundary conventions, and combinations of arm-level decisions. It is not evidence that the chosen boundaries are scientifically appropriate, that the test statistic has the intended distribution, or that the complete simulation produces correct operating characteristics.

Compared with running a large simulation, hand-built unit tests are easier to diagnose because the expected answer is known without Monte Carlo uncertainty. Compared with testing only an end-to-end simulation, they localize an error to a function and a particular decision branch. But they cover only the cases chosen. A function can pass several examples and still fail on an untested input, so use the decision partition and stated properties to design a compact but purposeful test set.

These checks also depend on a clearly specified contract. Some MAMS designs use different stopping conventions, such as stopping only when a particular set of hypotheses meets criteria. The illustrative “any arm meets efficacy” rule above must not be silently substituted for a protocol-specific rule. Translate the actual design’s logic into explicit conditions first, then construct tests for those conditions.

Key takeaway: A reliable interim-logic test starts from an explicit rule, supplies hand-constructed inputs on both sides of every threshold and exactly at each threshold, and asserts both arm-level decisions and the trial-level status.

Check Your Understanding

Use \(c_F=0.50\) and \(c_E=2.50\) with the decision contract stated above. Answer each question, then check the solutions.

  1. What is the arm-level decision when \(Z_j=0.50\), and which comparison determines it?
  2. For two active arms with statistics 0.10 and 0.40, what are the dropped-arm set and trial status?
  3. For three arms with statistics 0.20, 2.50, and 2.49, what is the trial status and which arm meets efficacy?
  4. Why should a test include \(Z_j=2.50\), rather than testing only values such as 2.40 and 2.60?
  5. If all arm-level decisions are correct but the status is “continue” when every arm was dropped, which part of the function should be examined?

Solutions. (1) The arm continues: \(0.50\not<0.50\), and it is below 2.50. (2) Both arms are dropped because \(0.10<0.50\) and \(0.40<0.50\); with none meeting efficacy and none remaining active, the status is “stop for futility.” (3) The status is “stop for efficacy”; the second arm meets efficacy because \(2.50\geq2.50\). (4) The exact boundary test checks whether equality is treated as specified; tests on either side alone can miss a strict-versus-inclusive comparison error. (5) Examine the trial-level aggregation and stopping logic, especially its all-arms-dropped condition.

Clinical Biostats