Django Full Stack Testing and BDD with Lettuce and Splinter
Example scenario that needs to wait for an AJAX request
Most of the problems I’ve encountered when writing full stack tests happen when you have to wait for an AJAX request. So here’s an example from a Django app with some AJAX action.
# apps/users/features/signup.feature Scenario: User enters a username that has been taken Given a user exists with username "joeb" When I go to the "/register/" URL And I fill in "username" with "joeb" And I move focus away from the username field Then I should see "not available"
That scenario is for when a user is registering. As soon as the user enters their username and moves on to the next field an AJAX request goes off and checks whether that username is available or not. When it receives a response it displays ”available” or ”not available” beside the username field.
Setting up Lettuce and Splinter
But before you do anything you might want to setup a couple of things in your lettuce terrain.py file.
- Prepare test environment before each test run.
- Tear down test environment after each test run.
- Flush your database before each scenario.
- Set up your browser instance using Splinter.
# terrain.py from lettuce import before, after, world from splinter.browser import Browser from django.test.utils import setup_test_environment, teardown_test_environment from django.core.management import call_command from django.db import connection from django.conf import settings @before.harvest def initial_setup(server): call_command('syncdb', interactive=False, verbosity=0) call_command('flush', interactive=False, verbosity=0) call_command('migrate', interactive=False, verbosity=0) call_command('loaddata', 'all', verbosity=0) setup_test_environment() world.browser = Browser('webdriver.firefox') @after.harvest def cleanup(server): connection.creation.destroy_test_db(settings.DATABASES['default']['NAME']) teardown_test_environment() @before.each_scenario def reset_data(scenario): # Clean up django. call_command('flush', interactive=False, verbosity=0) call_command('loaddata', 'all', verbosity=0) @after.all def teardown_browser(total): world.browser.quit()
Splinter makes it easy to implement your steps and control the browser by providing simple methods like the following:
- browser.fill(field, value)
Check out the splinter docs for a full list of what you can do.
So here are the steps for the “User enters a username that has been taken” scenario.
# apps/users/features/user-steps.py @step(u'I go to the "(.*)" URL') def i_go_to_the_url(step, url): world.response = world.browser.visit(django_url(url)) @step(u'a user exists with username "(.*)"') def a_user_exists_with_username(step, p_username): user = UserProfile(username=p_username, email@example.com') user.set_password('secret007') user.save() @step(u'I fill in "(.*)" with "(.*)"') def i_fill_in(step, field, value): world.browser.fill(field, value) @step(u'I move focus away from the username field') def i_move_focus_away_from_the_username_field(step): world.browser.fill("password", "value") world.browser.wait_for_xpath('//*[@id="availability" and text()="not available"]') @step(u'I should see "(.*)"') def i_should_see(step, text): assert text in world.browser.html
Running lettuce features
To run your features for the users app with a separate settings file you would use something like…
python manage.py harvest --apps=users --settings=settings_lettuce
This uses a separate settings file which is useful if you need a separate environement. For example if you want to use a sqlite3/in memory database to make your tests run quicker. Or one other thing I had to do was to reduce the global_log_level because the extra output in the terminal made it harder to read when lettuce was outputting the test results.
You can see all the code for this example at https://github.com/cillian/batucada/tree/full-stack-tests
Keep up the good work Lettuce and Splinter.