Testing extensions¶
CKAN extensions can have their own tests that are run using nosetests
in much the same way as running CKAN’s own tests (see Testing CKAN).
Continuing with our example_iauthfunctions extension,
first we need a CKAN config file to be used when running our tests.
Create the file ckanext-iauthfunctions/test.ini
with the following
contents:
[app:main]
use = config:../ckan/test-core.ini
The use
line declares that this config file inherits the settings from the
config file used to run CKAN’s own tests (../ckan
should be the path to
your CKAN source directory, relative to your test.ini
file).
The test.ini
file is a CKAN config file just like your /etc/ckan/default/development.ini
and /etc/ckan/default/production.ini files, and it can contain any
CKAN config file settings that you want
CKAN to use when running your tests, for example:
[app:main]
use = config:../ckan/test-core.ini
ckan.site_title = My Test CKAN Site
ckan.site_description = A test site for testing my CKAN extension
Next, make the directory that will contain our test modules:
mkdir ckanext-iauthfunctions/ckanext/iauthfunctions/tests/
Finally, create the file
ckanext-iauthfunctions/ckanext/iauthfunctions/tests/test_iauthfunctions.py
with the following contents:
# encoding: utf-8
'''Tests for the ckanext.example_iauthfunctions extension.
'''
from nose.tools import assert_raises
from nose.tools import assert_equal
import ckan.model as model
import ckan.plugins
from ckan.plugins.toolkit import NotAuthorized, ObjectNotFound
import ckan.tests.factories as factories
import ckan.logic as logic
import ckan.tests.helpers as helpers
class TestExampleIAuthFunctionsPluginV6ParentAuthFunctions(object):
'''Tests for the ckanext.example_iauthfunctions.plugin module.
Specifically tests that overriding parent auth functions will cause
child auth functions to use the overridden version.
'''
@classmethod
def setup_class(cls):
'''Nose runs this method once to setup our test class.'''
# Test code should use CKAN's plugins.load() function to load plugins
# to be tested.
ckan.plugins.load('example_iauthfunctions_v6_parent_auth_functions')
def teardown(self):
'''Nose runs this method after each test method in our test class.'''
# Rebuild CKAN's database after each test method, so that each test
# method runs with a clean slate.
model.repo.rebuild_db()
@classmethod
def teardown_class(cls):
'''Nose runs this method once after all the test methods in our class
have been run.
'''
# We have to unload the plugin we loaded, so it doesn't affect any
# tests that run after ours.
ckan.plugins.unload('example_iauthfunctions_v6_parent_auth_functions')
def test_resource_delete_editor(self):
'''Normally organization admins can delete resources
Our plugin prevents this by blocking delete organization.
Ensure the delete button is not displayed (as only resource delete
is checked for showing this)
'''
user = factories.User()
owner_org = factories.Organization(
users=[{'name': user['id'], 'capacity': 'admin'}]
)
dataset = factories.Dataset(owner_org=owner_org['id'])
resource = factories.Resource(package_id=dataset['id'])
with assert_raises(logic.NotAuthorized) as e:
logic.check_access('resource_delete', {'user': user['name']}, {'id': resource['id']})
assert_equal(e.exception.message, 'User %s not authorized to delete resource %s' % (user['name'], resource['id']))
def test_resource_delete_sysadmin(self):
'''Normally organization admins can delete resources
Our plugin prevents this by blocking delete organization.
Ensure the delete button is not displayed (as only resource delete
is checked for showing this)
'''
user = factories.Sysadmin()
owner_org = factories.Organization(
users=[{'name': user['id'], 'capacity': 'admin'}]
)
dataset = factories.Dataset(owner_org=owner_org['id'])
resource = factories.Resource(package_id=dataset['id'])
assert_equal(logic.check_access('resource_delete', {'user': user['name']}, {'id': resource['id']}), True)
class TestExampleIAuthFunctionsCustomConfigSetting(object):
'''Tests for the plugin_v5_custom_config_setting module.
'''
@classmethod
def setup_class(cls):
if not ckan.plugins.plugin_loaded('example_iauthfunctions_v5_custom_config_setting'):
ckan.plugins.load('example_iauthfunctions_v5_custom_config_setting')
@classmethod
def teardown_class(cls):
ckan.plugins.unload('example_iauthfunctions_v5_custom_config_setting')
def teardown(self):
# Delete any stuff that's been created in the db, so it doesn't
# interfere with the next test.
model.repo.rebuild_db()
@helpers.change_config('ckan.iauthfunctions.users_can_create_groups', False)
def test_sysadmin_can_create_group_when_config_is_False(self):
sysadmin = factories.Sysadmin()
context = {
'ignore_auth': False,
'user': sysadmin['name']
}
helpers.call_action('group_create', context, name='test-group')
@helpers.change_config('ckan.iauthfunctions.users_can_create_groups', False)
def test_user_cannot_create_group_when_config_is_False(self):
user = factories.User()
context = {
'ignore_auth': False,
'user': user['name']
}
assert_raises(
NotAuthorized, helpers.call_action, 'group_create',
context, name='test-group')
@helpers.change_config('ckan.iauthfunctions.users_can_create_groups', False)
def test_visitor_cannot_create_group_when_config_is_False(self):
context = {
'ignore_auth': False,
'user': None
}
assert_raises(
NotAuthorized, helpers.call_action, 'group_create',
context, name='test-group')
@helpers.change_config('ckan.iauthfunctions.users_can_create_groups', True)
def test_sysadmin_can_create_group_when_config_is_True(self):
sysadmin = factories.Sysadmin()
context = {
'ignore_auth': False,
'user': sysadmin['name']
}
helpers.call_action('group_create', context, name='test-group')
@helpers.change_config('ckan.iauthfunctions.users_can_create_groups', True)
def test_user_can_create_group_when_config_is_True(self):
user = factories.User()
context = {
'ignore_auth': False,
'user': user['name']
}
helpers.call_action('group_create', context, name='test-group')
@helpers.change_config('ckan.iauthfunctions.users_can_create_groups', True)
def test_visitor_cannot_create_group_when_config_is_True(self):
context = {
'ignore_auth': False,
'user': None
}
assert_raises(
NotAuthorized, helpers.call_action, 'group_create',
context, name='test-group')
class BaseTest(object):
def teardown(self):
# Rebuild CKAN's database after each test method, so that each test
# method runs with a clean slate.
model.repo.rebuild_db()
def _make_curators_group(self):
'''This is a helper method for test methods to call when they want
the 'curators' group to be created.
'''
sysadmin = factories.Sysadmin()
# Create a user who will *not* be a member of the curators group.
noncurator = factories.User()
# Create a user who will be a member of the curators group.
curator = factories.User()
# Create the curators group, with the 'curator' user as a member.
users = [{'name': curator['name'], 'capacity': 'member'}]
context = {
'ignore_auth': False,
'user': sysadmin['name']
}
curators_group = helpers.call_action(
'group_create', context, name='curators', users=users)
return (noncurator, curator, curators_group)
class TestExampleIAuthFunctionsPluginV4(BaseTest):
'''Tests for the ckanext.example_iauthfunctions.plugin module.
'''
@classmethod
def setup_class(cls):
'''Nose runs this method once to setup our test class.'''
# Test code should use CKAN's plugins.load() function to load plugins
# to be tested.
if not ckan.plugins.plugin_loaded('example_iauthfunctions_v4'):
ckan.plugins.load('example_iauthfunctions_v4')
@classmethod
def teardown_class(cls):
'''Nose runs this method once after all the test methods in our class
have been run.
'''
# We have to unload the plugin we loaded, so it doesn't affect any
# tests that run after ours.
ckan.plugins.unload('example_iauthfunctions_v4')
def test_group_create_with_no_curators_group(self):
'''Test that group_create doesn't crash when there's no curators group.
'''
sysadmin = factories.Sysadmin()
# Make sure there's no curators group.
assert 'curators' not in helpers.call_action('group_list', {})
# Make our sysadmin user create a group. CKAN should not crash.
context = {
'ignore_auth': False,
'user': sysadmin['name']
}
helpers.call_action('group_create', context, name='test-group')
def test_group_create_with_visitor(self):
'''A visitor (not logged in) should not be able to create a group.
Note: this also tests that the group_create auth function doesn't
crash when the user isn't logged in.
'''
noncurator, curator, curators_group = self._make_curators_group()
context = {
'ignore_auth': False,
'user': None
}
assert_raises(
NotAuthorized, helpers.call_action, 'group_create',
context, name='this_group_should_not_be_created')
def test_group_create_with_non_curator(self):
'''A user who isn't a member of the curators group should not be able
to create a group.
'''
noncurator, curator, curators_group = self._make_curators_group()
context = {
'ignore_auth': False,
'user': noncurator['name']
}
assert_raises(
NotAuthorized, helpers.call_action, 'group_create',
context, name='this_group_should_not_be_created')
def test_group_create_with_curator(self):
'''A member of the curators group should be able to create a group.
'''
noncurator, curator, curators_group = self._make_curators_group()
name = 'my-new-group'
context = {
'ignore_auth': False,
'user': curator['name']
}
result = helpers.call_action(
'group_create', context, name=name)
assert result['name'] == name
To run these extension tests, cd
into the ckanext-iauthfunctions
directory and run this command:
nosetests --ckan --with-pylons=test.ini ckanext/iauthfunctions/tests
Some notes on how these tests work:
- Nose has lots of useful functions for testing, see the nose documentation.
- We’re using a
paste.fixture.TestApp
object to simulate sending HTTP requests to the CKAN API or frontend. See Testing Applications with Paste for some documentation of this. - We’re calling
ckan.tests.call_action_api()
to post (simulated) HTTP requests to the CKAN API. This is a convenience function that CKAN provides for its own tests. - You might also find it useful to read the Pylons testing documentation.
- The Pylons book also has a chapter on testing.
- Avoid importing the plugin modules directly into your test modules (e.g from example_iauthfunctions import plugin_v5_custom_config_setting). This causes the plugin to be registered and loaded before the entire test run, so the plugin will be loaded for all tests. This can cause conflicts and test failures.
Todo
Link to CKAN guidelines for how to write tests, once those guidelines have been written. Also add any more extension-specific testing details here.