diff --git a/docs/tutorial/features/books.feature b/docs/tutorial/features/books.feature index 57385bc1..b7d52584 100644 --- a/docs/tutorial/features/books.feature +++ b/docs/tutorial/features/books.feature @@ -6,7 +6,7 @@ Scenario: The catalog can be searched by author name. | Author | Title | | Stephen King | The Shining | | James Baldwin | If Beale Street Could Talk | - When a name search is performed for "Stephen" + When a name search is performed for Stephen Then only these books will be returned | Author | Title | | Stephen King | The Shining | diff --git a/docs/tutorial/src/catalog.py b/docs/tutorial/src/catalog.py index 53ed588e..fab76deb 100644 --- a/docs/tutorial/src/catalog.py +++ b/docs/tutorial/src/catalog.py @@ -1,24 +1,27 @@ """This files represents simple `Application under test`""" -from typing import List +from dataclasses import dataclass, field +from typing import Iterable, List -from attr import Factory, attrib, attrs - -@attrs +@dataclass # Easy way to not write redundant __init__ https://docs.python.org/3/library/dataclasses.html class Book: - author = attrib(type=str) - title = attrib(type=str) + author: str + title: str -@attrs +@dataclass class Catalog: - storage = attrib(default=Factory(list)) + storage: List[Book] = field(default_factory=list) - def add_books_to_catalog(self, books: List[Book]): + def add_books_to_catalog(self, books: Iterable[Book]): self.storage.extend(books) def search_by_author(self, term: str): - return filter(lambda book: term in book.author, self.storage) + for book in self.storage: + if term in book.author: + yield book def search_by_title(self, term: str): - return filter(lambda book: term in book.title, self.storage) + for book in self.storage: + if term in book.title: + yield book diff --git a/docs/tutorial/tests/steps/library_steps.py b/docs/tutorial/tests/steps/library_steps.py index c461b86f..fd4c6a6c 100644 --- a/docs/tutorial/tests/steps/library_steps.py +++ b/docs/tutorial/tests/steps/library_steps.py @@ -1,5 +1,5 @@ import re -from typing import Literal +from typing import List, Literal from src.catalog import Book, Catalog @@ -8,13 +8,19 @@ def get_books_from_data_table(data_table: DataTable): - # Gherkin data-tables have no title row by default, but we could use if we want. - step_data_table_titles = [cell.value for cell in data_table.rows[0].cells] + # 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"] - author_and_title_list = [[cell.value for cell in row.cells] for row in data_table.rows[1:]] + books = [] + for row in book_rows: + books.append(Book(row.cells[0].value, row.cells[1].value)) - books = [Book(author, title) for author, title in author_and_title_list] return books @@ -25,25 +31,27 @@ def get_books_from_data_table(data_table: DataTable): target_fixture="catalog", ) def these_books_in_the_catalog( - # `step` fixture is injected by pytest DI mechanism into scope of step by default + # `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, ): - catalog = Catalog() - 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 (?Pname|title) search is performed for "(?P.+)"'), + re.compile("a (?Pname|title) search is performed for (?P.+)"), target_fixture="search_results", ) def a_search_type_is_performed_for_search_term( - # `search_results` is a usual pytest fixture defined somewhere else and injected by pytest DI mechanism. + # `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, + 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, @@ -51,22 +59,27 @@ def a_search_type_is_performed_for_search_term( catalog: Catalog, ): if search_type == "title": - search_results.extend(catalog.search_by_title(search_term)) + search = catalog.search_by_title elif search_type == "name": - search_results.extend(catalog.search_by_author(search_term)) + 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 is not required, + # 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, + search_results: List[Book], step: Step, catalog: Catalog, ): expected_books = get_books_from_data_table(step.data_table) - assert all([book in catalog.storage for book in expected_books]) + + for book in search_results: + if book not in expected_books: + assert False, f"Book ${book} is not expected"