Home

04 Requirements Engineering: From Tests to Specifications

lecture requirements specifications stakeholders user-stories agile

1. Introduction: Your Tests Are Green, But What Are You Actually Building?

Where you are so far:

From Chapter 03 (Testing Theory and Coverage), you discovered something uncomfortable: when you tried to improve test coverage for find_intersection(), you kept running into questions like:

You realized that coverage gaps reveal requirements gaps. You can’t write good tests if you don’t know what the software is supposed to do.

The uncomfortable truth:

Throughout this course, you’ve been writing tests against implicit requirements - docstrings, type hints, code comments, and your own assumptions. But real software projects need explicit requirements that everyone agrees on.

Today’s question: How do we systematically capture, document, and validate what we’re building - before we write the tests?


2. Learning Objectives

By the end of this lecture, you will:

  1. Understand what requirements are and why they matter for software quality
  2. Distinguish functional vs non-functional requirements with concrete examples
  3. Identify stakeholders and understand their different perspectives
  4. Apply quality criteria to requirements (testable, measurable, unambiguous)
  5. Use modern tools for requirements: User Stories, GitHub Issues, Acceptance Criteria
  6. Link requirements to tests for traceability

Continued in Part 2:

What you WON’T learn yet:


3. Requirements at the Code Level: Where Chapter 03 Left Off

Before we zoom out to business requirements, let’s start where we left off - at the code level.

3.1 Implicit Requirements in Code

In Chapter 03 (Testing Theory and Coverage), you discovered these implicit requirements hidden in find_intersection():

def find_intersection(
    x_road: NDArray[np.float64],  # Implicit: expects numpy array of floats
    y_road: NDArray[np.float64],  # Implicit: must be same length as x_road
    angle_degrees: float,          # Implicit: probably 0-360, but not enforced
    camera_x: float = 0,
    camera_y: float = 1.5,
) -> tuple[float | None, float | None, float | None]:
    """
    Find the intersection point between the camera ray and the road profile.

    Returns:
        tuple of (x, y, distance) or (None, None, None) if no intersection
    """

What’s missing?

3.2 Design by Contract: Making Requirements Explicit

Design by Contract (DbC) is a programming methodology where you explicitly state:

Example: Explicit requirements for find_intersection() (the comment approach)

def find_intersection(
    x_road: NDArray[np.float64],
    y_road: NDArray[np.float64],
    angle_degrees: float,
    camera_x: float = 0,
    camera_y: float = 1.5,
) -> tuple[float | None, float | None, float | None]:
    """
    Find the intersection point between the camera ray and the road profile.

    Preconditions (Requirements on input):
        - len(x_road) == len(y_road) >= 2
        - x_road values are monotonically increasing (sorted left to right)
        - angle_degrees is in range [0, 360) degrees
        - camera_y > max(y_road) (camera is above the road)

    Postconditions (Guarantees on output):
        - If intersection exists: returns (x, y, dist) where:
            - x is in range [min(x_road), max(x_road)]
            - y is the interpolated road height at x
            - dist > 0 is the Euclidean distance from camera to intersection
        - If no intersection: returns (None, None, None)

    Special cases:
        - Vertical ray (angle = 90 or 270): returns (None, None, None)
        - All road segments behind camera: returns (None, None, None)
        - Ray parallel to road segment: uses segment start point
    """

This is requirements engineering at the function level.

Each precondition and postcondition is a testable requirement. Each special case is an equivalence class you learned in Chapter 03 (Testing Fundamentals).

3.3 The Problem with Comments: They Lie

Important Disclaimer: The docstring approach shown above is not how we want you to do Design by Contract in modern code. We show it because:

  1. You will encounter this pattern in real-world codebases, especially older ones
  2. Many textbooks and tutorials still teach this approach
  3. Understanding its problems helps you appreciate better alternatives

Do not adopt this pattern for new code. There are better ways, as we’ll see next.

The docstring approach above looks professional, but it has serious problems:

Problem 1: Comments make code harder to read

Every function now starts with 20+ lines of documentation before you see any actual code. In a large codebase, this becomes overwhelming - you’re swimming in comments just to understand what the code does.

Problem 2: Comments get outdated

Nothing forces the docstring to stay synchronized with the code. When you refactor or add features, do you always update the docstring? Be honest. Most developers don’t - and now your “contract” is a lie.

Problem 3: Comments don’t prevent bugs

The docstring says len(x_road) == len(y_road), but what happens if someone passes arrays of different lengths? The code will crash somewhere with a confusing error, not at the point where the contract was violated.

3.4 Better Approach: Let the Type System Enforce Requirements

Instead of documenting requirements in comments, we can enforce them through better data representation.

Example: Enforcing x_road and y_road have the same length

The current signature allows this bug:

# BUG: Arrays have different lengths - but the type system allows it!
find_intersection(
    x_road=np.array([0, 10, 20]),
    y_road=np.array([0, 5])  # Oops, missing one element
)

Better design: Use a single array of (x, y) tuples

import numpy as np
from numpy.typing import NDArray

# Road profile is an Nx2 array where each row is (x, y)
# x and y are inherently linked - impossible to have mismatched lengths!
RoadProfile = NDArray[np.float64]  # Shape: (N, 2)

def find_intersection(
    road: RoadProfile,  # Shape (N, 2) - each row is (x, y)
    angle_degrees: float,
    camera_x: float = 0,
    camera_y: float = 1.5,
) -> tuple[float | None, float | None, float | None]:
    # Access points as road[i, 0] for x and road[i, 1] for y
    # Or unpack: x, y = road[i]
    ...

# Usage:
road = np.array([
    [0.0, 0.0],    # Point 1: x=0, y=0
    [10.0, 5.0],   # Point 2: x=10, y=5
    [20.0, 10.0],  # Point 3: x=20, y=10
])

Now the requirement “x and y must have the same length” is impossible to violate - each row contains both coordinates together.

Example: Enforcing valid angle ranges

The current signature allows this bug:

# BUG: What does angle=500 even mean? Is it 500° or 140° (500 mod 360)?
find_intersection(x_road, y_road, angle_degrees=500)

Better design: Create an Angle class

from dataclasses import dataclass
import math

@dataclass
class Angle:
    """An angle in degrees, normalized to [0, 360)."""
    _degrees: float

    def __init__(self, degrees: float):
        # Normalize to [0, 360) on construction
        self._degrees = degrees % 360

    @property
    def degrees(self) -> float:
        return self._degrees

    @property
    def radians(self) -> float:
        return math.radians(self._degrees)

    def is_vertical(self) -> bool:
        """Returns True if angle is approximately 90° or 270°."""
        return abs(math.cos(self.radians)) < 1e-10

# Now the function signature enforces valid angles:
def find_intersection(
    road: RoadProfile,
    angle: Angle,  # Always normalized, has is_vertical() method
    camera_x: float = 0,
    camera_y: float = 1.5,
) -> tuple[float | None, float | None, float | None]:
    if angle.is_vertical():
        return None, None, None
    ...

What changed?

3.5 Runtime Checks with Asserts (Rarely the Right Choice)

Some developers use assert statements to check requirements at runtime. You’ll see this in some codebases, so let’s understand it - but this is rarely the right approach for production code:

def find_intersection(
    x_road: NDArray[np.float64],
    y_road: NDArray[np.float64],
    angle_degrees: float,
    ...
) -> tuple[float | None, float | None, float | None]:
    # Runtime requirement checks
    assert len(x_road) == len(y_road), "x_road and y_road must have same length"
    assert len(x_road) >= 2, "Road must have at least 2 points"
    assert 0 <= angle_degrees < 360, f"Angle must be in [0, 360), got {angle_degrees}"

    # ... actual implementation

Pros:

Cons (and these are serious):

Bottom line: Asserts have a very narrow use case - checking internal invariants during development and testing. They are not a requirements enforcement mechanism. Don’t rely on them for production code. If you need to validate input, use proper validation with meaningful error handling (raise ValueError with a helpful message, return an error result, etc.).

3.6 Tests as Executable Requirements

Here’s a key insight that connects requirements to testing:

Tests can serve as executable documentation of requirements.

Instead of:

We can write tests that explicitly verify each requirement:

import numpy as np
import pytest
from road_profile_viewer.geometry import find_intersection, Angle

class TestFindIntersectionRequirements:
    """Tests documenting the requirements for find_intersection()."""

    def test_req_vertical_ray_returns_none(self):
        """REQ-GEOM-001: Vertical rays shall return None."""
        road = np.array([[0, 0], [10, 5]])
        result = find_intersection(road, Angle(90), camera_x=5, camera_y=10)
        assert result == (None, None, None)

    def test_req_intersection_distance_positive(self):
        """REQ-GEOM-002: When intersection exists, distance shall be > 0."""
        road = np.array([[0, 0], [10, 5], [20, 10]])
        x, y, dist = find_intersection(road, Angle(45), camera_x=0, camera_y=15)
        assert dist is not None
        assert dist > 0

    def test_req_intersection_on_road(self):
        """REQ-GEOM-003: Intersection point shall lie on the road profile."""
        road = np.array([[0, 0], [10, 5], [20, 10]])
        x, y, dist = find_intersection(road, Angle(45), camera_x=0, camera_y=15)
        assert x is not None
        assert 0 <= x <= 20  # Within road x-range

Notice:

We’ll explore this further in Section 8: Linking Requirements to Tests, where we’ll see how to:

But first, let’s zoom out and understand what requirements are at the business level.

3.7 The Bridge: From Code Requirements to Business Requirements

Notice what happened: We started with code coverage gaps and ended up with better data types and tests that verify requirements.

The same process happens at every level:

Code Coverage Gap → Function Requirement → Module Requirement → System Requirement → Business Need

Now let’s zoom out and look at requirements from the top down.


4. What Are Requirements?

4.1 Definition

Requirement: A condition or capability needed by a user to solve a problem or achieve an objective. — IEEE Standard 610.12

More practically:

A requirement describes what the system should do (or how well it should do it), without specifying how to implement it.

4.2 Functional vs Non-Functional Requirements

Type Definition Example (Road Profile Viewer)
Functional What the system does - specific behaviors and functions "The system shall display the intersection point when the user clicks on the road profile"
Non-Functional How well the system does it - quality attributes "The intersection calculation shall complete in less than 100ms"

Non-functional requirements categories:

4.3 Example: Road Profile Viewer Requirements

Functional Requirements:

Non-Functional Requirements:

4.4 Requirement IDs: Naming Conventions and What They Mean

You might have noticed we’re using IDs like FR-1 and NFR-1 here, but earlier in Section 3.6 we used REQ-GEOM-001. Let’s clarify this.

Why do requirements need IDs?

Common ID Naming Schemes:

Scheme Example When to Use
Type-based FR-1, NFR-2 Simple projects, distinguishes functional vs non-functional
Domain-based REQ-GEOM-001, REQ-UI-005 Larger projects with multiple modules/components
Hierarchical REQ-1.2.3 Requirements that decompose into sub-requirements
Feature-based LOAD-001, CALC-002 Organizing by user-facing features

Both schemes refer to the same concept - they’re just different conventions. FR-4 and REQ-GEOM-001 both identify a specific requirement that can be tested.

Does the number imply priority or order?

No! The number is just an identifier, not a priority ranking.

If you need to indicate priority, add it explicitly:

FR-4 [Priority: High]: The system shall calculate the intersection point...
FR-5 [Priority: Low]: The system shall export results to PDF...

Or use a separate priority field in your tracking system.

4.5 Where to Store Requirements: Formats and Tools

Requirements need to live somewhere. Here are the options, from informal to formal:

Informal (avoid for anything serious):

Semi-formal (good for most projects):

Formal (regulated industries, large projects):

What should you use?

For this course and most software projects:

  1. Start with GitHub Issues - one issue per requirement or user story
  2. Use labels to categorize: requirement, FR, NFR, priority:high, etc.
  3. Link issues to PRs - when you implement a requirement, reference the issue
  4. Link issues to tests - mention the requirement ID in test docstrings

Example GitHub Issue for FR-4:

Title: FR-4: Calculate and highlight intersection point

## Description
The system shall calculate and highlight the intersection point
between the camera ray and the road profile.

## Acceptance Criteria
- [ ] Intersection point is calculated correctly
- [ ] Point is visually highlighted on the chart
- [ ] Coordinates are displayed to the user

## Priority
High - Core functionality

## Related
- Implements: REQ-GEOM-001, REQ-GEOM-002, REQ-GEOM-003
- Tests: test_geometry.py::TestFindIntersectionRequirements

We’ll explore this workflow in more detail in Section 8: Linking Requirements to Tests.


5. Stakeholders: Who Cares About Your Software?

5.1 What is a Stakeholder?

Stakeholder: Any person or organization that has an interest in or is affected by the system.

Different stakeholders have different - sometimes conflicting - requirements.

5.2 Types of Stakeholders

graph TD
    System[Software System] --- Users
    System --- Customers
    System --- Dev[Developers]
    System --- Ops[Operations]
    System --- Business
    System --- Reg[Regulators]

    Users --> EndUser[End Users]
    Users --> Admin[Administrators]

    Customers --> Buyer[Purchaser]
    Customers --> Sponsor[Project Sponsor]

    Dev --> Architect[Architect]
    Dev --> Programmer[Programmer]
    Dev --> Tester[QA/Tester]

    Business --> PM[Product Manager]
    Business --> Marketing
    Business --> Support

Important: This diagram is not exhaustive. The stakeholder roles for your project depend entirely on the domain, scope, and regulatory environment.

Stakeholder complexity varies dramatically by project type:

Project Type Typical Stakeholders Why?
Medical Device Software End users, Patients, Doctors, Hospital IT, Clinical researchers, FDA/regulatory bodies, Quality assurance, Risk management, Cybersecurity officers, Data privacy officers, Insurance companies, Legal/compliance Lives at stake → heavy regulation (FDA, IEC 62304), audit requirements, liability concerns
Short Video Game (Indie) Players, Developer(s), Publisher (if any), Platform (Steam, etc.) Entertainment focus, minimal regulation, smaller scope
Banking Application Customers, Bank employees, Regulators (BaFin, ECB), Security officers, Fraud detection, Compliance officers, Auditors Financial regulation, security requirements, audit trails mandatory

Your first task in any project: Identify who has a stake in your system. Missing a stakeholder means missing their requirements - which you’ll discover painfully late.

5.3 Stakeholder Matrix: Road Profile Viewer

Stakeholder Role Primary Concerns Example Requirement
Road Engineer End User Accuracy, ease of use "Intersection accuracy within 0.1m"
Lab Manager Customer Cost, training time "Trainable in under 1 hour"
IT Admin Operations Installation, maintenance "Single-file deployment"
Developer Internal Code quality, testability "Modular architecture for testing"
Safety Officer Regulator Reliability, audit trail "All calculations logged"

Key insight: The same feature looks different to different stakeholders. The “intersection calculation” is:


6. What Makes a Good Requirement?

Not all requirements are created equal. A bad requirement leads to:

6.1 The INVEST Criteria (for User Stories)

Letter Criterion Description Bad Example Good Example
I Independent Can be implemented without depending on other stories "After login is done, show dashboard" "Show dashboard for authenticated users"
N Negotiable Details can be discussed, not set in stone "Use blue #0000FF for buttons" "Buttons should be visually prominent"
V Valuable Delivers value to stakeholder "Refactor database layer" "Load profiles 50% faster"
E Estimable Team can estimate effort "Make the system better" "Add export to PDF feature"
S Small Can be completed in one sprint "Implement all reporting" "Generate single-profile report"
T Testable Clear criteria for "done" "System should be fast" "Response time < 200ms"

6.2 The Testability Connection

Remember Chapter 03 (Testing Fundamentals)-7? The “Testable” criterion is where requirements engineering meets testing:

Untestable requirement:

“The system should be user-friendly”

How do you write a test for this? What does the test assert? “User-friendly” means different things to different people.

Testable requirement:

“The system shall load a road profile and calculate an intersection in under 2 seconds”

Now you can write a test:

def test_load_and_calculate_performance():
    """
    Performance test: Core workflow completes within time limit.

    Requirement: NFR-PERF-001
    """
    start_time = time.time()

    app.load_profile("sample_road.csv")
    app.set_camera_angle(45.0)
    result = app.calculate_intersection()

    elapsed = time.time() - start_time
    assert elapsed < 2.0, f"Workflow took {elapsed:.2f}s, exceeds 2 second limit"
    assert result is not None, "Calculation should return a result"

The key insight: if you can’t write a test for it, it’s not a good requirement. Vague requirements like “user-friendly” need to be refined into specific, measurable criteria before they’re useful.

6.3 Requirements and the Testing Pyramid

6.3.1 What Kind of Test Did We Just Write?

Look back at the test we wrote above:

def test_load_and_calculate_performance():
    app.load_profile("sample_road.csv")
    app.set_camera_angle(45.0)
    result = app.calculate_intersection()
    assert elapsed < 2.0

This is NOT a unit test! It tests multiple components working together:

Remember the Testing Pyramid from Chapter 03 (Testing Fundamentals)?

       /   \
      / E2E \       ← Few, slow, expensive
     /-------\
    / Module  \     ← Some, moderate speed  ◄── Our test is HERE
   /-----------\
  /    Unit     \   ← Many, fast, cheap
 /---------------\

Our performance test is a module test (or integration test) - it tests components working together. This raises a question: if stakeholder requirements are tested at the module/E2E level, what are all those unit tests for?

6.3.2 Why Higher-Level Tests for Stakeholder Requirements?

The stakeholder requirement was:

“The system shall load a road profile and calculate an intersection in under 2 seconds”

This is fundamentally a system-level requirement - it spans multiple components. You cannot test this with a unit test of find_intersection() alone because:

This is why the testing pyramid exists:

6.3.3 The Implementation Gap: Where Derived Requirements Come From

When implementing FR-CALC-001: "Calculate intersection and display result", developers make decisions:

  1. Data representation: Use NDArray[(N,2)] or separate x_road, y_road?
  2. Algorithm choice: Ray-casting? Interpolation?
  3. Edge case handling: What about vertical rays? Camera below road?
  4. API design: Use Angle class or raw floats?

The customer doesn’t care which approach you choose - both can satisfy FR-CALC-001.

But here’s the catch: These choices often depend implicitly on requirements - especially non-functional ones. Consider:

The customer’s explicit requirements (“calculate intersection”) don’t dictate these choices, but their implicit requirements (fast, memory-efficient, maintainable) do.

6.3.3.1 Design Decision Documents (DDDs)

When you make significant implementation choices, you should document them. A Design Decision Document (or Architectural Decision Record, ADR) captures:

  1. Context: What problem are we solving?
  2. Decision: What did we choose and why?
  3. Consequences: What are the trade-offs?
  4. Alternatives Considered: What else did we evaluate?

Why bother documenting decisions?

Where to store DDDs:

Option 1: GitHub Issues (recommended for discussion-heavy decisions)

Example GitHub Issue:

Title: DDD-001: Ray-casting algorithm for intersection calculation

## Context
FR-CALC-001 requires calculating intersection between camera ray and road profile.
NFR-PERF-001 requires < 100ms response time.

## Decision
Use ray-casting with early exit when intersection found.

## Rationale
- Benchmarked at 15ms for 10,000-point profiles (meets NFR-PERF-001)
- Early exit optimization reduces average case to O(n/2)
- Well-documented algorithm, easy to test

## Alternatives Considered
- **Interpolation search**: Faster for sorted data but complex edge cases
- **Binary search on segments**: Requires preprocessing, adds complexity

## Consequences
- Vertical rays (90°, 270°) require special handling → REQ-GEOM-001
- Performance degrades linearly with profile size

## Related
- Implements: FR-CALC-001, constrained by NFR-PERF-001
- Derived requirements: REQ-GEOM-001, REQ-GEOM-002

Option 2: Markdown files in the repository (recommended for stable, approved decisions)

Practical workflow:

  1. Open a GitHub Issue to discuss the decision with the team
  2. Once consensus is reached, create a markdown file to record it
  3. Reference the issue in the markdown file for discussion history
  4. Link the decision document from relevant code (docstrings, comments)

But once you decide, you create new requirements that your code must satisfy. These are called derived requirements.

6.3.4 Derived Requirements: The Official Definition

The term “derived requirement” is standardized in industry:

Derived requirement (ISO/IEC/IEEE 29148:2018): “A requirement deduced or inferred from the collection and organization of requirements into a particular system hierarchy.”

The standard uses parent/child terminology:

NASA’s Systems Engineering Handbook provides a practical definition:

“Derived requirements arise from constraints, consideration of issues implied but not explicitly stated in the high-level direction, or factors introduced by the selected architecture and design.”

NASA also uses the term self-derived requirements for requirements that emerge purely from design decisions - exactly what we’re discussing here.

6.3.5 Full Worked Example: From Stakeholder to Unit Test

Let’s trace the full chain:

Parent Requirement (Stakeholder):

FR-CALC-001: User shall see intersection point displayed on road profile chart

Module Test (Tests the stakeholder requirement):

def test_fr_calc_001_intersection_displayed():
    """
    Acceptance test for FR-CALC-001.
    Tests the full workflow from user perspective.
    """
    app = RoadProfileViewer()
    app.load_profile("test_road.csv")
    app.set_camera_angle(45.0)

    result = app.calculate_and_display()

    assert result.intersection_point is not None
    assert result.chart_shows_marker == True

Implementation Decision:

“We’ll implement find_intersection() using ray-casting with numpy arrays. Vertical rays are undefined and should return None.”

Derived Requirements (from this decision):

REQ-GEOM-001: find_intersection() shall return (None, None, None) for vertical rays (90°, 270°)
REQ-GEOM-002: When intersection exists, returned distance shall be > 0
REQ-GEOM-003: Returned intersection point shall lie within road x-range
REQ-GEOM-004: Function shall accept road profile as NDArray with shape (N, 2)

Unit Tests (Test the derived requirements):

class TestFindIntersectionDerivedRequirements:
    """Unit tests for derived requirements of find_intersection()."""

    def test_req_geom_001_vertical_ray(self):
        """REQ-GEOM-001: Vertical rays return None."""
        road = np.array([[0, 0], [10, 5]])
        result = find_intersection(road, Angle(90), camera_x=5, camera_y=10)
        assert result == (None, None, None)

    def test_req_geom_002_positive_distance(self):
        """REQ-GEOM-002: Distance is positive when intersection exists."""
        road = np.array([[0, 0], [10, 5], [20, 10]])
        x, y, dist = find_intersection(road, Angle(45), camera_x=0, camera_y=15)
        assert dist is not None and dist > 0

    def test_req_geom_003_on_road(self):
        """REQ-GEOM-003: Intersection lies within road bounds."""
        road = np.array([[0, 0], [10, 5], [20, 10]])
        x, y, dist = find_intersection(road, Angle(45), camera_x=0, camera_y=15)
        assert 0 <= x <= 20

The Traceability Chain:

FR-CALC-001 (Stakeholder)
    │
    ├──► test_fr_calc_001_intersection_displayed() [Module Test]
    │
    └──► find_intersection() [Implementation]
              │
              ├──► REQ-GEOM-001 ──► test_req_geom_001_vertical_ray() [Unit Test]
              ├──► REQ-GEOM-002 ──► test_req_geom_002_positive_distance() [Unit Test]
              └──► REQ-GEOM-003 ──► test_req_geom_003_on_road() [Unit Test]

6.3.6 Where to Document Derived Requirements?

Requirement Type Audience Where to Document
Stakeholder requirements Everyone GitHub Issues, User Stories
Derived requirements Developers Code (docstrings), test docstrings, internal docs

For this course, the pragmatic approach:

  1. Stakeholder requirements → GitHub Issues with FR-/NFR- IDs
  2. Derived requirements → Document in code:
    • Function docstrings (preconditions, postconditions)
    • Test class/method docstrings
    • Internal REQ-* comments in tests

Why NOT put derived requirements in GitHub Issues?

6.3.7 The Testing Pyramid Makes Sense Now

Test Level What It Tests Requirement Type Run Frequency
E2E Full user workflows Stakeholder (FR-*) Before release
Module Component integration Stakeholder (FR-*, NFR-*) On PR merge
Unit Implementation correctness Derived (REQ-*) Every commit

The insight:

Unit tests don’t directly test stakeholder requirements - they test that our implementation of those requirements is correct. The module/E2E tests verify the actual requirement.

6.3.8 Avoiding “Orphan” Requirements

NASA’s Software Engineering Handbook warns about orphan requirements - code or tests that can’t be traced to any parent requirement.

Signs of orphans:

Prevention: Every derived requirement should trace back to a stakeholder requirement. If you can’t explain how test_vertical_ray_returns_none() helps satisfy a stakeholder need, question whether you need it.

Bidirectional traceability means you can trace:

  • Forward: Stakeholder requirement → derived requirements → unit tests
  • Backward: Unit test → derived requirement → stakeholder requirement

If either direction breaks, you have a problem.


7. Requirements Documentation Tools

7.1 The Spectrum of Formality

Informal                                                              Formal
   |                                                                     |
   v                                                                     v
Emails → Sticky Notes → User Stories → Use Cases → IEEE 830 → Formal Specs (Z)

(IEEE 830 and its successor ISO/IEC/IEEE 29148 define standardized formats for requirements specifications - see Section 4.5 for details)

Most modern software development uses semi-formal approaches: structured enough to be clear, flexible enough to change.

7.2 User Stories

Format:

As a [role], I want [feature], so that [benefit].

Example:

As a road engineer, I want to see the intersection point highlighted on the chart, so that I can quickly verify my measurements.

Why this format works:

7.3 Acceptance Criteria: Given-When-Then

Each user story needs acceptance criteria - specific conditions that must be met for the story to be “done.”

Format (Gherkin syntax):

Given [initial context]
When [action occurs]
Then [expected outcome]

Example:

Feature: Intersection Calculation

  Scenario: Normal intersection with road
    Given a road profile is loaded with points [(0,0), (10,5), (20,10)]
    And the camera is at position (0, 15)
    When I set the viewing angle to 45 degrees
    Then the system should display an intersection point
    And the intersection should be between x=0 and x=20
    And the distance should be greater than 0

  Scenario: Vertical ray (edge case)
    Given a road profile is loaded
    And the camera is at position (5, 15)
    When I set the viewing angle to 90 degrees
    Then the system should display "No intersection"

This is directly testable! Tools like pytest-bdd can execute these as automated tests.

7.4 GitHub Issues as Requirements

Modern development often uses GitHub Issues to track requirements:

## User Story
As a road engineer, I want to export my results to PDF so that I can include them in reports.

## Acceptance Criteria
- [ ] Export button visible on main screen
- [ ] PDF includes road profile chart
- [ ] PDF includes intersection coordinates
- [ ] PDF includes calculation parameters
- [ ] File name defaults to "road_profile_YYYY-MM-DD.pdf"

## Technical Notes
- Use `reportlab` library for PDF generation
- Follow existing chart styling

## Links
- Related to #42 (Export feature epic)
- Blocks #45 (Reporting milestone)

Why GitHub Issues Excel for Requirements

Remember Section 6’s INVEST criteria? Terms like “Independent,” “Small,” and “Valuable” are inherently subjective. What one developer considers “small enough” might seem too large to another. What the product owner considers “valuable” might not align with engineering priorities.

This is where GitHub Issues shine - they’re built for collaborative discussion:

Example discussion on an issue:

@project-manager: I think this is small enough for one week.

@dev-alice: Actually, the PDF generation alone is 3 days. Can we split into “basic export” and “styled export”?

@dev-bob: Agreed. Also, is the date format in the filename testable? Which timezone?

@project-manager: Good points. Updated acceptance criteria to specify UTC and split the feature.

This collaborative refinement is exactly what you need for subjective criteria - the requirement improves through team discussion, and the conversation is preserved for future reference.

Additional Benefits:


8. Linking Requirements to Tests: Traceability

8.1 What is Traceability?

Traceability: The ability to link requirements → tests → code in both directions.

Why it matters:

8.2 Implementing Traceability with pytest

Using markers to link tests to requirements:

import pytest

# Define custom markers for requirements
pytestmark = pytest.mark.requirements

@pytest.mark.requirement("FR-4")
def test_intersection_found_for_normal_case():
    """
    Requirement FR-4: The system shall calculate and highlight
    the intersection point between ray and road.
    """
    result = find_intersection(x_road, y_road, angle=45.0, camera_x=0, camera_y=10)
    assert result[0] is not None, "Should find intersection"

@pytest.mark.requirement("FR-4")
@pytest.mark.requirement("NFR-2")
def test_intersection_performance():
    """
    Requirements:
    - FR-4: Calculate intersection
    - NFR-2: Complete within 100ms
    """
    import time
    start = time.time()
    result = find_intersection(x_road, y_road, angle=45.0, camera_x=0, camera_y=10)
    elapsed = (time.time() - start) * 1000

    assert result[0] is not None
    assert elapsed < 100, f"Took {elapsed}ms, exceeds 100ms requirement"

8.3 Requirements Coverage vs Code Coverage

Code coverage (Chapter 03 (TDD and CI)-7): Did we execute all the code?

Requirements coverage: Did we test all the requirements?

Metric What it measures Limitation
Code Coverage % of code lines/branches executed by tests 100% coverage ≠ all requirements tested
Requirements Coverage % of requirements with at least one test 100% coverage ≠ requirements correct

You need both:


9. Summary

Concept Key Point Connection to Testing
Requirements What the system should do (or how well) Tests verify requirements are met
Functional vs Non-Functional What it does vs how well it does it Different test types for each
Stakeholders Different perspectives, different requirements Different acceptance criteria
INVEST criteria Good requirements are testable Testability enables test design
User Stories As a [role], I want [feature], so that [benefit] Acceptance criteria = test cases
Traceability Requirements ↔ Tests ↔ Code Requirements coverage metric

What we covered:


10. What’s Next: Part 2

In Part 2, we’ll cover:

Continue to Part 2 → Requirements Engineering - From Process to Practice

© 2026 Dominik Mueller   •  Powered by Soopr   •  Theme  Moonwalk