Slide 1

Slide 1 text

TESTING IN DJANGO (also, how to solve the problems that comes with a big codebase)

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

UNIT AND FUNCTIONAL

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

UNIT TESTS Are fast because they don’t do I/O

Slide 6

Slide 6 text

UNIT TESTS Hard to write if you are testing existing code that is coupled, macaroni

Slide 7

Slide 7 text

UNIT TESTS Reasonably easy to write if you are testing existing code that is already decoupled

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

UNIT TESTS Here is were you might use mocks most part of the time

Slide 10

Slide 10 text

UNIT TESTS Here is were you might use mocks most part of the time

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

FUNCTIONAL TESTS Isolation! Isolation! Isolation!

Slide 13

Slide 13 text

ORGANIZING YOUR TEST CODEBASE

Slide 14

Slide 14 text

ORGANIZING YOUR TEST CODEBASE Nose: “If it looks like a test, it’s a test” Unclebob: “the faster ones first, slow ones last”

Slide 15

Slide 15 text

ORGANIZING YOUR TEST CODEBASE Unclebob! pip install unclebob

Slide 16

Slide 16 text

ORGANIZING YOUR TEST CODEBASE {appname}/tests/__init__.py {appname}/tests/unit/__init__.py {appname}/tests/unit/test_name.py {appname}/tests/functional/__init__.py {appname}/tests/functional/test_views.py Nose finds every python file name that starting with “test”

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

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

Slide 19

Slide 19 text

ORGANIZING YOUR TEST CODEBASE $ nosetests --verbosity=2 Single-line doctstrings provide a much better feedback ... ok -------------------------------------------------------------- Ran 1 test in 0.001s

Slide 20

Slide 20 text

5 KEY INGREDIENTS that will make your tests taste good!

Slide 21

Slide 21 text

GOOD ASSERTION MESSAGES What differed? Oh gosh, I gotta drop a pdb in code

Slide 22

Slide 22 text

GOOD ASSERTION MESSAGES It’s better, but I had to write the message by myself the test suite will become repetitive overtime

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

FLUENCY ON WRITING Setup Entry point Assertion

Slide 25

Slide 25 text

MOCK WHEN NECESSARY

Slide 26

Slide 26 text

MOCK WHEN NECESSARY

Slide 27

Slide 27 text

OPTIMIZE THE CODE FOR TESTABILITY Look at this guy

Slide 28

Slide 28 text

DEAL WITH GOOD CODE

Slide 29

Slide 29 text

DEAL WITH GOOD CODE

Slide 30

Slide 30 text

DEAL WITH GOOD CODE Okay

Slide 31

Slide 31 text

DEAL WITH GOOD CODE

Slide 32

Slide 32 text

SURE: WRITE FLUENT ASSERTIONS pip install sure

Slide 33

Slide 33 text

SURE: WRITE FLUENT ASSERTIONS A collection of assertions ready to use It provides meaningful and clear error messages

Slide 34

Slide 34 text

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:

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

SURE: WRITE FLUENT ASSERTIONS

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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:

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

MOCK pip install mock ISOLATE CODE, SPY CALLS AND FAKE BEHAVIOR OF CALLABLES

Slide 42

Slide 42 text

MOCK - RETURN VALUES

Slide 43

Slide 43 text

MOCK - SIDE EFFECT

Slide 44

Slide 44 text

MOCK - SIDE EFFECT

Slide 45

Slide 45 text

MOCK - ALWAYS RETURNS MOCKS >>> from mock import Mock >>> request = Mock() >>> request.user.is_authenticated.return_value = False >>> >>> print request >>> print request.user >>> print request.user.is_authenticated >>> >>> print request.user.is_authenticated() False

Slide 46

Slide 46 text

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!

Slide 47

Slide 47 text

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!

Slide 48

Slide 48 text

SOME OTHER INTERESTING THINGS

Slide 49

Slide 49 text

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)

Slide 50

Slide 50 text

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)

Slide 51

Slide 51 text

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"

Slide 52

Slide 52 text

SOME OTHER INTERESTING THINGS Lettuce for acceptance testing

Slide 53

Slide 53 text

Gabriel Falcão http://falcao.it @gabrielfalcao github.com/gabrielfalcao speakerdeck.com/u/gabrielfalcao Questions? Sure: http://falcao.it/sure Unclebob: http://falcao.it/unclebob

Slide 54

Slide 54 text

THANK YOU!