diff --git a/samples/book-app-project/book_app.py b/samples/book-app-project/book_app.py index f0100c2d..cee14f14 100644 --- a/samples/book-app-project/book_app.py +++ b/samples/book-app-project/book_app.py @@ -59,6 +59,15 @@ def handle_find(): show_books(books) +def handle_search(): + print("\nSearch Books\n") + + query = input("Search by title or author: ").strip() + books = collection.search_books(query) + + show_books(books) + + def show_help(): print(""" Book Collection Helper @@ -68,6 +77,7 @@ def show_help(): add - Add a new book remove - Remove a book by title find - Find books by author + search - Search books by title or author help - Show this help message """) @@ -87,6 +97,8 @@ def main(): handle_remove() elif command == "find": handle_find() + elif command == "search": + handle_search() elif command == "help": show_help() else: diff --git a/samples/book-app-project/books.py b/samples/book-app-project/books.py index 2110689f..7a51cf62 100644 --- a/samples/book-app-project/books.py +++ b/samples/book-app-project/books.py @@ -70,3 +70,10 @@ def remove_book(self, title: str) -> bool: def find_by_author(self, author: str) -> List[Book]: """Find all books by a given author.""" return [b for b in self.books if b.author.lower() == author.lower()] + + def search_books(self, query: str) -> List[Book]: + """Search books by title or author (case-insensitive partial match).""" + if not query: + return self.books + query_lower = query.lower() + return [b for b in self.books if query_lower in b.title.lower() or query_lower in b.author.lower()] diff --git a/samples/book-app-project/test_books.py b/samples/book-app-project/test_books.py new file mode 100644 index 00000000..ee01d9f1 --- /dev/null +++ b/samples/book-app-project/test_books.py @@ -0,0 +1,105 @@ +import pytest +from books import Book, BookCollection + + +@pytest.fixture +def collection(): + """Create a test collection with sample books.""" + bc = BookCollection() + bc.books = [ + Book(title="The Great Gatsby", author="F. Scott Fitzgerald", year=1925), + Book(title="To Kill a Mockingbird", author="Harper Lee", year=1960), + Book(title="1984", author="George Orwell", year=1949), + Book(title="Pride and Prejudice", author="Jane Austen", year=1813), + Book(title="The Hobbit", author="J.R.R. Tolkien", year=1937), + ] + return bc + + +class TestSearchBooks: + """Test cases for search_books() method.""" + + def test_search_by_title_exact(self, collection): + """Test exact title match.""" + results = collection.search_books("1984") + assert len(results) == 1 + assert results[0].title == "1984" + + def test_search_by_title_partial(self, collection): + """Test partial title match.""" + results = collection.search_books("gatsby") + assert len(results) == 1 + assert results[0].title == "The Great Gatsby" + + def test_search_by_author_exact(self, collection): + """Test exact author match.""" + results = collection.search_books("George Orwell") + assert len(results) == 1 + assert results[0].author == "George Orwell" + + def test_search_by_author_partial(self, collection): + """Test partial author match.""" + results = collection.search_books("tolkien") + assert len(results) == 1 + assert results[0].author == "J.R.R. Tolkien" + + def test_search_case_insensitive(self, collection): + """Test that search is case-insensitive.""" + results1 = collection.search_books("JANE") + results2 = collection.search_books("jane") + results3 = collection.search_books("JaNe") + assert len(results1) == len(results2) == len(results3) == 1 + assert results1[0].title == "Pride and Prejudice" + + def test_search_multiple_matches(self, collection): + """Test search returning multiple results.""" + results = collection.search_books("the") + assert len(results) == 3 + titles = {b.title for b in results} + assert titles == {"The Great Gatsby", "The Hobbit", "To Kill a Mockingbird"} + + def test_search_no_results(self, collection): + """Test search with no matching results.""" + results = collection.search_books("nonexistent") + assert len(results) == 0 + + def test_search_empty_query(self, collection): + """Test search with empty query returns all books.""" + results = collection.search_books("") + assert len(results) == len(collection.books) + + def test_search_whitespace_query(self, collection): + """Test search with whitespace-only query.""" + results = collection.search_books(" ") + assert len(results) == 0 + + def test_search_matches_title_or_author(self, collection): + """Test that search matches title OR author, not both.""" + results = collection.search_books("jane") + assert len(results) == 1 + assert results[0].title == "Pride and Prejudice" + assert results[0].author == "Jane Austen" + + def test_search_preserves_book_properties(self, collection): + """Test that search results preserve all book properties.""" + results = collection.search_books("hobbit") + assert len(results) == 1 + book = results[0] + assert book.title == "The Hobbit" + assert book.author == "J.R.R. Tolkien" + assert book.year == 1937 + assert book.read == False + + +class TestFindByAuthor: + """Test that existing find_by_author() still works.""" + + def test_find_by_author_works(self, collection): + """Ensure find_by_author() is not broken.""" + results = collection.find_by_author("Harper Lee") + assert len(results) == 1 + assert results[0].title == "To Kill a Mockingbird" + + +if __name__ == "__main__": + pytest.main([__file__, "-v"])