Skip to content

Commit

Permalink
Merge pull request #108 from elchupanebrej/docs
Browse files Browse the repository at this point in the history
Docs
  • Loading branch information
elchupanebrej authored Feb 4, 2024
2 parents 27a938c + f661732 commit 213ffbe
Show file tree
Hide file tree
Showing 22 changed files with 306 additions and 31 deletions.
10 changes: 10 additions & 0 deletions Features/Tutorial launch.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
Feature: Tutorial examples could be executed successfully

Scenario: Catalog example with simplest steps
Given Copy path from "docs/tutorial" to test path "tutorial"
When run pytest
|cli_args| --rootdir=tutorial| tutorial/tests |

Then pytest outcome must contain tests with statuses:
|passed|
| 1|
26 changes: 20 additions & 6 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ BDD library for the pytest runner
.. _pytest: https://docs.pytest.org
.. _Gherkin: https://cucumber.io/docs/gherkin/reference
.. _pytest-bdd-ng: https://pytest-bdd-ng.readthedocs.io/en/default/
.. _pytest-bdd: https://github.com/pytest-dev/pytest-bdd

**pytest-bdd-ng** combine descriptive clarity of Gherkin_ language
with power and fullness of pytest_ infrastructure.
Expand All @@ -27,7 +28,14 @@ mentioned in feature steps with dependency injection. This allows a true BDD
just-enough specification of the requirements without obligatory maintaining any context object
containing the side effects of Gherkin imperative declarations.

.. NOTE:: Project documentation: pytest-bdd-ng_
.. NOTE:: Project documentation on readthedocs: pytest-bdd-ng_


Why ``NG`` ?
------------

The current pytest plugin for cucumber is pytest-bdd_ , a popular project with 1.2k stars and used in 3k public repos and maintained by the pytest community. The upstream open-cucumber project does not have an official python release, so the current cucumber specs include features not available in pytest-bdd_ . This project is an effort to bridge the gap and also make it easier for pytest users to access new cucumber features.


Install pytest-bdd-ng
---------------------
Expand All @@ -38,12 +46,12 @@ Install pytest-bdd-ng

Project layout
--------------
**pytest-bdd-ng** automatically collect `*.feature` files from pytest_ tests directory.
**pytest-bdd-ng** automatically collects ``*.feature`` files from pytest_ tests directory.
Important to remember, that feature files are used by other team members as live documentation,
so it's not a very good idea to mix documentation and test code.

The more features and scenarios you have, the more important becomes the question about
their organization. So recommended way is to organize your feature files in the folders by
their organization. So the recommended way is to organize your feature files in the folders by
semantic groups:

::
Expand All @@ -56,7 +64,7 @@ semantic groups:
└──auth
└──login.feature

And tests for this features could be organized in the next manner:
And tests for these features would be organized in the following manner:

::

Expand All @@ -83,7 +91,7 @@ And tests for this features could be organized in the next manner:
│ └──backend_auth.feature -> ../../features/backend/auth.feature
...

Step definitions could be organized in the next way
The step definitions would then be organized like this:

::

Expand Down Expand Up @@ -112,11 +120,17 @@ Step definitions could be organized in the next way
...

To make links between feature files at features directory and test directory there are few options
(for more information please investigate project tests):
(for more information please examine the project's tests):

#. Symlinks
#. `.desktop` files
#. `.webloc` files
#. `.url` files

.. NOTE:: Link files also could be used to load features by http://


How to Contribute
-----------------

The project is now open to contributions. Please open an issue for more details.
Empty file added docs/__init__.py
Empty file.
36 changes: 21 additions & 15 deletions docs/features.rst
Original file line number Diff line number Diff line change
@@ -1,15 +1,21 @@
.. NOTE:: Features below are part of end-to-end test suite; You always could find most specific
use cases of **pytest-bdd-ng** by investigation of its regression
test suite https://github.com/elchupanebrej/pytest-bdd-ng/tree/default/tests

Gherkin feature launch by pytest.feature
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. include:: ../Features/Gherkin feature launch by pytest.feature
:code: gherkin

Tags for Scenario Outlines examples.feature
###########################################

.. include:: ../Features/Scenario/Outline/Tags for Scenario Outlines examples.feature
:code: gherkin
.. NOTE:: Features below are part of end-to-end test suite; You always could find most specific
use cases of **pytest-bdd-ng** by investigation of its regression
test suite https://github.com/elchupanebrej/pytest-bdd-ng/tree/default/tests

Tutorial launch.feature
!!!!!!!!!!!!!!!!!!!!!!!

.. include:: ../Features/Tutorial launch.feature
:code: gherkin

Gherkin feature launch by pytest.feature
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

.. include:: ../Features/Gherkin feature launch by pytest.feature
:code: gherkin

Tags for Scenario Outlines examples.feature
###########################################

.. include:: ../Features/Scenario/Outline/Tags for Scenario Outlines examples.feature
:code: gherkin
Empty file added docs/tutorial/__init__.py
Empty file.
12 changes: 12 additions & 0 deletions docs/tutorial/features/books.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Feature files represent `Application under test` functional capabilities
# in form of acceptance test with representative examples
Feature: Library book searches and book delivery
Scenario: The catalog can be searched by author name.
Given these books in the catalog
| Author | Title |
| Stephen King | The Shining |
| James Baldwin | If Beale Street Could Talk |
When a name search is performed for Stephen
Then only these books will be returned
| Author | Title |
| Stephen King | The Shining |
22 changes: 22 additions & 0 deletions docs/tutorial/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Tutorial for Behave/Cucumber users
==================================

.. _tutorial: https://thebddcoach.com/post/a-quick-introduction-to-pytest-bdd-ng-for-people-who-are-already-familiar-with-cucumber-or-behave/

Leslie's tutorial_ ...

.. toctree::
:glob:

tests/features/*.feature
src/*.py
tests/*.py
tests/*.desktop
tests/steps/*.py


Set up the tutorial
-------------------

From the project root, activate a virtual environment and run:
``pip install -r docs/tutorial/requirements.txt``
3 changes: 3 additions & 0 deletions docs/tutorial/pytest.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[pytest]
; Defines default rootpath and a lot of pytest configs
;
2 changes: 2 additions & 0 deletions docs/tutorial/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pytest
pytest_bdd_ng
27 changes: 27 additions & 0 deletions docs/tutorial/src/catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
"""This files represents simple `Application under test`"""
from dataclasses import dataclass, field
from typing import Iterable, List


@dataclass # Easy way to not write redundant __init__ https://docs.python.org/3/library/dataclasses.html
class Book:
author: str
title: str


@dataclass
class Catalog:
storage: List[Book] = field(default_factory=list)

def add_books_to_catalog(self, books: Iterable[Book]):
self.storage.extend(books)

def search_by_author(self, term: str):
for book in self.storage:
if term in book.author:
yield book

def search_by_title(self, term: str):
for book in self.storage:
if term in book.title:
yield book
Empty file added docs/tutorial/tests/__init__.py
Empty file.
6 changes: 6 additions & 0 deletions docs/tutorial/tests/books.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[Desktop Entry]
Type=Link
URL=features/books.feature
; Feature files are gathered as usual test modules, but also could be linked into
; directory hierarchy by symlinks.
; Some operation systems do not provide symlinks, so such files could replace them.
21 changes: 21 additions & 0 deletions docs/tutorial/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""
conftest.py is local per-directory plugin of pytest.
Its mission is to define fixtures, steps, and hooks which will be used
by tests gathered by pytest from directory structure below
https://docs.pytest.org/en/latest/how-to/writing_plugins.html#conftest-py-local-per-directory-plugins
https://docs.pytest.org/en/latest/explanation/goodpractices.html#test-discovery
"""

from pytest import fixture

from .steps.library_steps import (
a_search_type_is_performed_for_search_term,
only_these_books_will_be_returned,
these_books_in_the_catalog,
)


@fixture
def search_results() -> list:
return []
Empty file.
85 changes: 85 additions & 0 deletions docs/tutorial/tests/steps/library_steps.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import re
from typing import List, Literal

from messages import DataTable, Step # type:ignore[attr-defined]
from pytest_bdd import given, step, then, when

from ...src.catalog import Book, Catalog


def get_books_from_data_table(data_table: DataTable):
# Gherkin data-tables have no title row by default, but we could define them if we want.
title_row, *book_rows = data_table.rows

step_data_table_titles = []
for cell in title_row.cells:
step_data_table_titles.append(cell.value)

assert step_data_table_titles == ["Author", "Title"]

books = []
for row in book_rows:
books.append(Book(row.cells[0].value, row.cells[1].value))

return books


# Steps to be used in scenarios are defined with special decorators
@given(
"these books in the catalog",
# Steps are allowed to inject new fixtures or overwrite existing ones
target_fixture="catalog",
)
def these_books_in_the_catalog(
# `step` fixture is injected by pytest dependency injection mechanism into scope of step by default;
# So it could be used without extra effort
step: Step,
):
books = get_books_from_data_table(step.data_table)

catalog = Catalog()
catalog.add_books_to_catalog(books)

yield catalog


@when(
# Step definitions could have parameters. Here could be raw stings, cucumber expressions or regular expressions
re.compile("a (?P<search_type>name|title) search is performed for (?P<search_term>.+)"),
target_fixture="search_results",
)
def a_search_type_is_performed_for_search_term(
# `search_results` is a usual pytest fixture defined somewhere else (at conftest.py, plugin or module) and injected by pytest dependency injection mechanism.
# In this case it will be provided by conftest.py
search_results: List[Book],
# `search_type` and `search_term` are parameters of this step and are injected by step definition
search_type: Literal["name", "title"],
search_term: str,
# `catalog` is a fixture injected by another step
catalog: Catalog,
):
if search_type == "title":
search = catalog.search_by_title
elif search_type == "name":
search = catalog.search_by_author
else:
assert False, "Unknown"

found_books = search(search_term)
search_results.extend(found_books)
yield search_results


@then("only these books will be returned")
def only_these_books_will_be_returned(
# Fixtures persist during step execution, so usual `context` common for behave users is not required,
# so if you define fixture dependencies debugging becomes much easier.
search_results: List[Book],
step: Step,
catalog: Catalog,
):
expected_books = get_books_from_data_table(step.data_table)

for book in search_results:
if book not in expected_books:
assert False, f"Book ${book} is not expected"
7 changes: 6 additions & 1 deletion src/pytest_bdd/compatibility/pytest.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,14 @@ def is_pytest_version_greater(version: str):
return compare_distribution_version("pytest", version, ge)


PYTEST6, PYTEST61, PYTEST62, PYTEST7 = map(
PYTEST6, PYTEST61, PYTEST62, PYTEST7, PYTEST8 = map(
is_pytest_version_greater,
[
"6.0",
"6.1",
"6.2",
"7.0",
"8.0",
],
)

Expand Down Expand Up @@ -191,3 +192,7 @@ def is_set(obj):

def is_set(obj):
return type(obj) is not object


def get_metafunc_call_arg(call, arg):
return call.params[arg] if PYTEST8 else call.funcargs[arg]
16 changes: 12 additions & 4 deletions src/pytest_bdd/message_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,15 @@
TestStepStarted,
Timestamp,
)
from pytest_bdd.compatibility.pytest import Config, FixtureDef, FixtureRequest, Parser, get_config_root_path, is_set
from pytest_bdd.compatibility.pytest import (
Config,
FixtureDef,
FixtureRequest,
Parser,
get_config_root_path,
get_metafunc_call_arg,
is_set,
)
from pytest_bdd.packaging import get_distribution_version
from pytest_bdd.steps import StepHandler
from pytest_bdd.utils import PytestBDDIdGeneratorHandler, deepattrgetter
Expand Down Expand Up @@ -142,9 +150,9 @@ def pytest_generate_tests(self, metafunc):
feature_registry = set()
pickle_registry = set()
for call in metafunc._calls:
feature = call.funcargs["feature"]
pickle = call.funcargs["scenario"]
feature_source: Source = call.funcargs["feature_source"]
feature = get_metafunc_call_arg(call, "feature")
pickle = get_metafunc_call_arg(call, "scenario")
feature_source: Source = get_metafunc_call_arg(call, "feature_source")

if is_set(feature) and feature_source.uri not in feature_registry:
feature_registry.add(feature_source.uri)
Expand Down
3 changes: 3 additions & 0 deletions tests/e2e/Features/Tutorial launch.desktop
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[Desktop Entry]
Type=Link
URL=Features/Tutorial launch.feature
3 changes: 0 additions & 3 deletions tests/e2e/Gherkin feature launch by pytest.desktop

This file was deleted.

Loading

0 comments on commit 213ffbe

Please sign in to comment.