Skip to the content
FixTheRobot Explore Robotics / UAV / Module 8 / M8-T6

UAV Systems Engineering · Module 8

M8-T6: Test-case development

A test case is an experiment: it fixes what you do, what you expect, and how you decide, before you look at the data.

  • Lesson 6 of 18
  • Safety, Verification and Flight Testing
  • Established engineering
  • About 6 minutes to read

Good test cases share one trait: the verdict is decided by a rule written before the run. That requires preconditions, a stimulus that can be repeated, a numerical criterion, a statistic and a number of repetitions that supports it. For a navigation system the most valuable cases inject faults, because nominal operation tells you little about safety.

Learning objectives

After this lesson, you should be able to:

  • Write a complete test case with preconditions, stimulus, expected result, criterion, repetitions and evidence.
  • Design fault-injection stimuli for sensors and links and know the injection points.
  • Choose pass criteria that use the right statistic and sample size.
  • Implement a test case as an executable, seeded, deterministic regression test.

Conceptual explanation

Established The structure is standard in software and systems testing. The example scenario uses a toy estimator to illustrate the method: it is not a model of any particular flight stack.

A test case has seven parts:

undefined
PartContentExample
ID and traced requirementLink to the requirement it verifiesT-101 verifies R-10
PreconditionsBaseline, configuration, vehicle state, environmentBaseline v1.4, VIO fused, hover at 3 m, lit scene
StimulusScenario and injected faults, with timesDisable GNSS at t=30 st=30\,\mathrm{s}; fly a 20 m square; blank the camera for 2 s at t=60 st=60\,\mathrm{s}
ObservationsSignals recorded, rates, reference sourceEstimate, reference trajectory, innovations, mode
Expected result and criterionQuantity, threshold, statisticp95 horizontal error ≤ 2 m over the segment
RepetitionsNumber of runs and seeds or trials45 runs with different seeds
Verdict rulesPass, fail, and invalid (test did not execute as specified)Invalid if the reference is missing for > 1 s

Fault injection places a fault at a defined point in the data path: in the simulator (sensor model), between driver and consumer (a wrapper that drops, freezes or corrupts messages), in the network (delay, loss), or physically (disconnecting a cable on the ground). Choose fault types from the FMEA: dropout, freeze (stuck value), bias step, noise increase, delay, saturation, wrong frame.

Anatomy of a test caseA test case fixes the preconditions, the stimulus (including any injected fault), the expected result with a numerical pass criterion and a statistic, the number of repetitions, and the evidence to record. Executed automatically, it becomes a regression test.Preconditionbaseline, stateStimulusscenario, faultObservationwhat is loggedPass criterionnumber + statisticVerdictpass / fail / invalidA test with no numerical criterion is a demonstration, not a verification.
A test case is a contract fixed before the run.
Animation of repeated runs of one test case. In each run a position-hold error is recorded for two thousand samples. The mean of every run is far below the limit but the maximum of some runs exceeds it. Each run appears as a dot at its mean and a bar to its maximum; green runs pass the requirement on the maximum, red runs fail it. The verdict from the average would be pass for every run; the verdict from the maxima is a fail rate that depends on how many runs are flown.
Requirements are usually about worst-case values, so the verdict must be computed on the quantity the requirement names. The distribution of the run maxima, not of the samples, decides how many runs are needed. Illustrative zero-mean Gaussian error with σ=0.1 m\sigma =0.1\,\mathrm{m} and a 0.40 m limit on the absolute error; seeded runs. (Looping, about 5 seconds; drawn and computed for this course with illustrative values.)

Engineering intuition

The temptation after a run is to look at the plot and decide whether it “looks okay”. A written criterion removes that bias. It also protects you from the opposite problem: a result that looks bad but meets the criterion (noise in a plot) or that looks good but fails it on the tail. The “invalid” verdict matters as much as pass and fail: a run in which the reference system dropped out proves nothing and must not count toward either.

Seed everything random and record the seed with the result. A failure you cannot reproduce cannot be fixed; a failure with a seed becomes a unit test.

Mathematical formulation

For a continuous error metric the pass criterion is usually on a percentile of the error over runs or time. To verify “the 95th percentile of the error is below L” with confidence from nn independent run-level maxima or run-level metrics, count the runs exceeding LL: the claim is supported if the number of exceedances xx is small enough that the Clopper-Pearson upper bound on the exceedance probability is below 5 %:

Binomial upper bound on the exceedance probability
pexceed,upper=Beta−1(1−α; x+1, n−x)≤0.05p_{\text{exceed,upper}} = \mathrm{Beta}^{-1}(1 - \alpha;\ x + 1,\ n - x) \le 0.05

For x=0x=0 this needs n≥ln⁡ α/ln⁡ 0.95n\ge \ln \,\alpha /\ln \,0.95 trials (45 at α=0.10\alpha =0.10, 59 at α=0.05\alpha =0.05). A metric defined per sample (error at 10 Hz for 5 minutes) has strongly correlated samples: the effective sample size is much smaller than the sample count, so use a run-level statistic or a block bootstrap, not the raw count. For fault-detection requirements (“detect within 0.5 s”) the criterion is on the detection delay distribution and on the probability of detection, with a false-alarm limit measured over fault-free runs.

α\alpha
significance level.
xx, nn
number of exceedances and of runs.
LL
error threshold in the requirement.

Architecture and data flow

Test case file (YAML) → harness (SIL/HIL runner or flight card) → baseline loaded and verified → scenario with seeded fault injection → recorded logs → analysis function computing the metric → verdict logic (pass, fail, invalid) → result record with baseline ID, seed, data pointer → traceability matrix. In CI, the same harness runs a reduced set per commit and the full set per release.

Real-world UAV applications

  • Verifying the GNSS-loss failsafe timing.
  • Verifying drift bounds under VIO degradation.
  • Regression tests that catch estimator behaviour changes.
  • Fault-injection campaigns derived from the FMEA.

Implementation example

This runs as shown. An executable test case: a 1D position estimator (a Kalman filter fusing GNSS position and a drifting odometry velocity), a seeded GNSS-dropout injection, and a verdict function with a run-level p95 criterion, an invalid-run rule and the zero-failure sample size. The toy estimator stands in for the system under test.

import numpy as np, math

def run_case(seed, drop_at=30.0, T=120.0, dt=0.1, vo_bias_sd=0.01):
    rng = np.random.default_rng(seed); n = int(T / dt); x_true = 0.0; v = 1.0
    xh = np.array([0.0, v]); P = np.diag([1.0, 0.1]); F = np.array([[1, dt], [0, 1]]); Q = np.diag([1e-4, 1e-4]) * dt
    vo_bias = rng.normal(0, vo_bias_sd); err = []; ref_ok = True
    for k in range(n):
        t = k * dt; x_true += v * dt
        xh = F @ xh; P = F @ P @ F.T + Q
        if t < drop_at and k % 10 == 0:                                 # GNSS at 1 Hz, until the injected dropout
            H = np.array([[1.0, 0.0]]); S = H @ P @ H.T + 4.0; K = P @ H.T / S
            xh = xh + (K * (x_true + 2.0 * rng.standard_normal() - xh[0])).ravel(); P = (np.eye(2) - K @ H) @ P
        if k % 2 == 0:                                                  # visual odometry velocity at 5 Hz, with a constant bias
            H = np.array([[0.0, 1.0]]); S = H @ P @ H.T + 0.05 ** 2; K = P @ H.T / S
            xh = xh + (K * (v + vo_bias + 0.05 * rng.standard_normal() - xh[1])).ravel(); P = (np.eye(2) - K @ H) @ P
        if t >= drop_at: err.append(abs(xh[0] - x_true))
    return np.array(err), ref_ok
def verdict(errs, L=3.0, req_p=0.95, alpha=0.10):
    # run-level metric: the maximum error in the denied segment; criterion: <= L in at least req_p of runs (zero-failure demonstration)
    n_needed = math.ceil(math.log(alpha) / math.log(req_p)); maxima = np.array([e.max() for e in errs])
    x = int((maxima > L).sum())
    if len(errs) < n_needed: return 'INVALID (too few runs: need %d)' % n_needed, maxima
    return ('PASS' if x == 0 else 'FAIL (%d of %d runs exceeded %.1f m)' % (x, len(errs), L)), maxima
runs = [run_case(s)[0] for s in range(45)]
v1, maxima = verdict(runs)
print(f"45 runs: median of run maxima {np.median(maxima):.2f} m, worst {maxima.max():.2f} m -> {v1}")
v2, _ = verdict(runs[:20]); print("20 runs ->", v2)
assert v2.startswith('INVALID') and math.ceil(math.log(0.10) / math.log(0.95)) == 45
v3, _ = verdict([run_case(s, vo_bias_sd=0.2)[0] for s in range(45)]); print("with a larger odometry bias (0.2 m/s sd):", v3)
assert v1 == 'PASS' and v3.startswith('FAIL')
An executable fault-injection test case: seeded GNSS dropout, a run-level criterion, an INVALID verdict for too few runs, and the zero-failure sample size. The estimator is a toy stand-in.

Common engineering mistakes

Deciding the criterion after seeing the data

Write the pass rule before the run.

Using raw sample counts for correlated data

Effective sample size is far smaller; use run-level metrics or block statistics.

No invalid verdict

Runs where the reference or logging failed must not count.

Unseeded fault injection

A failure that cannot be replayed is hard to fix.

Only nominal cases

The value is in faults and edges; derive them from the FMEA.

Failure modes

Failure modes, symptoms and causes
Failure modeHow it shows upLikely cause
Flaky testPasses and fails at randomUnseeded randomness or timing dependence
False confidencePasses with few runsSample size below what the statistic needs
Undetected defectFailure not coveredNo fault-injection case for that failure mode
Disputed verdictArgument over a borderline runCriterion or statistic not specified

Debugging approach

  1. Re-run the failing seed with logging on and plot the estimate against the reference.
  2. Check the preconditions that were actually met against those specified.
  3. Compare run-level metrics between baselines to spot regressions.
  4. Review injected faults against the FMEA for coverage.

Practical exercises

  1. Write the test case table for a VIO dropout of 2 s with a detection-time requirement of 0.5 s.
  2. Compute the number of runs for 99 % at 90 % confidence with zero failures.
  3. Extend the example with a 10 s GNSS spoof (a constant 5 m offset) and decide the criterion.
  4. Add an invalid-run rule for missing reference data.

Interview questions

Write an answer in your own words, then reveal the model answer. Nothing is sent anywhere.

Question 1

What is the difference between fail and invalid?

Model answer

Fail means the system did not meet the criterion in a correctly executed test; invalid means the test did not execute as specified (missing reference, wrong baseline), so it says nothing about the system and must be rerun.

Question 2

Why not count every sample toward the statistics?

Model answer

Samples in a time series are strongly correlated, so the effective sample size is much smaller; use run-level metrics or block methods for the sample size.

Question 3

Why seed fault injection?

Model answer

To reproduce failures exactly, turn them into regression tests and attribute differences to code changes rather than chance.

References and further reading

Primary papers, official documentation and standards. Each claim above that is specific to a tool or version points here.

What’s next

Mark this lesson

Saved in this browser and synced to your account when you are signed in.

Next lesson

M8-T7: FMEA

Perform an FMEA for a UAV navigation subsystem, rank failure modes severity-first and link each to a verification.