Skip to content

Design Decisions

1. Behave parser reuse

Decision: Wrap Behave's parser instead of reimplementing Gherkin parsing.

Rationale: Behave's parser is battle-tested, handles edge cases, supports multiple languages, and is compatible with Tag Expression v2. Reimplementing Gherkin parsing would be a massive effort and would inevitably lag behind Behave's updates.

Trade-off: We depend on behave as a runtime dependency. This is acceptable since behave-model is designed for Behave projects.


2. Frozen dataclasses

Decision: All model classes are frozen dataclasses.

Rationale: Immutability prevents accidental modifications and makes the model safer to share between functions. The Location class is also frozen for the same reason.

Trade-off: Transformations need object.__setattr__ to modify frozen fields. This is an internal implementation detail that users don't see.


3. Composition over inheritance

Decision: Model classes are flat dataclasses, not a deep inheritance hierarchy.

Rationale: ScenarioOutline extends Scenario because they share the same interface. Everything else uses composition (e.g., Feature has rules, scenarios, tags — it doesn't inherit from them).

Trade-off: Some code duplication between Feature and Rule (both have background, scenarios, tags). This is preferable to a shared base class that would couple them.


4. Visitor pattern

Decision: Use the visitor pattern for tree traversal.

Rationale: Visitors decouple traversal logic from the model. You can add new operations (counting, collecting, analysis) without modifying model classes. The model just calls visitor.visit_* and the visitor decides what to do.

Trade-off: Adding a new model class requires adding a visit_* method to the base Visitor. This is a small, well-contained change.


5. In-place transformations

Decision: Transformations modify the model in place.

Rationale: Copying the entire model for every transformation would be expensive for large projects. Users who need immutability can copy.deepcopy before transforming.

Trade-off: Users must be aware that transformations are destructive. This is documented in the docstrings and guides.


6. Query mixin pattern

Decision: Query functions are mixed into Project and also available as standalone functions.

Rationale: project.find_scenarios(tag="@smoke") is more ergonomic than find_scenarios(project, tag="@smoke"). But standalone functions are useful for functional programming and composition.

Trade-off: The Project class has many methods. This is acceptable since they're all query-related and well-organized.


7. Gherkin v6 Rules as first-class objects

Decision: Rule is a first-class domain object, not just a grouping mechanism.

Rationale: Rules have their own background, tags, and scenarios. Treating them as first-class objects makes the model more accurate and enables Rule-specific queries, validation, and serialization.

Trade-off: feature.scenarios only returns scenarios not inside any Rule. Users must use project.all_scenarios() to get everything. This is documented clearly.


8. No external dependencies beyond Behave

Decision: behave-model only depends on behave at runtime.

Rationale: Minimizing dependencies makes the library easier to install and use in any environment. All dev dependencies (pytest, ruff, mkdocs) are optional.

Trade-off: We don't use libraries like pydantic for validation or rich for pretty printing. Our implementations are minimal but sufficient.