Can I put step definitions in a folder which is not "steps" with behave?

Question:

I am trying to work with Behave on Python.
I was wondering if there would be a way to put my .py files somewhere else instead of being forced to put them all inside the “steps” folder. My current structure would look like this

tests/
    features/
    steps/ #all code inside here, for now

What I would like to accomplish is something like

tests/
    features/ #with all the .feature files
    login/ #with all the .py files for logging in inside a service
    models/ #with all the .py files that represents a given object
    and so on

The only BDD framework that I used before Behave was Cucumber with Java, which allowed to insert the step definitions wherever I wanted to (and the rest was handled by Cucumber itself).
I am asking this because I would like to have a lot of classes in my project in order to organize my code in a better way.

Asked By: Gianmarco F.

||

Answers:

First, from the behave Documentation (Release 1.2.7.dev0):

behave works with three types of files:

  1. feature files written by your Business Analyst / Sponsor / whoever with your behaviour scenarios in it, and
  2. a “steps” directory with Python step implementations for the scenarios.
  3. optionally some environmental controls (code to run before and after steps, scenarios, features or the whole
    shooting match).

So a steps/ directory is required.

To accomplish a workaround similar to what you have in mind, I tried creating a subdirectory in the /steps directory: /steps/deeper/ and inserted my Python file there: /steps/deeper/testing.py. After running behave, I received the "NotImplementedError", meaning the step definitions in /deeper/testing.py were not found.

It appears that behave doesn’t search recursively through subdirectories of the steps/ directory for any additional Python files.

As for what you’re trying to do, I think it’s decent organizational idea, but since it’s not doable, you could do this: instead of having directories for the Python files in your tests/ directory, why not have a good naming convention for your Python file and separate the associated functions into their own Python files? That is:

tests/
    features/
    steps/
        login_prompt.py # contains all the functions for logging in inside a service
        login_ssh.py # contains all the functions for SSH login
        models_default.py # contains all the functions for the default object
        models_custom.py # contains all the functions for a custom object
        and so on...

Of course, at this point, it really doesn’t matter if you separate them into different Python files, since behave searches through all the Python files in steps/ when called, but for organization’s sake, it accomplishes the same effect.

Answered By: natn2323

This may be a bit late but you can do the following:

Have the structure like this:

tests/
    features/
        steps/
            login
            main_menu
            all_steps.py

In the subfolders in steps you can create your _steps.py file with the implementation and then in the all_steps.py(or how you want to name it) you just need to import them:

from tests.steps.login.<feature>_step import *
from tests.steps.main_menu.<feature>_step import *
etc

And when you run this it should find the step files. Alternatively you can have the files anywhere in the project as long as you have 1 Steps folder and a file in the file were you import all steps

Answered By: Adrian

You can do it with additional method, smth like this:

def import_steps_from_subdirs(dir_path):
    for directory in walk(dir_path):
        current_directory = directory[0] + '/'

        all_modules = [module_info[1] for module_info in iter_modules(path=[current_directory])]

        current_directory = current_directory.replace(Resources.BASE_DIR + '/', '')

        for module in all_modules:
            import_module(current_directory.replace('/', '.') + module)

Then call this method in before_all layer

Answered By: Bidonus

I adopted Bidonus' solution as follows, this should be copy/paste-able:

features/helpers/import_helper.py

from importlib import import_module
from pkgutil import iter_modules
from os import walk
from pathlib import Path

def import_steps(steps_dir: Path):
    assert steps_dir.exists()

    for (directory, _, _) in walk(steps_dir):
        if "__pycache__" in directory:
            continue

        if directory == str(steps_dir):
            continue

        current_directory = directory + "/"
        print(f"Importing Additional Steps: {current_directory}")

        all_modules = [module_info[1]
                       for module_info in iter_modules(path=[str(current_directory)])]
        current_directory = current_directory.replace(str(steps_dir) + '/', '')

        for module in all_modules:
            module_path = current_directory.replace('/', '.') + module
            import_module(module_path)

Then created an __init__.py file:

features/steps/__init__.py

from features.helpers.import_steps import import_steps
from pathlib import Path

STEPS_DIR_NAME = "steps"
STEPS_DIR = Path(__file__).parent
assert STEPS_DIR.name == STEPS_DIR_NAME

import_steps(STEPS_DIR)

Now when I run my tests I first have:

Importing Additional Steps: /workspaces/XXXXX/features/steps/create_model/
Importing Additional Steps: /workspaces/XXXXX/features/steps/list_operations/
Importing Additional Steps: /workspaces/XXXXX/features/steps/assertions/
Importing Additional Steps: /workspaces/XXXXX/features/steps/assertions/list_assertions/
Importing Additional Steps: /workspaces/XXXXX/features/steps/context_managers/
Importing Additional Steps: /workspaces/XXXXX/features/steps/type_switchers/
Importing Additional Steps: /workspaces/XXXXX/features/steps/test_setup/
Importing Additional Steps: /workspaces/XXXXX/features/steps/dao_function_callers/
…
Test Results
…
Answered By: Schalton
Categories: questions Tags: , ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.