Test Driven Development With Python

Test Driven Development (TDD) With Python book by Harry Percival

I suspect, that other than clearly-confusing (hah) situations, I'm more of a Test Last person - add a test every time a human catches a bug.

uses Python-v3 and Django and Selenium to build a To-Do List app

free online http://chimera.labs.oreilly.com/books/1234000000754

http://shop.oreilly.com/product/0636920029533.do

http://www.obeythetestinggoat.com/

== Trying to work through the book but using Python-2 and WebPy.... ==

(Feb'2014)

=== Chapter 1 ===

Go into the VirtualEnv I use for WebPy.

Do pip install --upgrade selenium

Do cd webpy

Do makedir tddwp for directory for book

Create functional_tests.py file, copy in code from book but change port to 8085. Run, it fails as expected.

There's no magic project-creator for WebPy, so (after opening another Terminal window, going into VirtualEnv, etc.) just mkdir superlists, copy my code.py from another WebPy project, tweak that file to refer to a superlists.db SQLite file. Do python code.py 8085 to launch it - no error.

Run functional_tests.py again - still get AssertionError because page has other errors because of me just copying stuff over.

  • also note that Selenium launched a whole new instance of FireFox, not just opening a new window or something.

Tweak my copied code.py until can run test without AssertionError from the page title.

Start Git {{{ git init git add .py git add superlists/.py echo "superlists/sessions/" >> .gitignore echo ".pyc" >> .gitignore git add .gitignore git commit }}}

=== Chapter 2 ===

Functional Test == Acceptance Test == End To End Test

First use of Unit Test:

  • TypeError: __init__() got an unexpected keyword argument 'warnings'
  • try removing the keyword pass: AttributeError: 'New Visitor Test' object has no attribute 'assertIn'
  • argh, I'm actually running python-2.6.1 here. If I run functional_tests with python2.7 then I get ImportError: No module named selenium
  • see VirtualEnv page for upgrading process
  • still had to leave out the warnings='ignore' arg
  • but otherwise works fine
  • realize I probably broke other WebPy bits with reinstall, so launch local Simplest Thing to test
    • need to pip install cryptacular
    • need to pip install markdown
    • hmm, lost my rewrite of WebPy's form generator that works with BootStrap. And don't seem to have notes in my log page! Dig through files, figure out what to do, copy files, add note to old notes page.

=== intermission ===

Jan'2015 - obviously I just dropped this. Picking it back up.

  • It looks like I hadn't even changed functional_tests.py to use the right port! (Or maybe something got over-written...)
  • And failing with can't load the profile - looks like I've updated FireFox which tends to break Selenium.
    • Currently running FireFox v34.0.5 - but downloading an update now... ok now v35.0
  • It looks like the latest Selenium Web Driver is 2.44.0
  • Do pip install --upgrade selenium - it upgrades to 2.44.0 (from existing 2.40.0)
  • Remind self: have 1 Terminal running to run the actual app/server, another for running the tests.
  • Run functional_tests.py again - it works! (Fails, but works.)
  • The book has changed versions, too. So going back to review....
    • Ch1: moved functional_tests.py into superlists directory. So I do that.
    • Ch2: no other changes

=== Chapter 3: Testing a Simple Home Page with Unit Tests ===

Suggests making a lists Django app. I'm not in the mood for all that separation, so keeping all my WebPy stuff in one app.

Django gives you a tests.py for each app, I'll just make my own equivalent following example from here.

  • use his silly intended-to-fail test. Run with python tests.py. Fails in nice way.

Unit Test to check that root URL resolves to home_page function. Relies on Django-specific testing stuff. Plus this seems awfully pointless, so not losing sleep over it. But change my route of / to home_page instead of index.

  • but I go ahead and make test_home_page_returns_correct_html(self)
    • this uses response.data instead of book's response.content
    • but runs/fails properly
    • actually I'm not sure it's getting the response object...
      • ah, it is, but it's getting a status=404 so response.data='not found'
      • for a bunch of other shim URLs I carried over from other projects, I get 405 Method Not Allowed
      • so obviously my from code import app is hitting something weird.
      • ah, needed to just use response = app.request('/') - now getting proper result
    • move things forward until all the tests pass, no fails

=== Chapter 4: What Are We Doing with All These Tests? ===

Pitching the philosophy of many small tests...

  • not really buying it, though like the "ratchet" metaphor - prevent Back Sliding.

Back to functional tests

  • adds a whole bunch of steps to the current test
  • starts using a template - I had gone straight to that.
  • this results in an extra newline at the end which breaks his earlier ends-with-html-close test - I had run into that same issue and figured out the strip solution. Yay.
  • Rule Of Thumb: when refactoring, work on either code or tests, but not both at once
  • sending ENTER didn't work, needed to add from selenium.webdriver.common.keys import Keys
  • philosophy: functional vs unit tests - he points to Emily Bache post on "DoubleLoop" TDD.

=== Chapter 5: Saving User Input ===

finish form, make a POST

  • WebPy unhappy, returns page with "None". Console shows 405-status because we have no POST for home_page.
  • but in book there's other issue first - lack of CSRF. I don't have that turned on for my code, so I guess I will now.
    • once I put in the CSRF decorator, I have to put a POST handler after it, so I do so with just a pass
    • now get the CSRF failure (not having the token in the form), so add it
    • now get different error: POST() takes exactly 2 arguments (1 given)
      • book does not have this error, its next error is from the POST not actually changing the returned data to get found in the next test.
      • ah, I had outsmarted myself by adding an arg to the POST function, when that's not what happens when not using the WebPy form handling stuff.
    • change POST handler to render the template. Now get same test-fail as book.

Processing a POST Request on the Server

  • separate Unit Test for this
    • generate HttpPost like this
    • CSRF again! Django must automatically ignore CSRF under certain conditions....
    • I see some other frameworks have a way to pass an argument to disable CSRF, but don't see an easy way to do it in WebPy, and not going to add shmutz to my code for this.
    • Try calling app.csrf_token() but that doesn't work either.
    • killing this path
  • do the standard WebPy bits to handle the input
    • Functional Test still fails
    • stick in a print statement - hrm web.input() is catching the CSRF token, but not the form
    • ah, the book uses id for naming each form item, but WebPy only recognizes name as handle. In fact now that I look at it, the book uses id and name - somehow I screwed that up.
    • now getting the same test-fail as in the book because of missing the initial number in the return.
    • now book is putting obviously-stupid things (static 1: in front of table entry) in template to make a test pass temporarily. Sorry, but I can't condone this insanity.
  • refactoring functional test to make it callable-by-param...
    • now I give up and put that static 1: in to get to same place as book.
  • persistence - ORM. Grrrr.
    • so I'll just build a SQL db and my own interface code. Looks like table is named item and has 1 field named text
    • I'm not writing a single Unit Test against silly CRUD stuff
    • create SQLite db, create table items(text varchar(64));
    • create model.py for my db interface functions, write couple functions
    • pull out config bits to config.py which is what I do in my other WebPy apps
    • call model bits in code, reference items bits in template
    • works - except still have that static 1: in place...
  • caught up with book dealing with static 1: - book just puts a counter in the template. WebPy uses $loop.index for this.
    • but my entry numbers don't match book's
    • bigger issue - every time I run the tests, the same entries get added to the db. The book has the same issue - hooray I didn't miss something earlier.

=== Chapter 6: Getting to the Minimum Viable Site ===

dealing with test-created data

  • book uses a Django system that creates a new db every time; WebPy doesn't have that
  • so I'll make a model.delete_test_data() function. And, unlike the book, I'll call it during the setUp rather than the tearDown because I often like to be able to browse the db after tests are done.

making app multi-list:

  • some sort of craziness in the design approach here, I'm reading ahead then making my own process
    • it appears each list will have its own slug/URL - later it turns out to just use the numeric lists.id for this
    • but you start creating a new list by entering a list item on the home_page? That's ridiculous.
    • when do you name the list? Ah, you don't. Each list just has its ID.
    • So I'm aiming for a simple HATEOAS-like walk-through.
      • You get a list of lists (and since just having a numeric ID is useless, I'm going to immediately offer chance for optional name, though the ID will be used for the URL) (which a chance to create a new one).
      • Then you see any existing entries in the list with a form to add another entry.
      • Actually, I take it back - I'll give explicit options for each.)
      • Also, while I'll put these get-started forms right on the home page, I'll have the POSTs go to use-specific pages, rather than washing back through the home page.

Edited:    |       |    Search Twitter for discussion