Skip to content

Commit

Permalink
Merge pull request #3 from elchupanebrej/doc-e2e
Browse files Browse the repository at this point in the history
Adjust tutorial to be executed during e2e test launch
  • Loading branch information
blaisep committed Jan 18, 2024
2 parents 2c5a446 + 7ef30de commit 1ad7881
Show file tree
Hide file tree
Showing 15 changed files with 208 additions and 30 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 "tutorial"
When run pytest
|cli_args| --rootdir=tutorial| tutorial/tests |

Then pytest outcome must contain tests with statuses:
|passed|
| 1|
6 changes: 6 additions & 0 deletions docs/features.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ Gherkin feature launch by pytest.feature
.. include:: ../Features/Gherkin feature launch by pytest.feature
:code: gherkin

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

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

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

Expand Down
Empty file added docs/tutorial/__init__.py
Empty file.
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
# 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
Expand Down
5 changes: 4 additions & 1 deletion docs/tutorial/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@ Leslie's tutorial_ ...
:glob:

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


Set up the tutorial
Expand Down
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
;
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.
24 changes: 20 additions & 4 deletions docs/tutorial/tests/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,21 @@
from steps.library_steps import *
"""
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
## or maybe:
# import steps
# ?
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 []
95 changes: 73 additions & 22 deletions docs/tutorial/tests/steps/library_steps.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,85 @@
from helper_methods.library_catalog import Catalog
from helper_methods.verification_helper_methods import verify_returned_books
from pytest import fixture
from pytest_bdd_ng import given, parsers, step, then, when
import re
from typing import List, Literal

from src.catalog import Book, Catalog

@fixture
def context()
""" Create a placeholder object to use in place of Cucumber's context object. The context object allows us to pass state between steps. """
class dummy():
pass
from messages import DataTable, Step # type:ignore[attr-defined]
from pytest_bdd import given, step, then, when


return dummy()
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)

@given("these books in the catalog")
def these_books_in_the_catalog(step):
context.catalog = Catalog()
context.catalog.add_books_to_catalog(step.data_table)
assert step_data_table_titles == ["Author", "Title"]

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

@when(parsers.re("a (?P<search_type>name|title) search is performed for " +
"(?P<search_term>.+)"))
def a_SEARCH_TYPE_is_performed_for_SEARCH_TERM(search_type, search_term):
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":
raise NotImplementedError("Title searches are not yet implemented.")
context.search_results = context.catalog.search_by_author(search_term)
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(step):
expected_books = context.catalog.read_books_from_table(step.data_table)
verify_returned_books(context.search_results, expected_books)
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"
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.

18 changes: 18 additions & 0 deletions tests/e2e/conftest.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import re
import shutil
from functools import partial
from itertools import islice
from operator import attrgetter, itemgetter
from pathlib import Path
from typing import TYPE_CHECKING

from pytest_bdd import given, step
from pytest_bdd.compatibility.pytest import assert_outcomes
from pytest_bdd.utils import compose

if TYPE_CHECKING:
from pytest_bdd.compatibility.pytest import Testdir


@given(re.compile('File "(?P<name>\\w+)(?P<extension>\\.\\w+)" with content:'))
def write_file(name, extension, testdir, step):
Expand Down Expand Up @@ -48,3 +54,15 @@ def check_pytest_stdout_lines(pytest_result, step):
lines = list(map(compose(attrgetter("value"), itemgetter(0)), map(attrgetter("cells"), step.data_table.rows)))

pytest_result.stdout.fnmatch_lines(lines)


@given(re.compile(r"Copy path from \"(?P<initial_path>(\w|\\|.)+)\" to \"(?P<final_path>(\w|\\|.)+)\""))
def copy_path(request, testdir: "Testdir", initial_path, final_path, step):
full_initial_path = Path(request.config.rootdir) / initial_path
full_final_path = Path(testdir.tmpdir) / final_path
if full_initial_path.is_file():
full_initial_path.parent.mkdir(parents=True, exist_ok=True)
full_final_path.parent.mkdir(parents=True, exist_ok=True)
shutil.copy(full_initial_path, full_final_path)
else:
shutil.copytree(full_initial_path, full_final_path, dirs_exist_ok=True)
36 changes: 36 additions & 0 deletions tests/feature/test_steps.py
Original file line number Diff line number Diff line change
Expand Up @@ -1391,3 +1391,39 @@ def in_test_b_test_b(t):

[thing1, thing2] = collect_dumped_objects(result)
assert thing1 == thing2 == "specific test_b_test_b"


def test_steps_parameters_injected_as_fixtures_are_not_shared_between_scenarios(testdir):
testdir.makefile(
".feature",
# language=gherkin
steps="""\
Feature: Steps parameters injected as fixtures are not shared between scenarios
Scenario: Steps parameters injected as fixture
Given I have a "foo" parameter which is injected as fixture
Scenario:
Then Fixture "foo" is inavailable
""",
)
testdir.makeconftest(
# language=python
"""\
from pytest_bdd.compatibility.pytest import FixtureLookupError
from pytest import raises
from pytest_bdd import given, then
@given('I have a "{foo}" parameter which is injected as fixture')
def inject_fixture(request):
assert request.getfixturevalue('foo') == "foo"
@then('Fixture "foo" is inavailable')
def foo_is_foo(request):
with raises(FixtureLookupError):
request.getfixturevalue('foo')
"""
)
result = testdir.runpytest()
result.assert_outcomes(passed=2, failed=0)

0 comments on commit 1ad7881

Please sign in to comment.