├── .gitignore ├── Chapter01 ├── 01_automatictests.py ├── 02_tests │ ├── tests_div │ │ ├── __init__.py │ │ └── tests_div.py │ └── tests_sum.py ├── 03_tdd.py ├── 04_unit.py └── 05_integration.py ├── Chapter02 ├── 01_chat_acceptance.py ├── 02_chat_dummy.py ├── 03_chat_stubs.py ├── 04_chat_spies.py ├── 05_chat_fakes.py ├── 06_acceptance_tests.py ├── 07_dependency_injection.py └── 08_pinject.py ├── Chapter03 ├── 01_todo │ ├── src │ │ ├── setup.py │ │ └── todo │ │ │ ├── __init__.py │ │ │ ├── __main__.py │ │ │ └── app.py │ └── tests │ │ ├── __init__.py │ │ └── test_acceptance.py ├── 02_codedesign │ ├── src │ │ ├── setup.py │ │ └── todo │ │ │ ├── __init__.py │ │ │ ├── __main__.py │ │ │ ├── app.py │ │ │ └── db.py │ └── tests │ │ ├── __init__.py │ │ ├── test_acceptance.py │ │ └── unit │ │ ├── __init__.py │ │ ├── test_basicdb.py │ │ └── test_todoapp.py └── 03_regression │ ├── src │ ├── setup.py │ └── todo │ │ ├── __init__.py │ │ ├── __main__.py │ │ ├── app.py │ │ └── db.py │ └── tests │ ├── __init__.py │ ├── test_acceptance.py │ ├── test_regressions.py │ └── unit │ ├── __init__.py │ ├── test_basicdb.py │ └── test_todoapp.py ├── Chapter04 ├── .travis.yml ├── benchmarks │ ├── __init__.py │ └── test_chat.py ├── src │ ├── chat │ │ ├── __init__.py │ │ ├── client.py │ │ └── server.py │ └── setup.py └── tests │ ├── __init__.py │ ├── e2e │ ├── __init__.py │ └── test_chat.py │ ├── functional │ ├── __init__.py │ ├── fakeserver.py │ └── test_chat.py │ └── unit │ ├── __init__.py │ ├── test_client.py │ └── test_connection.py ├── Chapter05 ├── conftest.py ├── pytest.ini ├── test_capsys.py ├── test_fixturesinj.py ├── test_markers.py ├── test_randomness.py ├── test_simple.py ├── test_tmppath.py └── test_usingfixtures.py ├── Chapter06 ├── pytest.ini ├── src │ ├── fizzbuzz │ │ ├── __init__.py │ │ ├── __main__.py │ │ └── testing │ │ │ ├── __init__.py │ │ │ └── fixtures.py │ └── setup.py └── tests │ ├── __init__.py │ ├── conftest.py │ ├── functional │ └── test_acceptance.py │ └── unit │ ├── test_checks.py │ └── test_output.py ├── Chapter07 ├── src │ ├── contacts │ │ ├── __init__.py │ │ └── __main__.py │ └── setup.py └── tests │ ├── __init__.py │ ├── acceptance │ ├── delete_contact.feature │ └── list_contacts.feature │ ├── conftest.py │ ├── functional │ └── test_acceptance.py │ └── unit │ ├── test_adding.py │ ├── test_application.py │ └── test_persistence.py ├── Chapter08 ├── benchmarks │ ├── __init__.py │ ├── pytest.ini │ └── test_persistence.py ├── src │ ├── contacts │ │ ├── __init__.py │ │ └── __main__.py │ └── setup.py └── tests │ ├── __init__.py │ ├── acceptance │ ├── __init__.py │ ├── features │ │ ├── delete_contact.feature │ │ └── list_contacts.feature │ ├── steps.py │ ├── test_adding.py │ ├── test_delete_contact.py │ └── test_list_contacts.py │ ├── conftest.py │ ├── functional │ ├── __init__.py │ ├── test_basic.py │ └── test_main.py │ ├── pytest.ini │ └── unit │ ├── __init__.py │ ├── test_adding.py │ ├── test_application.py │ ├── test_flaky.py │ └── test_persistence.py ├── Chapter09 ├── benchmarks │ ├── __init__.py │ ├── pytest.ini │ └── test_persistence.py ├── src │ ├── contacts │ │ ├── __init__.py │ │ └── __main__.py │ └── setup.py ├── tests │ ├── .testmondata │ ├── __init__.py │ ├── acceptance │ │ ├── __init__.py │ │ ├── features │ │ │ ├── delete_contact.feature │ │ │ └── list_contacts.feature │ │ ├── steps.py │ │ ├── test_adding.py │ │ ├── test_delete_contact.py │ │ └── test_list_contacts.py │ ├── conftest.py │ ├── functional │ │ ├── __init__.py │ │ ├── test_basic.py │ │ └── test_main.py │ ├── pytest.ini │ └── unit │ │ ├── __init__.py │ │ ├── test_adding.py │ │ ├── test_application.py │ │ ├── test_flaky.py │ │ └── test_persistence.py └── tox.ini ├── Chapter10 ├── doc │ ├── Makefile │ └── source │ │ ├── conf.py │ │ ├── contacts.rst │ │ ├── index.rst │ │ └── reference.rst ├── src │ ├── contacts │ │ ├── __init__.py │ │ ├── __main__.py │ │ └── utils.py │ └── setup.py └── tests │ ├── __init__.py │ └── test_properties.py ├── Chapter11 ├── djapp │ ├── djapp │ │ ├── __init__.py │ │ ├── asgi.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ ├── httpbin │ │ ├── __init__.py │ │ ├── admin.py │ │ ├── apps.py │ │ ├── migrations │ │ │ └── __init__.py │ │ ├── models.py │ │ ├── tests.py │ │ ├── urls.py │ │ └── views.py │ ├── manage.py │ └── pytest-tests │ │ └── test_djapp.py ├── httpclient │ ├── src │ │ ├── httpclient │ │ │ ├── __init__.py │ │ │ └── __main__.py │ │ └── setup.py │ └── tests │ │ └── test_httpclient.py ├── httpclient_with_webtest │ └── tests │ │ └── test_client_webtest.py ├── webframeworks │ ├── src │ │ ├── setup.py │ │ └── wbtframeworks │ │ │ ├── __init__.py │ │ │ ├── django │ │ │ └── __init__.py │ │ │ ├── flask │ │ │ └── __init__.py │ │ │ ├── pyramid │ │ │ └── __init__.py │ │ │ └── tg2 │ │ │ └── __init__.py │ └── tests │ │ ├── conftest.py │ │ └── test_wsgiapp.py └── webtest │ ├── src │ ├── setup.py │ └── wsgiwebtest │ │ ├── __init__.py │ │ └── __main__.py │ └── tests │ └── test_wsgiapp.py ├── Chapter12 ├── customkeywords.robot ├── hellolibrary │ ├── HelloLibrary │ │ └── __init__.py │ └── setup.py ├── hellotest.robot └── searchgoogle.robot ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *~ 4 | *.egg 5 | *.egg-info 6 | *.pyc 7 | *.pyo 8 | *.swp 9 | *.mak.py 10 | *.DS_Store 11 | .coverage 12 | .idea 13 | .noseids 14 | .project 15 | .pydevproject 16 | .settings 17 | nosetests.xml 18 | .hypothesis 19 | .vscode 20 | venv 21 | contacts.json 22 | todo.data 23 | -------------------------------------------------------------------------------- /Chapter01/01_automatictests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class MyTestCase(unittest.TestCase): 5 | def test_one(self): 6 | pass 7 | 8 | def notatest(self): 9 | pass 10 | 11 | 12 | class MySecondTestCase(unittest.TestCase): 13 | def test_two(self): 14 | pass 15 | 16 | def test_two_part2(self): 17 | pass 18 | 19 | 20 | if __name__ == '__main__': 21 | unittest.main() 22 | -------------------------------------------------------------------------------- /Chapter01/02_tests/tests_div/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter01/02_tests/tests_div/__init__.py -------------------------------------------------------------------------------- /Chapter01/02_tests/tests_div/tests_div.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | class TestDiv(unittest.TestCase): 4 | def test_div0(self): 5 | pass 6 | 7 | def test_div1(self): 8 | pass 9 | -------------------------------------------------------------------------------- /Chapter01/02_tests/tests_sum.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | class TestSum(unittest.TestCase): 4 | def test_sum0(self): 5 | pass 6 | 7 | def test_sum1(self): 8 | pass 9 | -------------------------------------------------------------------------------- /Chapter01/03_tdd.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | class AdditionTestCase(unittest.TestCase): 4 | def test_main(self): 5 | result = addition(3, 2) 6 | assert result == 5 7 | 8 | def test_threeargs(self): 9 | result = addition(3, 2, 1) 10 | assert result == 6 11 | 12 | def test_noargs(self): 13 | result = addition() 14 | assert result == 0 15 | 16 | 17 | def addition(*args): 18 | total = 0 19 | for a in args: 20 | total += a 21 | return total 22 | 23 | 24 | if __name__ == '__main__': 25 | unittest.main() 26 | -------------------------------------------------------------------------------- /Chapter01/04_unit.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | class AdditionTestCase(unittest.TestCase): 4 | def test_main(self): 5 | result = addition(3, 2) 6 | assert result == 5 7 | 8 | 9 | class MultiplyTestCase(unittest.TestCase): 10 | def test_main(self): 11 | result = multiply(3, 2) 12 | assert result == 6 13 | 14 | 15 | def main(): 16 | import sys 17 | num1, num2 = sys.argv[1:] 18 | num1, num2 = int(num1), int(num2) 19 | print(multiply(num1, num2)) 20 | 21 | 22 | def multiply(num1, num2): 23 | total = 0 24 | for _ in range(num2): 25 | total = addition(total, num1) 26 | return total 27 | 28 | 29 | def addition(*args): 30 | total = 0 31 | for a in args: 32 | total += a 33 | return total 34 | 35 | 36 | 37 | if __name__ == '__main__': 38 | main() 39 | #unittest.main() 40 | -------------------------------------------------------------------------------- /Chapter01/05_integration.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | class Authentication: 4 | USERS = [{"username": "user1", 5 | "password": "pwd1"}] 6 | 7 | def login(self, username, password): 8 | u = self.fetch_user(username) 9 | if not u or u["password"] != password: 10 | return None 11 | return u 12 | 13 | def fetch_user(self, username): 14 | for u in self.USERS: 15 | if u["username"] == username: 16 | return u 17 | else: 18 | return None 19 | 20 | 21 | class Authorization: 22 | PERMISSIONS = [{"user": "user1", 23 | "permissions": {"create", "edit", "delete"}}] 24 | 25 | def can(self, user, action): 26 | for u in self.PERMISSIONS: 27 | if u["user"] == user["username"]: 28 | return action in u["permissions"] 29 | else: 30 | return False 31 | 32 | 33 | class TestAuthentication(unittest.TestCase): 34 | def test_login(self): 35 | auth = Authentication() 36 | auth.USERS = [{"username": "testuser", "password": "testpass"}] 37 | 38 | resp = auth.login("testuser", "testpass") 39 | 40 | assert resp == {"username": "testuser", "password": "testpass"} 41 | 42 | def test_failed_login(self): 43 | auth = Authentication() 44 | 45 | resp = auth.login("usernotexisting", "") 46 | 47 | assert resp is None 48 | 49 | def test_wrong_password(self): 50 | auth = Authentication() 51 | auth.USERS = [{"username": "testuser", "password": "testpass"}] 52 | 53 | resp = auth.login("testuser", "wrongpass") 54 | 55 | assert resp == None 56 | 57 | def test_fetch_user(self): 58 | auth = Authentication() 59 | auth.USERS = [{"username": "testuser", "password": "testpass"}] 60 | 61 | user = auth.fetch_user("testuser") 62 | 63 | assert user == {"username": "testuser", "password": "testpass"} 64 | 65 | def test_fetch_user_not_existing(self): 66 | auth = Authentication() 67 | 68 | resp = auth.fetch_user("usernotexisting") 69 | 70 | assert resp is None 71 | 72 | 73 | class TestAuthorization(unittest.TestCase): 74 | def test_can(self): 75 | authz = Authorization() 76 | authz.PERMISSIONS = [{"user": "testuser", "permissions": {"create"}}] 77 | 78 | resp = authz.can({"username": "testuser"}, "create") 79 | 80 | assert resp is True 81 | 82 | def test_not_found(self): 83 | authz = Authorization() 84 | 85 | resp = authz.can({"username": "usernotexisting"}, "create") 86 | 87 | assert resp is False 88 | 89 | def test_unathorized(self): 90 | authz = Authorization() 91 | authz.PERMISSIONS = [{"user": "testuser", "permissions": {"create"}}] 92 | 93 | resp = authz.can({"username": "testuser"}, "delete") 94 | 95 | assert resp is False 96 | 97 | 98 | class TestAuthorizeAuthenticatedUser(unittest.TestCase): 99 | def test_auth(self): 100 | auth = Authentication() 101 | authz = Authorization() 102 | auth.USERS = [{"username": "testuser", "password": "testpass"}] 103 | authz.PERMISSIONS = [{"user": "testuser", "permissions": {"create"}}] 104 | 105 | u = auth.login("testuser", "testpass") 106 | resp = authz.can(u, "create") 107 | 108 | assert resp is True 109 | 110 | 111 | if __name__ == '__main__': 112 | unittest.main() 113 | -------------------------------------------------------------------------------- /Chapter02/01_chat_acceptance.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | class TestChatAcceptance(unittest.TestCase): 4 | def test_message_exchange(self): 5 | user1 = ChatClient("John Doe") 6 | user2 = ChatClient("Harry Potter") 7 | 8 | user1.send_message("Hello World") 9 | messages = user2.fetch_messages() 10 | 11 | assert messages == ["John Doe: Hello World"] 12 | 13 | 14 | class TestChatClient(unittest.TestCase): 15 | def test_nickname(self): 16 | client = ChatClient("User 1") 17 | 18 | assert client.nickname == "User 1" 19 | 20 | def test_send_message(self): 21 | client = ChatClient("User 1") 22 | sent_message = client.send_message("Hello World") 23 | 24 | assert sent_message == "User 1: Hello World" 25 | 26 | class ChatClient: 27 | def __init__(self, nickname): 28 | self.nickname = nickname 29 | 30 | def send_message(self, message): 31 | sent_message = "{}: {}".format(self.nickname, message) 32 | self.connection.broadcast(message) 33 | return sent_message 34 | 35 | 36 | if __name__ == '__main__': 37 | unittest.main() 38 | -------------------------------------------------------------------------------- /Chapter02/02_chat_dummy.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import unittest.mock 3 | 4 | 5 | class TestChatAcceptance(unittest.TestCase): 6 | def test_message_exchange(self): 7 | user1 = ChatClient("John Doe") 8 | user2 = ChatClient("Harry Potter") 9 | 10 | user1.send_message("Hello World") 11 | messages = user2.fetch_messages() 12 | 13 | assert messages == ["John Doe: Hello World"] 14 | 15 | 16 | class TestChatClient(unittest.TestCase): 17 | def test_nickname(self): 18 | client = ChatClient("User 1") 19 | 20 | assert client.nickname == "User 1" 21 | 22 | def test_send_message(self): 23 | client = ChatClient("User 1") 24 | client.connection = unittest.mock.Mock() 25 | sent_message = client.send_message("Hello World") 26 | 27 | assert sent_message == "User 1: Hello World" 28 | 29 | 30 | class _DummyConnection: 31 | def broadcast(*args, **kwargs): 32 | pass 33 | 34 | 35 | class ChatClient: 36 | def __init__(self, nickname): 37 | self.nickname = nickname 38 | 39 | def send_message(self, message): 40 | sent_message = "{}: {}".format(self.nickname, message) 41 | self.connection.broadcast(message) 42 | return sent_message 43 | 44 | 45 | 46 | if __name__ == '__main__': 47 | unittest.main() 48 | -------------------------------------------------------------------------------- /Chapter02/03_chat_stubs.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import unittest.mock 3 | 4 | 5 | class TestChatAcceptance(unittest.TestCase): 6 | def test_message_exchange(self): 7 | user1 = ChatClient("John Doe") 8 | user2 = ChatClient("Harry Potter") 9 | 10 | user1.send_message("Hello World") 11 | messages = user2.fetch_messages() 12 | 13 | assert messages == ["John Doe: Hello World"] 14 | 15 | 16 | class TestChatClient(unittest.TestCase): 17 | def test_nickname(self): 18 | client = ChatClient("User 1") 19 | 20 | assert client.nickname == "User 1" 21 | 22 | def test_send_message(self): 23 | client = ChatClient("User 1") 24 | client.connection = unittest.mock.Mock() 25 | 26 | sent_message = client.send_message("Hello World") 27 | 28 | assert sent_message == "User 1: Hello World" 29 | 30 | 31 | class TestConnection(unittest.TestCase): 32 | def test_broadcast(self): 33 | with unittest.mock.patch.object(Connection, "connect"): 34 | c = Connection(("localhost", 9090)) 35 | 36 | with unittest.mock.patch.object(c, "get_messages", return_value=[]): 37 | c.broadcast("some message") 38 | 39 | assert c.get_messages()[-1] == "some message" 40 | 41 | 42 | 43 | class ChatClient: 44 | def __init__(self, nickname): 45 | self.nickname = nickname 46 | 47 | def send_message(self, message): 48 | sent_message = "{}: {}".format(self.nickname, message) 49 | self.connection.broadcast(message) 50 | return sent_message 51 | 52 | 53 | from multiprocessing.managers import SyncManager 54 | class Connection(SyncManager): 55 | def __init__(self, address): 56 | self.register("get_messages") 57 | super().__init__(address=address) 58 | self.connect() 59 | 60 | def broadcast(self, message): 61 | messages = self.get_messages() 62 | messages.append(message) 63 | 64 | 65 | if __name__ == '__main__': 66 | unittest.main() 67 | -------------------------------------------------------------------------------- /Chapter02/04_chat_spies.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import unittest.mock 3 | 4 | 5 | class TestChatAcceptance(unittest.TestCase): 6 | def test_message_exchange(self): 7 | user1 = ChatClient("John Doe") 8 | user2 = ChatClient("Harry Potter") 9 | 10 | user1.send_message("Hello World") 11 | messages = user2.fetch_messages() 12 | 13 | assert messages == ["John Doe: Hello World"] 14 | 15 | 16 | class TestChatClient(unittest.TestCase): 17 | def test_nickname(self): 18 | client = ChatClient("User 1") 19 | 20 | assert client.nickname == "User 1" 21 | 22 | def test_send_message(self): 23 | client = ChatClient("User 1") 24 | client.connection = unittest.mock.Mock() 25 | 26 | sent_message = client.send_message("Hello World") 27 | 28 | assert sent_message == "User 1: Hello World" 29 | 30 | def test_client_connection(self): 31 | client = ChatClient("User 1") 32 | 33 | connection_spy = unittest.mock.MagicMock() 34 | with unittest.mock.patch.object(client, "_get_connection", 35 | return_value=connection_spy): 36 | client.send_message("Hello World") 37 | 38 | connection_spy.broadcast.assert_called_with(("User 1: Hello World")) 39 | 40 | class TestConnection(unittest.TestCase): 41 | def test_broadcast(self): 42 | with unittest.mock.patch.object(Connection, "connect"): 43 | c = Connection(("localhost", 9090)) 44 | 45 | with unittest.mock.patch.object(c, "get_messages", return_value=[]): 46 | c.broadcast("some message") 47 | 48 | assert c.get_messages()[-1] == "some message" 49 | 50 | 51 | class ChatClient: 52 | def __init__(self, nickname): 53 | self.nickname = nickname 54 | self._connection = None 55 | 56 | def send_message(self, message): 57 | sent_message = "{}: {}".format(self.nickname, message) 58 | self.connection.broadcast(sent_message) 59 | return sent_message 60 | 61 | @property 62 | def connection(self): 63 | if self._connection is None: 64 | self._connection = self._get_connection() 65 | return self._connection 66 | 67 | @connection.setter 68 | def connection(self, value): 69 | if self._connection is not None: 70 | self._connection.close() 71 | self._connection = value 72 | 73 | def _get_connection(self): 74 | return Connection(("localhost", 9090)) 75 | 76 | 77 | from multiprocessing.managers import SyncManager, ListProxy 78 | class Connection(SyncManager): 79 | def __init__(self, address): 80 | self.register("get_messages", proxytype=ListProxy) 81 | super().__init__(address=address, authkey=b'mychatsecret') 82 | self.connect() 83 | 84 | def broadcast(self, message): 85 | messages = self.get_messages() 86 | messages.append(message) 87 | 88 | 89 | if __name__ == '__main__': 90 | unittest.main() 91 | -------------------------------------------------------------------------------- /Chapter02/05_chat_fakes.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import unittest.mock 3 | 4 | 5 | class TestChatAcceptance(unittest.TestCase): 6 | def test_message_exchange(self): 7 | user1 = ChatClient("John Doe") 8 | user2 = ChatClient("Harry Potter") 9 | 10 | user1.send_message("Hello World") 11 | messages = user2.fetch_messages() 12 | 13 | assert messages == ["John Doe: Hello World"] 14 | 15 | 16 | class TestChatClient(unittest.TestCase): 17 | def test_nickname(self): 18 | client = ChatClient("User 1") 19 | 20 | assert client.nickname == "User 1" 21 | 22 | def test_send_message(self): 23 | client = ChatClient("User 1") 24 | client.connection = unittest.mock.Mock() 25 | 26 | sent_message = client.send_message("Hello World") 27 | 28 | assert sent_message == "User 1: Hello World" 29 | 30 | def test_client_connection(self): 31 | client = ChatClient("User 1") 32 | 33 | connection_spy = unittest.mock.MagicMock() 34 | with unittest.mock.patch.object(client, "_get_connection", 35 | return_value=connection_spy): 36 | client.send_message("Hello World") 37 | 38 | connection_spy.broadcast.assert_called_with(("User 1: Hello World")) 39 | 40 | 41 | class TestConnection(unittest.TestCase): 42 | def test_broadcast(self): 43 | with unittest.mock.patch.object(Connection, "connect"): 44 | c = Connection(("localhost", 9090)) 45 | 46 | with unittest.mock.patch.object(c, "get_messages", return_value=[]): 47 | c.broadcast("some message") 48 | messages = c.get_messages() 49 | 50 | assert messages[-1] == "some message" 51 | 52 | def test_exchange_with_server(self): 53 | with unittest.mock.patch("multiprocessing.managers.listener_client", new={ 54 | "pickle": (None, FakeServer()) 55 | }): 56 | c1 = Connection(("localhost", 9090)) 57 | c2 = Connection(("localhost", 9090)) 58 | 59 | c1.broadcast("connected message") 60 | 61 | assert c2.get_messages()[-1] == "connected message" 62 | 63 | 64 | class ChatClient: 65 | def __init__(self, nickname): 66 | self.nickname = nickname 67 | self._connection = None 68 | 69 | def send_message(self, message): 70 | sent_message = "{}: {}".format(self.nickname, message) 71 | self.connection.broadcast(sent_message) 72 | return sent_message 73 | 74 | def fetch_messages(self): 75 | return list(self.connection.get_messages()) 76 | 77 | @property 78 | def connection(self): 79 | if self._connection is None: 80 | self._connection = self._get_connection() 81 | return self._connection 82 | 83 | @connection.setter 84 | def connection(self, value): 85 | if self._connection is not None: 86 | self._connection.close() 87 | self._connection = value 88 | 89 | def _get_connection(self): 90 | c = Connection(("localhost", 9090)) 91 | c.connect() 92 | return c 93 | 94 | 95 | from multiprocessing.managers import SyncManager, ListProxy 96 | class Connection(SyncManager): 97 | def __init__(self, address): 98 | self.register("get_messages", proxytype=ListProxy) 99 | super().__init__(address=address, authkey=b'mychatsecret') 100 | self.connect() 101 | 102 | def broadcast(self, message): 103 | messages = self.get_messages() 104 | messages.append(message) 105 | 106 | 107 | class FakeServer: 108 | def __init__(self): 109 | self.last_command = None 110 | self.messages = [] 111 | 112 | def __call__(self, *args, **kwargs): 113 | return self 114 | 115 | def send(self, data): 116 | callid, command, args, kwargs = data 117 | self.last_command = command 118 | self.last_args = args 119 | 120 | def recv(self, *args, **kwargs): 121 | if self.last_command == "dummy": 122 | return "#RETURN", None 123 | elif self.last_command == "create": 124 | return "#RETURN", ("fakeid", tuple()) 125 | elif self.last_command == "append": 126 | self.messages.append(self.last_args[0]) 127 | return "#RETURN", None 128 | elif self.last_command == "__getitem__": 129 | return "#RETURN", self.messages[self.last_args[0]] 130 | elif self.last_command in ("incref", "decref", "accept_connection"): 131 | return "#RETURN", None 132 | else: 133 | return "#ERROR", ValueError("%s - %r" % (self.last_command, self.last_args)) 134 | 135 | def close(self): 136 | pass 137 | 138 | 139 | if __name__ == '__main__': 140 | unittest.main() 141 | -------------------------------------------------------------------------------- /Chapter02/06_acceptance_tests.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import unittest.mock 3 | 4 | 5 | class TestChatAcceptance(unittest.TestCase): 6 | def test_message_exchange(self): 7 | with new_chat_server() as srv: 8 | user1 = ChatClient("John Doe") 9 | user2 = ChatClient("Harry Potter") 10 | 11 | user1.send_message("Hello World") 12 | messages = user2.fetch_messages() 13 | 14 | assert messages == ["John Doe: Hello World"] 15 | 16 | 17 | class TestChatClient(unittest.TestCase): 18 | def test_nickname(self): 19 | client = ChatClient("User 1") 20 | 21 | assert client.nickname == "User 1" 22 | 23 | def test_send_message(self): 24 | client = ChatClient("User 1") 25 | client.connection = unittest.mock.Mock() 26 | 27 | sent_message = client.send_message("Hello World") 28 | 29 | assert sent_message == "User 1: Hello World" 30 | 31 | def test_client_connection(self): 32 | client = ChatClient("User 1") 33 | 34 | connection_spy = unittest.mock.MagicMock() 35 | with unittest.mock.patch.object(client, "_get_connection", 36 | return_value=connection_spy): 37 | client.send_message("Hello World") 38 | 39 | connection_spy.broadcast.assert_called_with(("User 1: Hello World")) 40 | 41 | def test_client_fetch_messages(self): 42 | client = ChatClient("User 1") 43 | client.connection = unittest.mock.Mock() 44 | client.connection.get_messages.return_value = ["message1", "message2"] 45 | 46 | starting_messages = client.fetch_messages() 47 | client.connection.get_messages().append("message3") 48 | new_messages = client.fetch_messages() 49 | 50 | assert starting_messages == ["message1", "message2"] 51 | assert new_messages == ["message3"] 52 | 53 | 54 | 55 | class TestConnection(unittest.TestCase): 56 | def test_broadcast(self): 57 | with unittest.mock.patch.object(Connection, "connect"): 58 | c = Connection(("localhost", 9090)) 59 | 60 | with unittest.mock.patch.object(c, "get_messages", return_value=[]): 61 | c.broadcast("some message") 62 | 63 | assert c.get_messages()[-1] == "some message" 64 | 65 | def test_exchange_with_server(self): 66 | with unittest.mock.patch("multiprocessing.managers.listener_client", new={ 67 | "pickle": (None, FakeServer()) 68 | }): 69 | c1 = Connection(("localhost", 9090)) 70 | c2 = Connection(("localhost", 9090)) 71 | 72 | c1.broadcast("connected message") 73 | 74 | assert c2.get_messages()[-1] == "connected message" 75 | 76 | 77 | class ChatClient: 78 | def __init__(self, nickname): 79 | self.nickname = nickname 80 | self._connection = None 81 | self._last_msg_idx = 0 82 | 83 | def send_message(self, message): 84 | sent_message = "{}: {}".format(self.nickname, message) 85 | self.connection.broadcast(sent_message) 86 | return sent_message 87 | 88 | def fetch_messages(self): 89 | messages = list(self.connection.get_messages()) 90 | new_messages = messages[self._last_msg_idx:] 91 | self._last_msg_idx = len(messages) 92 | return new_messages 93 | 94 | @property 95 | def connection(self): 96 | if self._connection is None: 97 | self._connection = self._get_connection() 98 | return self._connection 99 | 100 | @connection.setter 101 | def connection(self, value): 102 | if self._connection is not None: 103 | self._connection.close() 104 | self._connection = value 105 | 106 | def _get_connection(self): 107 | return Connection(("localhost", 9090)) 108 | 109 | 110 | from multiprocessing.managers import SyncManager, ListProxy 111 | class Connection(SyncManager): 112 | def __init__(self, address): 113 | self.register("get_messages", proxytype=ListProxy) 114 | super().__init__(address=address, authkey=b'mychatsecret') 115 | self.connect() 116 | 117 | def broadcast(self, message): 118 | messages = self.get_messages() 119 | messages.append(message) 120 | 121 | 122 | _messages = [] 123 | def _srv_get_messages(): 124 | return _messages 125 | class _ChatServerManager(SyncManager): 126 | pass 127 | _ChatServerManager.register("get_messages", 128 | callable=_srv_get_messages, 129 | proxytype=ListProxy) 130 | 131 | def new_chat_server(): 132 | return _ChatServerManager(("", 9090), authkey=b'mychatsecret') 133 | 134 | 135 | class FakeServer: 136 | def __init__(self): 137 | self.last_command = None 138 | self.messages = [] 139 | 140 | def __call__(self, *args, **kwargs): 141 | return self 142 | 143 | def send(self, data): 144 | callid, command, args, kwargs = data 145 | self.last_command = command 146 | self.last_args = args 147 | 148 | def recv(self, *args, **kwargs): 149 | if self.last_command == "dummy": 150 | return "#RETURN", None 151 | elif self.last_command == "create": 152 | return "#RETURN", ("fakeid", tuple()) 153 | elif self.last_command == "append": 154 | self.messages.append(self.last_args[0]) 155 | return "#RETURN", None 156 | elif self.last_command == "__getitem__": 157 | return "#RETURN", self.messages[self.last_args[0]] 158 | elif self.last_command in ("incref", "decref", "accept_connection"): 159 | return "#RETURN", None 160 | else: 161 | return "#ERROR", ValueError("%s - %r" % (self.last_command, self.last_args)) 162 | 163 | def close(self): 164 | pass 165 | 166 | 167 | if __name__ == '__main__': 168 | unittest.main() 169 | -------------------------------------------------------------------------------- /Chapter02/07_dependency_injection.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import unittest.mock 3 | 4 | 5 | class TestChatAcceptance(unittest.TestCase): 6 | def test_message_exchange(self): 7 | with new_chat_server() as srv: 8 | user1 = ChatClient("John Doe") 9 | user2 = ChatClient("Harry Potter") 10 | 11 | user1.send_message("Hello World") 12 | messages = user2.fetch_messages() 13 | 14 | assert messages == ["John Doe: Hello World"] 15 | 16 | 17 | class TestChatClient(unittest.TestCase): 18 | def test_nickname(self): 19 | client = ChatClient("User 1") 20 | 21 | assert client.nickname == "User 1" 22 | 23 | def test_send_message(self): 24 | client = ChatClient("User 1", connection_provider=unittest.mock.Mock()) 25 | 26 | sent_message = client.send_message("Hello World") 27 | 28 | assert sent_message == "User 1: Hello World" 29 | 30 | def test_client_connection(self): 31 | connection_spy = unittest.mock.MagicMock() 32 | 33 | client = ChatClient("User 1", connection_provider=lambda *args: connection_spy) 34 | client.send_message("Hello World") 35 | 36 | connection_spy.broadcast.assert_called_with(("User 1: Hello World")) 37 | 38 | def test_client_fetch_messages(self): 39 | connection = unittest.mock.Mock() 40 | connection.get_messages.return_value = ["message1", "message2"] 41 | 42 | client = ChatClient("User 1", connection_provider=lambda *args: connection) 43 | 44 | starting_messages = client.fetch_messages() 45 | client.connection.get_messages().append("message3") 46 | new_messages = client.fetch_messages() 47 | 48 | assert starting_messages == ["message1", "message2"] 49 | assert new_messages == ["message3"] 50 | 51 | 52 | 53 | class TestConnection(unittest.TestCase): 54 | def test_broadcast(self): 55 | with unittest.mock.patch.object(Connection, "connect"): 56 | c = Connection(("localhost", 9090)) 57 | 58 | with unittest.mock.patch.object(c, "get_messages", return_value=[]): 59 | c.broadcast("some message") 60 | 61 | assert c.get_messages()[-1] == "some message" 62 | 63 | def test_exchange_with_server(self): 64 | with unittest.mock.patch("multiprocessing.managers.listener_client", new={ 65 | "pickle": (None, FakeServer()) 66 | }): 67 | c1 = Connection(("localhost", 9090)) 68 | c2 = Connection(("localhost", 9090)) 69 | 70 | c1.broadcast("connected message") 71 | 72 | assert c2.get_messages()[-1] == "connected message" 73 | 74 | 75 | from multiprocessing.managers import SyncManager, ListProxy 76 | class Connection(SyncManager): 77 | def __init__(self, address): 78 | self.register("get_messages", proxytype=ListProxy) 79 | super().__init__(address=address, authkey=b'mychatsecret') 80 | self.connect() 81 | 82 | def broadcast(self, message): 83 | messages = self.get_messages() 84 | messages.append(message) 85 | 86 | 87 | class ChatClient: 88 | def __init__(self, nickname, connection_provider=Connection): 89 | self.nickname = nickname 90 | self._connection = None 91 | self._connection_provider = connection_provider 92 | self._last_msg_idx = 0 93 | 94 | def send_message(self, message): 95 | sent_message = "{}: {}".format(self.nickname, message) 96 | self.connection.broadcast(sent_message) 97 | return sent_message 98 | 99 | def fetch_messages(self): 100 | messages = list(self.connection.get_messages()) 101 | new_messages = messages[self._last_msg_idx:] 102 | self._last_msg_idx = len(messages) 103 | return new_messages 104 | 105 | @property 106 | def connection(self): 107 | if self._connection is None: 108 | self._connection = self._connection_provider(("localhost", 9090)) 109 | return self._connection 110 | 111 | 112 | _messages = [] 113 | def _srv_get_messages(): 114 | return _messages 115 | class _ChatServerManager(SyncManager): 116 | pass 117 | _ChatServerManager.register("get_messages", 118 | callable=_srv_get_messages, 119 | proxytype=ListProxy) 120 | 121 | def new_chat_server(): 122 | return _ChatServerManager(("", 9090), authkey=b'mychatsecret') 123 | 124 | 125 | class FakeServer: 126 | def __init__(self): 127 | self.last_command = None 128 | self.messages = [] 129 | 130 | def __call__(self, *args, **kwargs): 131 | return self 132 | 133 | def send(self, data): 134 | callid, command, args, kwargs = data 135 | self.last_command = command 136 | self.last_args = args 137 | 138 | def recv(self, *args, **kwargs): 139 | if self.last_command == "dummy": 140 | return "#RETURN", None 141 | elif self.last_command == "create": 142 | return "#RETURN", ("fakeid", tuple()) 143 | elif self.last_command == "append": 144 | self.messages.append(self.last_args[0]) 145 | return "#RETURN", None 146 | elif self.last_command == "__getitem__": 147 | return "#RETURN", self.messages[self.last_args[0]] 148 | elif self.last_command in ("incref", "decref", "accept_connection"): 149 | return "#RETURN", None 150 | else: 151 | return "#ERROR", ValueError("%s - %r" % (self.last_command, self.last_args)) 152 | 153 | def close(self): 154 | pass 155 | 156 | 157 | if __name__ == '__main__': 158 | unittest.main() 159 | -------------------------------------------------------------------------------- /Chapter02/08_pinject.py: -------------------------------------------------------------------------------- 1 | class ChatClient: 2 | def __init__(self, connection): 3 | print(self, "GOT", connection) 4 | 5 | class Connection: 6 | pass 7 | 8 | import pinject 9 | injector = pinject.new_object_graph() 10 | cli = injector.provide(ChatClient) 11 | 12 | 13 | class FakeConnection: 14 | pass 15 | 16 | 17 | class FakedBindingSpec(pinject.BindingSpec): 18 | def provide_connection(self): 19 | return FakeConnection() 20 | 21 | faked_injector = pinject.new_object_graph(binding_specs=[FakedBindingSpec()]) 22 | cli = faked_injector.provide(ChatClient) 23 | cli2 = faked_injector.provide(ChatClient) 24 | 25 | 26 | class PrototypeBindingSpec(pinject.BindingSpec): 27 | @pinject.provides(in_scope=pinject.PROTOTYPE) 28 | def provide_connection(self): 29 | return Connection() 30 | 31 | proto_injector = pinject.new_object_graph(binding_specs=[PrototypeBindingSpec()]) 32 | cli = proto_injector.provide(ChatClient) 33 | cli2 = proto_injector.provide(ChatClient) 34 | -------------------------------------------------------------------------------- /Chapter03/01_todo/src/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='todo', packages=['todo']) -------------------------------------------------------------------------------- /Chapter03/01_todo/src/todo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter03/01_todo/src/todo/__init__.py -------------------------------------------------------------------------------- /Chapter03/01_todo/src/todo/__main__.py: -------------------------------------------------------------------------------- 1 | from .app import TODOApp 2 | 3 | TODOApp().run() -------------------------------------------------------------------------------- /Chapter03/01_todo/src/todo/app.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | class TODOApp: 4 | def __init__(self, io=(input, functools.partial(print, end=""))): 5 | self._in, self._out = io 6 | self._quit = False 7 | self._db = None 8 | self._entries = [] 9 | 10 | def run(self): 11 | self._quit = False 12 | while not self._quit: 13 | self._out(self.prompt(self.items_list())) 14 | command = self._in() 15 | self._dispatch(command) 16 | self._out("bye!\n") 17 | 18 | def prompt(self, output): 19 | return """TODOs: 20 | {} 21 | 22 | > """.format(output) 23 | 24 | def items_list(self): 25 | return "\n".join("{}. {}".format(idx, entry) for idx, entry in enumerate(self._entries, start=1)) 26 | 27 | def _dispatch(self, cmd): 28 | cmd, *args = cmd.split(" ", 1) 29 | executor = getattr(self, "cmd_{}".format(cmd), None) 30 | if executor is None: 31 | self._out("Invalid command: {}\n".format(cmd)) 32 | return 33 | 34 | executor(*args) 35 | 36 | def cmd_quit(self, *_): 37 | self._quit = True 38 | 39 | def cmd_add(self, what): 40 | self._entries.append(what) 41 | 42 | def cmd_del(self, idx): 43 | idx = int(idx) - 1 44 | if idx < 0 or idx >= len(self._entries): 45 | self._out("Invalid index\n") 46 | return 47 | 48 | self._entries.pop(idx) 49 | 50 | -------------------------------------------------------------------------------- /Chapter03/01_todo/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter03/01_todo/tests/__init__.py -------------------------------------------------------------------------------- /Chapter03/01_todo/tests/test_acceptance.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import threading 3 | import queue 4 | 5 | from todo.app import TODOApp 6 | 7 | 8 | class TestTODOAcceptance(unittest.TestCase): 9 | def setUp(self): 10 | self.inputs = queue.Queue() 11 | self.outputs = queue.Queue() 12 | 13 | self.fake_output = lambda txt: self.outputs.put(txt) 14 | self.fake_input = lambda: self.inputs.get() 15 | 16 | self.get_output = lambda: self.outputs.get(timeout=1) 17 | self.send_input = lambda cmd: self.inputs.put(cmd) 18 | 19 | def test_main(self): 20 | app = TODOApp(io=(self.fake_input, self.fake_output)) 21 | 22 | app_thread = threading.Thread(target=app.run, daemon=True) 23 | app_thread.start() 24 | 25 | welcome = self.get_output() 26 | self.assertEqual(welcome, ( 27 | "TODOs:\n" 28 | "\n" 29 | "\n" 30 | "> " 31 | )) 32 | 33 | self.send_input("add buy milk") 34 | welcome = self.get_output() 35 | self.assertEqual(welcome, ( 36 | "TODOs:\n" 37 | "1. buy milk\n" 38 | "\n" 39 | "> " 40 | )) 41 | 42 | self.send_input("add buy eggs") 43 | welcome = self.get_output() 44 | self.assertEqual(welcome, ( 45 | "TODOs:\n" 46 | "1. buy milk\n" 47 | "2. buy eggs\n" 48 | "\n" 49 | "> " 50 | )) 51 | 52 | self.send_input("del 1") 53 | welcome = self.get_output() 54 | self.assertEqual(welcome, ( 55 | "TODOs:\n" 56 | "1. buy eggs\n" 57 | "\n" 58 | "> " 59 | )) 60 | 61 | self.send_input("quit") 62 | app_thread.join(timeout=1) 63 | 64 | self.assertEqual(self.get_output(), "bye!\n") -------------------------------------------------------------------------------- /Chapter03/02_codedesign/src/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='todo', packages=['todo']) -------------------------------------------------------------------------------- /Chapter03/02_codedesign/src/todo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter03/02_codedesign/src/todo/__init__.py -------------------------------------------------------------------------------- /Chapter03/02_codedesign/src/todo/__main__.py: -------------------------------------------------------------------------------- 1 | from .app import TODOApp 2 | from .db import BasicDB 3 | 4 | TODOApp(dbmanager=BasicDB("todo.data")).run() -------------------------------------------------------------------------------- /Chapter03/02_codedesign/src/todo/app.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | 4 | class TODOApp: 5 | def __init__(self, 6 | io=(input, functools.partial(print, end="")), 7 | dbmanager=None): 8 | self._in, self._out = io 9 | self._quit = False 10 | self._entries = [] 11 | self._dbmanager = dbmanager 12 | 13 | def run(self): 14 | if self._dbmanager is not None: 15 | self._entries = self._dbmanager.load() 16 | 17 | self._quit = False 18 | while not self._quit: 19 | self._out(self.prompt(self.items_list())) 20 | command = self._in() 21 | self._dispatch(command) 22 | 23 | if self._dbmanager is not None: 24 | self._dbmanager.save(self._entries) 25 | 26 | self._out("bye!\n") 27 | 28 | def prompt(self, output): 29 | return """TODOs: 30 | {} 31 | 32 | > """.format(output) 33 | 34 | def items_list(self): 35 | enumerated_items = enumerate(self._entries, start=1) 36 | return "\n".join( 37 | "{}. {}".format(idx, entry) for idx, entry in enumerated_items 38 | ) 39 | 40 | def _dispatch(self, cmd): 41 | cmd, *args = cmd.split(" ", 1) 42 | executor = getattr(self, "cmd_{}".format(cmd), None) 43 | if executor is None: 44 | self._out("Invalid command: {}\n".format(cmd)) 45 | return 46 | 47 | executor(*args) 48 | 49 | def cmd_quit(self, *_): 50 | self._quit = True 51 | 52 | def cmd_add(self, what): 53 | self._entries.append(what) 54 | 55 | def cmd_del(self, idx): 56 | idx = int(idx) - 1 # regression? 57 | if idx < 0 or idx >= len(self._entries): 58 | self._out("Invalid index\n") 59 | return 60 | 61 | self._entries.pop(idx) 62 | 63 | -------------------------------------------------------------------------------- /Chapter03/02_codedesign/src/todo/db.py: -------------------------------------------------------------------------------- 1 | 2 | 3 | class BasicDB: 4 | def __init__(self, path, _fileopener=open): 5 | self._path = path 6 | self._fileopener = _fileopener 7 | 8 | def load(self): 9 | try: 10 | with self._fileopener(self._path, "r", encoding="utf-8") as f: 11 | txt = f.read() 12 | return eval(txt) 13 | except FileNotFoundError: 14 | return [] 15 | 16 | def save(self, values): 17 | with self._fileopener(self._path, "w+", encoding="utf-8") as f: 18 | f.write(repr(values).replace("'", '"')) -------------------------------------------------------------------------------- /Chapter03/02_codedesign/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter03/02_codedesign/tests/__init__.py -------------------------------------------------------------------------------- /Chapter03/02_codedesign/tests/test_acceptance.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import threading 3 | import queue 4 | import tempfile 5 | import pathlib 6 | 7 | from todo.app import TODOApp 8 | from todo.db import BasicDB 9 | 10 | 11 | class TestTODOAcceptance(unittest.TestCase): 12 | def setUp(self): 13 | self.inputs = queue.Queue() 14 | self.outputs = queue.Queue() 15 | 16 | self.fake_output = lambda txt: self.outputs.put(txt) 17 | self.fake_input = lambda: self.inputs.get() 18 | 19 | self.get_output = lambda: self.outputs.get(timeout=1) 20 | self.send_input = lambda cmd: self.inputs.put(cmd) 21 | 22 | def test_main(self): 23 | app = TODOApp(io=(self.fake_input, self.fake_output)) 24 | 25 | app_thread = threading.Thread(target=app.run, daemon=True) 26 | app_thread.start() 27 | 28 | welcome = self.get_output() 29 | self.assertEqual(welcome, ( 30 | "TODOs:\n" 31 | "\n" 32 | "\n" 33 | "> " 34 | )) 35 | 36 | self.send_input("add buy milk") 37 | welcome = self.get_output() 38 | self.assertEqual(welcome, ( 39 | "TODOs:\n" 40 | "1. buy milk\n" 41 | "\n" 42 | "> " 43 | )) 44 | 45 | self.send_input("add buy eggs") 46 | welcome = self.get_output() 47 | self.assertEqual(welcome, ( 48 | "TODOs:\n" 49 | "1. buy milk\n" 50 | "2. buy eggs\n" 51 | "\n" 52 | "> " 53 | )) 54 | 55 | self.send_input("del 1") 56 | welcome = self.get_output() 57 | self.assertEqual(welcome, ( 58 | "TODOs:\n" 59 | "1. buy eggs\n" 60 | "\n" 61 | "> " 62 | )) 63 | 64 | self.send_input("quit") 65 | app_thread.join(timeout=1) 66 | 67 | self.assertEqual(self.get_output(), "bye!\n") 68 | 69 | def test_persistence(self): 70 | with tempfile.TemporaryDirectory() as tmpdirname: 71 | app_thread = threading.Thread( 72 | target=TODOApp( 73 | io=(self.fake_input, self.fake_output), 74 | dbmanager=BasicDB(pathlib.Path(tmpdirname, "db")) 75 | ).run, 76 | daemon=True 77 | ) 78 | app_thread.start() 79 | 80 | # First time the app starts it's empty. 81 | welcome = self.get_output() 82 | self.assertEqual(welcome, ( 83 | "TODOs:\n" 84 | "\n" 85 | "\n" 86 | "> " 87 | )) 88 | 89 | self.send_input("add buy milk") 90 | self.send_input("quit") 91 | app_thread.join(timeout=1) 92 | 93 | while True: 94 | try: 95 | self.get_output() 96 | except queue.Empty: 97 | break 98 | 99 | app_thread = threading.Thread( 100 | target=TODOApp( 101 | io=(self.fake_input, self.fake_output), 102 | dbmanager=BasicDB(pathlib.Path(tmpdirname, "db")) 103 | ).run, 104 | daemon=True 105 | ) 106 | app_thread.start() 107 | 108 | welcome = self.get_output() 109 | self.assertEqual(welcome, ( 110 | "TODOs:\n" 111 | "1. buy milk\n" 112 | "\n" 113 | "> " 114 | )) 115 | 116 | self.send_input("quit") 117 | app_thread.join(timeout=1) 118 | -------------------------------------------------------------------------------- /Chapter03/02_codedesign/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter03/02_codedesign/tests/unit/__init__.py -------------------------------------------------------------------------------- /Chapter03/02_codedesign/tests/unit/test_basicdb.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import unittest 3 | from unittest import mock 4 | 5 | 6 | from todo.db import BasicDB 7 | 8 | 9 | class TestBasicDB(unittest.TestCase): 10 | def test_load(self): 11 | mock_file = mock.MagicMock( 12 | read=mock.Mock(return_value='["first", "second"]') 13 | ) 14 | mock_file.__enter__.return_value = mock_file 15 | mock_opener = mock.Mock(return_value=mock_file) 16 | 17 | db = BasicDB(pathlib.Path("testdb"), _fileopener=mock_opener) 18 | loaded = db.load() 19 | 20 | self.assertEqual( 21 | mock_opener.call_args[0][0], 22 | pathlib.Path("testdb") 23 | ) 24 | mock_file.read.assert_called_with() 25 | self.assertEqual(loaded, ["first", "second"]) 26 | 27 | def test_missing_load(self): 28 | mock_opener = mock.Mock(side_effect=FileNotFoundError) 29 | 30 | db = BasicDB(pathlib.Path("testdb"), _fileopener=mock_opener) 31 | loaded = db.load() 32 | 33 | self.assertEqual( 34 | mock_opener.call_args[0][0], 35 | pathlib.Path("testdb") 36 | ) 37 | self.assertEqual(loaded, []) 38 | 39 | def test_save(self): 40 | mock_file = mock.MagicMock(write=mock.Mock()) 41 | mock_file.__enter__.return_value = mock_file 42 | mock_opener = mock.Mock(return_value=mock_file) 43 | 44 | db = BasicDB(pathlib.Path("testdb"), _fileopener=mock_opener) 45 | loaded = db.save(["first", "second"]) 46 | 47 | self.assertEqual( 48 | mock_opener.call_args[0][0:2], 49 | (pathlib.Path("testdb"), "w+") 50 | ) 51 | mock_file.write.assert_called_with('["first", "second"]') 52 | -------------------------------------------------------------------------------- /Chapter03/02_codedesign/tests/unit/test_todoapp.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock 3 | 4 | 5 | from todo.app import TODOApp 6 | 7 | 8 | class TestTODOApp(unittest.TestCase): 9 | def test_noloader(self): 10 | app = TODOApp(io=(Mock(return_value="quit"), Mock()), 11 | dbmanager=None) 12 | 13 | app.run() 14 | 15 | assert app._entries == [] 16 | 17 | def test_load(self): 18 | dbmanager = Mock( 19 | load=Mock(return_value=["buy milk", "buy water"]) 20 | ) 21 | app = TODOApp(io=(Mock(return_value="quit"), Mock()), 22 | dbmanager=dbmanager) 23 | 24 | app.run() 25 | 26 | dbmanager.load.assert_called_with() 27 | assert app._entries == ["buy milk", "buy water"] 28 | 29 | def test_save(self): 30 | dbmanager = Mock( 31 | load=Mock(return_value=["buy milk", "buy water"]), 32 | save=Mock() 33 | ) 34 | 35 | app = TODOApp(io=(Mock(return_value="quit"), Mock()), 36 | dbmanager=dbmanager) 37 | app.run() 38 | 39 | dbmanager.save.assert_called_with(["buy milk", "buy water"]) 40 | -------------------------------------------------------------------------------- /Chapter03/03_regression/src/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='todo', packages=['todo']) -------------------------------------------------------------------------------- /Chapter03/03_regression/src/todo/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter03/03_regression/src/todo/__init__.py -------------------------------------------------------------------------------- /Chapter03/03_regression/src/todo/__main__.py: -------------------------------------------------------------------------------- 1 | from .app import TODOApp 2 | from .db import BasicDB 3 | 4 | TODOApp(dbmanager=BasicDB("todo.data")).run() -------------------------------------------------------------------------------- /Chapter03/03_regression/src/todo/app.py: -------------------------------------------------------------------------------- 1 | import functools 2 | 3 | 4 | class TODOApp: 5 | def __init__(self, 6 | io=(input, functools.partial(print, end="")), 7 | dbmanager=None): 8 | self._in, self._out = io 9 | self._quit = False 10 | self._entries = [] 11 | self._dbmanager = dbmanager 12 | 13 | def run(self): 14 | if self._dbmanager is not None: 15 | self._entries = self._dbmanager.load() 16 | 17 | self._quit = False 18 | while not self._quit: 19 | self._out(self.prompt(self.items_list())) 20 | command = self._in() 21 | self._dispatch(command) 22 | 23 | if self._dbmanager is not None: 24 | self._dbmanager.save(self._entries) 25 | 26 | self._out("bye!\n") 27 | 28 | def prompt(self, output): 29 | return """TODOs: 30 | {} 31 | 32 | > """.format(output) 33 | 34 | def items_list(self): 35 | enumerated_items = enumerate(self._entries, start=1) 36 | return "\n".join( 37 | "{}. {}".format(idx, entry) for idx, entry in enumerated_items 38 | ) 39 | 40 | def _dispatch(self, cmd): 41 | cmd, *args = cmd.split(" ", 1) 42 | executor = getattr(self, "cmd_{}".format(cmd), None) 43 | if executor is None: 44 | self._out("Invalid command: {}\n".format(cmd)) 45 | return 46 | 47 | executor(*args) 48 | 49 | def cmd_quit(self, *_): 50 | self._quit = True 51 | 52 | def cmd_add(self, what): 53 | self._entries.append(what) 54 | 55 | def cmd_del(self, idx): 56 | idx = int(idx) - 1 # regression? 57 | if idx < 0 or idx >= len(self._entries): 58 | self._out("Invalid index\n") 59 | return 60 | 61 | self._entries.pop(idx) 62 | 63 | -------------------------------------------------------------------------------- /Chapter03/03_regression/src/todo/db.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | 4 | class BasicDB: 5 | def __init__(self, path, _fileopener=open): 6 | self._path = path 7 | self._fileopener = _fileopener 8 | 9 | def load(self): 10 | try: 11 | with self._fileopener(self._path, "r", encoding="utf-8") as f: 12 | return json.load(f) 13 | except FileNotFoundError: 14 | return [] 15 | 16 | def save(self, values): 17 | with self._fileopener(self._path, "w+", encoding="utf-8") as f: 18 | f.write(json.dumps(values)) 19 | -------------------------------------------------------------------------------- /Chapter03/03_regression/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter03/03_regression/tests/__init__.py -------------------------------------------------------------------------------- /Chapter03/03_regression/tests/test_acceptance.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import threading 3 | import queue 4 | import tempfile 5 | import pathlib 6 | 7 | from todo.app import TODOApp 8 | from todo.db import BasicDB 9 | 10 | 11 | class TestTODOAcceptance(unittest.TestCase): 12 | def setUp(self): 13 | self.inputs = queue.Queue() 14 | self.outputs = queue.Queue() 15 | 16 | self.fake_output = lambda txt: self.outputs.put(txt) 17 | self.fake_input = lambda: self.inputs.get() 18 | 19 | self.get_output = lambda: self.outputs.get(timeout=1) 20 | self.send_input = lambda cmd: self.inputs.put(cmd) 21 | 22 | def test_main(self): 23 | app = TODOApp(io=(self.fake_input, self.fake_output)) 24 | 25 | app_thread = threading.Thread(target=app.run, daemon=True) 26 | app_thread.start() 27 | 28 | welcome = self.get_output() 29 | self.assertEqual(welcome, ( 30 | "TODOs:\n" 31 | "\n" 32 | "\n" 33 | "> " 34 | )) 35 | 36 | self.send_input("add buy milk") 37 | welcome = self.get_output() 38 | self.assertEqual(welcome, ( 39 | "TODOs:\n" 40 | "1. buy milk\n" 41 | "\n" 42 | "> " 43 | )) 44 | 45 | self.send_input("add buy eggs") 46 | welcome = self.get_output() 47 | self.assertEqual(welcome, ( 48 | "TODOs:\n" 49 | "1. buy milk\n" 50 | "2. buy eggs\n" 51 | "\n" 52 | "> " 53 | )) 54 | 55 | self.send_input("del 1") 56 | welcome = self.get_output() 57 | self.assertEqual(welcome, ( 58 | "TODOs:\n" 59 | "1. buy eggs\n" 60 | "\n" 61 | "> " 62 | )) 63 | 64 | self.send_input("quit") 65 | app_thread.join(timeout=1) 66 | 67 | self.assertEqual(self.get_output(), "bye!\n") 68 | 69 | def test_persistence(self): 70 | with tempfile.TemporaryDirectory() as tmpdirname: 71 | app_thread = threading.Thread( 72 | target=TODOApp( 73 | io=(self.fake_input, self.fake_output), 74 | dbmanager=BasicDB(pathlib.Path(tmpdirname, "db")) 75 | ).run, 76 | daemon=True 77 | ) 78 | app_thread.start() 79 | 80 | # First time the app starts it's empty. 81 | welcome = self.get_output() 82 | self.assertEqual(welcome, ( 83 | "TODOs:\n" 84 | "\n" 85 | "\n" 86 | "> " 87 | )) 88 | 89 | self.send_input("add buy milk") 90 | self.send_input("quit") 91 | app_thread.join(timeout=1) 92 | 93 | while True: 94 | try: 95 | self.get_output() 96 | except queue.Empty: 97 | break 98 | 99 | app_thread = threading.Thread( 100 | target=TODOApp( 101 | io=(self.fake_input, self.fake_output), 102 | dbmanager=BasicDB(pathlib.Path(tmpdirname, "db")) 103 | ).run, 104 | daemon=True 105 | ) 106 | app_thread.start() 107 | 108 | welcome = self.get_output() 109 | self.assertEqual(welcome, ( 110 | "TODOs:\n" 111 | "1. buy milk\n" 112 | "\n" 113 | "> " 114 | )) 115 | 116 | self.send_input("quit") 117 | app_thread.join(timeout=1) 118 | -------------------------------------------------------------------------------- /Chapter03/03_regression/tests/test_regressions.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest import mock 3 | import io 4 | 5 | from todo.app import TODOApp 6 | from todo.db import BasicDB 7 | 8 | 9 | class TestRegressions(unittest.TestCase): 10 | def test_os_release(self): 11 | fakefile = io.StringIO() 12 | fakefile.close = mock.Mock() 13 | 14 | data = ["buy milk", 'install "Focal Fossa"'] 15 | 16 | dbmanager = BasicDB(None, _fileopener=mock.Mock( 17 | return_value=fakefile 18 | )) 19 | 20 | dbmanager.save(data) 21 | fakefile.seek(0) 22 | loaded_data = dbmanager.load() 23 | 24 | self.assertEqual(loaded_data, data) 25 | -------------------------------------------------------------------------------- /Chapter03/03_regression/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter03/03_regression/tests/unit/__init__.py -------------------------------------------------------------------------------- /Chapter03/03_regression/tests/unit/test_basicdb.py: -------------------------------------------------------------------------------- 1 | import pathlib 2 | import unittest 3 | from unittest import mock 4 | 5 | 6 | from todo.db import BasicDB 7 | 8 | 9 | class TestBasicDB(unittest.TestCase): 10 | def test_load(self): 11 | mock_file = mock.MagicMock( 12 | read=mock.Mock(return_value='["first", "second"]') 13 | ) 14 | mock_file.__enter__.return_value = mock_file 15 | mock_opener = mock.Mock(return_value=mock_file) 16 | 17 | db = BasicDB(pathlib.Path("testdb"), _fileopener=mock_opener) 18 | loaded = db.load() 19 | 20 | self.assertEqual( 21 | mock_opener.call_args[0][0], 22 | pathlib.Path("testdb") 23 | ) 24 | mock_file.read.assert_called_with() 25 | self.assertEqual(loaded, ["first", "second"]) 26 | 27 | def test_missing_load(self): 28 | mock_opener = mock.Mock(side_effect=FileNotFoundError) 29 | 30 | db = BasicDB(pathlib.Path("testdb"), _fileopener=mock_opener) 31 | loaded = db.load() 32 | 33 | self.assertEqual( 34 | mock_opener.call_args[0][0], 35 | pathlib.Path("testdb") 36 | ) 37 | self.assertEqual(loaded, []) 38 | 39 | def test_save(self): 40 | mock_file = mock.MagicMock(write=mock.Mock()) 41 | mock_file.__enter__.return_value = mock_file 42 | mock_opener = mock.Mock(return_value=mock_file) 43 | 44 | db = BasicDB(pathlib.Path("testdb"), _fileopener=mock_opener) 45 | loaded = db.save(["first", "second"]) 46 | 47 | self.assertEqual( 48 | mock_opener.call_args[0][0:2], 49 | (pathlib.Path("testdb"), "w+") 50 | ) 51 | mock_file.write.assert_called_with('["first", "second"]') 52 | -------------------------------------------------------------------------------- /Chapter03/03_regression/tests/unit/test_todoapp.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | from unittest.mock import Mock 3 | 4 | 5 | from todo.app import TODOApp 6 | 7 | 8 | class TestTODOApp(unittest.TestCase): 9 | def test_noloader(self): 10 | app = TODOApp(io=(Mock(return_value="quit"), Mock()), 11 | dbmanager=None) 12 | 13 | app.run() 14 | 15 | assert app._entries == [] 16 | 17 | def test_load(self): 18 | dbmanager = Mock( 19 | load=Mock(return_value=["buy milk", "buy water"]) 20 | ) 21 | app = TODOApp(io=(Mock(return_value="quit"), Mock()), 22 | dbmanager=dbmanager) 23 | 24 | app.run() 25 | 26 | dbmanager.load.assert_called_with() 27 | assert app._entries == ["buy milk", "buy water"] 28 | 29 | def test_save(self): 30 | dbmanager = Mock( 31 | load=Mock(return_value=["buy milk", "buy water"]), 32 | save=Mock() 33 | ) 34 | 35 | app = TODOApp(io=(Mock(return_value="quit"), Mock()), 36 | dbmanager=dbmanager) 37 | app.run() 38 | 39 | dbmanager.save.assert_called_with(["buy milk", "buy water"]) 40 | -------------------------------------------------------------------------------- /Chapter04/.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | 3 | os: linux 4 | dist: xenial 5 | 6 | python: 7 | - 3.7 8 | - &mainstream_python 3.8 9 | - nightly 10 | 11 | install: 12 | - "pip install -e src" 13 | 14 | script: 15 | - "python -m unittest discover tests -v" 16 | 17 | after_success: 18 | - "python -m unittest discover benchmarks -v" 19 | -------------------------------------------------------------------------------- /Chapter04/benchmarks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter04/benchmarks/__init__.py -------------------------------------------------------------------------------- /Chapter04/benchmarks/test_chat.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import timeit 3 | 4 | from chat.client import ChatClient 5 | from chat.server import new_chat_server 6 | 7 | 8 | class BenchmarkMixin: 9 | def bench(self, f, number): 10 | t = timeit.timeit(f, number=number) 11 | print(f"\n\ttime: {t:.2f}, iteration: {t/number:.2f}") 12 | 13 | 14 | class BenchmarkChat(unittest.TestCase, BenchmarkMixin): 15 | def test_sending_messages(self): 16 | with new_chat_server() as srv: 17 | user1 = ChatClient("User1") 18 | 19 | self.bench(lambda: user1.send_message("Hello World"), number=10) 20 | -------------------------------------------------------------------------------- /Chapter04/src/chat/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter04/src/chat/__init__.py -------------------------------------------------------------------------------- /Chapter04/src/chat/client.py: -------------------------------------------------------------------------------- 1 | from multiprocessing.managers import SyncManager, ListProxy 2 | 3 | 4 | class Connection(SyncManager): 5 | def __init__(self, address): 6 | self.register("get_messages", proxytype=ListProxy) 7 | super().__init__(address=address, authkey=b'mychatsecret') 8 | self.connect() 9 | 10 | def broadcast(self, message): 11 | messages = self.get_messages() 12 | messages.append(message) 13 | 14 | 15 | class ChatClient: 16 | def __init__(self, nickname, connection_provider=Connection): 17 | self.nickname = nickname 18 | self._connection = None 19 | self._connection_provider = connection_provider 20 | self._last_msg_idx = 0 21 | 22 | def send_message(self, message): 23 | sent_message = "{}: {}".format(self.nickname, message) 24 | self.connection.broadcast(sent_message) 25 | return sent_message 26 | 27 | def fetch_messages(self): 28 | messages = list(self.connection.get_messages()) 29 | new_messages = messages[self._last_msg_idx:] 30 | self._last_msg_idx = len(messages) 31 | return new_messages 32 | 33 | @property 34 | def connection(self): 35 | if self._connection is None: 36 | self._connection = self._connection_provider(("localhost", 9090)) 37 | return self._connection 38 | -------------------------------------------------------------------------------- /Chapter04/src/chat/server.py: -------------------------------------------------------------------------------- 1 | from multiprocessing.managers import SyncManager, ListProxy 2 | 3 | 4 | _messages = [] 5 | def _srv_get_messages(): 6 | return _messages 7 | class _ChatServerManager(SyncManager): 8 | pass 9 | _ChatServerManager.register("get_messages", 10 | callable=_srv_get_messages, 11 | proxytype=ListProxy) 12 | 13 | def new_chat_server(): 14 | return _ChatServerManager(("", 9090), authkey=b'mychatsecret') -------------------------------------------------------------------------------- /Chapter04/src/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='chat', packages=['chat']) 4 | -------------------------------------------------------------------------------- /Chapter04/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter04/tests/__init__.py -------------------------------------------------------------------------------- /Chapter04/tests/e2e/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter04/tests/e2e/__init__.py -------------------------------------------------------------------------------- /Chapter04/tests/e2e/test_chat.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import unittest.mock 3 | 4 | from chat.client import ChatClient 5 | from chat.server import new_chat_server 6 | 7 | 8 | class TestChatAcceptance(unittest.TestCase): 9 | def test_message_exchange(self): 10 | with new_chat_server() as srv: 11 | user1 = ChatClient("John Doe") 12 | user2 = ChatClient("Harry Potter") 13 | 14 | user1.send_message("Hello World") 15 | messages = user2.fetch_messages() 16 | 17 | assert messages == ["John Doe: Hello World"] 18 | 19 | def test_smoke_sending_message(self): 20 | with new_chat_server() as srv: 21 | user1 = ChatClient("User1") 22 | user1.send_message("Hello World") 23 | 24 | 25 | """ 26 | class TestChatMultiUser(unittest.TestCase): 27 | def test_many_users(self): 28 | with new_chat_server() as srv: 29 | firstUser = ChatClient("John Doe") 30 | 31 | for uid in range(5): 32 | moreuser = ChatClient(f"User {uid}") 33 | moreuser.send_message("Hello!") 34 | 35 | messages = firstUser.fetch_messages() 36 | assert len(messages) == 5 37 | 38 | def test_multiple_readers(self): 39 | with new_chat_server() as srv: 40 | user1 = ChatClient("John Doe") 41 | user2 = ChatClient("User 2") 42 | user3 = ChatClient("User 3") 43 | 44 | user1.send_message("Hi all") 45 | user2.send_message("Hello World") 46 | user3.send_message("Hi") 47 | 48 | user1_messages = user1.fetch_messages() 49 | user2_messages = user2.fetch_messages() 50 | 51 | self.assertEqual(user1_messages, user2_messages) 52 | """ 53 | -------------------------------------------------------------------------------- /Chapter04/tests/functional/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter04/tests/functional/__init__.py -------------------------------------------------------------------------------- /Chapter04/tests/functional/fakeserver.py: -------------------------------------------------------------------------------- 1 | 2 | class FakeServer: 3 | def __init__(self): 4 | self.last_command = None 5 | self.messages = [] 6 | 7 | def __call__(self, *args, **kwargs): 8 | return self 9 | 10 | def send(self, data): 11 | callid, command, args, kwargs = data 12 | self.last_command = command 13 | self.last_args = args 14 | 15 | def recv(self, *args, **kwargs): 16 | if self.last_command == "dummy": 17 | return "#RETURN", None 18 | elif self.last_command == "create": 19 | return "#RETURN", ("fakeid", tuple()) 20 | elif self.last_command == "append": 21 | self.messages.append(self.last_args[0]) 22 | return "#RETURN", None 23 | elif self.last_command == "__getitem__": 24 | return "#RETURN", self.messages[self.last_args[0]] 25 | elif self.last_command == "__len__": 26 | return "#RETURN", len(self.messages) 27 | elif self.last_command in ("incref", "decref", "accept_connection"): 28 | return "#RETURN", None 29 | else: 30 | return "#ERROR", ValueError("%s - %r" % (self.last_command, self.last_args)) 31 | 32 | def close(self): 33 | pass 34 | -------------------------------------------------------------------------------- /Chapter04/tests/functional/test_chat.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import unittest.mock 3 | 4 | from chat.client import ChatClient 5 | 6 | from .fakeserver import FakeServer 7 | 8 | 9 | class TestChatMessageExchange(unittest.TestCase): 10 | def setUp(self): 11 | self.fakeserver = unittest.mock.patch("multiprocessing.managers.listener_client", new={ 12 | "pickle": (None, FakeServer()) 13 | }) 14 | self.fakeserver.start() 15 | 16 | def tearDown(self): 17 | self.fakeserver.stop() 18 | 19 | def test_exchange_with_server(self): 20 | c1 = ChatClient("User1") 21 | c2 = ChatClient("User2") 22 | 23 | c1.send_message("connected message") 24 | 25 | assert c2.fetch_messages()[-1] == "User1: connected message" 26 | 27 | def test_many_users(self): 28 | firstUser = ChatClient("John Doe") 29 | 30 | for uid in range(5): 31 | moreuser = ChatClient(f"User {uid}") 32 | moreuser.send_message("Hello!") 33 | 34 | messages = firstUser.fetch_messages() 35 | assert len(messages) == 5 36 | 37 | def test_multiple_readers(self): 38 | user1 = ChatClient("John Doe") 39 | user2 = ChatClient("User 2") 40 | user3 = ChatClient("User 3") 41 | 42 | user1.send_message("Hi all") 43 | user2.send_message("Hello World") 44 | user3.send_message("Hi") 45 | 46 | user1_messages = user1.fetch_messages() 47 | user2_messages = user2.fetch_messages() 48 | 49 | self.assertEqual(user1_messages, user2_messages) -------------------------------------------------------------------------------- /Chapter04/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter04/tests/unit/__init__.py -------------------------------------------------------------------------------- /Chapter04/tests/unit/test_client.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import unittest.mock 3 | 4 | from chat.client import ChatClient 5 | 6 | 7 | class TestChatClient(unittest.TestCase): 8 | def test_nickname(self): 9 | client = ChatClient("User 1") 10 | 11 | assert client.nickname == "User 1" 12 | 13 | def test_send_message(self): 14 | client = ChatClient("User 1", connection_provider=unittest.mock.Mock()) 15 | 16 | sent_message = client.send_message("Hello World") 17 | 18 | assert sent_message == "User 1: Hello World" 19 | 20 | def test_client_connection(self): 21 | connection_spy = unittest.mock.MagicMock() 22 | 23 | client = ChatClient("User 1", connection_provider=lambda *args: connection_spy) 24 | client.send_message("Hello World") 25 | 26 | connection_spy.broadcast.assert_called_with(("User 1: Hello World")) 27 | 28 | def test_client_fetch_messages(self): 29 | connection = unittest.mock.Mock() 30 | connection.get_messages.return_value = ["message1", "message2"] 31 | 32 | client = ChatClient("User 1", connection_provider=lambda *args: connection) 33 | 34 | starting_messages = client.fetch_messages() 35 | client.connection.get_messages().append("message3") 36 | new_messages = client.fetch_messages() 37 | 38 | assert starting_messages == ["message1", "message2"] 39 | assert new_messages == ["message3"] 40 | 41 | 42 | -------------------------------------------------------------------------------- /Chapter04/tests/unit/test_connection.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import unittest.mock 3 | 4 | from chat.client import Connection 5 | 6 | 7 | class TestConnection(unittest.TestCase): 8 | def test_broadcast(self): 9 | with unittest.mock.patch.object(Connection, "connect"): 10 | c = Connection(("localhost", 9090)) 11 | 12 | with unittest.mock.patch.object(c, "get_messages", return_value=[]): 13 | c.broadcast("some message") 14 | 15 | assert c.get_messages()[-1] == "some message" 16 | -------------------------------------------------------------------------------- /Chapter05/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | @pytest.fixture(scope="session", autouse=True) 4 | def setupsuite(): 5 | print("STARTING TESTS") 6 | yield 7 | print("FINISHED TESTS") 8 | 9 | 10 | @pytest.fixture 11 | def random_number_generator(): 12 | import random 13 | def _number_provider(): 14 | return random.choice(range(10)) 15 | yield _number_provider 16 | -------------------------------------------------------------------------------- /Chapter05/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | markers = 3 | first: mark a test as a webtest. 4 | -------------------------------------------------------------------------------- /Chapter05/test_capsys.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def myapp(): 5 | print("MyApp Started") 6 | 7 | 8 | def test_capsys(capsys): 9 | myapp() 10 | 11 | out, err = capsys.readouterr() 12 | 13 | assert out == "MyApp Started\n" 14 | -------------------------------------------------------------------------------- /Chapter05/test_fixturesinj.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_something(random_number_generator): 5 | a = random_number_generator() 6 | b = 10 7 | assert a + b == 11 8 | 9 | 10 | @pytest.fixture 11 | def random_number_generator(): 12 | def _number_provider(): 13 | return 1 14 | yield _number_provider 15 | -------------------------------------------------------------------------------- /Chapter05/test_markers.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.mark.first 5 | def test_one(): 6 | assert True 7 | 8 | def test_two(): 9 | assert True 10 | -------------------------------------------------------------------------------- /Chapter05/test_randomness.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_something(random_number_generator): 5 | a = random_number_generator() 6 | b = 10 7 | assert a + b >= 10 8 | 9 | -------------------------------------------------------------------------------- /Chapter05/test_simple.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_something(): 5 | a = 5 6 | b = 10 7 | assert a + b == 11 8 | 9 | 10 | class TestMultiple: 11 | def test_first(self): 12 | assert 5 == 5 13 | 14 | def test_second(self): 15 | assert 10 == 10 16 | 17 | -------------------------------------------------------------------------------- /Chapter05/test_tmppath.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def test_tmp(tmp_path): 5 | f = tmp_path / "file.txt" 6 | print("FILE: ", f) 7 | 8 | f.write_text("Hello World") 9 | 10 | fread = tmp_path / "file.txt" 11 | assert fread.read_text() == "Hello World" 12 | -------------------------------------------------------------------------------- /Chapter05/test_usingfixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | @pytest.mark.usefixtures("provide_current_time") 4 | class TestMultiple: 5 | def test_first(self): 6 | print("RUNNING AT", self.now) 7 | assert 5 == 5 8 | 9 | @pytest.mark.usefixtures("greetings") 10 | def test_second(self): 11 | assert 10 == 10 12 | 13 | 14 | @pytest.fixture 15 | def greetings(): 16 | print("HELLO!") 17 | yield 18 | print("GOODBYE") 19 | 20 | 21 | @pytest.fixture(scope="class") 22 | def provide_current_time(request): 23 | import datetime 24 | request.cls.now = datetime.datetime.utcnow() 25 | print("ENTER CLS") 26 | yield 27 | print("EXIT CLS") 28 | 29 | -------------------------------------------------------------------------------- /Chapter06/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = -v -s 3 | filterwarnings = 4 | ignore::DeprecationWarning 5 | 6 | -------------------------------------------------------------------------------- /Chapter06/src/fizzbuzz/__init__.py: -------------------------------------------------------------------------------- 1 | def isfizz(n): 2 | return n % 3 == 0 3 | 4 | def isbuzz(n): 5 | return n % 5 == 0 6 | 7 | def outfizz(): 8 | print("fizz", end="") 9 | 10 | def outbuzz(): 11 | print("buzz", end="") 12 | 13 | def endnum(n): 14 | if isfizz(n) or isbuzz(n): 15 | n = "" 16 | print(n) 17 | 18 | def fizzbuzz(numbers): 19 | for n in numbers: 20 | if isfizz(n): 21 | outfizz() 22 | if isbuzz(n): 23 | outbuzz() 24 | endnum(n) 25 | 26 | def main(): 27 | fizzbuzz(range(100)) 28 | -------------------------------------------------------------------------------- /Chapter06/src/fizzbuzz/__main__.py: -------------------------------------------------------------------------------- 1 | from . import main 2 | 3 | main() -------------------------------------------------------------------------------- /Chapter06/src/fizzbuzz/testing/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter06/src/fizzbuzz/testing/__init__.py -------------------------------------------------------------------------------- /Chapter06/src/fizzbuzz/testing/fixtures.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | @pytest.fixture(scope="function", autouse=True) 5 | def announce(request): 6 | print("RUNNING", request.function) -------------------------------------------------------------------------------- /Chapter06/src/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='fizzbuzz', packages=['fizzbuzz']) 4 | -------------------------------------------------------------------------------- /Chapter06/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter06/tests/__init__.py -------------------------------------------------------------------------------- /Chapter06/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | pytest_plugins = ["fizzbuzz.testing.fixtures"] 4 | 5 | 6 | def pytest_runtest_setup(item): 7 | print("Hook announce", item) 8 | 9 | 10 | @pytest.fixture(scope="function", autouse=True) 11 | def enterexit(): 12 | print("ENTER") 13 | yield 14 | print("EXIT") 15 | 16 | 17 | def pytest_addoption(parser): 18 | parser.addoption( 19 | "--upper", action="store_true", 20 | help="test for uppercase behaviour" 21 | ) 22 | 23 | -------------------------------------------------------------------------------- /Chapter06/tests/functional/test_acceptance.py: -------------------------------------------------------------------------------- 1 | from fizzbuzz import fizzbuzz 2 | 3 | 4 | def test_fizzbuzz(capsys): 5 | numbers = range(20) 6 | 7 | fizzbuzz(numbers) 8 | 9 | -------------------------------------------------------------------------------- /Chapter06/tests/unit/test_checks.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from fizzbuzz import isfizz, isbuzz 4 | 5 | @pytest.mark.parametrize("n,res", [ 6 | (1, False), 7 | (3, True), 8 | (4, False), 9 | (6, True) 10 | ]) 11 | def test_isfizz(n, res): 12 | assert isfizz(n) is res 13 | 14 | 15 | @pytest.fixture(scope="function") 16 | def divisible_by5(n): 17 | return n % 5 == 0 18 | 19 | 20 | @pytest.mark.parametrize("n", [ 21 | 1, 3, 5, 6, 10 22 | ]) 23 | def test_isbuzz(n, divisible_by5): 24 | assert isbuzz(n) is divisible_by5 25 | -------------------------------------------------------------------------------- /Chapter06/tests/unit/test_output.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | import fizzbuzz 4 | from fizzbuzz import outfizz, outbuzz, endnum 5 | 6 | 7 | @pytest.fixture(params=["fizz", "buzz"]) 8 | def expected_output(request): 9 | text = request.param 10 | if request.config.getoption("--upper"): 11 | text = text.upper() 12 | 13 | textcasemarker = request.node.get_closest_marker("textcase") 14 | if textcasemarker: 15 | textcase, = textcasemarker.args 16 | if textcase == "upper": 17 | text = text.upper() 18 | elif textcase == "lower": 19 | text = text.lower() 20 | else: 21 | raise ValueError("Invalid Test Marker") 22 | 23 | yield getattr(fizzbuzz, "out{}".format(request.param)), text 24 | 25 | 26 | def test_output(expected_output, capsys): 27 | func, expected = expected_output 28 | 29 | func() 30 | 31 | out, _ = capsys.readouterr() 32 | assert out == expected 33 | 34 | 35 | @pytest.mark.textcase("lower") 36 | def test_lowercase_output(expected_output, capsys): 37 | func, expected = expected_output 38 | 39 | func() 40 | 41 | out, _ = capsys.readouterr() 42 | assert out == expected 43 | 44 | 45 | class TestEndNum: 46 | def test_plainnum(self, capsys): 47 | endnum(1) 48 | endnum(4) 49 | endnum(7) 50 | 51 | out, _ = capsys.readouterr() 52 | assert out == "1\n4\n7\n" 53 | 54 | def test_omitnum(self, capsys): 55 | endnum(3) 56 | endnum(5) 57 | endnum(15) 58 | 59 | out, _ = capsys.readouterr() 60 | assert out == "\n\n\n" 61 | -------------------------------------------------------------------------------- /Chapter07/src/contacts/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | 4 | 5 | class Application: 6 | PHONE_EXPR = re.compile('^[+]?[0-9]{3,}$') 7 | 8 | def __init__(self): 9 | self._clear() 10 | 11 | def _clear(self): 12 | self._contacts = [] 13 | 14 | def run(self, text): 15 | text = text.strip() 16 | _, cmd = text.split(maxsplit=1) 17 | try: 18 | cmd, args = cmd.split(maxsplit=1) 19 | except ValueError: 20 | args = None 21 | 22 | if cmd == "add": 23 | name, num = args.rsplit(maxsplit=1) 24 | try: 25 | self.add(name, num) 26 | except ValueError as err: 27 | print(err) 28 | return 29 | elif cmd == "del": 30 | self.delete(args) 31 | elif cmd == "ls": 32 | self.printlist() 33 | else: 34 | raise ValueError(f"Invalid command: {cmd}") 35 | 36 | def save(self): 37 | with open("./contacts.json", "w+") as f: 38 | json.dump({"_contacts": self._contacts}, f) 39 | 40 | def load(self): 41 | with open("./contacts.json") as f: 42 | self._contacts = [ 43 | tuple(t) for t in json.load(f)["_contacts"] 44 | ] 45 | 46 | def add(self, name, phonenum): 47 | if not isinstance(phonenum, str): 48 | raise ValueError("A valid phone number is required") 49 | 50 | if not self.PHONE_EXPR.match(phonenum): 51 | raise ValueError(f"Invalid phone number: {phonenum}") 52 | 53 | self._contacts.append((name, phonenum)) 54 | self.save() 55 | 56 | def delete(self, name): 57 | self._contacts = [ 58 | c for c in self._contacts if c[0] != name 59 | ] 60 | self.save() 61 | 62 | def printlist(self): 63 | for c in self._contacts: 64 | print(f"{c[0]} {c[1]}") 65 | 66 | 67 | def main(): 68 | raise NotImplementedError() 69 | -------------------------------------------------------------------------------- /Chapter07/src/contacts/__main__.py: -------------------------------------------------------------------------------- 1 | from . import main 2 | 3 | main() -------------------------------------------------------------------------------- /Chapter07/src/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='contacts', packages=['contacts']) 4 | -------------------------------------------------------------------------------- /Chapter07/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter07/tests/__init__.py -------------------------------------------------------------------------------- /Chapter07/tests/acceptance/delete_contact.feature: -------------------------------------------------------------------------------- 1 | Feature: Deleting Contacts 2 | Contacts added to our contact book can be removed. 3 | 4 | Scenario: Removing a Basic Contact 5 | Given I have a contact book 6 | And I have a "John" contact 7 | When I run the "contacts del John" command 8 | Then My contacts book is now empty 9 | 10 | -------------------------------------------------------------------------------- /Chapter07/tests/acceptance/list_contacts.feature: -------------------------------------------------------------------------------- 1 | Feature: Listing Contacts 2 | Contacts added to our contact book can be listed back. 3 | 4 | Scenario: Listing Added Contacts 5 | Given I have a contact book 6 | And I have a first contact 7 | And I have a second contact 8 | When I run the "contacts ls" command 9 | Then the output contains contacts 10 | 11 | Examples: 12 | | first | second | listed_contacts | 13 | | Mario | Luigi | Mario,Luigi | 14 | | John | Jane | John,Jane | 15 | -------------------------------------------------------------------------------- /Chapter07/tests/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter07/tests/conftest.py -------------------------------------------------------------------------------- /Chapter07/tests/functional/test_acceptance.py: -------------------------------------------------------------------------------- 1 | import contacts 2 | 3 | from pytest_bdd import scenario, given, when, then, parsers 4 | 5 | 6 | class TestAddingEntries: 7 | def test_basic(self): 8 | app = contacts.Application() 9 | 10 | app.run("contacts add NAME 3345554433") 11 | 12 | assert app._contacts == [ 13 | ("NAME", "3345554433") 14 | ] 15 | 16 | def test_surnames(self): 17 | app = contacts.Application() 18 | 19 | app.run("contacts add Mario Mario 3345554433") 20 | app.run("contacts add Luigi Mario 3345554434") 21 | app.run("contacts add Princess Peach Toadstool 3339323323") 22 | 23 | assert app._contacts == [ 24 | ("Mario Mario", "3345554433"), 25 | ("Luigi Mario", "3345554434"), 26 | ("Princess Peach Toadstool", "3339323323") 27 | ] 28 | 29 | def test_international_numbers(self): 30 | app = contacts.Application() 31 | 32 | app.run("contacts add NAME +393345554433") 33 | 34 | assert app._contacts == [ 35 | ("NAME", "+393345554433") 36 | ] 37 | 38 | def test_invalid_strings(self): 39 | app = contacts.Application() 40 | 41 | app.run("contacts add NAME InvalidString") 42 | 43 | assert app._contacts == [] 44 | 45 | def test_reload(self): 46 | app = contacts.Application() 47 | 48 | app.run("contacts add NAME 3345554433") 49 | 50 | assert app._contacts == [ 51 | ("NAME", "3345554433") 52 | ] 53 | 54 | app._clear() 55 | app.load() 56 | 57 | assert app._contacts == [ 58 | ("NAME", "3345554433") 59 | ] 60 | 61 | 62 | @scenario("../acceptance/delete_contact.feature", 63 | "Removing a Basic Contact") 64 | def test_deleting_contacts(): 65 | pass 66 | 67 | @then("My contacts book is now empty") 68 | def emptylist(contactbook): 69 | assert contactbook._contacts == [] 70 | 71 | 72 | @scenario("../acceptance/list_contacts.feature", 73 | "Listing Added Contacts") 74 | def test_listing_added_contacts(capsys): 75 | pass 76 | 77 | @given("I have a first contact") 78 | def have_a_first_contact(contactbook, first): 79 | contactbook.add(first, "000") 80 | return first 81 | 82 | @given("I have a second contact") 83 | def have_a_second_contact(contactbook, second): 84 | contactbook.add(second, "000") 85 | return second 86 | 87 | @then("the output contains contacts") 88 | def outputcontains(listed_contacts, capsys): 89 | expected_list = "".join([f"{c} 000\n" for c in listed_contacts.split(",")]) 90 | out, _ = capsys.readouterr() 91 | assert out == expected_list 92 | 93 | 94 | @given("I have a contact book", target_fixture="contactbook") 95 | def contactbook(): 96 | return contacts.Application() 97 | 98 | @given(parsers.parse("I have a \"{contactname}\" contact")) 99 | def have_a_contact(contactbook, contactname): 100 | contactbook.add(contactname, "000") 101 | 102 | @when(parsers.parse("I run the \"{command}\" command")) 103 | def runcommand(contactbook, command): 104 | contactbook.run(command) 105 | -------------------------------------------------------------------------------- /Chapter07/tests/unit/test_adding.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from contacts import Application 4 | 5 | 6 | class TestAddContacts: 7 | def test_basic(self): 8 | app = Application() 9 | 10 | app.add("NAME", "323232") 11 | 12 | assert app._contacts == [ 13 | ("NAME", "323232") 14 | ] 15 | 16 | def test_special(self): 17 | app = Application() 18 | 19 | app.add("Emergency", "911") 20 | 21 | assert app._contacts == [ 22 | ("Emergency", "911") 23 | ] 24 | 25 | def test_international(self): 26 | app = Application() 27 | 28 | app.add("NAME", "+39323232") 29 | 30 | assert app._contacts == [ 31 | ("NAME", "+39323232") 32 | ] 33 | 34 | def test_invalid(self): 35 | app = Application() 36 | 37 | with pytest.raises(ValueError) as err: 38 | app.add("NAME", "not_a_number") 39 | 40 | assert str(err.value) == "Invalid phone number: not_a_number" 41 | 42 | def test_short(self): 43 | app = Application() 44 | 45 | with pytest.raises(ValueError) as err: 46 | app.add("NAME", "19") 47 | 48 | assert str(err.value) == "Invalid phone number: 19" 49 | 50 | def test_missing(self): 51 | app = Application() 52 | 53 | with pytest.raises(ValueError) as err: 54 | app.add("NAME", None) 55 | 56 | assert str(err.value) == "A valid phone number is required" 57 | -------------------------------------------------------------------------------- /Chapter07/tests/unit/test_application.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | 5 | from contacts import Application 6 | 7 | def test_application(): 8 | app = Application() 9 | 10 | assert app._contacts == [] 11 | assert hasattr(app, "run") 12 | 13 | 14 | def test_clear(): 15 | app = Application() 16 | app._contacts == [("NAME", "NUM")] 17 | 18 | app._clear() 19 | 20 | assert app._contacts == [] 21 | 22 | 23 | class TestRun: 24 | def test_add(self): 25 | app = Application() 26 | 27 | with mock.patch.object(app, "add") as mockadd: 28 | app.run("cmd add NAME 333") 29 | 30 | mockadd.assert_called_with("NAME", "333") 31 | 32 | def test_add_surname(self): 33 | app = Application() 34 | 35 | with mock.patch.object(app, "add") as mockadd: 36 | app.run("cmd add NAME SURNAME 333 ") 37 | 38 | mockadd.assert_called_with("NAME SURNAME", "333") 39 | 40 | def test_empty(self): 41 | app = Application() 42 | 43 | with pytest.raises(ValueError): 44 | app.run("") 45 | 46 | def test_nocmd(self): 47 | app = Application() 48 | 49 | with pytest.raises(ValueError): 50 | app.run("nocmd") 51 | 52 | def test_invalid(self): 53 | app = Application() 54 | 55 | with pytest.raises(ValueError): 56 | app.run("contacts invalid") 57 | 58 | -------------------------------------------------------------------------------- /Chapter07/tests/unit/test_persistence.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from contacts import Application 5 | 6 | 7 | class TestLoading: 8 | def test_load(self): 9 | app = Application() 10 | 11 | with open("./contacts.json", "w+") as f: 12 | json.dump({"_contacts": [("NAME SURNAME", "3333")]}, f) 13 | 14 | app.load() 15 | 16 | assert app._contacts == [ 17 | ("NAME SURNAME", "3333") 18 | ] 19 | 20 | 21 | class TestSaving: 22 | def test_save(self): 23 | app = Application() 24 | app._contacts = [ 25 | ("NAME SURNAME", "3333") 26 | ] 27 | 28 | try: 29 | os.unlink("./contacts.json") 30 | except FileNotFoundError: 31 | pass 32 | 33 | app.save() 34 | 35 | with open("./contacts.json") as f: 36 | assert json.load(f) == {"_contacts": [["NAME SURNAME", "3333"]]} 37 | -------------------------------------------------------------------------------- /Chapter08/benchmarks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter08/benchmarks/__init__.py -------------------------------------------------------------------------------- /Chapter08/benchmarks/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --benchmark-min-rounds=1 3 | -------------------------------------------------------------------------------- /Chapter08/benchmarks/test_persistence.py: -------------------------------------------------------------------------------- 1 | from contacts import Application 2 | 3 | def test_loading(benchmark): 4 | app = Application() 5 | app._contacts = [(f"Name {n}", "number") for n in range(1000)] 6 | app.save() 7 | 8 | benchmark(app.load) 9 | -------------------------------------------------------------------------------- /Chapter08/src/contacts/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | 4 | 5 | class Application: 6 | PHONE_EXPR = re.compile('^[+]?[0-9]{3,}$') 7 | 8 | def __init__(self): 9 | self._clear() 10 | 11 | def _clear(self): 12 | self._contacts = [] 13 | 14 | def run(self, text): 15 | text = text.strip() 16 | _, cmd = text.split(maxsplit=1) 17 | try: 18 | cmd, args = cmd.split(maxsplit=1) 19 | except ValueError: 20 | args = None 21 | 22 | if cmd == "add": 23 | name, num = args.rsplit(maxsplit=1) 24 | try: 25 | self.add(name, num) 26 | except ValueError as err: 27 | print(err) 28 | return 29 | elif cmd == "del": 30 | self.delete(args) 31 | elif cmd == "ls": 32 | self.printlist() 33 | else: 34 | raise ValueError(f"Invalid command: {cmd}") 35 | 36 | def save(self): 37 | with open("./contacts.json", "w+") as f: 38 | json.dump({"_contacts": self._contacts}, f) 39 | 40 | def load(self): 41 | with open("./contacts.json") as f: 42 | self._contacts = [ 43 | tuple(t) for t in json.load(f)["_contacts"] 44 | ] 45 | 46 | def add(self, name, phonenum): 47 | if not isinstance(phonenum, str): 48 | raise ValueError("A valid phone number is required") 49 | 50 | if not self.PHONE_EXPR.match(phonenum): 51 | raise ValueError(f"Invalid phone number: {phonenum}") 52 | 53 | self._contacts.append((name, phonenum)) 54 | self.save() 55 | 56 | def delete(self, name): 57 | self._contacts = [ 58 | c for c in self._contacts if c[0] != name 59 | ] 60 | self.save() 61 | 62 | def printlist(self): 63 | for c in self._contacts: 64 | print(f"{c[0]} {c[1]}") 65 | 66 | 67 | def main(): 68 | import sys 69 | a = Application() 70 | a.load() 71 | a.run(' '.join(sys.argv)) 72 | -------------------------------------------------------------------------------- /Chapter08/src/contacts/__main__.py: -------------------------------------------------------------------------------- 1 | from . import main # pragma: no cover 2 | 3 | main() # pragma: no cover 4 | -------------------------------------------------------------------------------- /Chapter08/src/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='contacts', packages=['contacts']) 4 | -------------------------------------------------------------------------------- /Chapter08/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter08/tests/__init__.py -------------------------------------------------------------------------------- /Chapter08/tests/acceptance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter08/tests/acceptance/__init__.py -------------------------------------------------------------------------------- /Chapter08/tests/acceptance/features/delete_contact.feature: -------------------------------------------------------------------------------- 1 | Feature: Deleting Contacts 2 | Contacts added to our contact book can be removed. 3 | 4 | Scenario: Removing a Basic Contact 5 | Given I have a contact book 6 | And I have a "John" contact 7 | When I run the "contacts del John" command 8 | Then My contacts book is now empty 9 | 10 | -------------------------------------------------------------------------------- /Chapter08/tests/acceptance/features/list_contacts.feature: -------------------------------------------------------------------------------- 1 | Feature: Listing Contacts 2 | Contacts added to our contact book can be listed back. 3 | 4 | Scenario: Listing Added Contacts 5 | Given I have a contact book 6 | And I have a first contact 7 | And I have a second contact 8 | When I run the "contacts ls" command 9 | Then the output contains contacts 10 | 11 | Examples: 12 | | first | second | listed_contacts | 13 | | Mario | Luigi | Mario,Luigi | 14 | | John | Jane | John,Jane | 15 | -------------------------------------------------------------------------------- /Chapter08/tests/acceptance/steps.py: -------------------------------------------------------------------------------- 1 | from pytest_bdd import scenario, given, when, then, parsers 2 | 3 | import contacts 4 | 5 | 6 | @given("I have a contact book", target_fixture="contactbook") 7 | def contactbook(): 8 | return contacts.Application() 9 | 10 | 11 | @given(parsers.parse("I have a \"{contactname}\" contact")) 12 | def have_a_contact(contactbook, contactname): 13 | contactbook.add(contactname, "000") 14 | 15 | 16 | @when(parsers.parse("I run the \"{command}\" command")) 17 | def runcommand(contactbook, command): 18 | contactbook.run(command) 19 | -------------------------------------------------------------------------------- /Chapter08/tests/acceptance/test_adding.py: -------------------------------------------------------------------------------- 1 | import contacts 2 | 3 | 4 | class TestAddingEntries: 5 | def test_basic(self): 6 | app = contacts.Application() 7 | 8 | app.run("contacts add NAME 3345554433") 9 | 10 | assert app._contacts == [ 11 | ("NAME", "3345554433") 12 | ] 13 | 14 | def test_surnames(self): 15 | app = contacts.Application() 16 | 17 | app.run("contacts add Mario Mario 3345554433") 18 | app.run("contacts add Luigi Mario 3345554434") 19 | app.run("contacts add Princess Peach Toadstool 3339323323") 20 | 21 | assert app._contacts == [ 22 | ("Mario Mario", "3345554433"), 23 | ("Luigi Mario", "3345554434"), 24 | ("Princess Peach Toadstool", "3339323323") 25 | ] 26 | -------------------------------------------------------------------------------- /Chapter08/tests/acceptance/test_delete_contact.py: -------------------------------------------------------------------------------- 1 | from pytest_bdd import scenario, given, when, then, parsers 2 | 3 | import contacts 4 | 5 | from .steps import * 6 | 7 | 8 | @scenario("features/delete_contact.feature", 9 | "Removing a Basic Contact") 10 | def test_deleting_contacts(): 11 | pass 12 | 13 | 14 | @then("My contacts book is now empty") 15 | def emptylist(contactbook): 16 | assert contactbook._contacts == [] 17 | -------------------------------------------------------------------------------- /Chapter08/tests/acceptance/test_list_contacts.py: -------------------------------------------------------------------------------- 1 | from pytest_bdd import scenario, given, when, then, parsers 2 | 3 | import contacts 4 | 5 | from .steps import * 6 | 7 | @scenario("features/list_contacts.feature", 8 | "Listing Added Contacts") 9 | def test_listing_added_contacts(capsys): 10 | pass 11 | 12 | 13 | @given("I have a first contact") 14 | def have_a_first_contact(contactbook, first): 15 | contactbook.add(first, "000") 16 | return first 17 | 18 | 19 | @given("I have a second contact") 20 | def have_a_second_contact(contactbook, second): 21 | contactbook.add(second, "000") 22 | return second 23 | 24 | 25 | @then("the output contains contacts") 26 | def outputcontains(listed_contacts, capsys): 27 | expected_list = "".join([f"{c} 000\n" for c in listed_contacts.split(",")]) 28 | out, _ = capsys.readouterr() 29 | assert out == expected_list 30 | -------------------------------------------------------------------------------- /Chapter08/tests/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter08/tests/conftest.py -------------------------------------------------------------------------------- /Chapter08/tests/functional/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter08/tests/functional/__init__.py -------------------------------------------------------------------------------- /Chapter08/tests/functional/test_basic.py: -------------------------------------------------------------------------------- 1 | import contacts 2 | 3 | 4 | class TestBasicFeatures: 5 | def test_international_numbers(self): 6 | app = contacts.Application() 7 | 8 | app.run("contacts add NAME +393345554433") 9 | 10 | assert app._contacts == [ 11 | ("NAME", "+393345554433") 12 | ] 13 | 14 | def test_invalid_strings(self): 15 | app = contacts.Application() 16 | 17 | app.run("contacts add NAME InvalidString") 18 | 19 | assert app._contacts == [] 20 | 21 | 22 | class TestStorage: 23 | def test_reload(self): 24 | app = contacts.Application() 25 | 26 | app.run("contacts add NAME 3345554433") 27 | 28 | assert app._contacts == [ 29 | ("NAME", "3345554433") 30 | ] 31 | 32 | app._clear() 33 | app.load() 34 | 35 | assert app._contacts == [ 36 | ("NAME", "3345554433") 37 | ] -------------------------------------------------------------------------------- /Chapter08/tests/functional/test_main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from unittest import mock 3 | 4 | import contacts 5 | 6 | 7 | class TestMain: 8 | def test_main(self, capsys): 9 | def _stub_load(self): 10 | self._contacts = [("name", "number")] 11 | 12 | with mock.patch.object(contacts.Application, "load", new=_stub_load): 13 | with mock.patch.object(sys, "argv", new=["contacts", "ls"]): 14 | contacts.main() 15 | 16 | out, _ = capsys.readouterr() 17 | assert out == "name number\n" -------------------------------------------------------------------------------- /Chapter08/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --cov=contacts --cov-report=term-missing 3 | -------------------------------------------------------------------------------- /Chapter08/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter08/tests/unit/__init__.py -------------------------------------------------------------------------------- /Chapter08/tests/unit/test_adding.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from contacts import Application 4 | 5 | 6 | class TestAddContacts: 7 | def test_basic(self): 8 | app = Application() 9 | 10 | app.add("NAME", "323232") 11 | 12 | assert app._contacts == [ 13 | ("NAME", "323232") 14 | ] 15 | 16 | def test_special(self): 17 | app = Application() 18 | 19 | app.add("Emergency", "911") 20 | 21 | assert app._contacts == [ 22 | ("Emergency", "911") 23 | ] 24 | 25 | def test_international(self): 26 | app = Application() 27 | 28 | app.add("NAME", "+39323232") 29 | 30 | assert app._contacts == [ 31 | ("NAME", "+39323232") 32 | ] 33 | 34 | def test_invalid(self): 35 | app = Application() 36 | 37 | with pytest.raises(ValueError) as err: 38 | app.add("NAME", "not_a_number") 39 | 40 | assert str(err.value) == "Invalid phone number: not_a_number" 41 | 42 | def test_short(self): 43 | app = Application() 44 | 45 | with pytest.raises(ValueError) as err: 46 | app.add("NAME", "19") 47 | 48 | assert str(err.value) == "Invalid phone number: 19" 49 | 50 | def test_missing(self): 51 | app = Application() 52 | 53 | with pytest.raises(ValueError) as err: 54 | app.add("NAME", None) 55 | 56 | assert str(err.value) == "A valid phone number is required" 57 | -------------------------------------------------------------------------------- /Chapter08/tests/unit/test_application.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | 5 | from contacts import Application 6 | 7 | def test_application(): 8 | app = Application() 9 | 10 | assert app._contacts == [] 11 | assert hasattr(app, "run") 12 | 13 | 14 | def test_clear(): 15 | app = Application() 16 | app._contacts == [("NAME", "NUM")] 17 | 18 | app._clear() 19 | 20 | assert app._contacts == [] 21 | 22 | 23 | class TestRun: 24 | def test_add(self): 25 | app = Application() 26 | 27 | with mock.patch.object(app, "add") as mockadd: 28 | app.run("cmd add NAME 333") 29 | 30 | mockadd.assert_called_with("NAME", "333") 31 | 32 | def test_add_surname(self): 33 | app = Application() 34 | 35 | with mock.patch.object(app, "add") as mockadd: 36 | app.run("cmd add NAME SURNAME 333 ") 37 | 38 | mockadd.assert_called_with("NAME SURNAME", "333") 39 | 40 | def test_empty(self): 41 | app = Application() 42 | 43 | with pytest.raises(ValueError): 44 | app.run("") 45 | 46 | def test_nocmd(self): 47 | app = Application() 48 | 49 | with pytest.raises(ValueError): 50 | app.run("nocmd") 51 | 52 | def test_invalid(self): 53 | app = Application() 54 | 55 | with pytest.raises(ValueError): 56 | app.run("contacts invalid") 57 | 58 | -------------------------------------------------------------------------------- /Chapter08/tests/unit/test_flaky.py: -------------------------------------------------------------------------------- 1 | def flaky_appender(l, numbers): 2 | from multiprocessing.pool import ThreadPool 3 | 4 | with ThreadPool(5) as pool: 5 | pool.map(lambda n: l.append(n), numbers) 6 | 7 | 8 | from flaky import flaky 9 | 10 | @flaky 11 | def test_appender(): 12 | l = [] 13 | flaky_appender(l, range(7000)) 14 | assert l == list(range(7000)) -------------------------------------------------------------------------------- /Chapter08/tests/unit/test_persistence.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from contacts import Application 5 | 6 | 7 | class TestLoading: 8 | def test_load(self): 9 | app = Application() 10 | 11 | with open("./contacts.json", "w+") as f: 12 | json.dump({"_contacts": [("NAME SURNAME", "3333")]}, f) 13 | 14 | app.load() 15 | 16 | assert app._contacts == [ 17 | ("NAME SURNAME", "3333") 18 | ] 19 | 20 | 21 | class TestSaving: 22 | def test_save(self): 23 | app = Application() 24 | app._contacts = [ 25 | ("NAME SURNAME", "3333") 26 | ] 27 | 28 | try: 29 | os.unlink("./contacts.json") 30 | except FileNotFoundError: 31 | pass 32 | 33 | app.save() 34 | 35 | with open("./contacts.json") as f: 36 | assert json.load(f) == {"_contacts": [["NAME SURNAME", "3333"]]} 37 | -------------------------------------------------------------------------------- /Chapter09/benchmarks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter09/benchmarks/__init__.py -------------------------------------------------------------------------------- /Chapter09/benchmarks/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --benchmark-min-rounds=1 3 | -------------------------------------------------------------------------------- /Chapter09/benchmarks/test_persistence.py: -------------------------------------------------------------------------------- 1 | from contacts import Application 2 | 3 | def test_loading(benchmark): 4 | app = Application() 5 | app._contacts = [(f"Name {n}", "number") for n in range(1000)] 6 | app.save() 7 | 8 | benchmark(app.load) 9 | -------------------------------------------------------------------------------- /Chapter09/src/contacts/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | 4 | 5 | class Application: 6 | PHONE_EXPR = re.compile('^[+]?[0-9]{3,}$') 7 | 8 | def __init__(self): 9 | self._clear() 10 | 11 | def _clear(self): 12 | self._contacts = [] 13 | 14 | def run(self, text): 15 | text = text.strip() 16 | _, cmd = text.split(maxsplit=1) 17 | try: 18 | cmd, args = cmd.split(maxsplit=1) 19 | except ValueError: 20 | args = None 21 | 22 | if cmd == "add": 23 | name, num = args.rsplit(maxsplit=1) 24 | try: 25 | self.add(name, num) 26 | except ValueError as err: 27 | print(err) 28 | return 29 | elif cmd == "del": 30 | self.delete(args) 31 | elif cmd == "ls": 32 | self.printlist() 33 | else: 34 | raise ValueError(f"Invalid command: {cmd}") 35 | 36 | def save(self): 37 | with open("./contacts.json", "w+") as f: 38 | json.dump({"_contacts": self._contacts}, f) 39 | 40 | def load(self): 41 | with open("./contacts.json") as f: 42 | self._contacts = [ 43 | tuple(t) for t in json.load(f)["_contacts"] 44 | ] 45 | 46 | def add(self, name, phonenum): 47 | if not isinstance(phonenum, str): 48 | raise ValueError("A valid phone number is required") 49 | 50 | if not self.PHONE_EXPR.match(phonenum): 51 | raise ValueError(f"Invalid phone number: {phonenum}") 52 | 53 | self._contacts.append((name, phonenum)) 54 | self.save() 55 | 56 | def delete(self, name): 57 | self._contacts = [ 58 | c for c in self._contacts if c[0] != name 59 | ] 60 | self.save() 61 | 62 | def printlist(self): 63 | for c in self._contacts: 64 | print(f"{c[0]} {c[1]}") 65 | 66 | 67 | def main(): 68 | import sys 69 | a = Application() 70 | a.load() 71 | a.run(' '.join(sys.argv)) 72 | -------------------------------------------------------------------------------- /Chapter09/src/contacts/__main__.py: -------------------------------------------------------------------------------- 1 | from . import main # pragma: no cover 2 | 3 | main() # pragma: no cover 4 | -------------------------------------------------------------------------------- /Chapter09/src/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='contacts', packages=['contacts']) 4 | -------------------------------------------------------------------------------- /Chapter09/tests/.testmondata: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter09/tests/.testmondata -------------------------------------------------------------------------------- /Chapter09/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter09/tests/__init__.py -------------------------------------------------------------------------------- /Chapter09/tests/acceptance/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter09/tests/acceptance/__init__.py -------------------------------------------------------------------------------- /Chapter09/tests/acceptance/features/delete_contact.feature: -------------------------------------------------------------------------------- 1 | Feature: Deleting Contacts 2 | Contacts added to our contact book can be removed. 3 | 4 | Scenario: Removing a Basic Contact 5 | Given I have a contact book 6 | And I have a "John" contact 7 | When I run the "contacts del John" command 8 | Then My contacts book is now empty 9 | 10 | -------------------------------------------------------------------------------- /Chapter09/tests/acceptance/features/list_contacts.feature: -------------------------------------------------------------------------------- 1 | Feature: Listing Contacts 2 | Contacts added to our contact book can be listed back. 3 | 4 | Scenario: Listing Added Contacts 5 | Given I have a contact book 6 | And I have a first contact 7 | And I have a second contact 8 | When I run the "contacts ls" command 9 | Then the output contains contacts 10 | 11 | Examples: 12 | | first | second | listed_contacts | 13 | | Mario | Luigi | Mario,Luigi | 14 | | John | Jane | John,Jane | 15 | -------------------------------------------------------------------------------- /Chapter09/tests/acceptance/steps.py: -------------------------------------------------------------------------------- 1 | from pytest_bdd import scenario, given, when, then, parsers 2 | 3 | import contacts 4 | 5 | 6 | @given("I have a contact book", target_fixture="contactbook") 7 | def contactbook(): 8 | return contacts.Application() 9 | 10 | 11 | @given(parsers.parse("I have a \"{contactname}\" contact")) 12 | def have_a_contact(contactbook, contactname): 13 | contactbook.add(contactname, "000") 14 | 15 | 16 | @when(parsers.parse("I run the \"{command}\" command")) 17 | def runcommand(contactbook, command): 18 | contactbook.run(command) 19 | -------------------------------------------------------------------------------- /Chapter09/tests/acceptance/test_adding.py: -------------------------------------------------------------------------------- 1 | import contacts 2 | 3 | 4 | class TestAddingEntries: 5 | def test_basic(self): 6 | app = contacts.Application() 7 | 8 | app.run("contacts add NAME 3345554433") 9 | 10 | assert app._contacts == [ 11 | ("NAME", "3345554433") 12 | ] 13 | 14 | def test_surnames(self): 15 | app = contacts.Application() 16 | 17 | app.run("contacts add Mario Mario 3345554433") 18 | app.run("contacts add Luigi Mario 3345554434") 19 | app.run("contacts add Princess Peach Toadstool 3339323323") 20 | 21 | assert app._contacts == [ 22 | ("Mario Mario", "3345554433"), 23 | ("Luigi Mario", "3345554434"), 24 | ("Princess Peach Toadstool", "3339323323") 25 | ] 26 | -------------------------------------------------------------------------------- /Chapter09/tests/acceptance/test_delete_contact.py: -------------------------------------------------------------------------------- 1 | from pytest_bdd import scenario, given, when, then, parsers 2 | 3 | import contacts 4 | 5 | from .steps import * 6 | 7 | 8 | @scenario("features/delete_contact.feature", 9 | "Removing a Basic Contact") 10 | def test_deleting_contacts(): 11 | pass 12 | 13 | 14 | @then("My contacts book is now empty") 15 | def emptylist(contactbook): 16 | assert contactbook._contacts == [] 17 | -------------------------------------------------------------------------------- /Chapter09/tests/acceptance/test_list_contacts.py: -------------------------------------------------------------------------------- 1 | from pytest_bdd import scenario, given, when, then, parsers 2 | 3 | import contacts 4 | 5 | from .steps import * 6 | 7 | @scenario("features/list_contacts.feature", 8 | "Listing Added Contacts") 9 | def test_listing_added_contacts(capsys): 10 | pass 11 | 12 | 13 | @given("I have a first contact") 14 | def have_a_first_contact(contactbook, first): 15 | contactbook.add(first, "000") 16 | return first 17 | 18 | 19 | @given("I have a second contact") 20 | def have_a_second_contact(contactbook, second): 21 | contactbook.add(second, "000") 22 | return second 23 | 24 | 25 | @then("the output contains contacts") 26 | def outputcontains(listed_contacts, capsys): 27 | expected_list = "".join([f"{c} 000\n" for c in listed_contacts.split(",")]) 28 | out, _ = capsys.readouterr() 29 | assert out == expected_list 30 | -------------------------------------------------------------------------------- /Chapter09/tests/conftest.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter09/tests/conftest.py -------------------------------------------------------------------------------- /Chapter09/tests/functional/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter09/tests/functional/__init__.py -------------------------------------------------------------------------------- /Chapter09/tests/functional/test_basic.py: -------------------------------------------------------------------------------- 1 | import contacts 2 | 3 | 4 | class TestBasicFeatures: 5 | def test_international_numbers(self): 6 | app = contacts.Application() 7 | 8 | app.run("contacts add NAME +393345554433") 9 | 10 | assert app._contacts == [ 11 | ("NAME", "+393345554433") 12 | ] 13 | 14 | def test_invalid_strings(self): 15 | app = contacts.Application() 16 | 17 | app.run("contacts add NAME InvalidString") 18 | 19 | assert app._contacts == [] 20 | 21 | 22 | class TestStorage: 23 | def test_reload(self): 24 | app = contacts.Application() 25 | 26 | app.run("contacts add NAME 3345554433") 27 | 28 | assert app._contacts == [ 29 | ("NAME", "3345554433") 30 | ] 31 | 32 | app._clear() 33 | app.load() 34 | 35 | assert app._contacts == [ 36 | ("NAME", "3345554433") 37 | ] -------------------------------------------------------------------------------- /Chapter09/tests/functional/test_main.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from unittest import mock 3 | 4 | import contacts 5 | 6 | 7 | class TestMain: 8 | def test_main(self, capsys): 9 | def _stub_load(self): 10 | self._contacts = [("name", "number")] 11 | 12 | with mock.patch.object(contacts.Application, "load", new=_stub_load): 13 | with mock.patch.object(sys, "argv", new=["contacts", "ls"]): 14 | contacts.main() 15 | 16 | out, _ = capsys.readouterr() 17 | assert out == "name number\n" -------------------------------------------------------------------------------- /Chapter09/tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | addopts = --cov=contacts --cov-report=term-missing 3 | -------------------------------------------------------------------------------- /Chapter09/tests/unit/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter09/tests/unit/__init__.py -------------------------------------------------------------------------------- /Chapter09/tests/unit/test_adding.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | from contacts import Application 4 | 5 | 6 | class TestAddContacts: 7 | def test_basic(self): 8 | app = Application() 9 | 10 | app.add("NAME", "323232") 11 | 12 | assert app._contacts == [ 13 | ("NAME", "323232") 14 | ] 15 | 16 | def test_special(self): 17 | app = Application() 18 | 19 | app.add("Emergency", "911") 20 | 21 | assert app._contacts == [ 22 | ("Emergency", "911") 23 | ] 24 | 25 | def test_international(self): 26 | app = Application() 27 | 28 | app.add("NAME", "+39323232") 29 | 30 | assert app._contacts == [ 31 | ("NAME", "+39323232") 32 | ] 33 | 34 | def test_invalid(self): 35 | app = Application() 36 | 37 | with pytest.raises(ValueError) as err: 38 | app.add("NAME", "not_a_number") 39 | 40 | assert str(err.value) == "Invalid phone number: not_a_number" 41 | 42 | def test_short(self): 43 | app = Application() 44 | 45 | with pytest.raises(ValueError) as err: 46 | app.add("NAME", "19") 47 | 48 | assert str(err.value) == "Invalid phone number: 19" 49 | 50 | def test_missing(self): 51 | app = Application() 52 | 53 | with pytest.raises(ValueError) as err: 54 | app.add("NAME", None) 55 | 56 | assert str(err.value) == "A valid phone number is required" 57 | -------------------------------------------------------------------------------- /Chapter09/tests/unit/test_application.py: -------------------------------------------------------------------------------- 1 | from unittest import mock 2 | 3 | import pytest 4 | 5 | from contacts import Application 6 | 7 | def test_application(): 8 | app = Application() 9 | 10 | assert app._contacts == [] 11 | assert hasattr(app, "run") 12 | 13 | 14 | def test_clear(): 15 | app = Application() 16 | app._contacts == [("NAME", "NUM")] 17 | 18 | app._clear() 19 | 20 | assert app._contacts == [] 21 | 22 | 23 | class TestRun: 24 | def test_add(self): 25 | app = Application() 26 | 27 | with mock.patch.object(app, "add") as mockadd: 28 | app.run("cmd add NAME 333") 29 | 30 | mockadd.assert_called_with("NAME", "333") 31 | 32 | def test_add_surname(self): 33 | app = Application() 34 | 35 | with mock.patch.object(app, "add") as mockadd: 36 | app.run("cmd add NAME SURNAME 333 ") 37 | 38 | mockadd.assert_called_with("NAME SURNAME", "333") 39 | 40 | def test_empty(self): 41 | app = Application() 42 | 43 | with pytest.raises(ValueError): 44 | app.run("") 45 | 46 | def test_nocmd(self): 47 | app = Application() 48 | 49 | with pytest.raises(ValueError): 50 | app.run("nocmd") 51 | 52 | def test_invalid(self): 53 | app = Application() 54 | 55 | with pytest.raises(ValueError): 56 | app.run("contacts invalid") 57 | 58 | -------------------------------------------------------------------------------- /Chapter09/tests/unit/test_flaky.py: -------------------------------------------------------------------------------- 1 | def flaky_appender(l, numbers): 2 | from multiprocessing.pool import ThreadPool 3 | 4 | with ThreadPool(5) as pool: 5 | pool.map(lambda n: l.append(n), numbers) 6 | 7 | 8 | from flaky import flaky 9 | 10 | @flaky 11 | def test_appender(): 12 | l = [] 13 | flaky_appender(l, range(7000)) 14 | assert l == list(range(7000)) -------------------------------------------------------------------------------- /Chapter09/tests/unit/test_persistence.py: -------------------------------------------------------------------------------- 1 | import os 2 | import json 3 | 4 | from contacts import Application 5 | 6 | 7 | class TestLoading: 8 | def test_load(self): 9 | app = Application() 10 | 11 | with open("./contacts.json", "w+") as f: 12 | json.dump({"_contacts": [("NAME SURNAME", "3333")]}, f) 13 | 14 | app.load() 15 | 16 | assert app._contacts == [ 17 | ("NAME SURNAME", "3333") 18 | ] 19 | 20 | 21 | class TestSaving: 22 | def test_save(self): 23 | app = Application() 24 | app._contacts = [ 25 | ("NAME SURNAME", "3333") 26 | ] 27 | 28 | try: 29 | os.unlink("./contacts.json") 30 | except FileNotFoundError: 31 | pass 32 | 33 | app.save() 34 | 35 | with open("./contacts.json") as f: 36 | assert json.load(f) == {"_contacts": [["NAME SURNAME", "3333"]]} 37 | -------------------------------------------------------------------------------- /Chapter09/tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | setupdir = ./src 3 | envlist = py27, py37 4 | 5 | [testenv] 6 | deps = 7 | pytest == 6.0.2 8 | pytest-bdd == 3.4.0 9 | flaky == 3.7.0 10 | pytest-benchmark == 3.2.3 11 | pytest-cov == 2.10.1 12 | commands = 13 | pytest --cov=contacts --benchmark-skip {posargs} 14 | 15 | [testenv:benchmarks] 16 | commands = 17 | pytest --no-cov ./benchmarks {posargs} 18 | 19 | [testenv:py27] 20 | deps = 21 | pytest == 4.6.11 22 | pytest-bdd == 3.4.0 23 | flaky == 3.7.0 24 | pytest-benchmark == 3.2.3 25 | pytest-cov == 2.10.1 -------------------------------------------------------------------------------- /Chapter10/doc/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | SOURCEDIR = source 8 | BUILDDIR = build 9 | 10 | # Put it first so that "make" without argument is like "make help". 11 | help: 12 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 13 | 14 | .PHONY: help Makefile 15 | 16 | # Catch-all target: route all unknown targets to Sphinx using the new 17 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 18 | %: Makefile 19 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -------------------------------------------------------------------------------- /Chapter10/doc/source/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Configuration file for the Sphinx documentation builder. 4 | # 5 | # This file does only contain a selection of the most common options. For a 6 | # full list see the documentation: 7 | # http://www.sphinx-doc.org/en/master/config 8 | 9 | # -- Path setup -------------------------------------------------------------- 10 | 11 | # If extensions (or modules to document with autodoc) are in another directory, 12 | # add these directories to sys.path here. If the directory is relative to the 13 | # documentation root, use os.path.abspath to make it absolute, like shown here. 14 | # 15 | # import os 16 | # import sys 17 | # sys.path.insert(0, os.path.abspath('.')) 18 | 19 | 20 | # -- Project information ----------------------------------------------------- 21 | 22 | project = u'Doc' 23 | copyright = u'2020, Alessandro Molina' 24 | author = u'Alessandro Molina' 25 | 26 | # The short X.Y version 27 | version = u'' 28 | # The full version, including alpha/beta/rc tags 29 | release = u'' 30 | 31 | 32 | # -- General configuration --------------------------------------------------- 33 | 34 | # If your documentation needs a minimal Sphinx version, state it here. 35 | # 36 | # needs_sphinx = '1.0' 37 | 38 | # Add any Sphinx extension module names here, as strings. They can be 39 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 40 | # ones. 41 | extensions = [ 42 | 'sphinx.ext.autodoc', 43 | 'sphinx.ext.doctest', 44 | 'sphinx.ext.intersphinx', 45 | 'sphinx.ext.todo', 46 | 'sphinx.ext.coverage', 47 | 'sphinx.ext.mathjax', 48 | 'sphinx.ext.ifconfig', 49 | 'sphinx.ext.viewcode', 50 | ] 51 | 52 | # Add any paths that contain templates here, relative to this directory. 53 | templates_path = ['_templates'] 54 | 55 | # The suffix(es) of source filenames. 56 | # You can specify multiple suffix as a list of string: 57 | # 58 | # source_suffix = ['.rst', '.md'] 59 | source_suffix = '.rst' 60 | 61 | # The master toctree document. 62 | master_doc = 'index' 63 | 64 | # The language for content autogenerated by Sphinx. Refer to documentation 65 | # for a list of supported languages. 66 | # 67 | # This is also used if you do content translation via gettext catalogs. 68 | # Usually you set "language" from the command line for these cases. 69 | language = None 70 | 71 | # List of patterns, relative to source directory, that match files and 72 | # directories to ignore when looking for source files. 73 | # This pattern also affects html_static_path and html_extra_path. 74 | exclude_patterns = [] 75 | 76 | # The name of the Pygments (syntax highlighting) style to use. 77 | pygments_style = None 78 | 79 | 80 | # -- Options for HTML output ------------------------------------------------- 81 | 82 | # The theme to use for HTML and HTML Help pages. See the documentation for 83 | # a list of builtin themes. 84 | # 85 | html_theme = 'alabaster' 86 | 87 | # Theme options are theme-specific and customize the look and feel of a theme 88 | # further. For a list of options available for each theme, see the 89 | # documentation. 90 | # 91 | # html_theme_options = {} 92 | 93 | # Add any paths that contain custom static files (such as style sheets) here, 94 | # relative to this directory. They are copied after the builtin static files, 95 | # so a file named "default.css" will overwrite the builtin "default.css". 96 | html_static_path = ['_static'] 97 | 98 | # Custom sidebar templates, must be a dictionary that maps document names 99 | # to template names. 100 | # 101 | # The default sidebars (for documents that don't match any pattern) are 102 | # defined by theme itself. Builtin themes are using these templates by 103 | # default: ``['localtoc.html', 'relations.html', 'sourcelink.html', 104 | # 'searchbox.html']``. 105 | # 106 | # html_sidebars = {} 107 | 108 | 109 | # -- Options for HTMLHelp output --------------------------------------------- 110 | 111 | # Output file base name for HTML help builder. 112 | htmlhelp_basename = 'Docdoc' 113 | 114 | 115 | # -- Options for LaTeX output ------------------------------------------------ 116 | 117 | latex_elements = { 118 | # The paper size ('letterpaper' or 'a4paper'). 119 | # 120 | # 'papersize': 'letterpaper', 121 | 122 | # The font size ('10pt', '11pt' or '12pt'). 123 | # 124 | # 'pointsize': '10pt', 125 | 126 | # Additional stuff for the LaTeX preamble. 127 | # 128 | # 'preamble': '', 129 | 130 | # Latex figure (float) alignment 131 | # 132 | # 'figure_align': 'htbp', 133 | } 134 | 135 | # Grouping the document tree into LaTeX files. List of tuples 136 | # (source start file, target name, title, 137 | # author, documentclass [howto, manual, or own class]). 138 | latex_documents = [ 139 | (master_doc, 'Doc.tex', u'Doc Documentation', 140 | u'Alessandro Molina', 'manual'), 141 | ] 142 | 143 | 144 | # -- Options for manual page output ------------------------------------------ 145 | 146 | # One entry per manual page. List of tuples 147 | # (source start file, name, description, authors, manual section). 148 | man_pages = [ 149 | (master_doc, 'doc', u'Doc Documentation', 150 | [author], 1) 151 | ] 152 | 153 | 154 | # -- Options for Texinfo output ---------------------------------------------- 155 | 156 | # Grouping the document tree into Texinfo files. List of tuples 157 | # (source start file, target name, title, author, 158 | # dir menu entry, description, category) 159 | texinfo_documents = [ 160 | (master_doc, 'Doc', u'Doc Documentation', 161 | author, 'Doc', 'One line description of project.', 162 | 'Miscellaneous'), 163 | ] 164 | 165 | 166 | # -- Options for Epub output ------------------------------------------------- 167 | 168 | # Bibliographic Dublin Core info. 169 | epub_title = project 170 | 171 | # The unique identifier of the text. This can be a ISBN number 172 | # or the project homepage. 173 | # 174 | # epub_identifier = '' 175 | 176 | # A unique identification for the text. 177 | # 178 | # epub_uid = '' 179 | 180 | # A list of files that should not be packed into the epub file. 181 | epub_exclude_files = ['search.html'] 182 | 183 | 184 | # -- Extension configuration ------------------------------------------------- 185 | 186 | # -- Options for intersphinx extension --------------------------------------- 187 | 188 | # Example configuration for intersphinx: refer to the Python standard library. 189 | intersphinx_mapping = {'https://docs.python.org/': None} 190 | 191 | # -- Options for todo extension ---------------------------------------------- 192 | 193 | # If true, `todo` and `todoList` produce output, else they produce nothing. 194 | todo_include_todos = True 195 | -------------------------------------------------------------------------------- /Chapter10/doc/source/contacts.rst: -------------------------------------------------------------------------------- 1 | =============== 2 | Manage Contacts 3 | =============== 4 | 5 | .. contents:: 6 | 7 | Contacts can be managed through an instance of 8 | :class:`contacts.Application`, use :meth:`contacts.Application.run` 9 | to execute any command like you would in the shell. 10 | 11 | .. testsetup:: 12 | 13 | from contacts import Application 14 | 15 | app = Application() 16 | 17 | 18 | Adding Contancts 19 | ================ 20 | 21 | .. testcode:: 22 | 23 | app.run("contacts add Name 0123456789") 24 | 25 | Listing Contacts 26 | ================ 27 | 28 | .. testcode:: 29 | 30 | app.run("contacts ls") 31 | 32 | .. testoutput:: 33 | 34 | Name 0123456789 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /Chapter10/doc/source/index.rst: -------------------------------------------------------------------------------- 1 | .. Doc documentation master file, created by 2 | sphinx-quickstart on Mon Nov 9 22:49:54 2020. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to Doc's documentation! 7 | =============================== 8 | 9 | .. toctree:: 10 | :maxdepth: 2 11 | :caption: Contents: 12 | 13 | contacts 14 | reference 15 | 16 | Indices and tables 17 | ================== 18 | 19 | * :ref:`genindex` 20 | * :ref:`modindex` 21 | * :ref:`search` 22 | -------------------------------------------------------------------------------- /Chapter10/doc/source/reference.rst: -------------------------------------------------------------------------------- 1 | ============== 2 | Code Reference 3 | ============== 4 | 5 | .. autoclass:: contacts.Application 6 | :members: 7 | -------------------------------------------------------------------------------- /Chapter10/src/contacts/__init__.py: -------------------------------------------------------------------------------- 1 | import re 2 | import json 3 | 4 | 5 | class Application: 6 | """Manages a contact book serving the provided commands. 7 | 8 | The contact book itself is saved in a contacts.json file 9 | in the same directory where the application is started 10 | and it's loaded back on every new run. 11 | 12 | A contact is composed by any name followed by a valid 13 | phone number. 14 | """ 15 | PHONE_EXPR = re.compile('^[+]?[0-9]{3,}$') 16 | 17 | def __init__(self): 18 | self._clear() 19 | 20 | def _clear(self): 21 | self._contacts = [] 22 | 23 | def run(self, text): 24 | """Run a provided command. 25 | 26 | :param str text: The string containing the command to run. 27 | 28 | Takes the command to run as a string as it would 29 | come from the shell, parses it and runs it. 30 | 31 | Each command can support zero or multiple arguments 32 | separate by an empty space. 33 | 34 | Currently supported commands are: 35 | 36 | - add 37 | - del 38 | - ls 39 | """ 40 | text = text.strip() 41 | _, cmd = text.split(maxsplit=1) 42 | try: 43 | cmd, args = cmd.split(maxsplit=1) 44 | except ValueError: 45 | args = None 46 | 47 | if cmd == "add": 48 | try: 49 | name, num = args.rsplit(maxsplit=1) 50 | except ValueError: 51 | print("A contact must provide a name and phone number") 52 | return 53 | try: 54 | self.add(name, num) 55 | except ValueError as err: 56 | print(err) 57 | return 58 | elif cmd == "del": 59 | self.delete(args) 60 | elif cmd == "ls": 61 | self.printlist() 62 | else: 63 | raise ValueError(f"Invalid command: {cmd}") 64 | 65 | def save(self): 66 | with open("./contacts.json", "w+") as f: 67 | json.dump({"_contacts": self._contacts}, f) 68 | 69 | def load(self): 70 | with open("./contacts.json") as f: 71 | self._contacts = [ 72 | tuple(t) for t in json.load(f)["_contacts"] 73 | ] 74 | 75 | def add(self, name, phonenum): 76 | if not isinstance(phonenum, str): 77 | raise ValueError("A valid phone number is required") 78 | 79 | if not self.PHONE_EXPR.match(phonenum): 80 | raise ValueError(f"Invalid phone number: {phonenum}") 81 | 82 | self._contacts.append((name, phonenum)) 83 | self.save() 84 | 85 | def delete(self, name): 86 | self._contacts = [ 87 | c for c in self._contacts if c[0] != name 88 | ] 89 | self.save() 90 | 91 | def printlist(self): 92 | for c in self._contacts: 93 | print(f"{c[0]} {c[1]}") 94 | 95 | 96 | def main(): 97 | raise NotImplementedError() 98 | -------------------------------------------------------------------------------- /Chapter10/src/contacts/__main__.py: -------------------------------------------------------------------------------- 1 | from . import main 2 | 3 | main() -------------------------------------------------------------------------------- /Chapter10/src/contacts/utils.py: -------------------------------------------------------------------------------- 1 | def sum1(a: int, b: int) -> int: 2 | return a + b 3 | 4 | def sum2(a: int, b: int) -> int: 5 | return sum((a, b)) 6 | 7 | -------------------------------------------------------------------------------- /Chapter10/src/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='contacts', packages=['contacts']) 4 | -------------------------------------------------------------------------------- /Chapter10/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter10/tests/__init__.py -------------------------------------------------------------------------------- /Chapter10/tests/test_properties.py: -------------------------------------------------------------------------------- 1 | from hypothesis import given 2 | import hypothesis.strategies as st 3 | 4 | from contacts import Application 5 | 6 | 7 | @given(st.text()) 8 | def test_adding_contacts(name): 9 | app = Application() 10 | 11 | app.run(f"contacts add {name} 3456789") 12 | 13 | name = name.strip() 14 | if name: 15 | assert app._contacts == [(name, "3456789")] 16 | else: 17 | assert app._contacts == [] 18 | 19 | -------------------------------------------------------------------------------- /Chapter11/djapp/djapp/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter11/djapp/djapp/__init__.py -------------------------------------------------------------------------------- /Chapter11/djapp/djapp/asgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | ASGI config for djapp project. 3 | 4 | It exposes the ASGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/asgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.asgi import get_asgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djapp.settings') 15 | 16 | application = get_asgi_application() 17 | -------------------------------------------------------------------------------- /Chapter11/djapp/djapp/settings.py: -------------------------------------------------------------------------------- 1 | """ 2 | Django settings for djapp project. 3 | 4 | Generated by 'django-admin startproject' using Django 3.1.4. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/topics/settings/ 8 | 9 | For the full list of settings and their values, see 10 | https://docs.djangoproject.com/en/3.1/ref/settings/ 11 | """ 12 | 13 | from pathlib import Path 14 | 15 | # Build paths inside the project like this: BASE_DIR / 'subdir'. 16 | BASE_DIR = Path(__file__).resolve().parent.parent 17 | 18 | 19 | # Quick-start development settings - unsuitable for production 20 | # See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ 21 | 22 | # SECURITY WARNING: keep the secret key used in production secret! 23 | SECRET_KEY = 'zt5$k!by2-hli0#c@w8pvtr$5d6ve6+5)a4tly6*mgx1#)y*tu' 24 | 25 | # SECURITY WARNING: don't run with debug turned on in production! 26 | DEBUG = True 27 | 28 | ALLOWED_HOSTS = ["httpbin.org", "127.0.0.1"] 29 | 30 | 31 | # Application definition 32 | 33 | INSTALLED_APPS = [ 34 | 'django.contrib.admin', 35 | 'django.contrib.auth', 36 | 'django.contrib.contenttypes', 37 | 'django.contrib.sessions', 38 | 'django.contrib.messages', 39 | 'django.contrib.staticfiles', 40 | ] 41 | 42 | MIDDLEWARE = [ 43 | 'django.middleware.security.SecurityMiddleware', 44 | 'django.contrib.sessions.middleware.SessionMiddleware', 45 | 'django.middleware.common.CommonMiddleware', 46 | #'django.middleware.csrf.CsrfViewMiddleware', 47 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 48 | 'django.contrib.messages.middleware.MessageMiddleware', 49 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 50 | ] 51 | 52 | ROOT_URLCONF = 'djapp.urls' 53 | 54 | TEMPLATES = [ 55 | { 56 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 57 | 'DIRS': [], 58 | 'APP_DIRS': True, 59 | 'OPTIONS': { 60 | 'context_processors': [ 61 | 'django.template.context_processors.debug', 62 | 'django.template.context_processors.request', 63 | 'django.contrib.auth.context_processors.auth', 64 | 'django.contrib.messages.context_processors.messages', 65 | ], 66 | }, 67 | }, 68 | ] 69 | 70 | WSGI_APPLICATION = 'djapp.wsgi.application' 71 | 72 | 73 | # Database 74 | # https://docs.djangoproject.com/en/3.1/ref/settings/#databases 75 | 76 | DATABASES = { 77 | 'default': { 78 | 'ENGINE': 'django.db.backends.sqlite3', 79 | 'NAME': BASE_DIR / 'db.sqlite3', 80 | } 81 | } 82 | 83 | 84 | # Password validation 85 | # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators 86 | 87 | AUTH_PASSWORD_VALIDATORS = [ 88 | { 89 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 90 | }, 91 | { 92 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 93 | }, 94 | { 95 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 96 | }, 97 | { 98 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 99 | }, 100 | ] 101 | 102 | 103 | # Internationalization 104 | # https://docs.djangoproject.com/en/3.1/topics/i18n/ 105 | 106 | LANGUAGE_CODE = 'en-us' 107 | 108 | TIME_ZONE = 'UTC' 109 | 110 | USE_I18N = True 111 | 112 | USE_L10N = True 113 | 114 | USE_TZ = True 115 | 116 | 117 | # Static files (CSS, JavaScript, Images) 118 | # https://docs.djangoproject.com/en/3.1/howto/static-files/ 119 | 120 | STATIC_URL = '/static/' 121 | -------------------------------------------------------------------------------- /Chapter11/djapp/djapp/urls.py: -------------------------------------------------------------------------------- 1 | """djapp URL Configuration 2 | 3 | The `urlpatterns` list routes URLs to views. For more information please see: 4 | https://docs.djangoproject.com/en/3.1/topics/http/urls/ 5 | Examples: 6 | Function views 7 | 1. Add an import: from my_app import views 8 | 2. Add a URL to urlpatterns: path('', views.home, name='home') 9 | Class-based views 10 | 1. Add an import: from other_app.views import Home 11 | 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') 12 | Including another URLconf 13 | 1. Import the include() function: from django.urls import include, path 14 | 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) 15 | """ 16 | from django.contrib import admin 17 | from django.urls import path, include 18 | 19 | 20 | urlpatterns = [ 21 | path('admin/', admin.site.urls), 22 | path("", include("httpbin.urls")) 23 | ] 24 | -------------------------------------------------------------------------------- /Chapter11/djapp/djapp/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for djapp project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/3.1/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | from django.core.wsgi import get_wsgi_application 13 | 14 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djapp.settings') 15 | 16 | application = get_wsgi_application() 17 | -------------------------------------------------------------------------------- /Chapter11/djapp/httpbin/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter11/djapp/httpbin/__init__.py -------------------------------------------------------------------------------- /Chapter11/djapp/httpbin/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | 3 | # Register your models here. 4 | -------------------------------------------------------------------------------- /Chapter11/djapp/httpbin/apps.py: -------------------------------------------------------------------------------- 1 | from django.apps import AppConfig 2 | 3 | 4 | class HttpbinConfig(AppConfig): 5 | name = 'httpbin' 6 | -------------------------------------------------------------------------------- /Chapter11/djapp/httpbin/migrations/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter11/djapp/httpbin/migrations/__init__.py -------------------------------------------------------------------------------- /Chapter11/djapp/httpbin/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | # Create your models here. 4 | -------------------------------------------------------------------------------- /Chapter11/djapp/httpbin/tests.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.test import TestCase 4 | 5 | 6 | class HttpbinTests(TestCase): 7 | def test_home(self): 8 | response = self.client.get("/") 9 | self.assertContains(response, "Hello World") 10 | 11 | def test_GET(self): 12 | response = self.client.get("/get").content.decode("utf-8") 13 | 14 | assert '"Host": "httpbin.org"' in response 15 | assert '"args": {}' in response 16 | 17 | def test_GET_params(self): 18 | response = json.loads(self.client.get("/get?alpha=1").content) 19 | 20 | assert response["headers"]["Host"] == "httpbin.org" 21 | assert response["args"] == {"alpha": "1"} 22 | 23 | def test_POST(self): 24 | response = json.loads(self.client.post( 25 | "/get?alpha=1", {"beta": "2"} 26 | ).content) 27 | 28 | assert response["headers"]["Host"] == "httpbin.org" 29 | assert response["args"] == {"alpha": "1"} 30 | assert response["form"] == {"beta": "2"} 31 | 32 | def test_DELETE(self): 33 | response = self.client.delete( 34 | "/anything/27" 35 | ).content.decode("utf-8") 36 | 37 | assert '"method": "DELETE"' in response 38 | assert '"url": "http://httpbin.org/anything/27"' in response -------------------------------------------------------------------------------- /Chapter11/djapp/httpbin/urls.py: -------------------------------------------------------------------------------- 1 | from django.urls import re_path 2 | 3 | from . import views 4 | 5 | urlpatterns = [ 6 | re_path(r'^get$', views.get), 7 | re_path(r"^anything.*$", views.get), 8 | re_path(r'^$', views.home) 9 | ] -------------------------------------------------------------------------------- /Chapter11/djapp/httpbin/views.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | from django.http import HttpResponse 4 | 5 | 6 | def home(request): 7 | return HttpResponse('Hello World') 8 | 9 | def get(request): 10 | if request.META.get("SERVER_PORT") == "80": 11 | http_host = request.META.get("HTTP_HOST", "httpbin.org") 12 | host_no_default_port = http_host.replace(":80", "") 13 | request.META["HTTP_HOST"] = host_no_default_port 14 | host = request.META["HTTP_HOST"] 15 | 16 | response = HttpResponse(json.dumps({ 17 | "method": request.META["REQUEST_METHOD"], 18 | "headers": {"Host": host}, 19 | "args": { 20 | p: v for (p, v) in request.GET.items() 21 | }, 22 | "form": { 23 | p: v for (p, v) in request.POST.items() 24 | }, 25 | "url": request.build_absolute_uri() 26 | }, sort_keys=True)) 27 | response['Content-Type'] = 'application/json' 28 | return response -------------------------------------------------------------------------------- /Chapter11/djapp/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """Django's command-line utility for administrative tasks.""" 3 | import os 4 | import sys 5 | 6 | 7 | def main(): 8 | """Run administrative tasks.""" 9 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'djapp.settings') 10 | try: 11 | from django.core.management import execute_from_command_line 12 | except ImportError as exc: 13 | raise ImportError( 14 | "Couldn't import Django. Are you sure it's installed and " 15 | "available on your PYTHONPATH environment variable? Did you " 16 | "forget to activate a virtual environment?" 17 | ) from exc 18 | execute_from_command_line(sys.argv) 19 | 20 | 21 | if __name__ == '__main__': 22 | main() 23 | -------------------------------------------------------------------------------- /Chapter11/djapp/pytest-tests/test_djapp.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import webtest 3 | 4 | sys.path.append(".") 5 | from djapp.wsgi import application as wsgiapp 6 | 7 | 8 | class TestWSGIApp: 9 | def test_home(self): 10 | client = webtest.TestApp(wsgiapp) 11 | 12 | response = client.get("http://httpbin.org/").text 13 | 14 | assert 'Hello World' in response 15 | 16 | def test_GET(self): 17 | client = webtest.TestApp(wsgiapp) 18 | 19 | response = client.get("http://httpbin.org/get").text 20 | 21 | assert '"Host": "httpbin.org"' in response 22 | assert '"args": {}' in response 23 | 24 | def test_GET_params(self): 25 | client = webtest.TestApp(wsgiapp) 26 | 27 | response = client.get(url="http://httpbin.org/get?alpha=1").json 28 | 29 | assert response["headers"]["Host"] == "httpbin.org" 30 | assert response["args"] == {"alpha": "1"} 31 | 32 | def test_POST(self): 33 | client = webtest.TestApp(wsgiapp) 34 | 35 | response = client.post(url="http://httpbin.org/get?alpha=1", 36 | params={"beta": "2"}).json 37 | 38 | assert response["headers"]["Host"] == "httpbin.org" 39 | assert response["args"] == {"alpha": "1"} 40 | assert response["form"] == {"beta": "2"} 41 | 42 | def test_DELETE(self): 43 | client = webtest.TestApp(wsgiapp) 44 | 45 | response = client.delete(url="http://httpbin.org/anything/27").text 46 | 47 | assert '"method": "DELETE"' in response 48 | assert '"url": "http://httpbin.org/anything/27"' in response -------------------------------------------------------------------------------- /Chapter11/httpclient/src/httpclient/__init__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | import urllib.parse 3 | import requests 4 | 5 | 6 | class HTTPClient: 7 | def __init__(self, url, requests=requests): 8 | self._url = url 9 | self._requests = requests 10 | 11 | def follow(self, path): 12 | baseurl = self._url 13 | if not baseurl.endswith("/"): 14 | baseurl += "/" 15 | return HTTPClient(urllib.parse.urljoin(baseurl, path)) 16 | 17 | def GET(self): 18 | return self._requests.get(self._url).text 19 | 20 | def POST(self, **kwargs): 21 | return self._requests.post(self._url, kwargs).text 22 | 23 | def DELETE(self): 24 | return self._requests.delete(self._url).text 25 | 26 | 27 | def parse_args(): 28 | cmd = sys.argv[0] 29 | args = sys.argv[1:] 30 | try: 31 | method, url, *params = args 32 | except ValueError: 33 | raise ValueError("Not enough arguments, " 34 | "at least METHOD URL must be provided") 35 | 36 | try: 37 | params = dict((p.split("=", 1) for p in params)) 38 | except ValueError: 39 | raise ValueError("Invalid request body parameters. " 40 | "They must be in name=value format, " 41 | f"not {params}") 42 | 43 | return method.upper(), url, params 44 | 45 | 46 | def main(): 47 | try: 48 | method, url, params = parse_args() 49 | except ValueError as err: 50 | print(err) 51 | return 52 | 53 | client = HTTPClient(url) 54 | print(getattr(client, method)(**params)) -------------------------------------------------------------------------------- /Chapter11/httpclient/src/httpclient/__main__.py: -------------------------------------------------------------------------------- 1 | from httpclient import main 2 | 3 | main() -------------------------------------------------------------------------------- /Chapter11/httpclient/src/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='httpclient', packages=['httpclient']) 4 | -------------------------------------------------------------------------------- /Chapter11/httpclient/tests/test_httpclient.py: -------------------------------------------------------------------------------- 1 | import json 2 | from httpclient import HTTPClient 3 | import requests_mock 4 | 5 | 6 | class TestHTTPClient: 7 | def test_GET(self): 8 | client = HTTPClient(url="http://httpbin.org/get") 9 | 10 | with requests_mock.Mocker() as m: 11 | m.get(client._url, text='{"Host": "httpbin.org", "args": {}}') 12 | response = client.GET() 13 | 14 | assert '"Host": "httpbin.org"' in response 15 | assert '"args": {}' in response 16 | 17 | def test_GET_params(self): 18 | client = HTTPClient(url="http://httpbin.org/get?alpha=1") 19 | 20 | with requests_mock.Mocker() as m: 21 | m.get(client._url, text='''{"headers": {"Host": "httpbin.org"}, 22 | "args": {"alpha": "1"}}''') 23 | response = client.GET() 24 | 25 | response = json.loads(response) 26 | assert response["headers"]["Host"] == "httpbin.org" 27 | assert response["args"] == {"alpha": "1"} 28 | 29 | def test_POST(self): 30 | client = HTTPClient(url="http://httpbin.org/post?alpha=1") 31 | 32 | with requests_mock.Mocker() as m: 33 | m.post(client._url, json={"headers": {"Host": "httpbin.org"}, 34 | "args": {"alpha": "1"}, 35 | "form": {"beta": "2"}}) 36 | response = client.POST(beta=2) 37 | 38 | response = json.loads(response) 39 | assert response["headers"]["Host"] == "httpbin.org" 40 | assert response["args"] == {"alpha": "1"} 41 | assert response["form"] == {"beta": "2"} 42 | 43 | def test_DELETE(self): 44 | client = HTTPClient(url="http://httpbin.org/anything/27") 45 | 46 | with requests_mock.Mocker() as m: 47 | m.delete(client._url, json={ 48 | "method": "DELETE", 49 | "url": "http://httpbin.org/anything/27" 50 | }) 51 | response = client.DELETE() 52 | 53 | assert '"method": "DELETE"' in response 54 | assert '"url": "http://httpbin.org/anything/27"' in response 55 | 56 | def test_follow(self): 57 | client = HTTPClient(url="http://httpbin.org/anything") 58 | 59 | assert client._url == "http://httpbin.org/anything" 60 | 61 | client2 = client.follow("me") 62 | 63 | assert client2._url == "http://httpbin.org/anything/me" 64 | 65 | -------------------------------------------------------------------------------- /Chapter11/httpclient_with_webtest/tests/test_client_webtest.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import webtest 4 | 5 | from wsgiwebtest import Application 6 | from httpclient import HTTPClient 7 | 8 | 9 | class TestHTTPClientWebTest: 10 | def test_GET(self): 11 | client = HTTPClient(url="http://httpbin.org/get", 12 | requests=webtest.TestApp(Application())) 13 | 14 | response = client.GET() 15 | 16 | assert '"Host": "httpbin.org"' in response 17 | assert '"args": {}' in response 18 | 19 | def test_GET_params(self): 20 | client = HTTPClient(url="http://httpbin.org/get?alpha=1", 21 | requests=webtest.TestApp(Application())) 22 | 23 | response = client.GET() 24 | response = json.loads(response) 25 | 26 | assert response["headers"]["Host"] == "httpbin.org" 27 | assert response["args"] == {"alpha": "1"} 28 | 29 | def test_POST(self): 30 | client = HTTPClient(url="http://httpbin.org/post?alpha=1", 31 | requests=webtest.TestApp(Application())) 32 | 33 | response = client.POST(beta=2) 34 | response = json.loads(response) 35 | 36 | assert response["headers"]["Host"] == "httpbin.org" 37 | assert response["args"] == {"alpha": "1"} 38 | assert response["form"] == {"beta": "2"} 39 | 40 | def test_DELETE(self): 41 | client = HTTPClient(url="http://httpbin.org/anything/27", 42 | requests=webtest.TestApp(Application())) 43 | 44 | response = client.DELETE() 45 | 46 | assert '"method": "DELETE"' in response 47 | assert '"url": "http://httpbin.org/anything/27"' in response 48 | -------------------------------------------------------------------------------- /Chapter11/webframeworks/src/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='wbtframeworks', packages=['wbtframeworks']) 4 | -------------------------------------------------------------------------------- /Chapter11/webframeworks/src/wbtframeworks/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PacktPublishing/Crafting-Test-Driven-Software-with-Python/28ece831e68014c3f38a173a2dfadfd90b9330b0/Chapter11/webframeworks/src/wbtframeworks/__init__.py -------------------------------------------------------------------------------- /Chapter11/webframeworks/src/wbtframeworks/django/__init__.py: -------------------------------------------------------------------------------- 1 | import sys, json 2 | from django.conf.urls import re_path 3 | from django.conf import settings 4 | from django.http import HttpResponse 5 | 6 | settings.configure( 7 | DEBUG=True, 8 | ROOT_URLCONF=sys.modules[__name__], 9 | ALLOWED_HOSTS=["httpbin.org"] 10 | ) 11 | 12 | 13 | def home(request): 14 | return HttpResponse('Hello World') 15 | 16 | def get(request): 17 | if request.META.get("SERVER_PORT") == "80": 18 | host_no_default_port = request.META["HTTP_HOST"].replace(":80", "") 19 | request.META["HTTP_HOST"] = host_no_default_port 20 | host = request.META["HTTP_HOST"] 21 | 22 | response = HttpResponse(json.dumps({ 23 | "method": request.META["REQUEST_METHOD"], 24 | "headers": {"Host": host}, 25 | "args": { 26 | p: v for (p, v) in request.GET.items() 27 | }, 28 | "form": { 29 | p: v for (p, v) in request.POST.items() 30 | }, 31 | "url": request.build_absolute_uri() 32 | }, sort_keys=True)) 33 | response['Content-Type'] = 'application/json' 34 | return response 35 | 36 | urlpatterns = [ 37 | re_path(r'^get$', get), 38 | re_path(r"^anything.*$", get), 39 | re_path(r'^$', home), 40 | ] 41 | 42 | def make_application(): 43 | import os 44 | from django.core.wsgi import get_wsgi_application 45 | 46 | os.environ.setdefault('DJANGO_SETTINGS_MODULE', 47 | 'wbtframeworks.django.settings') 48 | 49 | return get_wsgi_application() 50 | -------------------------------------------------------------------------------- /Chapter11/webframeworks/src/wbtframeworks/flask/__init__.py: -------------------------------------------------------------------------------- 1 | from flask import Flask 2 | app = Flask(__name__) 3 | 4 | @app.route('/') 5 | def hello_world(): 6 | return 'Hello World' 7 | 8 | 9 | def make_application(): 10 | return app.wsgi_app -------------------------------------------------------------------------------- /Chapter11/webframeworks/src/wbtframeworks/pyramid/__init__.py: -------------------------------------------------------------------------------- 1 | from pyramid.config import Configurator 2 | from pyramid.response import Response 3 | 4 | 5 | def hello_world(request): 6 | return Response('Hello World!') 7 | 8 | 9 | def make_application(): 10 | with Configurator() as config: 11 | config.add_route('hello', '/') 12 | config.add_view(hello_world, route_name='hello') 13 | return config.make_wsgi_app() -------------------------------------------------------------------------------- /Chapter11/webframeworks/src/wbtframeworks/tg2/__init__.py: -------------------------------------------------------------------------------- 1 | from tg import expose, TGController 2 | 3 | class RootController(TGController): 4 | @expose() 5 | def index(self): 6 | return 'Hello World' 7 | 8 | 9 | def make_application(): 10 | from tg import MinimalApplicationConfigurator 11 | 12 | config = MinimalApplicationConfigurator() 13 | config.update_blueprint({ 14 | 'root_controller': RootController() 15 | }) 16 | 17 | return config.make_wsgi_app() -------------------------------------------------------------------------------- /Chapter11/webframeworks/tests/conftest.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | 3 | 4 | def pytest_addoption(parser): 5 | parser.addoption( 6 | "--framework", action="store", 7 | help="Choose which framework to use for " 8 | "the web application: [tg2, django, flask, pyramid]" 9 | ) 10 | 11 | 12 | @pytest.fixture 13 | def wsgiapp(request): 14 | framework = request.config.getoption("--framework") 15 | 16 | if framework == "tg2": 17 | from wbtframeworks.tg2 import make_application 18 | elif framework == "flask": 19 | from wbtframeworks.flask import make_application 20 | elif framework == "pyramid": 21 | from wbtframeworks.pyramid import make_application 22 | elif framework == "django": 23 | from wbtframeworks.django import make_application 24 | else: 25 | make_application = None 26 | 27 | if make_application is not None: 28 | return make_application() 29 | 30 | if framework is None: 31 | raise ValueError("Please pick a framework with --framework option") 32 | else: 33 | raise ValueError(f"Invalid framework {framework}") -------------------------------------------------------------------------------- /Chapter11/webframeworks/tests/test_wsgiapp.py: -------------------------------------------------------------------------------- 1 | import webtest 2 | 3 | 4 | class TestWSGIApp: 5 | def test_home(self, wsgiapp): 6 | client = webtest.TestApp(wsgiapp) 7 | 8 | response = client.get("http://httpbin.org/").text 9 | 10 | assert 'Hello World' in response 11 | 12 | def test_GET(self, wsgiapp): 13 | client = webtest.TestApp(wsgiapp) 14 | 15 | response = client.get("http://httpbin.org/get").text 16 | 17 | assert '"Host": "httpbin.org"' in response 18 | assert '"args": {}' in response 19 | 20 | def test_GET_params(self, wsgiapp): 21 | client = webtest.TestApp(wsgiapp) 22 | 23 | response = client.get(url="http://httpbin.org/get?alpha=1").json 24 | 25 | assert response["headers"]["Host"] == "httpbin.org" 26 | assert response["args"] == {"alpha": "1"} 27 | 28 | def test_POST(self, wsgiapp): 29 | client = webtest.TestApp(wsgiapp) 30 | 31 | response = client.post(url="http://httpbin.org/get?alpha=1", 32 | params={"beta": "2"}).json 33 | 34 | assert response["headers"]["Host"] == "httpbin.org" 35 | assert response["args"] == {"alpha": "1"} 36 | assert response["form"] == {"beta": "2"} 37 | 38 | def test_DELETE(self, wsgiapp): 39 | client = webtest.TestApp(wsgiapp) 40 | 41 | response = client.delete(url="http://httpbin.org/anything/27").text 42 | 43 | assert '"method": "DELETE"' in response 44 | assert '"url": "http://httpbin.org/anything/27"' in response -------------------------------------------------------------------------------- /Chapter11/webtest/src/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='wsgiwebtest', packages=['wsgiwebtest']) 4 | -------------------------------------------------------------------------------- /Chapter11/webtest/src/wsgiwebtest/__init__.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import urllib.parse 4 | 5 | 6 | class Application: 7 | def __call__(self, environ, start_response): 8 | start_response( 9 | '200 OK', 10 | [('Content-type', 'application/json; charset=utf-8')] 11 | ) 12 | 13 | form_params = {} 14 | if environ.get('CONTENT_TYPE') == 'application/x-www-form-urlencoded': 15 | req_body = environ["wsgi.input"].read().decode("ascii") 16 | form_params = { 17 | k: v for k, v in urllib.parse.parse_qsl(req_body) 18 | } 19 | 20 | if environ.get("SERVER_PORT") == "80": 21 | host = environ["SERVER_NAME"] 22 | else: 23 | host = environ["HTTP_HOST"] 24 | 25 | return [json.dumps({ 26 | "method": environ["REQUEST_METHOD"], 27 | "headers": {"Host": host}, 28 | "url": "{e[wsgi.url_scheme]}://{host}{e[PATH_INFO]}".format( 29 | e=environ, 30 | host=host 31 | ), 32 | "args": { 33 | k: v for k, v in urllib.parse.parse_qsl(environ["QUERY_STRING"]) 34 | }, 35 | "form": form_params 36 | }).encode("utf-8")] 37 | -------------------------------------------------------------------------------- /Chapter11/webtest/src/wsgiwebtest/__main__.py: -------------------------------------------------------------------------------- 1 | from wsgiref.simple_server import make_server 2 | 3 | from wsgiwebtest import Application 4 | 5 | 6 | def main(): 7 | app = Application() 8 | with make_server('', 8000, app) as httpd: 9 | print("Serving on port 8000...") 10 | httpd.serve_forever() 11 | 12 | main() -------------------------------------------------------------------------------- /Chapter11/webtest/tests/test_wsgiapp.py: -------------------------------------------------------------------------------- 1 | import webtest 2 | 3 | from wsgiwebtest import Application 4 | 5 | 6 | class TestWSGIApp: 7 | def test_GET(self): 8 | client = webtest.TestApp(Application()) 9 | 10 | response = client.get("http://httpbin.org/get").text 11 | 12 | assert '"Host": "httpbin.org"' in response 13 | assert '"args": {}' in response 14 | 15 | def test_GET_params(self): 16 | client = webtest.TestApp(Application()) 17 | 18 | response = client.get(url="http://httpbin.org/get?alpha=1").json 19 | 20 | assert response["headers"]["Host"] == "httpbin.org" 21 | assert response["args"] == {"alpha": "1"} 22 | 23 | def test_POST(self): 24 | client = webtest.TestApp(Application()) 25 | 26 | response = client.post(url="http://httpbin.org/get?alpha=1", 27 | params={"beta": "2"}).json 28 | 29 | assert response["headers"]["Host"] == "httpbin.org" 30 | assert response["args"] == {"alpha": "1"} 31 | assert response["form"] == {"beta": "2"} 32 | 33 | def test_DELETE(self): 34 | client = webtest.TestApp(Application()) 35 | 36 | response = client.delete(url="http://httpbin.org/anything/27").text 37 | 38 | assert '"method": "DELETE"' in response 39 | assert '"url": "http://httpbin.org/anything/27"' in response 40 | -------------------------------------------------------------------------------- /Chapter12/customkeywords.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library HelloLibrary 3 | 4 | *** Keywords *** 5 | Echo Hello 6 | Log Hello! 7 | 8 | *** Test Cases *** 9 | Use Custom Keywords 10 | Echo Hello 11 | Say Hello -------------------------------------------------------------------------------- /Chapter12/hellolibrary/HelloLibrary/__init__.py: -------------------------------------------------------------------------------- 1 | class HelloLibrary: 2 | 3 | def say_hello(self): 4 | print("Hello from Python!") 5 | -------------------------------------------------------------------------------- /Chapter12/hellolibrary/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | setup(name='robotframework-hellolibrary', packages=['HelloLibrary']) 4 | -------------------------------------------------------------------------------- /Chapter12/hellotest.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library OperatingSystem 3 | 4 | *** Test Cases *** 5 | Hello World 6 | Run echo "Hello World" > hello.txt 7 | ${filecontent} = Get File hello.txt 8 | Should Contain ${filecontent} Bye -------------------------------------------------------------------------------- /Chapter12/searchgoogle.robot: -------------------------------------------------------------------------------- 1 | *** Settings *** 2 | Library SeleniumLibrary 3 | Library ScreenCapLibrary 4 | Test Setup Start Video Recording 5 | Test Teardown Stop Video Recording 6 | Suite Teardown Close All Browsers 7 | 8 | *** Variables *** 9 | ${BROWSER} headlesschrome 10 | ${NOTHEADLESS}= "headlesschrome" not in "${BROWSER}" 11 | 12 | *** Test Cases *** 13 | Search On Google 14 | Open Browser http://www.google.com ${BROWSER} 15 | Run Keyword If ${NOTHEADLESS} Wait Until Page Contains Element cnsw 16 | Run Keyword If ${NOTHEADLESS} Select Frame //iframe 17 | Run Keyword If ${NOTHEADLESS} Submit Form //form 18 | Unselect Frame 19 | Input Text name=q Stephen\ Hawking 20 | Press Keys name=q SPACE 21 | Press Keys name=q ENTER 22 | Wait Until Page Contains Element id=res 23 | Page Should Contain Wikipedia 24 | Close Window 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Packt 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # Crafting Test-Driven Software with Python 5 | 6 | Book Name 7 | 8 | This is the code repository for [Crafting Test-Driven Software with Python](https://www.packtpub.com/product/crafting-test-driven-software-with-python/9781838642655), published by Packt. 9 | 10 | **Write test suites that scale with your applications needs and complexity, using Python and PyTest** 11 | 12 | ## What is this book about? 13 | Test-driven development (TDD) is a set of best practices that helps developers to build more scalable software, and is used to increase the robustness of software by using automatic tests. This book shows you how to apply TDD practices efficiently in Python projects. 14 | 15 | This book covers the following exciting features: 16 | * Find out how tests can make your life easier as a developer and discover related best practices 17 | * Explore and learn PyTest, the most widespread testing framework for Python 18 | * Get to grips with the most common PyTest plugins, including coverage, flaky, xdist, and picked 19 | * Write functional tests for WSGI web applications with WebTest 20 | * Run end-to-end tests for web applications using the Robot framework 21 | 22 | If you feel this book is for you, get your [copy](https://www.amazon.com/dp/183864265X) today! 23 | 24 | https://www.packtpub.com/ 26 | 27 | ## Instructions and Navigations 28 | All of the code is organized into folders. For example, Chapter01. 29 | 30 | The code will look like the following: 31 | ``` 32 | import unittest 33 | 34 | class MyTestCase(unittest.TestCase): 35 | def test_one(self): 36 | pass 37 | ``` 38 | 39 | **Following is what you need for this book:** 40 | This book is for Python developers looking to get started with test-driven development and developers who want to learn about the testing tools available in Python. Developers who want to create web applications with Django and Python and plan to implement TDD methodology with PyTest will find this book useful. Basic knowledge of Python programming is required. 41 | 42 | With the following software and hardware list you can run all code files present in the book (Chapter 1-12). 43 | 44 | ### Software and Hardware List 45 | 46 | | Chapter | Software required | OS required | 47 | | -------- | ------------------------------------| -----------------------------------| 48 | | 1-12 | Python 3.9,3.8 or 3.7 | Windows, Mac OS X, and Linux (Any) | 49 | | 5-10 | PyTest 6.0.2+ | Windows, Mac OS X, and Linux (Any) | 50 | | 1-12 | pip 18+ | Windows, Mac OS X, and Linux (Any) | 51 | | 1-12 | Google Chrome or Firefox | Windows, Mac OS X, and Linux (Any) | 52 | 53 | ### Related products 54 | * Django 3 By Example - Third Edition [[Packt]](https://www.packtpub.com/product/django-3-by-example-third-edition/9781838981952) [[Amazon]](https://www.amazon.com/dp/1838981950) 55 | 56 | * 40 Algorithms Every Programmer Should Know [[Packt]](https://www.packtpub.com/product/40-algorithms-every-programmer-should-know/9781789801217) [[Amazon]](https://www.amazon.com/dp/1789801214) 57 | 58 | ## Get to Know the Author 59 | **Alessandro Molina** 60 | is a Python developer since 2001, who has always been interested in Python as a Web Development Platform. He worked as CTO and team leader of Python teams for the past 10 years and is currently core developer of the TurboGears2 web framework and maintainer of Beaker Caching/Session framework. He also authored the Modern Python Standard Library Cookbook. created the DEPOT file storage framework and the DukPy JavaScript interpreter for Python and collaborated with various Python projects related to web development, such as FormEncode, ToscaWidgets, and the Ming MongoDB ORM. 61 | 62 | ## Other books by the author 63 | * [Modern Python Standard Library Cookbook](https://www.packtpub.com/product/modern-python-standard-library-cookbook/9781788830829) 64 | 65 | 66 | 67 | ### Download a free PDF 68 | 69 | If you have already purchased a print or Kindle version of this book, you can get a DRM-free PDF version at no cost.
Simply click on the link to claim your free PDF.
70 |

https://packt.link/free-ebook/9781838642655

--------------------------------------------------------------------------------