CincyPy Presentation for Requirements Tracer Server


Introduction

In this presentation I will be showing:

  • The concept of requirement traceability and the value I see in it.
  • A prototype web application to automate some of the process of achieving traceability.

Questions

These are some common questions I faced pretty often as a developer:

  • "Is this product behavior correct?"
  • "What is this code supposed to do?"
  • "Are we testing this feature?"
  • "Do we still need this functionality?"
  • "What data can I expect to receive? What should I produce? Is this field necessary?"

How does this happen?

  • Large, legacy codebase.
  • Distributed systems, distributed teams.
  • Bitrot, people coming and going, silos and braindrain.

These questions are productivity killers.

  • Lower confidence of making and deploying changes.
  • Generate meetings and inter-team conflict.
  • Encourages rewriting systems without solving the core problem.

Requirement Traceability

Idea: maintain a database of requirements and tests, and link them together.


Answers

Building a robust database of relationships between requirements and tests allows you to answer:

  • What requirements are untested or undertested? How often is this requirement tested?
  • What defects have been verified as fixed? What part of a system did a defect affect?
  • What tests/suites cover which requirements? What tests would comprise a good regression test?
  • What requirements are the cause of the most defects? (Pareto Analysis)
  • What code affects which requirements?
  • What manual tests are due to run and possibly update?
  • ... And all of the questions listed above.

So how do we do it?

Requirement traceability can be tough.

  • Maintaining this database is a time consuming, tedius, error prone task.
  • Like documentation: as soon as it is written, it is stale.
  • There's often a disconnect between developers and project managers.

But it is possible, with:

  • Spreadsheets, wikis, and other shared documentation.
  • Project management tools like Jira.
  • Homegrown/legacy/specialized requirements analysis software.

requirements-tracer-server

A prototype tool for assisting in the automation of requirements traceability.

Some details:

  • Written with FastAPI, uv, SQLModel, pytest, and Pydantic.
  • Reads a git-backed config file of requirements to build the requirements database.
  • A REST API to collect test results and link them to the requirements.
  • Currently done via a pytest plugin, but is flexible and could be even used with manual tests.

Some Code

The schema file, made of SQLModel (Pydantic) models.


class Requirement(SQLModel, table=True):
    id: int | None = Field(primary_key=True, default=None)
    title: str
    description: str = ""
    state: RequirementStates = RequirementStates.closed
    product: str = ""
    stakeholder: str = ""
    note: str = ""
    software_release: str = ""
    priority: RequirementPriority = RequirementPriority.low
    severity: RequirementSeverity = RequirementSeverity.normal
    decline_note: str = ""
    verification_method: RequirementVerificationMethod = RequirementVerificationMethod.testing
    verification_status: RequirementVerificationStatus = RequirementVerificationStatus.incomplete

    #todo: functional_spec fields?

    # Many-to-many testcase relationship for testcases
    testcases: list["TestCase"] = Relationship(back_populates="requirements", link_model=RequirementTestCaseLink)
    
class TestCase(SQLModel, table=True):
    #metadata
    id: int | None = Field(primary_key=True, default=None)
    title: str
    status: TestCaseStatus = TestCaseStatus.released
    objective: str = ""
    author: str = ""
    category: TestCategory = TestCategory.other
    #test procedure
    setup: str = ""
    steps: str = ""
    cleanup: str = ""
    pass_fail_criteria: str = ""
    #traceability
    requirements: list["Requirement"] = Relationship(back_populates="testcases", link_model=RequirementTestCaseLink)
    testsuites: list["TestSuite"] = Relationship(back_populates="testcases", link_model=TestCaseTestSuiteLink)
    testresults: list["TestResult"] = Relationship(back_populates="testcase")
    defects: list["Defect"] = Relationship(back_populates="testcases", link_model=TestCaseDefectLink)
    #automation
    automated: bool = True
    candidate_for_automation: bool = True
    automation_priority: TestCaseAutomationPriority = TestCaseAutomationPriority.low

class TestSuite(SQLModel, table=True):
    #metadata
    id: int = Field(primary_key=True)
    title: str
    objective: str = ""
    category: TestCategory = TestCategory.other
    regression: bool = False
    test_cycle: int = 1
    #traceability
    testcases: list["TestCase"] = Relationship(back_populates="testsuites", link_model=TestCaseTestSuiteLink)

class TestResult(SQLModel, table=True):
    #metadata
    id: int = Field(primary_key=True)
    status: TestResultStatus = TestResultStatus.untested
    run_date: datetime
    time: timedelta
    release: str = ""
    
    #traceability
    testcase_id: int | None = Field(default=None, foreign_key="testcase.id")
    testcase: TestCase = Relationship(back_populates="testresults")

class Defect(SQLModel, table=True):
    id: int = Field(primary_key=True)
    state: DefectState = DefectState.new
    headline: str
    severity: DefectSeverity = DefectSeverity.low
    priority: DefectPriority = DefectPriority.low
    reproducible: bool = False
    crash: bool = False
    keywords: str = ""
    product: str = ""
    category: TestCategory = TestCategory.other
    release: str = ""
    submit_date: datetime
    description: str = ""
    notes: str = ""
    fix_description: str = ""
    fix_date: str = ""
    duplicate_defect_id: int | None = Field(default=None, foreign_key="defect.id")
    new_requirement_title: str = ""
    #orthogonal defect classification
    activity: str = ""
    trigger: str = ""
    impact: str = ""
    target: DefectODCTarget = DefectODCTarget.code
    defect_type: str = ""
    qualifier: DefectODCQualifier = DefectODCQualifier.incorrect
    source: DefectODCSource = DefectODCSource.source
    age: DefectODCAge = DefectODCAge.new
    #traceability
    testcases: list["TestCase"] = Relationship(back_populates="defects", link_model=TestCaseDefectLink)

    

A test, using a decorator to enable communication with server. Docstring is for TestCase metadata.


@requirements_tracer_server_enabled
def test_read_requirements(base_db, db_session, db_testdata, test_client):
    """
    {
    "title": "test_read_requirements",
    "requirements": ["requirements_read_all"],
    "test_suites": ["unit_tests"]
    }
    """
    response = test_client.get("/requirements/")
    assert response.status_code == 200
    assert response.json() == [json.loads(Requirement(title="Test Requirement", id=1).model_dump_json())]
    

The pytest plugin that sends results to the API.


@pytest.hookimpl(tryfirst=True, hookwrapper=True)
def pytest_runtest_makereport(item, call):
    #Pre hook section
        
    outcome = yield

    #Post hook section
    result = outcome.get_result()
    if result.when == 'call': #for call, not setup or teardown
        if REQUIREMENTS_TRACER_SERVER_ENABLED == "true" and hasattr(item.function, "requirements_tracer_server_enabled"):
            #update testcase
            testcase_metadata = json.loads(item.function.__doc__)
            url = "{}/testcases".format(REQUIREMENTS_TRACER_SERVER_HOST)
            r = httpx.post(url, json=testcase_metadata, follow_redirects=True)
            if r.status_code != 201:
                pass #TODO: error handling
            #update result
            test_result = {
                "status": result.outcome,
                "run_date": datetime.now().isoformat(),
                "time": str(result.duration),
                "testcase": testcase_metadata["title"]
            }
            url = "{}/testresults".format(REQUIREMENTS_TRACER_SERVER_HOST)
            r = httpx.post(url, json=test_result, follow_redirects=True)
            if r.status_code != 201:
                pass #TODO: error handling
    

Demo


Conclusion

Here's what I want to work on next:

  • Better code quality, tests, cleanup. (dogfooding)
  • Finish schema for Defects and TestSuites.
  • UI Dashboard with HTMX and Jinja2.
  • Different data format, like TOML, for requirements and test case metadata.
  • Keep experimenting, refining, learning this concept.

Any thoughts? I'd love to hear what you think.

  • Do you see value in requirement traceability?
  • What other ways could you accomplish automated traceability?
  • What do you do for traceability?