Slide 1

Slide 1 text

~ Julien Phalip Integration and Functional Testing with LiveServerTestCase, Selenium and More. September 5th, 2013 DJANGOCON US 2013

Slide 2

Slide 2 text

CONNECTED PERSONAL OBJECTS 5/2012 About me... ‣ Worked with Django since 2007 (version 0.96). ‣ Django core committer since 2011.

Slide 3

Slide 3 text

CONNECTED PERSONAL OBJECTS 5/2012 www.nurun.com

Slide 4

Slide 4 text

CONNECTED PERSONAL OBJECTS 5/2012 Definitions (subject to debate)

Slide 5

Slide 5 text

CONNECTED PERSONAL OBJECTS 5/2012 Definitions (subject to debate) ‣ Unit testing Ensure that small parts work in isolation.

Slide 6

Slide 6 text

CONNECTED PERSONAL OBJECTS 5/2012 Definitions (subject to debate) ‣ Unit testing Ensure that small parts work in isolation. ‣ Integration testing Ensure that those parts work well together.

Slide 7

Slide 7 text

CONNECTED PERSONAL OBJECTS 5/2012 Definitions (subject to debate) ‣ Unit testing Ensure that small parts work in isolation. ‣ Integration testing Ensure that those parts work well together. ‣ Functional testing Ensure that the application works, from a user’s perspective.

Slide 8

Slide 8 text

CONNECTED PERSONAL OBJECTS 5/2012 Testing in Django (the traditional way)

Slide 9

Slide 9 text

CONNECTED PERSONAL OBJECTS 5/2012 Testing in Django (the traditional way) from django.test import TestCase class MyTests(TestCase): def test_hello(self): r = self.client.get('/hello') self.assertTrue('Hello' in r.content)

Slide 10

Slide 10 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.TestCase

Slide 11

Slide 11 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.TestCase DJANGO THREAD

Slide 12

Slide 12 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.TestCase Application code DJANGO THREAD

Slide 13

Slide 13 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.TestCase TestCase Application code DJANGO THREAD

Slide 14

Slide 14 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.TestCase Database TestCase Application code DJANGO THREAD

Slide 15

Slide 15 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.TestCase “Dummy” http client (self.client) Database TestCase Application code DJANGO THREAD

Slide 16

Slide 16 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.TestCase “Dummy” http client (self.client) Database TestCase Application code MockRequest DJANGO THREAD

Slide 17

Slide 17 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.TestCase “Dummy” http client (self.client) Database TestCase Application code MockRequest DJANGO THREAD

Slide 18

Slide 18 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.TestCase “Dummy” http client (self.client) Database TestCase Application code MockRequest HttpResponse DJANGO THREAD

Slide 19

Slide 19 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.TestCase “Dummy” http client (self.client) Database TestCase Application code MockRequest HttpResponse DJANGO THREAD

Slide 20

Slide 20 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.TestCase “Dummy” http client (self.client) Database TestCase Application code MockRequest HttpResponse DJANGO THREAD

Slide 21

Slide 21 text

CONNECTED PERSONAL OBJECTS 5/2012 Pretty good but incomplete...

Slide 22

Slide 22 text

CONNECTED PERSONAL OBJECTS 5/2012 Pretty good but incomplete... ‣ Does not exercise the full http protocol.

Slide 23

Slide 23 text

CONNECTED PERSONAL OBJECTS 5/2012 Pretty good but incomplete... ‣ Does not exercise the full http protocol. ‣ Does not allow to test the user interface.

Slide 24

Slide 24 text

And then came LiveServerTestCase...

Slide 25

Slide 25 text

And then came LiveServerTestCase...

Slide 26

Slide 26 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.LiveServerTestCase

Slide 27

Slide 27 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.LiveServerTestCase LiveServerTestCase DJANGO THREAD #1

Slide 28

Slide 28 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.LiveServerTestCase LiveServerTestCase Application code DJANGO THREAD #2 DJANGO THREAD #1 Live http server

Slide 29

Slide 29 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.LiveServerTestCase “Real” http client LiveServerTestCase Application code DJANGO THREAD #2 DJANGO THREAD #1 Live http server

Slide 30

Slide 30 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.LiveServerTestCase “Real” http client Database LiveServerTestCase Application code DJANGO THREAD #2 DJANGO THREAD #1 Live http server

Slide 31

Slide 31 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.LiveServerTestCase “Real” http client Database LiveServerTestCase Application code DJANGO THREAD #2 DJANGO THREAD #1 Live http server

Slide 32

Slide 32 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.LiveServerTestCase “Real” http client Database LiveServerTestCase http Application code DJANGO THREAD #2 DJANGO THREAD #1 Live http server

Slide 33

Slide 33 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.LiveServerTestCase “Real” http client Database LiveServerTestCase http Application code DJANGO THREAD #2 DJANGO THREAD #1 Live http server wsgi

Slide 34

Slide 34 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.LiveServerTestCase “Real” http client Database LiveServerTestCase http Application code DJANGO THREAD #2 DJANGO THREAD #1 Live http server wsgi

Slide 35

Slide 35 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.LiveServerTestCase “Real” http client Database LiveServerTestCase http Application code DJANGO THREAD #2 DJANGO THREAD #1 Live http server wsgi

Slide 36

Slide 36 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.LiveServerTestCase “Real” http client Database LiveServerTestCase http Application code DJANGO THREAD #2 DJANGO THREAD #1 Live http server wsgi

Slide 37

Slide 37 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.LiveServerTestCase “Real” http client Database LiveServerTestCase http Application code DJANGO THREAD #2 DJANGO THREAD #1 Live http server wsgi

Slide 38

Slide 38 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.LiveServerTestCase “Real” http client Database LiveServerTestCase http Application code DJANGO THREAD #2 DJANGO THREAD #1 Live http server wsgi

Slide 39

Slide 39 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.LiveServerTestCase Database LiveServerTestCase http python-requests Application code DJANGO THREAD #2 DJANGO THREAD #1 Live http server wsgi

Slide 40

Slide 40 text

CONNECTED PERSONAL OBJECTS 5/2012 django.test.LiveServerTestCase Database LiveServerTestCase http Application code DJANGO THREAD #2 DJANGO THREAD #1 Live http server wsgi

Slide 41

Slide 41 text

CONNECTED PERSONAL OBJECTS 5/2012 Some important details ‣ Live server listens to localhost:8081 by default ‣ Address can be overridden: ./manage.py test --liveserver=localhost:8082 ./manage.py test --liveserver=localhost:9000-9200 ‣ Address accessible from the test’s code: self.live_server_url

Slide 42

Slide 42 text

CONNECTED PERSONAL OBJECTS 5/2012 Testing with python-requests

Slide 43

Slide 43 text

CONNECTED PERSONAL OBJECTS 5/2012 Testing with python-requests ‣ Testing REST APIs:

Slide 44

Slide 44 text

CONNECTED PERSONAL OBJECTS 5/2012 Testing with python-requests ‣ Testing REST APIs: import requests class MyTests(LiveServerTestCase): def test_api_user(self): r = requests.get( self.live_server_url + '/api/user/1/') self.assertEqual(r.status_code, 200) self.assertEqual(r.json, {'username': 'john.doe', 'email': '[email protected]'})

Slide 45

Slide 45 text

CONNECTED PERSONAL OBJECTS 5/2012 Testing with python-requests

Slide 46

Slide 46 text

CONNECTED PERSONAL OBJECTS 5/2012 Testing with python-requests ‣ Testing OAUTH workflows:

Slide 47

Slide 47 text

CONNECTED PERSONAL OBJECTS 5/2012 Testing with python-requests ‣ Testing OAUTH workflows: import requests from requests_oauthlib import OAuth2 from oauthlib.oauth2 import BackendApplicationClient class MyTests(LiveServerTestCase): def test_oauth(self): backend = BackendApplicationClient('mybackend') oauth2 = OAuth2(client=backend, token=TOKEN) r = requests.get( self.live_server_url + '/oauth', auth=oauth2) self.assertEqual(r.status_code, 200) self.assertTrue('Hello' in r.content)

Slide 48

Slide 48 text

Browser automators

Slide 49

Slide 49 text

CONNECTED PERSONAL OBJECTS 5/2012 Why use browser automators?

Slide 50

Slide 50 text

CONNECTED PERSONAL OBJECTS 5/2012 Why use browser automators? ‣ Closer to real Web user environment.

Slide 51

Slide 51 text

CONNECTED PERSONAL OBJECTS 5/2012 Why use browser automators? ‣ Closer to real Web user environment. ‣ Media & static files (CSS/images/etc.) implicitly get loaded.

Slide 52

Slide 52 text

CONNECTED PERSONAL OBJECTS 5/2012 Why use browser automators? ‣ Closer to real Web user environment. ‣ Media & static files (CSS/images/etc.) implicitly get loaded. ‣ Javascript code & Ajax calls implicitly get executed.

Slide 53

Slide 53 text

CONNECTED PERSONAL OBJECTS 5/2012 Why use browser automators? ‣ Closer to real Web user environment. ‣ Media & static files (CSS/images/etc.) implicitly get loaded. ‣ Javascript code & Ajax calls implicitly get executed. ‣ User interactions can be tested.

Slide 54

Slide 54 text

CONNECTED PERSONAL OBJECTS 5/2012 Why use browser automators? ‣ Closer to real Web user environment. ‣ Media & static files (CSS/images/etc.) implicitly get loaded. ‣ Javascript code & Ajax calls implicitly get executed. ‣ User interactions can be tested. ‣ Browser compatibility can be tested.

Slide 55

Slide 55 text

CONNECTED PERSONAL OBJECTS 5/2012 Why use browser automators? ‣ Closer to real Web user environment. ‣ Media & static files (CSS/images/etc.) implicitly get loaded. ‣ Javascript code & Ajax calls implicitly get executed. ‣ User interactions can be tested. ‣ Browser compatibility can be tested. ‣ It’s fun!

Slide 56

Slide 56 text

CONNECTED PERSONAL OBJECTS 5/2012 Selenium

Slide 57

Slide 57 text

CONNECTED PERSONAL OBJECTS 5/2012 Selenium ‣ API to become W3C standard (currently in draft)

Slide 58

Slide 58 text

CONNECTED PERSONAL OBJECTS 5/2012 Selenium ‣ API to become W3C standard (currently in draft) ‣ Client libraries in Java, C#, .NET, Ruby, PHP, Perl, Javascript and Python.

Slide 59

Slide 59 text

CONNECTED PERSONAL OBJECTS 5/2012 Selenium ‣ API to become W3C standard (currently in draft) ‣ Client libraries in Java, C#, .NET, Ruby, PHP, Perl, Javascript and Python. ‣ Works across multiple browsers (Firefox, Chrome, IE, Safari, Opera)

Slide 60

Slide 60 text

CONNECTED PERSONAL OBJECTS 5/2012 Selenium ‣ API to become W3C standard (currently in draft) ‣ Client libraries in Java, C#, .NET, Ruby, PHP, Perl, Javascript and Python. ‣ Works across multiple browsers (Firefox, Chrome, IE, Safari, Opera) ‣ Easy to install: pip install selenium

Slide 61

Slide 61 text

CONNECTED PERSONAL OBJECTS 5/2012 from django.test import LiveServerTestCase from selenium.webdriver.firefox.webdriver import \ WebDriver class MyTests(LiveServerTestCase):

Slide 62

Slide 62 text

CONNECTED PERSONAL OBJECTS 5/2012 from django.test import LiveServerTestCase from selenium.webdriver.firefox.webdriver import \ WebDriver class MyTests(LiveServerTestCase): def setUp(self): self.driver = WebDriver() super(MyTests, self).setUp() def tearDown(self): self.driver.quit() super(MyTests, self).tearDown()

Slide 63

Slide 63 text

CONNECTED PERSONAL OBJECTS 5/2012 from django.test import LiveServerTestCase from selenium.webdriver.firefox.webdriver import \ WebDriver class MyTests(LiveServerTestCase): def setUp(self): self.driver = WebDriver() super(MyTests, self).setUp() def tearDown(self): self.driver.quit() super(MyTests, self).tearDown()

Slide 64

Slide 64 text

CONNECTED PERSONAL OBJECTS 5/2012 from django.test import LiveServerTestCase from selenium.webdriver.firefox.webdriver import \ WebDriver class MyTests(LiveServerTestCase): ...

Slide 65

Slide 65 text

CONNECTED PERSONAL OBJECTS 5/2012 from django.test import LiveServerTestCase from selenium.webdriver.firefox.webdriver import \ WebDriver class MyTests(LiveServerTestCase): def test_hello(self): self.driver.get( self.live_server_url + '/hello') e = self.driver.find_element_by_id('btn') e.click() ... ...

Slide 66

Slide 66 text

CONNECTED PERSONAL OBJECTS 5/2012 DEMO Django Core Test Suite

Slide 67

Slide 67 text

Tips and goodies

Slide 68

Slide 68 text

CONNECTED PERSONAL OBJECTS 5/2012 Testing visuals with Needle

Slide 69

Slide 69 text

CONNECTED PERSONAL OBJECTS 5/2012 Testing visuals with Needle ‣ https://github.com/bfirsh/needle

Slide 70

Slide 70 text

CONNECTED PERSONAL OBJECTS 5/2012 Testing visuals with Needle ‣ https://github.com/bfirsh/needle ‣ Based on Selenium.

Slide 71

Slide 71 text

CONNECTED PERSONAL OBJECTS 5/2012 Testing visuals with Needle ‣ https://github.com/bfirsh/needle ‣ Based on Selenium. ‣ Tests visuals by comparing screenshots.

Slide 72

Slide 72 text

CONNECTED PERSONAL OBJECTS 5/2012 Testing visuals with Needle ‣ Test the looks of an entire page...

Slide 73

Slide 73 text

CONNECTED PERSONAL OBJECTS 5/2012 Testing visuals with Needle ‣ Test the looks of an entire page...

Slide 74

Slide 74 text

CONNECTED PERSONAL OBJECTS 5/2012 Testing visuals with Needle ‣ Or just parts of it.

Slide 75

Slide 75 text

CONNECTED PERSONAL OBJECTS 5/2012 Testing visuals with Needle ‣ Or just parts of it.

Slide 76

Slide 76 text

CONNECTED PERSONAL OBJECTS 5/2012 from needle.cases import NeedleTestCase from from selenium.webdriver import firefox class MyTests(NeedleTestCase, LiveServerTestCase): driver_command_executor = firefox.ExtensionConnection( "127.0.0.1", firefox.FirefoxProfile()) def test_homepage_menu(self): self.driver.get(self.live_server_url) self.assertScreenshot('ul.menu', 'menu') Testing visuals with Needle

Slide 77

Slide 77 text

CONNECTED PERSONAL OBJECTS 5/2012 ====================================================================== FAIL: test_homepage_menu (myapp.tests.MyTests) ---------------------------------------------------------------------- Traceback (most recent call last): File "myapp/tests.py", line 115, in test_homepage_menu self.assertScreenshot('ul.menu', 'menu') File "/.virtualenvs/myproject/src/needle/needle/cases.py", line 99, in assertScreenshot pass File "/System/Library/Frameworks/Python.framework/Versions/2.6/lib/ python2.6/contextlib.py", line 23, in __exit__ self.gen.next() File "/.virtualenvs/myproject/src/needle/needle/cases.py", line 160, in compareScreenshot % (filename, distance)) AssertionError: The saved screenshot for 'menu' did not match the screenshot captured (by a distance of 26.17) Testing visuals with Needle

Slide 78

Slide 78 text

CONNECTED PERSONAL OBJECTS 5/2012 Test coverage

Slide 79

Slide 79 text

CONNECTED PERSONAL OBJECTS 5/2012 Test coverage ‣ Yes, it does work too.

Slide 80

Slide 80 text

CONNECTED PERSONAL OBJECTS 5/2012 Test coverage ‣ Yes, it does work too. $ coverage run manage.py test \ myapp.SeleniumTests $ coverage report

Slide 81

Slide 81 text

CONNECTED PERSONAL OBJECTS 5/2012 Headless mode for continuous integration

Slide 82

Slide 82 text

CONNECTED PERSONAL OBJECTS 5/2012 Headless mode for continuous integration ‣ Install a browser and fonts: $ sudo apt-get install -y firefox xfonts-100dpi xfonts-75dpi xfonts- scalable xfonts-cyrillic

Slide 83

Slide 83 text

CONNECTED PERSONAL OBJECTS 5/2012 Headless mode for continuous integration ‣ Install a browser and fonts: $ sudo apt-get install -y firefox xfonts-100dpi xfonts-75dpi xfonts- scalable xfonts-cyrillic ‣ Install Xvfb: $ sudo apt-get install -y xvfb

Slide 84

Slide 84 text

CONNECTED PERSONAL OBJECTS 5/2012 ‣ Either with the Xvfb command: $ Xvfb -ac :99 2>/dev/null & $ DISPLAY=:99 ./manage.py test Headless mode for continuous integration

Slide 85

Slide 85 text

CONNECTED PERSONAL OBJECTS 5/2012 ‣ Either with the Xvfb command: $ Xvfb -ac :99 2>/dev/null & $ DISPLAY=:99 ./manage.py test ‣ ... or with Jenkins: Xvfb plugin. Headless mode for continuous integration

Slide 86

Slide 86 text

CONNECTED PERSONAL OBJECTS 5/2012 ‣ Either with the Xvfb command: $ Xvfb -ac :99 2>/dev/null & $ DISPLAY=:99 ./manage.py test ‣ ... or with Jenkins: Xvfb plugin. ‣ ... or with Travis: before_install: - "export DISPLAY=:99.0" - "sh -e /etc/init.d/xvfb start" Headless mode for continuous integration

Slide 87

Slide 87 text

CONNECTED PERSONAL OBJECTS 5/2012 ‣ ... or with PyVirtualDisplay: from pyvirtualdisplay import Display class MyTests(LiveServerTestCase) def setUp(self): self.display = Display( 'xvfb', visible=1,size=(1280, 1024)) self.display.start() def tearDown(self): self.display.stop() Headless mode for continuous integration

Slide 88

Slide 88 text

CONNECTED PERSONAL OBJECTS 5/2012 Sauce Labs

Slide 89

Slide 89 text

CONNECTED PERSONAL OBJECTS 5/2012 ‣ Runs remotely in the cloud (no local browser needed). Sauce Labs

Slide 90

Slide 90 text

CONNECTED PERSONAL OBJECTS 5/2012 ‣ Runs remotely in the cloud (no local browser needed). ‣ Gives access to multiple browsers (IE, FF, Opera, Chrome, Safari...) and multiple platforms (Windows, Linux, OSX, Android, iPad, iPhone). Sauce Labs

Slide 91

Slide 91 text

CONNECTED PERSONAL OBJECTS 5/2012 ‣ Runs remotely in the cloud (no local browser needed). ‣ Gives access to multiple browsers (IE, FF, Opera, Chrome, Safari...) and multiple platforms (Windows, Linux, OSX, Android, iPad, iPhone). ‣ Allows tests to be run in parallel. Sauce Labs

Slide 92

Slide 92 text

CONNECTED PERSONAL OBJECTS 5/2012

Slide 93

Slide 93 text

CONNECTED PERSONAL OBJECTS 5/2012

Slide 94

Slide 94 text

Not just Selenium...

Slide 95

Slide 95 text

CONNECTED PERSONAL OBJECTS 5/2012 Ghost.py ‣ http://jeanphix.me/Ghost.py/

Slide 96

Slide 96 text

CONNECTED PERSONAL OBJECTS 5/2012 Ghost.py from ghost import Ghost class MyTests(LiveServerTestCase): def setUp(self): self.ghost = Ghost() super(MyTests, self).setUp() def test_hello(self): self.ghost.open(self.live_server_url + '/hello') self.assertTrue('Hello' in self.ghost.content) ‣ http://jeanphix.me/Ghost.py/

Slide 97

Slide 97 text

CONNECTED PERSONAL OBJECTS 5/2012 CasperJS ‣ http://casperjs.org/ ‣ https://github.com/dobarkod/django-casper

Slide 98

Slide 98 text

CONNECTED PERSONAL OBJECTS 5/2012 CasperJS from casper.tests import CasperTestCase class MyTests(CasperTestCase): def test_something(self): self.assertTrue(self.casper('test.js')) ‣ http://casperjs.org/ ‣ https://github.com/dobarkod/django-casper

Slide 99

Slide 99 text

CONNECTED PERSONAL OBJECTS 5/2012 Splinter ‣ http://splinter.cobrateam.info

Slide 100

Slide 100 text

CONNECTED PERSONAL OBJECTS 5/2012 Splinter from splinter import Browser class MyTests(LiveServerTestCase): def setUp(self): self.browser = Browser() super(MyTests, self).setUp() def test_hello(self): self.browser.visit(self.live_server_url + '/hello') self.assertTrue( self.browser.is_text_present('Hello')) ‣ http://splinter.cobrateam.info

Slide 101

Slide 101 text

Epilogue...

Slide 102

Slide 102 text

CONNECTED PERSONAL OBJECTS 5/2012 A word of caution...

Slide 103

Slide 103 text

CONNECTED PERSONAL OBJECTS 5/2012 A word of caution... ‣ Selenium tests can be a little flaky.

Slide 104

Slide 104 text

CONNECTED PERSONAL OBJECTS 5/2012 A word of caution... ‣ Selenium tests can be a little flaky. ‣ Integration & functional tests are slow. Use them with moderation.

Slide 105

Slide 105 text

CONNECTED PERSONAL OBJECTS 5/2012 A word of caution... ‣ Selenium tests can be a little flaky. ‣ Integration & functional tests are slow. Use them with moderation. ‣ Use LiveServerTestCase or Selenium only for what the dummy client cannot already achieve.

Slide 106

Slide 106 text

CONNECTED PERSONAL OBJECTS 5/2012 Integration & functional tests are important.

Slide 107

Slide 107 text

CONNECTED PERSONAL OBJECTS 5/2012 Integration & functional tests are important. ‣ Increase your confidence in your code.

Slide 108

Slide 108 text

CONNECTED PERSONAL OBJECTS 5/2012 Integration & functional tests are important. ‣ Increase your confidence in your code. ‣ Increase your test coverage.

Slide 109

Slide 109 text

CONNECTED PERSONAL OBJECTS 5/2012 Integration & functional tests are important. ‣ Increase your confidence in your code. ‣ Increase your test coverage. ‣ Test the integration frontend/backend.

Slide 110

Slide 110 text

CONNECTED PERSONAL OBJECTS 5/2012 Integration & functional tests are important. ‣ Increase your confidence in your code. ‣ Increase your test coverage. ‣ Test the integration frontend/backend. ‣ Ensure the user interface works.

Slide 111

Slide 111 text

CONNECTED PERSONAL OBJECTS 5/2012 Integration & functional tests are important. ‣ Increase your confidence in your code. ‣ Increase your test coverage. ‣ Test the integration frontend/backend. ‣ Ensure the user interface works. ‣ Have fun in the process!

Slide 112

Slide 112 text

CONNECTED PERSONAL OBJECTS 5/2012 Photo credits ‣ http://www.flickr.com/photos/t0msk/3148160756/ ‣ http://www.flickr.com/photos/loozrboy/7311771718/ ‣ http://www.flickr.com/photos/johnonolan/5836211096/ ‣ http://www.flickr.com/photos/randomskk/3057595640/

Slide 113

Slide 113 text

@julienphalip http:/ /julienphalip.com http:/ /nurun.com Thank you!