Collaboration and Communication

EARS: Requirements Your Firmware Team Can Actually Test

jrhaws October 24, 2026 20 min read

"The controller shall stop the pump quickly if the motor current is too high." Every reviewer nods at that sentence. Then someone has to write the test, and finds there’s nothing to test. How quickly? What’s too high? For how long? Which controller?

EARS, the Easy Approach to Requirements Syntax, fixes that with a handful of sentence patterns. Alistair Mavin and colleagues at Rolls-Royce developed it while untangling airworthiness regulations for a jet engine’s control system, and published it in 2009. It’s still plain English, but every requirement takes the same shape, and that shape makes the gaps obvious.

This post walks through the patterns, rewrites a realistic firmware draft with them, and checks the result with a small linter. Then it shows how each clause turns into part of a test.

TL;DR

  • Every EARS requirement has the same shape: While precondition, when trigger, the system shall response, in that order.
  • The keyword tells you the pattern: none (always), While (state), When (event), Where (optional feature), If/then (unwanted behavior), or a combination.
  • Writing in EARS forces the decisions a vague requirement hides: the threshold, the duration, the deadline.
  • A simple linter catches the usual problems: two "shall"s, no trigger, conditions after the response, vague words.
  • Each clause maps to part of a test: While is setup, When or If is the stimulus, the response is the assertion.

The patterns

The generic shape comes straight from Mavin’s EARS page: zero or more preconditions, zero or one trigger, one system name, and one or more responses. The clauses always appear in the same order, because that’s the order things happen in.

The EARS template: optional preconditions, an optional trigger, the system name, and the response, always in that order; six patterns come from which clauses are presentTop: the generic EARS sentence, While precondition, when trigger, the system name shall system response, with an arrow showing the clauses always appear in that order. Below, six patterns: ubiquitous with no keyword; state-driven with While; event-driven with When; optional feature with Where; unwanted behaviour with If and then; complex, which combines keywords. Each has a pump controller example.One sentence shape, six patternsClauses always appear in the same order: state, then trigger, then the system and its response.While<precondition>,zero or morewhen<trigger>,zero or onethe<system>oneshall<response>one or moreubiquitous(none)The pump controller shall show the tank level in percent.state-drivenWhileWhile maintenance mode is active, … shall ignore network starts.event-drivenWhenWhen the operator presses STOP, … shall stop the pump within 100 ms.optional featureWhereWhere a cellular modem is fitted, … shall send each alarm by SMS.unwanted behaviourIf … thenIf the motor current exceeds 12 A for 200 ms, then … shall stop the pump.complexcombinedWhile the pump is running, if … then … shall raise a dry-run alarm.Patterns from Alistair Mavin's EARS (Rolls-Royce, published 2009). Examples: this post's pump controller.
One template, six patterns. Which keywords appear tells you which pattern you’re writing.

Two details do most of the work:

  • Name the system in every requirement. "The pump controller shall…", not "the system", not "the pump", and not a passive "the pump shall be stopped." Each requirement is then clearly about one thing you can test.
  • Unwanted behavior gets its own keyword. Faults, bad input, and failures use If … then, so you can find every fault-handling requirement by searching for one word, and notice when there aren’t many.

A draft, linted

Here’s a first draft for a borehole pump controller, the kind of thing that gets written in a meeting:

PMP-001: The controller shall stop the pump quickly if the motor current is too high.
PMP-002: The pump shall start when the tank level drops below 20% and shall stop at 90%.
PMP-003: If the inlet pressure stays below 0.2 bar for 5 s, the pump controller shall stop the pump and raise a dry-run alarm.
PMP-004: The system should log faults.
PMP-005: The display shall be user-friendly.
PMP-006: While maintenance mode is active, the pump controller shall ignore start commands received over the network.
PMP-007: When the operator presses STOP the pump controller shall stop the pump within 100 ms.
PMP-008: Where a cellular modem is fitted, the pump controller shall send each alarm by SMS within 60 s.Code language: plaintext (plaintext)

Reviewing for structure is tedious and easy to automate, so here’s a small linter that does it. It splits off the leading clauses, finds "the system shall response", and checks the parts. Its core is two regular expressions:

CLAUSE = re.compile(r"(while|when|where|if)\b(.*?),\s*(?=(?:while|when|where|if|then|the)\b)", NOCASE)
MAIN = re.compile(r"(then\s+)?the\s+(.+?)\s+shall\s+(.+?)\.?$", NOCASE)Code language: Python (python)

On the draft:

$ python3 ears_lint.py --system 'pump controller' reqs-draft.txt; echo "exit $?"
PMP-001   ubiquitous          subject is 'controller', not the pump controller
                              'if' after the response: move the condition to the front
                              vague: 'quickly'
                              vague: 'too high'
PMP-002   ubiquitous          2 x 'shall': split into 2 requirements
                              subject is 'pump', not the pump controller
                              'when' after the response: move the condition to the front
PMP-003   unwanted behaviour  'If' needs 'then' before the main clause
PMP-004   ubiquitous          no 'shall' (uses 'should')
PMP-005   ubiquitous          subject is 'display', not the pump controller
                              vague: 'user-friendly'
PMP-006   state-driven        ok
PMP-007   (unparsed)          can't find where the condition ends: put a comma before the main clause
PMP-008   optional feature    ok
6 of the requirements need work
exit 1Code language: Shell Session (shell)

Look at how PMP-001 and PMP-002 are classified: "ubiquitous", meaning always active. That’s wrong, and it’s the most useful thing in the output. Both have a condition tacked on after the response, where it reads like an afterthought, so the linter can’t see it as a trigger. Putting the condition first, as EARS requires, makes the trigger impossible to miss and hard to leave vague.

The rewrite

Fixing the structure forces the real work. "Quickly" and "too high" have to become numbers, and the draft never said what they were. Someone who knows the motor has to decide. That decision is the point; the syntax just makes it unavoidable.

PMP-001: If the motor current exceeds 12 A for more than 200 ms, then the pump controller shall stop the pump within 50 ms.
PMP-002: When the tank level falls below 20 %, the pump controller shall start the pump.
PMP-009: When the tank level rises above 90 %, the pump controller shall stop the pump.
PMP-003: While the pump is running, if the inlet pressure stays below 0.2 bar for 5 s, then the pump controller shall stop the pump and raise a dry-run alarm.
PMP-004: When the pump controller detects a fault, the pump controller shall record the fault code and time in the fault log.
PMP-005: The pump controller shall show the tank level on the display in percent.
PMP-006: While maintenance mode is active, the pump controller shall ignore start commands received over the network.
PMP-007: When the operator presses STOP, the pump controller shall stop the pump within 100 ms.
PMP-008: Where a cellular modem is fitted, the pump controller shall send each alarm by SMS within 60 s.Code language: plaintext (plaintext)

A few changes beyond the obvious:

  • PMP-002 became two requirements, because "start at 20 %" and "stop at 90 %" can pass or fail separately. The new one gets a new ID; the old ID keeps its meaning.
  • PMP-003 gained a precondition. The draft would raise a dry-run alarm with the pump off, whenever the inlet pressure was low. That’s a design question the original wording hid.
  • PMP-005 changed meaning. "User-friendly" can’t be verified, so it became something that can. If usability matters, it needs its own measurable requirements, not one adjective.
$ python3 ears_lint.py --system 'pump controller' reqs-ears.txt; echo "exit $?"
PMP-001   unwanted behaviour  ok
PMP-002   event-driven        ok
PMP-009   event-driven        ok
PMP-003   complex             ok
PMP-004   event-driven        ok
PMP-005   ubiquitous          ok
PMP-006   state-driven        ok
PMP-007   event-driven        ok
PMP-008   optional feature    ok
all requirements follow EARS
exit 0Code language: Shell Session (shell)

It also checks clause order, since EARS puts the state before the trigger:

$ python3 ears_lint.py --system 'pump controller' /tmp/misordered.txt
PMP-010   complex             'While' must come before 'When'/'If'
1 of the requirements need workCode language: Shell Session (shell)

Passing the linter means the requirements are well-formed, not that they’re right. It can’t tell you whether 12 A is the correct limit or whether two requirements contradict each other. That’s still a review. Because the linter exits non-zero, though, you can run it in CI on the requirements file the same way you’d lint code.

Every clause is part of a test

An EARS requirement is close to a test plan already. The precondition is the setup, the trigger is the stimulus, and the response is what you assert:

Each EARS clause maps to a part of a test: the While clause is the setup, the If or When clause is the stimulus, the response is the assertion, and numbers become boundary casesPMP-003 split into its clauses. While the pump is running maps to the test's setup: start the pump. If the inlet pressure stays below 0.2 bar for 5 s maps to the stimulus: feed low pressure for 5 s. Then the pump controller shall stop the pump and raise a dry-run alarm maps to the assertions. The numbers in the clauses become extra cases: 4.9 s must not trip, and a stopped pump must not raise the alarm.An EARS requirement is already a test planPMP-003, clause by clause, and where each one lands in test_pump.py.While the pump is running,setupctl.start()if the inlet pressure stays below 0.2 bar for 5 s,stimulusrun_low_pressure(ctl, 5000)then the pump controller shall stop the pumpassertassert not ctl.runningand raise a dry-run alarm.assertassert ctl.alarms == ["dry-run"]The numbers are test cases too: 4.9 s of low pressure must not trip it,and a stopped pump (While clause false) must not raise the alarm.
PMP-003, clause by clause. The numbers in the requirement give you the boundary cases for free.

Here it is against a toy model of the controller, with tests named for the requirement they verify:

def test_pmp_003_dry_run_stops_pump_after_5_s():
    ctl = PumpController()
    ctl.start()                    # While the pump is running,
    run_low_pressure(ctl, 5000)    # if the inlet pressure stays below 0.2 bar for 5 s,
    assert not ctl.running         # then the pump controller shall stop the pump
    assert ctl.alarms == ["dry-run"]  # and raise a dry-run alarm.


def test_pmp_003_does_not_trip_before_5_s():
    ctl = PumpController()
    ctl.start()
    run_low_pressure(ctl, 4900)
    assert ctl.running and not ctl.alarms


def test_pmp_003_needs_the_pump_running():
    ctl = PumpController()         # pump stopped: the While clause is false
    run_low_pressure(ctl, 6000)
    assert not ctl.alarmsCode language: Python (python)
$ python3 -m pytest -v test_pump.py 2>&1 | grep -E 'PASSED|FAILED|passed|failed'
test_pump.py::test_pmp_003_dry_run_stops_pump_after_5_s PASSED           [ 25%]
test_pump.py::test_pmp_003_does_not_trip_before_5_s PASSED               [ 50%]
test_pump.py::test_pmp_003_needs_the_pump_running PASSED                 [ 75%]
test_pump.py::test_pmp_006_maintenance_ignores_network_start PASSED      [100%]
============================== 4 passed in 0.01s ===============================Code language: Shell Session (shell)

Since the linter already splits each requirement into its parts, it can print them as a starting point for a requirement-to-test matrix. The empty last column is where test names go:

$ python3 ears_lint.py --system 'pump controller' reqs-ears.txt --csv
id,pattern,precondition,trigger,response,test
PMP-001,unwanted behaviour,,the motor current exceeds 12 A for more than 200 ms,stop the pump within 50 ms,
PMP-002,event-driven,,the tank level falls below 20 %,start the pump,
PMP-009,event-driven,,the tank level rises above 90 %,stop the pump,
PMP-003,complex,the pump is running,the inlet pressure stays below 0.2 bar for 5 s,stop the pump and raise a dry-run alarm,
PMP-004,event-driven,,the pump controller detects a fault,record the fault code and time in the fault log,
PMP-005,ubiquitous,,,show the tank level on the display in percent,
PMP-006,state-driven,maintenance mode is active,,ignore start commands received over the network,
PMP-007,event-driven,,the operator presses STOP,stop the pump within 100 ms,
PMP-008,optional feature,a cellular modem is fitted,,send each alarm by SMS within 60 s,Code language: Shell Session (shell)

What EARS doesn’t do

EARS shapes individual sentences. It won’t tell you whether you have all the requirements you need, and it doesn’t record why each one says what it says. Keep a short rationale next to each requirement (why 12 A, where the 5 s came from), so the next person can change a number without guessing. And interface details, such as message formats, register maps, and timing on a bus, belong in an interface control document that the requirements point to, not in a hundred "shall" sentences.

Wrap-up

  • Write every requirement as: While state, when trigger, the system shall response.
  • Use If/then for faults and bad input, so you can find all of them.
  • Name the system, use one "shall" per requirement, and put conditions first.
  • Expect the rewrite to raise questions. The numbers the draft left out are decisions someone has to make.
  • Lint requirements like code, then map each clause to a test’s setup, stimulus, and assertion.

Which requirement in your current spec would fail this linter first?


Keep going

If you want to take requirements further, into a model that traces each one to the design and the tests, that’s where The Model Is Not the Code picks up, the book I’m writing on SysML v2 for software engineers.

If you’d like help with a spec now, the Requirements & SOW Package delivers up to 60 testable requirements with rationale and a verification method for each, and the Test & Traceability Package maps the requirements you have to the tests you have and reports the gaps. See the Services page or get in touch.

Tags firmware requirements systems engineering testing

Leave a Reply

Your email address will not be published. Required fields are marked *