97
testing in django (browser-based testing too) 17 january 2013 Kevin Harvey @kevinharvey [email protected]

Testing in Django

Embed Size (px)

DESCRIPTION

Slides from "Testing in Django", the Django Boston Meetup on January 17, 2013.

Citation preview

Page 1: Testing in Django

testing in django(browser-based testing too)

17 january 2013

Kevin Harvey@[email protected]

Page 2: Testing in Django

Who is this guy?

Application Architect at story+structureDjangonaut since 2007 (version 0.96)

For those of you who didn’t read the bio, I’m Kevin Harvey. I’m the Application Architect at story+structure. We’re based in Brookline and we do software for higher ed. I cut my teeth on Django 0.96 in 2007 with djangobook.com

Page 3: Testing in Django

These are my twin sons Lane & Hank. They are almost 5 months old and awesome.

Page 4: Testing in Django

Who are y’all?

A quick poll...

I’d like to get an idea of who’s in the crowd tonight. If you would please raise your hand if you’ve: - ever started a Python interpreter in Terminal or Command Prompt - ever started a Django project (even just to play around) - gone through the “Polls” app in the Django docs (or feel that you could) - written a single automated test (even just to play around) - released or written a project with a good test suite

Page 5: Testing in Django

What is a test?

An evolutionary perspective

So, what is a test? I’d like to answer that question from an evolutionary perspective.

Page 6: Testing in Django

What is a test?An evolutionary perspective

def multiply_these_two_numbers(a,b):! """! Add 'b' to itself 'a' times! """! i = 0! product = 0! while a > i:! ! product += b! ! i += 1! return product

Let’s say you wrote the function “multiply_these_two_numbers()”, which takes two arguments and adds the second argument to itself by a factor of the first argument. It’s not pretty....

Page 7: Testing in Django

What is a test?An evolutionary perspective

>>> from myapp.my_funcs import multiply_these_two_numbers>>> multiply_these_two_numbers(4,5)20>>> multiply_these_two_numbers(7,8)56>>> multiply_these_two_numbers(720,617)444240

... but it works! You can import it, you can give it two arguments, and it returns what you would expect.

Page 8: Testing in Django

What is a test?An evolutionary perspective

>>> from myapp.my_funcs import multiply_these_two_numbers>>> multiply_these_two_numbers(4,5)20

This is a test. No, it’s not automated, and yes you wrote it by hand in the terminal. But you did confirm that the function: 1) was able to be imported, 2) took two arguments, and 3) returned the expected result.

Page 9: Testing in Django

What is a test?An evolutionary perspective

from myapp.my_funcs import multiply_these_two_numbersimport math

def area_of_rectangle(length, width):! """! Length times width! """! return multiply_these_two_numbers(length, width)!def surface_area_of_sphere(radius):! """! 4 times pi times the radius squared! """! radius_squared = multiply_these_two_numbers(radius, radius)! four_times_pi = multiply_these_two_numbers(4, math.pi)! return multiply_these_two_numbers(radius_squared, four_times_pi)

You tell your colleagues about your new function, and they’re impressed. They start using your function in other parts of the application.

As the code base grows, you’re starting to realize how important your little function is.

Page 10: Testing in Django

What is a test?An evolutionary perspective

from myapp.my_funcs import multiply_these_two_numbers

print multiply_these_two_numbers(4,5)

$ python my_tests.py20$

As you find yourself checking that the function works more and more, you decide to save your test in a file called ‘my_tests.py’ with a print statement to verify the result. Now you can run this saved statement whenever you want and know that your function is working.

Page 11: Testing in Django

What is a test?An evolutionary perspective

>>> import time>>> time.sleep(604800)

Weeks go by. You work on other parts of the project. You work on different projects. You code in different languages.

Page 12: Testing in Django

You have kids. Your mind is erased.

Page 13: Testing in Django

What is a test?An evolutionary perspective

$ python my_tests.py20$

You’ve totally forgotten the context for this test. What does ’20’ mean? You would have to look at the file to see what the inputs were. In short, you don’t know whether this test passed or failed. Can’t we just let Python handle all of this stuff, from setting up the test to remembering what the appropriate output should be?

Page 14: Testing in Django

What is a test?An evolutionary perspective

from django.test import TestCasefrom myapp.my_funcs import multiply_these_two_numbers

class SillyTest(TestCase): def test_multiply_these_two_numbers(self): """ Tests that the function knows how to do math """ self.assertEqual(multiply_these_two_numbers(4,5), 20)

Enter Django’s TestCase with it’s assertEqual() method (based on Python’s own ‘assert’). Here we have a test that knows how to run itself, and knows whether it failed or not.

Page 15: Testing in Django

What is a test?An evolutionary perspective

$ python manage.py test myappCreating test database for alias 'default'....---------------------------------------------------------Ran 1 test in 0.000s

OKDestroying test database for alias 'default'...

Just as a primer, here’s what it looks like when we run that test and it passes. We’ll talk about running tests and the output you get in a moment.

Page 16: Testing in Django

Why write tests?

Why are we doing this?

What’s the point of all these tests? At the outset, it looks like more work: - more code to write - more code to debug... but in fact, we write tests ....

Page 17: Testing in Django

Why write tests?

1. Drink More Beer

To drink more beer, or because we’re lazy. However you want to describe it, we write tests because we want to fix stuff once and only once. Stop worrying about that rickety linchpin of a function holding your entire project together and write a test for it.

Page 18: Testing in Django

Why write tests?

2. Take the fear out of refactoring

Speaking of rickety functions, let’s look back at that awful function we wrote earlier.

Page 19: Testing in Django

Why write tests?UGLY CODE

def multiply_these_two_numbers(a,b):! """! Add 'b' to itself 'a' times! """! i = 0! product = 0! while a > i:! ! product += b! ! i += 1! return product

Bleh. I’d love to fix this, wouldn’t you?

Page 20: Testing in Django

Why write tests?UGLY, VITAL CODE

from myapp.my_funcs import multiply_these_two_numbersimport math

def area_of_rectangle(length, width):! """! Length times width! """! return multiply_these_two_numbers(length, width)!def surface_area_of_sphere(radius):! """! 4 times pi times the radius squared! """! radius_squared = multiply_these_two_numbers(radius, radius)! four_times_pi = multiply_these_two_numbers(4, math.pi)! return multiply_these_two_numbers(radius_squared, four_times_pi)

But it’s used everywhere in our app! What if we screw it up during the refactor?

Page 21: Testing in Django

Why write tests?UGLY, VITAL, TESTED CODE

from django.test import TestCasefrom myapp.my_funcs import multiply_these_two_numbers

class SillyTest(TestCase): def test_multiply_these_two_numbers(self): """ Tests that the function knows how to do math """ self.assertEqual(multiply_these_two_numbers(4,5), 20)

This test (plus a couple more) severely limit the possibility that we’d screw up this function during a refactor. This test is a guarantee that the function, given two integers, will return the product of those two integers.

Page 22: Testing in Django

Why write tests?BETTER CODE

def multiply_these_two_numbers(a,b):! """! Multiply a times b! """#! i = 0#! product = 0#! while a > i:#! ! product += b#! ! i += 1#! return product! return a*b

So refactor...

Page 23: Testing in Django

Why write tests?The test guarantees the refactor

$ python manage.py test myappCreating test database for alias 'default'....---------------------------------------------------------Ran 1 test in 0.000s

OKDestroying test database for alias 'default'...

... and run your tests. If the tests pass, you’re refactor works. Now wouldn’t it be nice if there were tests for all those other functions that your team wrote...

Page 24: Testing in Django

Why write tests?

3. Explain your code

Good tests will serve as technical documentation for your code. You can show your colleagues how your code works by walking them through your tests. And get new developers up to speed quickly by explaining how your tests work.

Page 25: Testing in Django

Why write tests?

4. Clarify your thinking

This is sort of related to the last one, but for me writing tests really help me think through my app. If all my code gets tested, I know I’m only writing the code necessary to get the job done. Testing forces you to write code that’s more modular, which is easier to debug.

Page 26: Testing in Django

Why write tests?

5. Allow more developers to contribute

Think back about the first version of our multiply function: that code was obviously written by a junior developer. Kinda nasty, BUT IT WORKED. It got us from point a to point b. We moved forward because that junior developer contributed a function. We can hack faster because of the test, and we can go back and clean up the mess later.

Page 27: Testing in Django

Why write tests?

6. Be taken seriously

Are you creating something you intend for other developers to use? The first thing I do when evaluating a package from PyPI is check out the tests. It helps me to understand the code, and let’s me know that the package is going to do what it’s supposed to do. It also gives me a glimpse into the way (or whether) the developer thought about the package during development.

Page 28: Testing in Django

Why write tests?

7. Try something out before you screw up your dev environment

This is specific to Django, but I think it’s worth mentioning here.

Page 29: Testing in Django

Why write tests?Keep your dev database clean

$ python manage.py test myappCreating test database for alias 'default'....---------------------------------------------------------Ran 1 test in 0.000s

OKDestroying test database for alias 'default'...

Did you notice the “Creating test database” bit when we ran the test? Django tests create a database from scratch for you every time you run them, and deletes the database when your done. That means you can write tests for new model fields and not have to do the syncdb/migrate/upgradedb dance.

Page 30: Testing in Django

Types of Tests

1. Functional tests2. Unit tests3. Performance tests (which I won’t cover)

The differences between functional tests exist on a series of spectra.

Page 31: Testing in Django

What’s the Difference?

Fewer things(ideally one)

Many things(possibly all)

Unit Functional

Unit tests test a small number of things in your app. Functional tests test a lot.

Page 32: Testing in Django

What’s the Difference?

Small, write many Big, write few

Unit Functional

Unit tests are small and you’ll write a ton. Functional tests are big and you’ll write just a few.

Page 33: Testing in Django

What’s the Difference?

Stuff developerscare about

Stuff userscare about

Unit Functional

Unit tests in general cover things that developers are worried about. Functional tests test things users care about.

Page 34: Testing in Django

What’s the Difference?

Fast Slow

Unit Functional

Unit tests run fast (thousandths of a second). Functional tests run slow (seconds or more).

Page 35: Testing in Django

A few examples

Django tests in action!

Page 36: Testing in Django

Unit test for Model

def test_questions_increment_votes_up(self):! """! Test voting up a question! """! question_1 = Question(text="How can my team get started?",! ! ! ! ! ! votes=7)! ! ! ! ! !! question_1.increment_votes(4)!! self.assertEquals(question_1.votes, 11)

A few examples

Unit testing a model.

Page 37: Testing in Django

Unit test for Model

class Question(models.Model):! text = models.CharField("Question", max_length=500)! votes = models.IntegerField()!! def increment_votes(self, num):! ! self.votes += num! ! self.save()

A few examples

This code makes the previous test pass.

Page 38: Testing in Django

Unit test for a Form

def test_question_form_excludes_all_but_text(self):! """! The user can only supply the text of a question! """! form = QuestionForm()! self.assertEquals(form.fields.keys(), ['text'])! self.assertNotEquals(form.fields.keys(),

['text', 'votes'])

A few examples

Page 39: Testing in Django

Unit test for a Form

class QuestionForm(forms.ModelForm):! class Meta:! ! ! model = Question! ! ! fields = ('text',)

A few examples

Make the previous form unit test pass.

Page 40: Testing in Django

Unit test for a POST action

def test_ask_question(self):! """! Test that POSTing the right data will result in a new question

! """! response = self.client.post('/ask/',

{'text': 'Is there any more pizza?'})! !! self.assertRedirects(response, '/')! !! self.assertEqual(Question.objects.filter(text='Is there any more pizza?').count(), 1)

A few examples

Page 41: Testing in Django

Unit test for a POST action

def ask(request):if request.method == "POST":

! question_form = QuestionForm(request.POST)! question_form.save()return HttpResponseRedirect('/')

A few examples

urlpatterns = patterns('',...

url(r'^ask/$', 'questions.views.ask', name='ask'),...

You have to edit two files to get the previous test to pass.

Page 42: Testing in Django

Functional test: Logging in

def test_admin_can_manage_questions(self):

self.browser.get(self.live_server_url + '/admin/')! ! username = self.browser.find_element_by_css_selector("input#id_username") username.clear() username.send_keys("peter") password = self.browser.find_element_by_css_selector("input#id_password") password.clear() password.send_keys("password") self.browser.find_element_by_css_selector("input[type='submit']").click()

body = self.browser.find_element_by_tag_name('body') self.assertIn('Site administration', body.text)

A few examples

This test uses Selenium to test that an admin user is able to login in to the admin site.

Page 43: Testing in Django

Functional test: Logging in

# set up the admin site

A few examples

Just set up the admin site to get it to pass.

Page 44: Testing in Django

Test Driven Development

What is Test Driven Development?

Simply put, TDD means writing tests that fail and THEN writing the code to make them pass. Guards against code explosion because you only write enough code to make the test pass.

Page 45: Testing in Django

TDD by Example

Our example project: Torquemada

Page 46: Testing in Django

TDD by Example

http://bit.ly/XHjcAior

http://infinite-meadow-8366.herokuapp.com/

Codehttps://github.com/kcharvey/testing-in-django

or use the link at the demo

See the demo at either of the first URLs. The code is available on GitHub.

Page 47: Testing in Django

TDD by Example

Torquemada allows attendees at “Testing in Django (browser based testing too)” to inquire of the presenter asynchronously.

Our example project: Torquemada

http://bit.ly/XHjcAi

A description of the app we’ll be building.

Page 48: Testing in Django

TDD by Example

Use Case: Isabella the Inquisitive

Isabel has learned a lot by attending the “Testing in Django” MeetUp but still has a few questions for the presenter. She visits Torquemada in her web browser, where she can see if any of her inquiries have been addressed and see the question that is being currently discussed. She is able to ‘vote up’ questions she would like the presenter to answer, and ‘vote down’ questions she thinks are unimportant. She is also able to ask her own question.

Our example project: Torquemada

http://bit.ly/XHjcAi

A use case for a user.

Page 49: Testing in Django

TDD by Example

Use Case: Peter the Presenter

Peter is presenting at the “Testing in Django” MeetUp and would like to answer any questions the attendees may have during set periods in the talk. He views Torquemada in his web browser to see what questions attendees have asked, including relative importance based on the number of votes they’ve received. When the group is discussing a question, he uses the Django admin site to set the question’s status to “Current”. After the question has been discussed, he sets the status to “Archived”

Our example project: Torquemada

http://bit.ly/XHjcAi

A use case for an admin.

Page 50: Testing in Django

Let’s do this.

TDD by example

Page 51: Testing in Django

http://vimeo.com/57692050

Screencast 1

(watch the video, then come back for the rest of the slides)

1) Set up a workspace 2) create a virtualenv 3) install django 4) startproject 5) startapp 6) test the app. You can start TDD on a Django project without even touching settings.py!

Page 52: Testing in Django

TDD by example

Text

from django.test import TestCase

class SimpleTest(TestCase): def test_basic_addition(self): """ Tests that 1 + 1 always equals 2. """ self.assertEqual(1 + 1, 2)

The test that ‘manage.py startapp’ generated.

Page 53: Testing in Django

Texthttp://vimeo.com/57693303

Screencast 2

Replace the auto generated test with a meaningful one.

Page 54: Testing in Django

TDD by example

Text

from django.test import LiveServerTestCasefrom selenium import webdriver

class QuestionsTest(LiveServerTestCase):

def setUp(self): self.browser = webdriver.Firefox() self.browser.implicitly_wait(3)

def tearDown(self): self.browser.quit()...

Start with a functional test that tells (at least part of) a use case.

Page 55: Testing in Django

TDD by example

Text

...class QuestionsTest(LiveServerTestCase):...def test_can_read_v..._a_question(self):# Isabel opens her web browser and# visits Torquemadaself.browser.get(self.live_server_url + '/')

# TODO self.fail('finish this test')

Page 56: Testing in Django

Texthttp://vimeo.com/57692852

Screencast 3

Extend the functional test with some use case as comments, and add a test for an h1 element.

Page 57: Testing in Django

TDD by example

Text# She knows it's Torquemada because she sees the name in the headingheading = self.browser.find_element_by_css_selector("h1#trq-heading")self.assertEqual(heading.text, "Torquemada")

Here’s the assertion, using Selenium.

Page 58: Testing in Django

Texthttps://vimeo.com/57693096

Screencast 4

Watch the new version of the functional test fail.

Page 59: Testing in Django

We have a working (but failing) test.Commit it.

TDD by example

Meaningful failure = important unit of work we’ve done. Let’s commit it.

Page 60: Testing in Django

Let’s refactor a bit...

TDD by example

Page 61: Testing in Django

Texthttp://vimeo.com/57693617

Screencast 5

Refactor the test.py file into a tests package.

Page 62: Testing in Django

... and commit that.

TDD by example

Page 63: Testing in Django

So, where are we exactly?

TDD by example

Page 64: Testing in Django

Texthttp://vimeo.com/57694735

Screencast 6

In the final screencast, we finish up this round of TDD: dive into a unit test for a view, write the code to get the test to pass, extend the unit test to cover more functionality, get it to pass, then confirm that we’ve satisfied our functional test (as it is).

Page 65: Testing in Django

Rinse and repeat.

TDD by example

Repeat the cycle of writing tests that fail (or extending tests to make them fail) and writing code to make the tests pass.

Page 66: Testing in Django

How about a demo?

Page 67: Testing in Django

Getting Around with Selenium

self.browser.get()

This command (if you’ve set self.browser = webdriver.FireFox(), or something like it), does a GET to open a web page with Selenium. It can open any URL.

Page 68: Testing in Django

Getting Around with Selenium

self.browser.find_element_by_<METHOD>()

Find elements on the page using one of Seleniums ‘find_element’ methods.

Page 69: Testing in Django

Getting Around with Selenium

find_element_by_idfind_element_by_namefind_element_by_xpathfind_element_by_link_textfind_element_by_partial_link_textfind_element_by_tag_namefind_element_by_class_namefind_element_by_css_selector # use this one

find_element_by_css_selector is my favorite. It feels like jQuery.

Page 70: Testing in Django

Getting Around with Selenium

find_elements_by_idfind_elements_by_namefind_elements_by_xpathfind_elements_by_link_textfind_elements_by_partial_link_textfind_elements_by_tag_namefind_elements_by_class_namefind_elements_by_css_selector

# return Python lists

You can get lists of elements by pluralizing ‘elements’ in the method name.

Page 71: Testing in Django

Getting Around with Selenium

element.click()

Once you find an element, you can .click() it. This will behave just like a real user click, and respects an JavaScript event.preventDefault() handlers.

Page 72: Testing in Django

Getting Around with Selenium

text_field = self.browser.find_element_by_css_selector("#id_text")text_field.clear()text_field.send_keys("Why aren't you using reverse()?")

self.browser.find_element_by_css_selector("input#trq-submit-question").click()

Working with form elements

Use .clear() and .send_keys() to type test into a form element.

Page 73: Testing in Django

How do I get started?

I want to start testing on my team. What’s the best way?

Page 74: Testing in Django

How do I get started?

1. Write tests for bug reports

An excellent way to start is to write tests for your bug reports. Find out the exact steps it takes to reproduce a problem and write either a functional or unit test THAT FAILS as an illustration of the bug. Then, fix the bug. Check in your fix and your test and rest easy knowing you’ve GUARANTEED that the bug is fixed.

Page 75: Testing in Django

How do I get started?

2. Use TDD for new features

There’s no time like the present to start writing tests. When your team decides on a new feature to implement, start by writing some use cases and develop from a functional test.

Page 76: Testing in Django

How do I get started?

3. Write unit tests for existing code you use while doing 1. and 2.

If you’re using a function from somewhere else in the system when you write code, you need to be able to guarantee that function does what you expect it to. Get in the habit of writing tests for parts of your project as you come in to contact with them, PARTICULARLY if they are involved in a bug fix.

Page 77: Testing in Django

What about continuous integration?

Continuous integration tools automatically checkout our code, build it, and run the tests. It’s to protect ourselves from developers on our team that forget to run the tests before they commit.

Page 78: Testing in Django

What about continuous integration?

We’re trying out Jenkins.

s+s is just starting to play with Jenkins, a CI platform in written in Java. NORMALLY, I’M LIKE...

Page 79: Testing in Django

.WAR

Page 80: Testing in Django

WHAT

Page 81: Testing in Django

IS

Page 82: Testing in Django

IT

Page 83: Testing in Django

GOOD

Page 84: Testing in Django

FOR?

Page 85: Testing in Django

What about continuous integration?

But in this case I’ll make an exception.

Jenkins is super easy to get up and running on your local machine, and there are plugins that play nice with tools we’re all using.

Page 86: Testing in Django

Configuring Jenkins$ pip install django-jenkins

INSTALLED_APPS = ( ... 'django_jenkins',)...

JENKINS_TASKS = ( 'django_jenkins.tasks.run_pylint', 'django_jenkins.tasks.with_coverage', 'django_jenkins.tasks.django_tests', # there are more of these)

$ python manage.py jenkins # Jenkins will run this command

django-jenkins is a plugin that runs out tests and outputs the files Jenkins needs to show our build stats. pip install and add just a few lines to your settings.py

Page 87: Testing in Django

Configuring Jenkins

$ wget http://mirrors.jenkins-ci.org/war/latest/jenkins.war$ java -jar jenkins.war

http://localhost:8080

Just get the .war file, run it, and hit port 8080 on your machine.

Page 88: Testing in Django

Configuring JenkinsYou’ll need some plugins:

- Jenkins Violations Plugin- Jenkins Git Plugin- Jenkins Cobertura Plugin

Install a few plugins.

Page 89: Testing in Django

Configuring Jenkins

1. Configure a new test (name, description)2. Give it your repo URL3. Tell it how often to build4. Tell it the commands to run5. Configure where to save the reports6. Click “Build Now”

Check out the tutorials in the “Resources” section of this slide deck for more on configuring your repo. It’ll take about 15 minutes the first time.

Page 90: Testing in Django

Configuring Jenkins

DONE

That’s it.

Page 91: Testing in Django

Next Steps508 compliance

http://wave.webaim.org/toolbar

JavaScript Testing

http://doctestjs.org/

Doctest.js

There are things we definitely didn’t test. We’re looking into automated 508 compliance testing. If you’re working in higher ed or the non-profit world, a non-compliant template should definitley “break the build”.

Page 95: Testing in Django

Using fixtures

Manage data for tests runs

Sometimes you want to create models explicitly. Other times you want to create a lot of reusable data. Enter fixtures.

Page 96: Testing in Django

Using fixtures[ { "pk": 1, "model": "questions.question", "fields": { "status": "new", "text": "How can my team get started with testing?", "votes": 0, "created": "2013-01-17T16:15:37.786Z" } }, { "pk": 2, "model": "questions.question", "fields": { "status": "new", "text": "Does Selenium only work in Firefox?", "votes": 0, "created": "2013-01-17T16:17:48.381Z" } }]

A fixture is just structured data (defaulting to JSON) that Django knows how to import and export: - loaddata - dumpdata

Page 97: Testing in Django

Using fixtures

$ python manage.py runserver...

# Use the Django /admin site to make some test data

...

$ mkdir questions/fixtures/$ python manage.py dumpdata questions --indent=4 > questions/fixtures/questions.json

Do NOT try to write fixtures by hand

Use the admin site and ‘manage.py dumpdata’ to make fixtures easily.