Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Testing in Django

Testing in Django

also, how to solve the problems that comes with a big codebase

Gabriel Falcão

August 23, 2012
Tweet

Transcript

  1. WHAT WE WILL SEE: * Unit and functional tests *

    Organizing your Django test codebase * 5 key ingredients for making testing a solution instead of a problem * Achieving fluency * How mocking works * Good tests require good code
  2. UNIT TESTS •Are “unplugged”: should not make I/O at all,

    or make the least possible •Hard to write if you are testing existing code that is coupled, macaroni •Reasonably easy to write if you are testing existing code that is already decoupled •Easy to write if you writing code from scratch with TDD •Here is were you might use mocks most part of the time
  3. UNIT TESTS Reasonably easy to write if you are testing

    existing code that is already decoupled
  4. UNIT TESTS Just follow the 3 laws of TDD •You

    are not allowed to write any production code unless it is to make a failing unit test pass. •You are not allowed to write any more of a unit test than is sufficient to fail; and runtime errors are failures. •You are not allowed to write any more production code than is sufficient to pass the one failing unit test. Uncle Bob Martin, 2005 Easy to write if you writing code from scratch with TDD
  5. FUNCTIONAL TESTS •Test the entire thing •I/O is allowed, but

    not to outreach 3rd party resources •Slow •Still shouldn’t depend on 3rd party resources •Most part of the time you don’t need mocks •Unless you need to isolate 3rd party resources •Intense database usage
  6. ORGANIZING YOUR TEST CODEBASE Nose: “If it looks like a

    test, it’s a test” Unclebob: “the faster ones first, slow ones last”
  7. ORGANIZING YOUR TEST CODEBASE def test_functions_start_with_test(): (u”Single-line doctstrings provide ”

    “a much better feedback”) assert True, “:)” Any module-level functions that begin with “test” is called with no parameters by Nose
  8. ORGANIZING YOUR TEST CODEBASE class TestClasses(object): def test_have_methods_starting_with_test(self): (u”Single-line doctstrings

    provide ” “a much better feedback”) assert True, “:)” Any module-level classes that begin with “Test” is instantiated by nose, then all the methods containing with “test” will be found and run
  9. ORGANIZING YOUR TEST CODEBASE $ nosetests --verbosity=2 Single-line doctstrings provide

    a much better feedback ... ok -------------------------------------------------------------- Ran 1 test in 0.001s
  10. GOOD ASSERTION MESSAGES It’s better, but I had to write

    the message by myself the test suite will become repetitive overtime
  11. GOOD ASSERTION MESSAGES This was a huge dictionary comparison, but

    it shows what exacly is the problem: the second deal is missing the twitter url
  12. SURE: WRITE FLUENT ASSERTIONS A collection of assertions ready to

    use It provides meaningful and clear error messages
  13. SURE: WRITE FLUENT ASSERTIONS Two APIs: assert that(source).equals(destination) source.should.be.equal(destination) or

    assert source.should.be.equal(destination) Old one: New one: Only available for CPython
  14. SURE: WRITE FLUENT ASSERTIONS Equality assert source == destination, (

    u”%r != %r” % (source, destination)) assert that(source).equals(destination) Old school: Sure: source.should.be.equal(destination) or
  15. SURE: WRITE FLUENT ASSERTIONS Length assert len(source) is 5, (

    u”%r should have 5 items but has %d” % (source, len(source))) assert that(source).len_is(5) Old school: Sure: source.should.have.length_of(5) or
  16. SURE: WRITE FLUENT ASSERTIONS Exceptions def get_deal_by_id(id): return Deal.objects.get(id=id) Code:

    try: get_deal_by_id(5) raise AssertionError(“calling get_deal_by_id(5) “ ”should have raised but didn’t”) except Deal.DoesNotExist, e: assert unicode(e) == “Deal with id 5 does not exist”, ... Old school:
  17. SURE: WRITE FLUENT ASSERTIONS Exceptions def get_deal_by_id(id): return Deal.objects.get(id=id) Code:

    assert that(get_deal_by_id, with_args=(5,)).raises(Deal.DoesNotExist) Sure: get_deal_by_id.when.called_with(5).should.throw( Deal.DoesNotExist, "Deal with id 5 does not exist", ) or
  18. MOCK - ALWAYS RETURNS MOCKS >>> from mock import Mock

    >>> request = Mock() >>> request.user.is_authenticated.return_value = False >>> >>> print request <Mock id='4297658320'> >>> print request.user <Mock name='mock.user' id='4297609680'> >>> print request.user.is_authenticated <Mock name='mock.user.is_authenticated' id='4297658256'> >>> >>> print request.user.is_authenticated() False
  19. MOCK - SOME FACTS •If you are using too many

    mocks and your test becomes hard to undersdand: isolate your code •If it's too hard to setup your test, your production code is doing too much, isolate it! •In the end of the day, if still can't mock your code you have to do refactoring!
  20. MOCK - SOME FACTS •If you are using too many

    mocks and your test becomes hard to undersdand: isolate your code •If it's too hard to setup your test, your production code is doing too much, isolate it! •In the end of the day, if still can't mock your code you have to do refactoring!
  21. SOME OTHER INTERESTING THINGS Django test client + lxml or

    PyQuery You call urls with: import sure from django.test.client import Client from lxml import html http = Client() dom = html.fromstring(http.get(“/deals/ny.html”).content) dom.cssselect(“section.deal”).should.have.length_of(100)
  22. SOME OTHER INTERESTING THINGS Django test client + lxml or

    PyQuery You call urls with: import sure from django.test.client import Client from pyquery import PyQuery http = Client() dom = PyQuery(http.get(“/deals/ny.html”).content) dom.find(“section.deal”).should.have.length_of(100)
  23. SOME OTHER INTERESTING THINGS Splinter from splinter.browser import Browser browser

    = Browser() # Visit URL url = "http://www.google.com" browser.visit(url) browser.fill('q', 'splinter - python acceptance testing for web applications') # Find and click the 'search' button button = browser.find_by_name('btnG') # Interact with elements button.click() if browser.is_text_present('splinter.cobrateam.info'): print "Yes, the official website was found!" else: print "No, it wasn't found... We need to improve our SEO techniques"