diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 77249230f..d9fdc8766 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -34,6 +34,7 @@ jobs: test_chapter_19_mocking, test_chapter_20_fixtures_and_wait_decorator, test_chapter_21_server_side_debugging, + test_chapter_22_outside_in, unit-test, ] diff --git a/.gitmodules b/.gitmodules index c50c3dbd9..2c67af44e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -53,7 +53,7 @@ path = source/chapter_21_server_side_debugging/superlists url = git@github.com:hjwp/book-example.git [submodule "source/chapter_19/superlists"] - path = source/chapter_outside_in/superlists + path = source/chapter_22_outside_in/superlists url = git@github.com:hjwp/book-example.git [submodule "source/chapter_20/superlists"] path = source/chapter_purist_unit_tests/superlists diff --git a/Makefile b/Makefile index 3b2acfbc7..a833923cd 100644 --- a/Makefile +++ b/Makefile @@ -137,9 +137,9 @@ test_chapter_20_fixtures_and_wait_decorator: chapter_20_fixtures_and_wait_decora .PHONY: test_chapter_21_server_side_debugging test_chapter_21_server_side_debugging: chapter_21_server_side_debugging.html $(TMPDIR) $(VENV)/bin $(VENV)/bin/pytest -s --tb=short ./tests/test_chapter_21_server_side_debugging.py -.PHONY: test_chapter_outside_in -test_chapter_outside_in: chapter_outside_in.html $(TMPDIR) $(VENV)/bin - $(VENV)/bin/pytest -s --tb=short ./tests/test_chapter_outside_in.py +.PHONY: test_chapter_22_outside_in +test_chapter_22_outside_in: chapter_22_outside_in.html $(TMPDIR) $(VENV)/bin + $(VENV)/bin/pytest -s --tb=short ./tests/test_chapter_22_outside_in.py .PHONY: test_chapter_purist_unit_tests test_chapter_purist_unit_tests: chapter_purist_unit_tests.html $(TMPDIR) $(VENV)/bin $(VENV)/bin/pytest -s --tb=short ./tests/test_chapter_purist_unit_tests.py diff --git a/appendix_IX_cheat_sheet.asciidoc b/appendix_IX_cheat_sheet.asciidoc index cdd9c5e97..d425a006d 100644 --- a/appendix_IX_cheat_sheet.asciidoc +++ b/appendix_IX_cheat_sheet.asciidoc @@ -161,6 +161,6 @@ If you do find yourself writing tests with lots of mocks, and they feel painful, remember “__listen to your tests__”—ugly, mocky tests may be trying to tell you that your code could be simplified. -Relevant chapters: <>, <>, +Relevant chapters: <>, <>, <> diff --git a/appendix_bdd.asciidoc b/appendix_bdd.asciidoc index e5a9817b6..62dea7910 100644 --- a/appendix_bdd.asciidoc +++ b/appendix_bdd.asciidoc @@ -43,7 +43,7 @@ https://pythonhosted.org/behave-django/[behave-django]. ********************************************************************** ((("code examples, obtaining and using")))I'm -going to use the example from <>. +going to use the example from <>. We have a basic to-do lists site, and we want to add a new feature: logged-in users should be able to view the lists they've authored in one place. Up until this point, all lists are effectively anonymous. diff --git a/appendix_github_links.asciidoc b/appendix_github_links.asciidoc index 14e1957a8..ac773b6be 100644 --- a/appendix_github_links.asciidoc +++ b/appendix_github_links.asciidoc @@ -40,7 +40,7 @@ Full List of Links for Each Chapter <>:: https://github.com/hjwp/book-example/tree/chapter_19_mocking <>:: https://github.com/hjwp/book-example/tree/chapter_20_fixtures_and_wait_decorator <>:: https://github.com/hjwp/book-example/tree/chapter_21_server_side_debugging -<>:: https://github.com/hjwp/book-example/tree/chapter_outside_in +<>:: https://github.com/hjwp/book-example/tree/chapter_22_outside_in <>:: https://github.com/hjwp/book-example/tree/chapter_purist_unit_tests <>:: https://github.com/hjwp/book-example/tree/chapter_CI <>:: https://github.com/hjwp/book-example/tree/chapter_page_pattern diff --git a/atlas.json b/atlas.json index 62cdd7215..468e4f376 100644 --- a/atlas.json +++ b/atlas.json @@ -34,7 +34,7 @@ "chapter_19_mocking.asciidoc", "chapter_20_fixtures_and_wait_decorator.asciidoc", "chapter_21_server_side_debugging.asciidoc", - "chapter_outside_in.asciidoc", + "chapter_22_outside_in.asciidoc", "chapter_purist_unit_tests.asciidoc", "chapter_CI.asciidoc", "chapter_page_pattern.asciidoc", diff --git a/book.asciidoc b/book.asciidoc index 6637f364e..4d27f4b1b 100644 --- a/book.asciidoc +++ b/book.asciidoc @@ -45,7 +45,7 @@ include::chapter_18_spiking_custom_auth.asciidoc[] include::chapter_19_mocking.asciidoc[] include::chapter_20_fixtures_and_wait_decorator.asciidoc[] include::chapter_21_server_side_debugging.asciidoc[] -include::chapter_outside_in.asciidoc[] +include::chapter_22_outside_in.asciidoc[] include::chapter_purist_unit_tests.asciidoc[] include::chapter_CI.asciidoc[] include::chapter_page_pattern.asciidoc[] diff --git a/chapter_07_working_incrementally.asciidoc b/chapter_07_working_incrementally.asciidoc index e3a8b8191..37c016f8b 100644 --- a/chapter_07_working_incrementally.asciidoc +++ b/chapter_07_working_incrementally.asciidoc @@ -2543,7 +2543,7 @@ YAGNI:: just because it suggests itself at the time. Chances are, you won't use it, or you won't have anticipated your future requirements correctly. - See <> for one methodology that helps us avoid this trap. + See <> for one methodology that helps us avoid this trap. ((("Test-Driven Development (TDD)", "philosophy of", "YAGNI"))) ((("YAGNI (You ain’t gonna need it!)"))) diff --git a/chapter_outside_in.asciidoc b/chapter_22_outside_in.asciidoc similarity index 54% rename from chapter_outside_in.asciidoc rename to chapter_22_outside_in.asciidoc index c60171fd5..16134f428 100644 --- a/chapter_outside_in.asciidoc +++ b/chapter_22_outside_in.asciidoc @@ -1,55 +1,41 @@ -[[chapter_outside_in]] -Finishing "My Lists": Outside-In TDD ------------------------------------- +[[chapter_22_outside_in]] +== Finishing "My Lists": Outside-In TDD -.Warning, Chapter Not Updated +.Warning, Chapter Update In Progress ******************************************************************************* -🚧 Warning, this Chapter is the 2e version, and uses Django 1.11 +🚧 Warning, this Chapter half updated for the 3e. -This chapter and all the following ones are the second edition versions, so they still use Django 1.11, Python 3.8, and so on. +I'm currently updating the listings for the latest Django + Python, +some things may not be quite right yet. -To follow along with this chapter, it’s probably easiest to reset your code to match my example code as it was in the 2e, by resetting to: https://github.com/hjwp/book-example/tree/chapter_20_fixtures_and_wait_decorator - -And you should also probably delete and re-create your virtualenv with -* Python 3.8 or 3.9 -* and Django 1.11 (pip install "django <2") - -Alternatively, you can muddle through -and try and figure out how to make things work with Django 4 etc, -but be aware that the listings below won’t be quite right. -All the fabric stuff needs to be migrated to ansible, for a start. ******************************************************************************* +((("Test-Driven Development (TDD)", "outside-in technique", id="TTDoutside22"))) +In this chapter I'd like to talk about a technique called Outside-In TDD. +It's pretty much what we've been doing all along. +Our "double-loop" TDD process, +in which we write the functional test first and then the unit tests, +is already a manifestation of outside-in--we +design the system from the outside, and build up our code in layers. +Now I'll make it explicit, and talk about some of the common issues involved. +=== The Alternative: "Inside-Out" -((("Test-Driven Development (TDD)", "outside-in technique", id="TTDoutside22")))In -this chapter I'd like to talk about a technique called Outside-In TDD. -It's pretty much what we've been doing all along. Our "double-loop" TDD -process, in which we write the functional test first and then the unit tests, -is already a manifestation of outside-in--we design the system from the -outside, and build up our code in layers. Now I'll make it explicit, and talk -about some of the common issues involved. - - -The Alternative: "Inside-Out" -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - - - -The alternative to "outside-in" is to work "inside-out", which is the way most -people intuitively work before they encounter TDD. After -coming up with a design, the natural inclination is sometimes to implement it -starting with the innermost, lowest-level components first. +The alternative to "outside-in" is to work "inside-out", +which is the way most people intuitively work before they encounter TDD. +After coming up with a design, the natural inclination is sometimes +to implement it starting with the innermost, lowest-level components first. -For example, when faced with our current problem, providing users with a -"My Lists" page of saved lists, the temptation is to start by adding an "owner" -attribute to the `List` model object, reasoning that an attribute like this is -"obviously" going to be required. Once that's in place, we would modify the -more peripheral layers of code, such as views and templates, taking advantage -of the new attribute, and then finally add URL routing to point to the new -view. +For example, when faced with our current problem, +providing users with a "My Lists" page of saved lists, +the temptation is to start at the models layer: +we probably want to add an "owner" attribute to the `List` model object, +reasoning that an attribute like this is "obviously" going to be required. +Once that's in place, we would modify the more peripheral layers of code, +such as views and templates, taking advantage of the new attribute, +and then finally add URL routing to point to the new view. It feels comfortable because it means you're never working on a bit of code that is dependent on something that hasn't yet been implemented. Each bit of @@ -57,12 +43,12 @@ work on the inside is a solid foundation on which to build the next layer out. But working inside-out like this also has some weaknesses. -Why Prefer "Outside-In"? -~~~~~~~~~~~~~~~~~~~~~~~~ +=== Why Prefer "Outside-In"? -((("Outside-In TDD", "vs. inside-out", secondary-sortas="inside-out")))((("inside-out TDD")))The -most obvious problem with inside-out is that it requires us to stray from a +((("Outside-In TDD", "vs. inside-out", secondary-sortas="inside-out"))) +((("inside-out TDD"))) +The most obvious problem with inside-out is that it requires us to stray from a TDD workflow. Our functional test's first failure might be due to missing URL routing, but we decide to ignore that and go off adding attributes to our database model objects instead. @@ -82,15 +68,14 @@ with inner components which, you later realise, don't actually solve the problem that your outer layers need solved. In contrast, working outside-in allows you to use each layer to imagine the -most convenient API you could want from the layer beneath it. Let's see it in +most convenient API you could want from the layer beneath it. Let's see it in action. -The FT for "My Lists" -~~~~~~~~~~~~~~~~~~~~~ +=== The FT for "My Lists" -((("functional tests (FTs)", "outside-in technique")))As -we work through the following functional test, we start with the most +((("functional tests (FTs)", "outside-in technique"))) +As we work through the following functional test, we start with the most outward-facing (presentation layer), through to the view functions (or "controllers"), and lastly the innermost layers, which in this case will be model code. @@ -100,29 +85,32 @@ write our FT to look for a "My Lists" page: [role="sourcecode"] -.functional_tests/test_my_lists.py (ch19l001-1) +.src/functional_tests/test_my_lists.py (ch22l001) ==== [source,python] ---- +from selenium.webdriver.common.by import By +[...] + def test_logged_in_users_lists_are_saved_as_my_lists(self): # Edith is a logged-in user - self.create_pre_authenticated_session('edith@example.com') + self.create_pre_authenticated_session("edith@example.com") # She goes to the home page and starts a list self.browser.get(self.live_server_url) - self.add_list_item('Reticulate splines') - self.add_list_item('Immanentize eschaton') + self.add_list_item("Reticulate splines") + self.add_list_item("Immanentize eschaton") first_list_url = self.browser.current_url # She notices a "My lists" link, for the first time. - self.browser.find_element_by_link_text('My lists').click() + self.browser.find_element(By.LINK_TEXT, "My lists").click() # She sees that her list is in there, named according to its # first list item self.wait_for( - lambda: self.browser.find_element_by_link_text('Reticulate splines') + lambda: self.browser.find_element(By.LINK_TEXT, "Reticulate splines") ) - self.browser.find_element_by_link_text('Reticulate splines').click() + self.browser.find_element(By.LINK_TEXT, "Reticulate splines").click() self.wait_for( lambda: self.assertEqual(self.browser.current_url, first_list_url) ) @@ -140,9 +128,9 @@ in the list. Let's validate that it really works by creating a second list, and seeing that appear on the My Lists page as well. The FT continues, and while we're at it, we check that only logged-in users can see the "My Lists" page: - + [role="sourcecode"] -.functional_tests/test_my_lists.py (ch19l001-2) +.src/functional_tests/test_my_lists.py (ch22l002) ==== [source,python] ---- @@ -150,28 +138,28 @@ we check that only logged-in users can see the "My Lists" page: self.wait_for( lambda: self.assertEqual(self.browser.current_url, first_list_url) ) - + # She decides to start another list, just to see self.browser.get(self.live_server_url) - self.add_list_item('Click cows') + self.add_list_item("Click cows") second_list_url = self.browser.current_url # Under "my lists", her new list appears - self.browser.find_element_by_link_text('My lists').click() - self.wait_for( - lambda: self.browser.find_element_by_link_text('Click cows') - ) - self.browser.find_element_by_link_text('Click cows').click() + self.browser.find_element(By.LINK_TEXT, "My lists").click() + self.wait_for(lambda: self.browser.find_element(By.LINK_TEXT, "Click cows")) + self.browser.find_element(By.LINK_TEXT, "Click cows").click() self.wait_for( lambda: self.assertEqual(self.browser.current_url, second_list_url) ) # She logs out. The "My lists" option disappears - self.browser.find_element_by_link_text('Log out').click() - self.wait_for(lambda: self.assertEqual( - self.browser.find_elements_by_link_text('My lists'), - [] - )) + self.browser.find_element(By.LINK_TEXT, "Log out").click() + self.wait_for( + lambda: self.assertEqual( + self.browser.find_elements(By.LINK_TEXT, "My lists"), + [], + ) + ) ---- ==== @@ -180,7 +168,7 @@ text into the right input box. We define it in 'base.py': [role="sourcecode small-code"] -.functional_tests/base.py (ch19l001-3) +.src/functional_tests/base.py (ch22l003) ==== [source,python] ---- @@ -188,11 +176,11 @@ from selenium.webdriver.common.keys import Keys [...] def add_list_item(self, item_text): - num_rows = len(self.browser.find_elements_by_css_selector('#id_list_table tr')) + num_rows = len(self.browser.find_elements(By.CSS_SELECTOR, "#id_list_table tr")) self.get_item_input_box().send_keys(item_text) self.get_item_input_box().send_keys(Keys.ENTER) item_number = num_rows + 1 - self.wait_for_row_in_list_table(f'{item_number}: {item_text}') + self.wait_for_row_in_list_table(f"{item_number}: {item_text}") ---- ==== @@ -200,12 +188,12 @@ from selenium.webdriver.common.keys import Keys And while we're at it we can use it in a few of the other FTs, like this: -[role="sourcecode currentcontents dofirst-ch19l001-4"] -.functional_tests/test_list_item_validation.py +[role="sourcecode currentcontents dofirst-ch22l004"] +.src/functional_tests/test_list_item_validation.py ==== [source,python] ---- - self.add_list_item('Buy wellies') + self.add_list_item("Buy wellies") ---- ==== @@ -219,36 +207,37 @@ The first error should look like this: [subs="specialcharacters,macros"] ---- -$ pass:quotes[*python3 manage.py test functional_tests.test_my_lists*] +$ pass:quotes[*python src/manage.py test functional_tests.test_my_lists*] [...] selenium.common.exceptions.NoSuchElementException: Message: Unable to locate -element: My lists +element: My lists; [...] ---- -The Outside Layer: Presentation and Templates -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +=== The Outside Layer: Presentation and Templates + +((("Outside-In TDD", "outside layer"))) +The test is currently failing saying that it can't find a link saying "My Lists". +We can address that at the presentation layer, in _base.html_, in our navigation bar. +Here's the minimal code change: -((("Outside-In TDD", "outside layer")))The -test is currently failing saying that it can't find a link saying "My -Lists". We can address that at the presentation layer, in 'base.html', in -our navigation bar. Here's the minimal code change: +* TODO: update this link for latest bootstrap / style nicely [role="sourcecode small-code"] -.lists/templates/base.html (ch19l002-1) +.src/lists/templates/base.html (ch22l005) ==== [source,html] ---- - {% if user.email %} - - +