diff --git a/chapter_24_outside_in.asciidoc b/chapter_24_outside_in.asciidoc index 62ef5fc8..84180ebf 100644 --- a/chapter_24_outside_in.asciidoc +++ b/chapter_24_outside_in.asciidoc @@ -92,7 +92,7 @@ to look for a "My Lists" page: [role="sourcecode"] -.src/functional_tests/test_my_lists.py (ch22l001) +.src/functional_tests/test_my_lists.py (ch24l001) ==== [source,python] ---- @@ -105,15 +105,23 @@ from selenium.webdriver.common.by import By # 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("Reticulate splines") # <1> 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() - # She sees that her list is in there, named according to its - # first list item + # She sees her email is there in the page heading + self.wait_for( + lambda: self.assertIn( + "edith@example.com", + self.browser.find_element(By.CSS_SELECTOR, "h1").text, + ) + ) + + # And 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") ) @@ -124,11 +132,11 @@ from selenium.webdriver.common.by import By ---- ==== +<1> We'll define this `add_list_item()` shortly. -//TODO: add a check for email in my lists page header, reflow logic -// to fit better with workshops.. +// Programming by wishful thinking, as always! -We create a list with a couple of items, +As you can see, we create a list with a couple of items, then we check that this list appears on a new "My Lists" page, and that it's "named" after the first item in the list. @@ -138,7 +146,7 @@ The FT continues, and while we're at it, we check that only logged-in users can see the "My Lists" page: [role="sourcecode"] -.src/functional_tests/test_my_lists.py (ch22l002) +.src/functional_tests/test_my_lists.py (ch24l002) ==== [source,python] ---- @@ -177,7 +185,7 @@ We define it in 'base.py': [role="sourcecode small-code"] -.src/functional_tests/base.py (ch22l003) +.src/functional_tests/base.py (ch24l003) ==== [source,python] ---- @@ -198,8 +206,8 @@ And while we're at it we can use it in a few of the other FTs, like this for example: -[role="sourcecode dofirst-ch22l004-1"] -.src/functional_tests/test_layout_and_styling.py (ch22l004-2) +[role="sourcecode dofirst-ch24l004-1"] +.src/functional_tests/test_layout_and_styling.py (ch24l004-2) ==== [source,diff] ---- @@ -213,13 +221,12 @@ like this for example: ---- ==== -I think it makes the FTs a lot more readable. I made a total of six -changes--see if you agree with me. +I think it makes the FTs a lot more readable. +I made a total of six changes--see if you agree with me. A quick run of all FTs, a commit, and then back to the FT we're working on. The first error should look like this: -//IDEA: add a thing that looks for her email address in an h1? [subs="specialcharacters,macros"] ---- @@ -239,10 +246,8 @@ We can address that at the presentation layer, in _base.html_, in our navigation Here's the minimal code change: -* TODO: update this link for latest bootstrap / style nicely - [role="sourcecode small-code"] -.src/lists/templates/base.html (ch22l005) +.src/lists/templates/base.html (ch24l005) ==== [source,html] ---- @@ -250,45 +255,46 @@ Here's the minimal code change:
Superlists {% if user.email %} - My lists + My lists Logged in as {{ user.email }}
[...] ---- ==== -Of course, that link doesn't actually go anywhere, but it does get us along to -the next failure: +Of course the `href="#"` means that link doesn't actually go anywhere, +but it _does_ get our FT along to the next failure: -* TODO: address issue with default list item ordering here. -[subs="specialcharacters,macros"] +[subs=""] ---- -$ pass:quotes[*python src/manage.py test functional_tests.test_my_lists*] -[...] - self.wait_for( - ~~~~~~~~~~~~~^ - lambda: self.browser.find_element(By.LINK_TEXT, "Reticulate splines") - ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +$ python src/manage.py test functional_tests.test_my_lists [...] -selenium.common.exceptions.NoSuchElementException: Message: Unable to locate -element: Reticulate splines; [...] + lambda: self.assertIn( + ~~~~~~~~~~~~~^ + "edith@example.com", + ^^^^^^^^^^^^^^^^^^^^ + self.browser.find_element(By.CSS_SELECTOR, "h1").text, + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + ) + ^ +AssertionError: 'edith@example.com' not found in 'Your To-Do list' ---- -Which is telling us we're going to have to build a page that lists all of a -user's lists by title. Let's start with the basics--a URL and a placeholder -template for it. +Which is telling us we're going to have to build a page +that at least has the user's email in its header. +Let's start with the basics--a URL and a placeholder template for it. -Again, we can go outside-in, starting at the presentation layer with just the -URL and nothing else: +Again, we can go outside-in, +starting at the presentation layer with just the URL and nothing else: [role="sourcecode"] -.src/lists/templates/base.html (ch22l006) +.src/lists/templates/base.html (ch24l006) ==== [source,html] ---- {% if user.email %} - My lists + My lists ---- ==== @@ -296,14 +302,33 @@ URL and nothing else: === Moving Down One Layer to View Functions (the Controller) ((("Outside-In TDD", "controller layer"))) -That will cause a template error, so we'll start to move down from the -presentation layer and URLs down to the controller layer, Django's view -functions. +That will cause a template error in the FT: + +[subs=""] +---- +$ ./src/manage.py test functional_tests.test_my_lists +[...] +Internal Server Error: / +[...] + File "...goat-book/src/lists/views.py", line 8, in home_page + return render(request, "home.html", {"form": ItemForm()}) +[...] +django.urls.exceptions.NoReverseMatch: Reverse for 'my_lists' not found. +'my_lists' is not a valid view function or pattern name. +[...] +ERROR: test_logged_in_users_lists_are_saved_as_my_lists [...] +[...] +selenium.common.exceptions.NoSuchElementException: [...] +---- -As always, we start with a test: +To fix it, we'll need to start to move from working at the presentation layer, +gradually into the controller layer, Django's view functions. + +As always, we start with a test. +In this layer, a unit test is the way to go: [role="sourcecode"] -.src/lists/tests/test_views.py (ch22l007) +.src/lists/tests/test_views.py (ch24l007) ==== [source,python] ---- @@ -320,11 +345,12 @@ That gives: AssertionError: No templates used to render the response ---- -And we fix it, still at the presentation level, in 'urls.py': +That's because the URL doesn't exist yet, and a 404 has no template. +Let's start our fix in _urls.py_: [role="sourcecode"] -.src/lists/urls.py (ch22l008) +.src/lists/urls.py (ch24l008) ==== [source,python] ---- @@ -337,8 +363,10 @@ urlpatterns = [ ==== -That gives us a test failure, which informs us of what we should do as we -move down to the next level: +That gives us a new test failure, +which informs us of what we should do. +As you can see, it's pointing us at a _views.py_, +we're clearly in the controller layer: ---- path("users//", views.my_lists, name="my_lists"), @@ -347,11 +375,10 @@ AttributeError: module 'lists.views' has no attribute 'my_lists' ---- -We move in from the presentation layer to the views layer, and create a -minimal placeholder: +Let's create a minimal placeholder then: [role="sourcecode"] -.src/lists/views.py (ch22l009) +.src/lists/views.py (ch24l009) ==== [source,python] ---- @@ -360,22 +387,30 @@ def my_lists(request, email): ---- ==== -And a minimal template: +And a minimal template, with no real content +except for the header that shows the user's email address: [role="sourcecode"] -.src/lists/templates/my_lists.html (ch22l010) +.src/lists/templates/my_lists.html (ch24l010) ==== [source,html] ---- {% extends 'base.html' %} -{% block header_text %}My Lists{% endblock %} +{% block header_text %}{{user.email}}'s Lists{% endblock %} ---- ==== -That gets our unit tests passing, but our FT is still at the same point, -saying that the "My Lists" page doesn't yet show any lists. It wants -them to be clickable links named after the first item: +That gets our unit tests passing. + +[subs="specialcharacters,quotes"] +---- +$ *./src/manage.py test lists* +[...] +OK +---- + +And hopefully it will address the current error in our FT: [subs="specialcharacters,macros"] ---- @@ -385,115 +420,374 @@ selenium.common.exceptions.NoSuchElementException: Message: Unable to locate element: Reticulate splines; [...] ---- +Step by step! Sure enough, the FT get a little further. +It _can_ now find the email in the `

`, +but it's now saying that the "My Lists" page doesn't yet show any lists. +It wants them to appear as clickable links, named after the first item. + === Another Pass, Outside-In + ((("Outside-In TDD", "FT-driven development", id="OITDDft22"))) -At each stage, we still let the FT drive what development we do. +At each stage, we're still letting the FT drive what development we do. Starting again at the outside layer, in the template, we begin to write the template code we'd like to use -to get the "My Lists" page to work the way we want it to. +to get the "My Lists" page to work the way we want it to. As we do so, we start to specify the API we want from the code at the layers below. +// Programming by wishful thinking, as always! -==== A Quick Restructure of the Template Inheritance Hierarchy +==== A Quick Restructure of Our Template Composition -((("templates", "inheritance hierarchy"))) -Currently there's no place in our base template for us to put any new content. +((("templates", "composition"))) +Let's take a look at our base template, _base.html_. +It currently has a lot of content that's specific to editing todo lists, +which our "My Lists" page doesn't need: -[role="sourcecode"] -.src/lists/templates/base.html (ch22l011-2) +[role="sourcecode currentcontents"] +.src/lists/templates/base.html ==== [source,html] ---- -
-
- {% block table %} - {% endblock %} +
+ + + + {% if messages %} + [...] + {% endif %} + +
+
+

{% block header_text %}{% endblock %}

+ + <1> + [...] +
-
-
- {% block extra_content %} +
+
+ {% block table %} <2> {% endblock %}
+
- - [...] + <3> + [...] ---- ==== -Also, the "My Lists" page doesn't need the new item form, -so we'll put that into a block too, making it optional. +<1> The `
` tag is definitely something we only want on pages where we edit lists. + Everything else up to this point is generic enough to be on any page. -[role="sourcecode"] -.src/lists/templates/base.html (ch22l011-1) +<2> Similarly the `{% block table %}` isn't something we'd need on the "My Lists" page. + +<3> Finally the ` +- ++ {% block scripts %} ++ {% endblock %} + + + ---- ==== +You can see we've replaced all the lists-specific stuff with 3 new blocks: +* `extra_header` for anything we want to put in the big header section +* `content` for the main content of the page +* `scripts` for any javascript we want to include. -==== Designing Our API Using the Template +Let's paste in the `` tag into a file at _src/lists/templates/form.html_: -((("templates", "designing APIs using"))) -Meanwhile, in _my_lists.html_ we override the `list_form` -and say it should be empty... -// TODO: proper commits for these +[role="sourcecode small-code"] +.src/lists/templates/form.html (ch24l010-2) +==== +[source,html] +---- + <1> + {% csrf_token %} + + {% if form.errors %} +
+ {{ form.errors.text.0 }} +
+ {% endif %} +
+---- +==== + +<1> This is the only change, + we've replaced the `{% block form_action %}` with `{{ form_action }}`. + + +Let's paste the scripts tags verbatim +into a new file at _src/lists/templates/scripts.html_: [role="sourcecode"] -.src/lists/templates/my_lists.html (ch22l010-1) +.src/lists/templates/scripts.html (ch24l010-3) +==== +[source,html] +---- + + + +---- +==== + +Now let's look at how to use the include, +and how the `form_action` change plays out, +in the changes to _home.html_: + +[role="sourcecode small-code"] +.src/lists/templates/home.html (ch24l010-4) ==== [source,html] ---- {% extends 'base.html' %} -{% block header_text %}My Lists{% endblock %} +{% block header_text %}Start a new To-Do list{% endblock %} -{% block list_form %}{% endblock %} +{% block extra_header %} + {% url 'new_list' as form_action %} <1> + {% include "form.html" with form=form form_action=form_action %} <2> +{% endblock %} + +{% block scripts %} <3> + {% include "scripts.html" %} +{% endblock %} ---- ==== -And then we can just work inside the `extra_content` block: +<1> The `{% url ... as %}` syntax lets us define a template variable in-line + +<2> Then we use `{% include ... with key=value... %}` + to pull in the contents of the `form.html` template, + with the appropriate context variables passed in--a bit like + calling a function. + +<3> The `scripts` block is just a straightforward `include` + with no variables. + +Now let's see it in _list.html_: [role="sourcecode"] -.src/lists/templates/my_lists.html (ch22l010-2) +.src/lists/templates/list.html (ch24l010-5) ==== -[source,html] +[source,diff] ---- +@@ -2,12 +2,24 @@ + + {% block header_text %}Your To-Do list{% endblock %} + +-{% block form_action %}{% url 'view_list' list.id %}{% endblock %} + +-{% block table %} ++{% block extra_header %} <1> ++ {% url 'view_list' list.id as form_action %} ++ {% include "form.html" with form=form form_action=form_action %} ++{% endblock %} ++ ++{% block content %} <2> ++
++
+ + {% for item in list.item_set.all %} + + {% endfor %} +
{{ forloop.counter }}: {{ item.text }}
++
++
++{% endblock %} ++ ++{% block scripts %} <3> ++ {% include "scripts.html" %} + {% endblock %} + +---- +==== + +<1> The `block table` becomes an `extra_header` block, + and we use the `include` to pull in the form. + +<2> The `block table` becomes a `content` block, + with all the html we need for our table. + +<3> And the scripts block is the same as the one from _home.html_. + + +We can have a little click around our site, +and then a little re-run of all our FTs to make sure we haven't broken anything, and then commit. + +[subs="specialcharacters,quotes"] +---- +$ *./src/manage.py test functional_tests* +[...] +selenium.common.exceptions.NoSuchElementException: Message: Unable to locate +element: Reticulate splines; [...] [...] +Ran 8 tests in X.Xs + +FAILED (errors=1) +---- + +8 tests with 1 failure, the same one we had before, we haven't broken anything. Hooray! + +[subs="specialcharacters,quotes"] +---- +$ *git add src/lists/templates* +$ *git commit -m "refactor templates to use composition/includes"* +---- + + +Now let's get back to our outside-in process, +and working in our template to drive out the requirements +for our views layer: + + +==== Designing Our API Using the Template + +((("templates", "designing APIs using"))) +So, in _my_lists.html_ we can now work in the `content` block: + +[role="sourcecode"] +.src/lists/templates/my_lists.html (ch24l010-6) +==== +[source,html] +---- +{% extends 'base.html' %} -{% block list_form %}{% endblock %} +{% block header_text %}{{user.email}}'s Lists{% endblock %} -{% block extra_content %} +{% block content %}

{{ owner.email }}'s lists

<1>
    - {% for list in owner.list_set.all %} <2> + {% for list in owner.lists.all %} <2>
  • {{ list.name }}
  • <3> {% endfor %}
@@ -507,21 +801,21 @@ which are going to filter their way down through the code: <1> We want a variable called `owner` to represent the user in our template. <2> We want to be able to iterate through the lists created by the user - using `owner.list_set.all` - (I happen to know we get this for free from the Django ORM). + using `owner.lists.all` + (I happen to know how to make this work with the Django ORM). <3> We want to use `list.name` to print out the "name" of the list, which is currently specified as the text of its first element. NOTE: Outside-In TDD is sometimes called "programming by wishful thinking",footnote:[ - This phrase "programming by wishful thinking" was perhaps first used in + The phrase "programming by wishful thinking" was first popularised by the amazing, mind-expanding textbook https://en.wikipedia.org/wiki/Structure_and_Interpretation_of_Computer_Programs[SICP], which I _cannot_ recommend highly enough.] and you can see why. We start writing code at the higher levels - based on what we wish we had at the lower levels, - even though it doesn't exist yet! + based on what we _wish_ we had at the lower levels, even though it doesn't exist yet... + A bit like when we write test for code that doesn't exist yet! We can rerun our FTs, to check that we didn't break anything, @@ -559,7 +853,7 @@ by giving it the objects it needs. In this case, the list owner: [role="sourcecode"] -.src/lists/tests/test_views.py (ch22l011) +.src/lists/tests/test_views.py (ch24l011) ==== [source,python] ---- @@ -590,7 +884,7 @@ KeyError: 'owner' So: [role="sourcecode"] -.src/lists/views.py (ch22l012) +.src/lists/views.py (ch24l012) ==== [source,python] ---- @@ -606,12 +900,12 @@ def my_lists(request, email): ---- ==== -That gets our new test passing, but we'll also see an error from -the previous test. We just need to add a user for it as well: +That gets our new test passing, but we'll also see an error from the previous test. +We just need to add a user for it as well: [role="sourcecode"] -.src/lists/tests/test_views.py (ch22l013) +.src/lists/tests/test_views.py (ch24l013) ==== [source,python] ---- @@ -621,8 +915,8 @@ the previous test. We just need to add a user for it as well: ---- ==== -((("", startref="OITDDft22")))And -we get to an OK: +And we get to an OK: +((("", startref="OITDDft22"))) ---- @@ -642,7 +936,7 @@ Here's a first crack at writing the test: [role="sourcecode"] -.src/lists/tests/test_views.py (ch22l014) +.src/lists/tests/test_views.py (ch24l014) ==== [source,python] ---- @@ -670,7 +964,7 @@ AttributeError: 'List' object has no attribute 'owner' To fix this, we can try writing code like this: [role="sourcecode"] -.src/lists/views.py (ch22l015) +.src/lists/views.py (ch24l015) ==== [source,python] ---- @@ -702,9 +996,6 @@ AttributeError: 'List' object has no attribute 'owner' ==== A Decision Point: Whether to Proceed to the Next Layer with a Failing Test -* TODO: rewrite this section if we do decide to drop the next chapter. - - ((("Outside-In TDD", "model layer", id="OITDDmodel21"))) In order to get this test passing, as it's written now, we have to move down to the model layer. @@ -719,7 +1010,10 @@ and it can lead to tests that are harder to read. On the other hand, advocates of what's known as "London School" TDD are very keen on the approach. Read more in <>. -Let's do a commit, and then 'tag' the commit as a way of remembering our +For now we'll accept the tradeoff, moving down one layer with failing tests, +but avoiding the extra mocks. + +Let's do a commit, and then _tag_ the commit as a way of remembering our position for that appendix: [subs="specialcharacters,quotes"] @@ -733,15 +1027,14 @@ $ *git tag revisit_this_point_with_isolated_tests* Our outside-in design has driven out two requirements for the model layer: we want to be able to assign an owner to a list using the attribute `.owner`, -and we want to be able to access the list's owner with the API `owner.list_set.all()`. +and we want to be able to access the list's owner with the API `owner.lists.all()`. -// TODO: let's make this owner.lists.all() ? Let's write a test for that: [role="sourcecode"] -.src/lists/tests/test_models.py (ch22l018) +.src/lists/tests/test_models.py (ch24l018) ==== [source,python] ---- @@ -759,7 +1052,7 @@ class ListModelTest(TestCase): def test_lists_can_have_owners(self): user = User.objects.create(email="a@b.com") mylist = List.objects.create(owner=user) - self.assertIn(mylist, user.list_set.all()) + self.assertIn(mylist, user.lists.all()) ---- ==== @@ -789,7 +1082,7 @@ that too: [role="sourcecode"] -.src/lists/tests/test_models.py (ch22l020) +.src/lists/tests/test_models.py (ch24l020) ==== [source,python] ---- @@ -801,7 +1094,7 @@ that too: The correct implementation is this: [role="sourcecode"] -.src/lists/models.py (ch22l021) +.src/lists/models.py (ch24l021) ==== [source,python] ---- @@ -810,7 +1103,11 @@ from django.conf import settings class List(models.Model): owner = models.ForeignKey( - settings.AUTH_USER_MODEL, blank=True, null=True, on_delete=models.CASCADE + settings.AUTH_USER_MODEL, + related_name="lists", + blank=True, + null=True, + on_delete=models.CASCADE, ) def get_absolute_url(self): @@ -838,7 +1135,7 @@ Migrations for 'lists': ---- //22 -We're almost there; a couple more failures: +We're almost there; a couple more failures in some of our old tests: ---- ERROR: test_can_save_a_POST_request @@ -856,12 +1153,13 @@ be a "User" instance. ---- - We're moving back up to the views layer now, just doing a little tidying up. -Notice that these are in the old test for the `new_list` view, +Notice that these are in the existing test for the `new_list` view, when we haven't got a logged-in user. -We should only save the list owner when the user is actually logged in. -The `.is_authenticated` attribute we defined in <> + +The tests are reminding us to think of this use case too: +we should only save the list owner when the user is actually logged in. +The `.is_authenticated` attribute we came across in <> comes in useful now (when they're not logged in, Django represents users using a class called `AnonymousUser`, @@ -869,7 +1167,7 @@ whose `.is_authenticated` is always `False`): [role="sourcecode"] -.src/lists/views.py (ch22l023) +.src/lists/views.py (ch24l023) ==== [source,python] ---- @@ -913,7 +1211,7 @@ which wanted to be able to access a list "name" based on the text of its first item: [role="sourcecode"] -.src/lists/tests/test_models.py (ch22l024) +.src/lists/tests/test_models.py (ch24l024) ==== [source,python] ---- @@ -927,7 +1225,7 @@ its first item: [role="sourcecode"] -.src/lists/models.py (ch22l025) +.src/lists/models.py (ch24l025) ==== [source,python] ---- @@ -949,17 +1247,22 @@ Ran 8 tests in 93.819s OK ---- +[[my-lists-page]] +.The "My Lists" page, in all its glory (and proof I did test on Windows) +image::images/twp2_2201.png["Screenshot of new My Lists page"] + .The @property Decorator in Python ******************************************************************************* -((("@property decorator")))((("decorators", "property decorator")))((("Python 3", "@property decorator")))If -you haven't seen it before, the `@property` decorator transforms a method +((("@property decorator"))) +((("decorators", "property decorator"))) +((("Python 3", "@property decorator"))) +If you haven't seen it before, the `@property` decorator transforms a method on a class to make it appear to the outside world like an attribute. - -((("duck typing")))This -is a powerful feature of the language, because it makes it easy to +((("duck typing"))) +This is a powerful feature of the language, because it makes it easy to implement "duck typing", to change the implementation of a property without changing the interface of the class. In other words, if we decide to change `.name` into being a "real" attribute on the model, which is stored as text in @@ -976,40 +1279,40 @@ even if it didn't have `@property`, but that's a particularity of Django, and doesn't apply to Python in general... ******************************************************************************* -((("", startref="OITDDmodel21")))But -we know we cheated to get there. The Testing Goat is eyeing us +((("", startref="OITDDmodel21"))) +But we know we cheated to get there. The Testing Goat is eyeing us suspiciously. We left a test failing at one layer while we implemented its dependencies at the lower layer. Let's see how things would play out if we were to use better test isolation... -[[my-lists-page]] -.The "My Lists" page, in all its glory (and proof I did test on Windows) -image::images/twp2_2201.png["Screenshot of new My Lists page"] - .Outside-In TDD ******************************************************************************* Outside-In TDD:: - ((("Outside-In TDD", "defined")))A -methodology for building code, driven by tests, which proceeds by - starting from the "outside" layers (presentation, GUI), and moving - "inwards" step by step, via view/controller layers, down towards - the model layer. The idea is to drive the design of your code from - the use to which it is going to be put, rather than trying to anticipate - requirements from the ground up. + A methodology for building code, driven by tests, + which proceeds by starting from the "outside" layers (presentation, GUI), + and moving "inwards" step by step, via view/controller layers, + down towards the model layer. + The idea is to drive the design of your code from how it will be used, + rather than trying to anticipate requirements from the bottom up. + ((("Outside-In TDD", "defined"))) Programming by wishful thinking:: - ((("programming by wishful thinking")))The -outside-in process is sometimes called "programming by wishful - thinking". Actually, any kind of TDD involves some wishful thinking. + The outside-in process is sometimes called "programming by wishful thinking". + Actually, any kind of TDD involves some wishful thinking. We're always writing tests for things that don't exist yet. + ((("programming by wishful thinking"))) The pitfalls of outside-in:: - ((("Outside-In TDD", "drawbacks of")))Outside-in isn't a silver bullet. It encourages us to focus on things - that are immediately visible to the user, but it won't automatically - remind us to write other critical tests that are less user-visible--things like security, for example. You'll need to remember them yourself.((("", startref="TTDoutside22"))) + Outside-in isn't a silver bullet. + It encourages us to focus on things that are immediately visible to the user, + but it won't automatically remind us to write other critical tests + that are less user-visible--things like security, for example. + You'll need to remember them yourself. + ((("", startref="TTDoutside22"))) + ((("Outside-In TDD", "drawbacks of"))) ******************************************************************************* diff --git a/chapter_26_page_pattern.asciidoc b/chapter_26_page_pattern.asciidoc index 025b7cdb..6d66f63c 100644 --- a/chapter_26_page_pattern.asciidoc +++ b/chapter_26_page_pattern.asciidoc @@ -315,7 +315,7 @@ from .my_lists_page import MyListsPage # Onesiphorus now goes to the lists page with his browser self.browser = oni_browser - MyListsPage(self).go_to_my_lists_page() + MyListsPage(self).go_to_my_lists_page("onesiphorus@example.com") # He sees Edith's list in there! self.browser.find_element(By.LINK_TEXT, "Get help").click() @@ -336,13 +336,13 @@ class MyListsPage: def __init__(self, test): self.test = test - def go_to_my_lists_page(self): + def go_to_my_lists_page(self, email): self.test.browser.get(self.test.live_server_url) self.test.browser.find_element(By.LINK_TEXT, "My lists").click() self.test.wait_for( - lambda: self.test.assertEqual( + lambda: self.test.assertIn( + email, self.test.browser.find_element(By.TAG_NAME, "h1").text, - "My Lists", ) ) return self diff --git a/source/chapter_24_outside_in/superlists b/source/chapter_24_outside_in/superlists index d328428b..d3284a31 160000 --- a/source/chapter_24_outside_in/superlists +++ b/source/chapter_24_outside_in/superlists @@ -1 +1 @@ -Subproject commit d328428b49c3eeed096cd23252b41d673b6f0279 +Subproject commit d3284a316696e0dce9527e6aa3c23b60f210c7ff diff --git a/source/chapter_25_CI/superlists b/source/chapter_25_CI/superlists index d40d71e9..990bca7f 160000 --- a/source/chapter_25_CI/superlists +++ b/source/chapter_25_CI/superlists @@ -1 +1 @@ -Subproject commit d40d71e9074be4f4697e464519ff9fdb0efa9fcd +Subproject commit 990bca7fc18b5c7ee1b1fbda6a76d7f1d8b296e9 diff --git a/source/chapter_26_page_pattern/superlists b/source/chapter_26_page_pattern/superlists index 536a2d75..b251a502 160000 --- a/source/chapter_26_page_pattern/superlists +++ b/source/chapter_26_page_pattern/superlists @@ -1 +1 @@ -Subproject commit 536a2d75feff00cc33578963e692ff2ea5b7b924 +Subproject commit b251a5026febd8c03ad99278a8979c11600c89e3 diff --git a/tests/test_chapter_24_outside_in.py b/tests/test_chapter_24_outside_in.py index 15ce2607..c769bf8e 100644 --- a/tests/test_chapter_24_outside_in.py +++ b/tests/test_chapter_24_outside_in.py @@ -5,7 +5,7 @@ from book_tester import ChapterTest -class Chapter19Test(ChapterTest): +class Chapter24Test(ChapterTest): chapter_name = "chapter_24_outside_in" previous_chapter = "chapter_23_debugging_prod"