DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Related

  • An Overview of Creating Databases With Python
  • The Enterprise, the Database, the Problem, and the Solution
  • DuckDB for Python Developers
  • Beyond Request-Response: Architecting Stateful Agentic Chatbots with the Command and State Patterns

Trending

  • LangChain With SQL Databases: Natural Language to SQL Queries
  • An XGBoost Property Valuation Postmortem: Leakage, Overfitting, and SHAP Surprises
  • Building Your API Gateway From OpenAPI Specs: A Spec-Driven Approach
  • Add Observability to Your React Native Application in 5 Minutes
  1. DZone
  2. Coding
  3. Languages
  4. From Gherkin to Source Code Without Losing the Business Language

From Gherkin to Source Code Without Losing the Business Language

Instead of hiding business rules inside technical layers, I'll show how to keep them visible and explicit in the code. The code reads like the original Gherkin scenario.

By 
Douglas Cardoso user avatar
Douglas Cardoso
·
Jul. 14, 26 · Presentation
Likes (0)
Comment
Save
Tweet
Share
318 Views

Join the DZone community and get the full member experience.

Join For Free

Picture this: you are a software developer building an education platform, and you receive from the product owner some requirements written in business language (Gherkin). You need to implement these scenarios in Python.

Probably you will start creating models and service modules. You will create some classes to represent the entities described in the scenarios, like Student, Course, and Subject. You will add conditionals and loops in the entity classes to control the business logic and restrict paths in the code:

Python
 
# Enroll a student in a course
if course.status == "active" and student.course == None:
    student.course = course
raise BusinessError("Student already in a course")

Also, you will create a class to represent the persistence layer (database) and methods like list_students, get_course_by_name, and create_student to add, delete, update, and return data from the database.

You will probably create facades to group the classes in a logical sequence, add more ifs, elses, and loops to control the code flow.

At the end of the sprint, you have a scenario implemented and tested.

There is nothing wrong with its style of implementation. It is a common process. However, something loses importance in this process: the business scenario itself.

In this article, I’ll showcase a behavior-driven development approach that converts business languages directly to executable code. The intention is to keep the implementation closer to the business language and promote the code to the source of truth.

Gherkin Scenarios for the Education Platform

Going back to the fictional (not much) story. Here are some scenarios an education platform could have:

Gherkin
 
Feature: Student GPA and approval

  Scenario: Student is approved when GPA is 7 or higher and all subjects are passed
    Given a student named "John" is enrolled in the "Computer Science" course
    And the course has the subjects "Math", "Physics", and "Programming"
    And the student has the following grades:
      | Subject     | Grade |
      | Math        | 7     |
      | Physics     | 8     |
      | Programming | 9     |
    When the system calculates the student's GPA
    Then the GPA should be 8
    And the student status should be "Approved"

Feature: Student enrollment in course subjects

  Scenario: Student cannot enroll in a subject from another course
    Given a student named "Carlos" is enrolled in the "Medicine" course
    And the subject "Algorithms" belongs to the "Computer Science" course
    When the student tries to enroll in the subject "Algorithms"
    Then the enrollment should be rejected
    And the system should show the message "Students can only enroll in subjects from their own course"

Feature: Student enrollment in a course

  Scenario: Student enrolls in an active course
    Given a course named "Architecture" is active
    When a student named "Julia" tries to enroll in the "Architecture" course
    Then the enrollment should be accepted
    And the system should show the message "Student enrolled in course"

Feature: Course cancellation

  Scenario: Students cannot enroll in a canceled course
    Given a course named "Architecture" has been canceled by the general coordinator
    When a student named "Julia" tries to enroll in the "Architecture" course
    Then the enrollment should be rejected
    And the system should show the message "Canceled courses cannot accept new enrollments"


They are pretty, readable, easy to understand, and find inconsistencies. Now, a possible implementation as described in the previous story. It was simplified for the sake of this article. Let us look at a more traditional implementation.

Python
 
# Entities
class Course:
    def __init__(self, course_id, name):
        self.course_id = course_id
        self.name = name
        self.is_canceled = False


class Student:
    def __init__(self, student_id, name):
        self.student_id = student_id
        self.name = name
        self.course = None


# Application
class UniversityService:
    def __init__(self):
        self.courses = {}
        self.students = {}

    def create_course(self, course_id, name):
        self.courses[course_id] = Course(course_id, name)

    def create_student(self, student_id, name):
        self.students[student_id] = Student(student_id, name)

    def cancel_course(self, course_id):
        course = self.courses.get(course_id)

        if course is None:
            raise ValueError("Course not found")

        course.is_canceled = True

    def enroll_student_in_course(self, student_id, course_id):
        student = self.students.get(student_id)
        course = self.courses.get(course_id)

        if student is None:
            raise ValueError("Student not found")

        if course is None:
            raise ValueError("Course not found")

        if course.is_canceled:
            raise ValueError("Canceled courses cannot accept new enrollments")

        student.course = course


# Scenario: Students cannot enroll in a canceled course
service = UniversityService()

service.create_course("C1", "Architecture")
service.create_student("S1", "Julia")
service.cancel_course("C1")

try:
    service.enroll_student_in_course("S1", "C1")
    print("Unexpected result: student enrolled in a canceled course")
except ValueError as e:
    print(e)


It was done in a traditional style. Notice the technical references like service and the preconditions and business logic spread in many ifs in the code. We forgot to represent the system behavior in a simple and explicit way.

The scenario was spread into many pieces, and it may be hard to put all of them together when we need to understand the code in the future.

Consider that more features will be integrated into the code, and more if/else statements will be introduced to control the business logic and new flows.

In summary, the scenario cannot be read as it was presented by the business team. It is hard to validate that the system is doing what it should do without proper unit tests and careful code review.

We can try to test its integration with Python Behave to bring the explicit behavior back to the game, but it may be hard to do it without coming up against technical stuff like services.

The system works, but it is hard to prove that it behaves as expected just by reading the code.

At this point, the development team and the business team are not talking the same language anymore. There is a translation from business language to production code (technical stuff).

Behavior-Driven Development

Now, using the framework Guará to represent the scenarios directly in the code. The code now tells the story.

For example, the scenario Student enrollment in a course can be written like this:

Python
 
from guara.application import Application

eduapp = Application()

(
    eduapp.given(IsActiveCourse, course_id=course_id)
    .and_(IsNotStudentInACouse, student_id=student_id)
    .when(
        EnrollStudentInCourse,
        student_id=student_id,
        course_id=course_id,
    )
    .then(it.IsEqualTo, "Student enrolled in course")
)


The preconditions IsActiveCourse and IsNotStudentInACourse are now explicit and are at a higher level of the code. Not buried in the methods in the form of if conditionals. The precondition and action classes have single responsibilities.

Python
 
from guara.transaction import AbstractTransaction

class IsActiveCourse(AbstractTransaction):
    def do(self, course_id):
        print(f"Checking the status of course {course_id}")
        status = database.courses.get_status(course_id=course_id)
        if status == "Active":
            return True
        raise CourseCanceledException("Course canceled")


class IsNotStudentInACourse(AbstractTransaction):
    def do(self, student_id):
        print(f"Checking if student in a course")
        course = database.student.get_course()
        if course:
            raise StudentException("Student already in a course")


class EnrollStudentInCourse(AbstractTransaction):
    def do(self, student_id, course_id):
        print(f"Enrolling student {student_id} in course {course_id}")
        status = database.enroll_course(course_id, student_id)
        return "Student enrolled in course"


In the end, it is easier to compare the code against the scenario steps and assert they are present in the code.

Python
 
import argparse
from guara.transaction import Application
from guara import it

eduapp = Application()

def main():
    parser = argparse.ArgumentParser()

    parser.add_argument("--action", required=True)
    parser.add_argument("--student-id")
    parser.add_argument("--course-id")

    args = parser.parse_args()

    if args.action == "enroll_course":
        try:
            (
                eduapp.given(HasCourse, course_id=args.course_id)
                .and_(IsActiveCourse, course_id=args.course_id)
                .and_(HasStudent, student_id=args.student_id)
                .and_(IsNotStudentEnrolledInCourse, student_id=args.student_id)
                .when(
                    EnrollStudentInCourse,
                    student_id=args.student_id,
                    course_id=args.course_id,
                )
                .asserts(it.IsTrue)
            )
        except Exception as e:
            print(str(e))
            app.undo()

# Calling the CLI            
python edu.py enroll-course --course-id 10 --student-id 1324


Benefits

  • The production code is now the source of truth
  • It can be compared directly to the business scenarios
  • The responsibilities are encapsulated in dedicated classes
  • It is possible to undo operations easily once the framework is based on the Command Pattern (GoF)
  • It is easy to add more behavior to the code without changing other classes
  • The classes are reusable
  • It hides the technical stuff. They still exist, but now the actions are first-class citizens

Points of attention

  • It is not a one-size-fits-all style. It is necessary to evaluate whether the system under development will benefit from this code style
  • Makes more sense when the scenarios are defined in Gherkin language; otherwise, it will be necessary to translate the requirement to code as done in the traditional implementation

Conclusion

The important difference is that the source code still reads almost like the original Gherkin scenario. Instead of hiding business rules inside technical layers, we keep them visible and explicit in the code.

Behavior-driven development Business logic Command pattern Database Implementation Software developer code style Id (programming language) Python (language) Self (programming language)

Published at DZone with permission of Douglas Cardoso. See the original article here.

Opinions expressed by DZone contributors are their own.

Related

  • An Overview of Creating Databases With Python
  • The Enterprise, the Database, the Problem, and the Solution
  • DuckDB for Python Developers
  • Beyond Request-Response: Architecting Stateful Agentic Chatbots with the Command and State Patterns

Partner Resources

×

Comments

The likes didn't load as expected. Please refresh the page and try again.

  • RSS
  • X
  • Facebook

ABOUT US

  • About DZone
  • Support and feedback
  • Community research

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 215
  • Nashville, TN 37211
  • [email protected]

Let's be friends:

  • RSS
  • X
  • Facebook