Repository: Kinto/kinto

Issue:
[cliquet] Split tests from kinto package
original : https://github.com/mozilla-services/cliquet/issues/267

Should we have tests within the kinto package or outside ? that was discussed but I don't think a decision was made.

Another related question is not wether tests is in kinto/ or outside, but rather : do we ship tests when we release kinto ? some people strip them when they build the release.

[cliquet] Split tests from kinto package
original : https://github.com/mozilla-services/cliquet/issues/267

Should we have tests within the kinto package or outside ? that was discussed but I don't think a decision was made.

Another related question is not wether tests is in kinto/ or outside, but rather : do we ship tests when we release kinto ? some people strip them when they build the release.



Discussion:
I would be in favor of moving them outside (if possible), and let test helpers in something like `kinto.testing`.

I don't know if it could help us decide but we currently specify the test suite in `setup.py` https://github.com/Kinto/kinto/blob/master/setup.py#L105 

I would be in favor of moving them outside (if possible), and let test helpers in something like `kinto.testing`.

I don't know if it could help us decide but we currently specify the test suite in `setup.py` https://github.com/Kinto/kinto/blob/master/setup.py#L105 


Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -54,6 +54,15 @@ def read_file(filename):
     'kinto_redis'
 ]
 
+SETUP_REQUIRES = [
+    'pytest-runner'
+]
+
+TEST_REQUIREMENTS = [
+    'pytest',
+    'WebTest'
+]
+
 DEPENDENCY_LINKS = [
 ]
 
@@ -100,6 +109,8 @@ def read_file(filename):
       package_data={'': ['*.rst', '*.py']},
       include_package_data=True,
       zip_safe=False,
+      setup_requires=SETUP_REQUIRES,
+      tests_require=TEST_REQUIREMENTS,
       install_requires=REQUIREMENTS,
       extras_require={
           'redis': REDIS_REQUIRES,
@@ -107,6 +118,6 @@ def read_file(filename):
           'monitoring': MONITORING_REQUIRES,
           ":python_version=='2.7'": ["functools32", "futures"],
       },
-      test_suite="kinto.tests",
+      test_suite="tests",
       dependency_links=DEPENDENCY_LINKS,
       entry_points=ENTRY_POINTS)


Test patch:
diff --git a/docs/core/testing.rst b/docs/core/testing.rst
new file mode 100644
--- /dev/null
+++ b/docs/core/testing.rst
@@ -0,0 +1,9 @@
+.. _testing:
+
+
+Testing
+#######
+
+
+.. automodule:: kinto.core.testing
+    :members:
diff --git a/kinto/tests/core/support.py b/kinto/core/testing.py
similarity index 53%
rename from kinto/tests/core/support.py
rename to kinto/core/testing.py
--- a/kinto/tests/core/support.py
+++ b/kinto/core/testing.py
@@ -1,34 +1,25 @@
-from collections import defaultdict
-import mock
 import os
-import threading
-import functools
-
-try:
-    import unittest2 as unittest
-except ImportError:
-    import unittest  # NOQA
+import unittest
+from collections import defaultdict
 
+import mock
 import webtest
-
 from cornice import errors as cornice_errors
-from pyramid.url import parse_url_overrides
-from pyramid.security import IAuthorizationPolicy, Authenticated, Everyone
-from zope.interface import implementer
 from enum import Enum
+from pyramid.url import parse_url_overrides
 
 from kinto.core import DEFAULT_SETTINGS
-from kinto.core.authorization import PRIVATE
 from kinto.core.storage import generators
-from kinto.tests.core.testapp import main as testapp
-from kinto.core.utils import sqlalchemy, follow_subrequest
+from kinto.core.utils import sqlalchemy, follow_subrequest, encode64
 
-# This is the principal a connected user should have (in the tests).
-USER_PRINCIPAL = ('basicauth:9f2d363f98418b13253d6d7193fc88690302'
-                  'ab0ae21295521f6029dffe9dc3b0')
+skip_if_travis = unittest.skipIf('TRAVIS' in os.environ, "travis")
+skip_if_no_postgresql = unittest.skipIf(sqlalchemy is None,
+                                        "postgresql is not installed.")
 
 
 class DummyRequest(mock.MagicMock):
+    """Fully mocked request.
+    """
     def __init__(self, *args, **kwargs):
         super(DummyRequest, self).__init__(*args, **kwargs)
         self.upath_info = '/v0/'
@@ -68,16 +59,56 @@ def blank(cls, path, *args, **kwargs):
     return PrefixedRequestClass
 
 
+class FormattedErrorMixin(object):
+    """Test mixin in order to perform advanced error responses assertions.
+    """
+
+    def assertFormattedError(self, response, code, errno, error,
+                             message=None, info=None):
+        if isinstance(errno, Enum):
+            errno = errno.value
+
+        self.assertEqual(response.headers['Content-Type'],
+                         'application/json; charset=UTF-8')
+        self.assertEqual(response.json['code'], code)
+        self.assertEqual(response.json['errno'], errno)
+        self.assertEqual(response.json['error'], error)
+
+        if message is not None:
+            self.assertIn(message, response.json['message'])
+        else:  # pragma: no cover
+            self.assertNotIn('message', response.json)
+
+        if info is not None:
+            self.assertIn(info, response.json['info'])
+        else:  # pragma: no cover
+            self.assertNotIn('info', response.json)
+
+
+def get_user_headers(user):
+    """Helper to obtain a Basic Auth authorization headers from the specified
+    `user` (e.g. ``"user:pass"``)
+
+    :rtype: dict
+    """
+    credentials = "%s:secret" % user
+    authorization = 'Basic {0}'.format(encode64(credentials))
+    return {
+        'Authorization': authorization
+    }
+
+
 class BaseWebTest(object):
-    """Base Web Test to test your cornice service.
+    """Base Web Test to test your kinto.core service.
 
     It setups the database before each test and delete it after.
     """
 
     api_prefix = "v0"
-    authorization_policy = 'kinto.tests.core.support.AllowAuthorizationPolicy'
-    collection_url = '/mushrooms'
-    principal = USER_PRINCIPAL
+    """URL version prefix"""
+
+    entry_point = None
+    """Main application entry"""
 
     def __init__(self, *args, **kwargs):
         super(BaseWebTest, self).__init__(*args, **kwargs)
@@ -85,124 +116,55 @@ def __init__(self, *args, **kwargs):
         self.storage = self.app.app.registry.storage
         self.cache = self.app.app.registry.cache
         self.permission = self.app.app.registry.permission
+
+        self.storage.initialize_schema()
+        self.permission.initialize_schema()
+        self.cache.initialize_schema()
+
         self.headers = {
-            'Content-Type': 'application/json',
-            'Authorization': 'Basic bWF0OjE='
+            'Content-Type': 'application/json'
         }
 
     def make_app(self, settings=None, config=None):
-        wsgi_app = testapp(self.get_app_settings(settings), config=config)
+        """Instantiate the application and setup requests to use the api
+        prefix.
+
+        :param dict settings: extra settings values
+        :param pyramid.config.Configurator config: already initialized config
+        :returns: webtest application instance
+        """
+        settings = self.get_app_settings(extras=settings)
+
+        try:
+            main = self.entry_point.__func__
+        except AttributeError:  # pragma: no cover
+            main = self.entry_point.im_func
+
+        wsgi_app = main({}, config=config, **settings)
         app = webtest.TestApp(wsgi_app)
         app.RequestClass = get_request_class(self.api_prefix)
         return app
 
-    def get_app_settings(self, additional_settings=None):
+    def get_app_settings(self, extras=None):
+        """Application settings to be used. Override to tweak default settings
+        for the tests.
+
+        :param dict extras: extra settings values
+        :rtype: dict
+        """
         settings = DEFAULT_SETTINGS.copy()
 
         settings['storage_backend'] = 'kinto.core.storage.memory'
         settings['cache_backend'] = 'kinto.core.cache.memory'
         settings['permission_backend'] = 'kinto.core.permission.memory'
 
-        settings['project_name'] = 'myapp'
-        settings['project_version'] = '0.0.1'
-        settings['project_docs'] = 'https://kinto.readthedocs.io/'
-        settings['multiauth.authorization_policy'] = self.authorization_policy
+        if extras is not None:
+            settings.update(extras)
 
-        if additional_settings is not None:
-            settings.update(additional_settings)
         return settings
 
-    def get_item_url(self, id=None):
-        """Return the URL of the item using self.item_url."""
-        if id is None:
-            id = self.record['id']
-        return self.collection_url + '/' + str(id)
-
     def tearDown(self):
         super(BaseWebTest, self).tearDown()
         self.storage.flush()
         self.cache.flush()
         self.permission.flush()
-
-
-class ThreadMixin(object):
-
-    def setUp(self):
-        super(ThreadMixin, self).setUp()
-        self._threads = []
-
-    def tearDown(self):
-        super(ThreadMixin, self).tearDown()
-
-        for thread in self._threads:
-            thread.join()
-
-    def _create_thread(self, *args, **kwargs):
-        thread = threading.Thread(*args, **kwargs)
-        self._threads.append(thread)
-        return thread
-
-
-class FormattedErrorMixin(object):
-
-    def assertFormattedError(self, response, code, errno, error,
-                             message=None, info=None):
-        # make sure we translate Enum instances to their values
-        if isinstance(error, Enum):
-            error = error.value
-
-        if isinstance(errno, Enum):
-            errno = errno.value
-
-        self.assertEqual(response.headers['Content-Type'],
-                         'application/json; charset=UTF-8')
-        self.assertEqual(response.json['code'], code)
-        self.assertEqual(response.json['errno'], errno)
-        self.assertEqual(response.json['error'], error)
-
-        if message is not None:
-            self.assertIn(message, response.json['message'])
-        else:
-            self.assertNotIn('message', response.json)
-
-        if info is not None:
-            self.assertIn(info, response.json['info'])
-        else:
-            self.assertNotIn('info', response.json)
-
-
-@implementer(IAuthorizationPolicy)
-class AllowAuthorizationPolicy(object):
-    def permits(self, context, principals, permission):
-        if permission == PRIVATE:
-            return Authenticated in principals
-        if Everyone in principals:
-            return True
-        # Kinto-Core default authz policy uses prefixed_userid.
-        prefixed = [context.prefixed_userid]
-        return USER_PRINCIPAL in (principals + prefixed)
-
-    def principals_allowed_by_permission(self, context, permission):
-        raise NotImplementedError()  # PRAGMA NOCOVER
-
-
-def authorize(permits=True, authz_class=None):
-    """Patch the default authorization policy to return what is specified
-    in :param:permits.
-    """
-    if authz_class is None:
-        authz_class = 'kinto.tests.core.support.AllowAuthorizationPolicy'
-
-    def wrapper(f):
-        @functools.wraps(f)
-        def wrapped(*args, **kwargs):
-            with mock.patch(
-                    '%s.permits' % authz_class,
-                    return_value=permits):
-                return f(*args, **kwargs)
-        return wrapped
-    return wrapper
-
-skip_if_travis = unittest.skipIf('TRAVIS' in os.environ, "travis")
-skip_if_no_postgresql = unittest.skipIf(sqlalchemy is None,
-                                        "postgresql is not installed.")
diff --git a/kinto/tests/support.py b/kinto/tests/support.py
deleted file mode 100644
--- a/kinto/tests/support.py
+++ /dev/null
@@ -1,82 +0,0 @@
-try:
-    import unittest2 as unittest
-except ImportError:
-    import unittest  # NOQA
-
-import webtest
-from kinto.core import utils
-from kinto.tests.core import support as core_support
-from kinto import main as testapp
-from kinto import DEFAULT_SETTINGS
-
-
-MINIMALIST_BUCKET = {}
-MINIMALIST_COLLECTION = {}
-MINIMALIST_GROUP = {'data': dict(members=['fxa:user'])}
-MINIMALIST_RECORD = {'data': dict(name="Hulled Barley",
-                                  type="Whole Grain")}
-USER_PRINCIPAL = 'basicauth:3a0c56d278def4113f38d0cfff6db1b06b84fcc4384ee890' \
-                 'cf7bbaa772317e10'
-
-
-class BaseWebTest(object):
-
-    def __init__(self, *args, **kwargs):
-        super(BaseWebTest, self).__init__(*args, **kwargs)
-        self.principal = USER_PRINCIPAL
-        self.app = self._get_test_app()
-        self.storage = self.app.app.registry.storage
-        self.permission = self.app.app.registry.permission
-        self.cache = self.app.app.registry.cache
-        self.storage.initialize_schema()
-        self.permission.initialize_schema()
-        self.cache.initialize_schema()
-        self.headers = {
-            'Content-Type': 'application/json',
-        }
-        self.headers.update(get_user_headers('mat'))
-
-    def _get_test_app(self, settings=None):
-        app = webtest.TestApp(testapp({}, **self.get_app_settings(settings)))
-        app.RequestClass = core_support.get_request_class(prefix="v1")
-        return app
-
-    def get_app_settings(self, additional_settings=None):
-        settings = core_support.DEFAULT_SETTINGS.copy()
-        settings.update(**DEFAULT_SETTINGS)
-        settings['cache_backend'] = 'kinto.core.cache.memory'
-        settings['storage_backend'] = 'kinto.core.storage.memory'
-        settings['permission_backend'] = 'kinto.core.permission.memory'
-        settings['userid_hmac_secret'] = "this is not a secret"
-        settings['includes'] = "kinto.plugins.default_bucket"
-
-        if additional_settings is not None:
-            settings.update(additional_settings)
-        return settings
-
-    def tearDown(self):
-        super(BaseWebTest, self).tearDown()
-        self.storage.flush()
-        self.cache.flush()
-        self.permission.flush()
-
-    def create_group(self, bucket_id, group_id, members=None):
-        if members is None:
-            group = MINIMALIST_GROUP
-        else:
-            group = {'data': {'members': members}}
-        group_url = '/buckets/%s/groups/%s' % (bucket_id, group_id)
-        self.app.put_json(group_url, group,
-                          headers=self.headers, status=201)
-
-    def create_bucket(self, bucket_id):
-        self.app.put_json('/buckets/%s' % bucket_id, MINIMALIST_BUCKET,
-                          headers=self.headers, status=201)
-
-
-def get_user_headers(user):
-    credentials = "%s:secret" % user
-    authorization = 'Basic {0}'.format(utils.encode64(credentials))
-    return {
-        'Authorization': authorization
-    }
diff --git a/kinto/tests/__init__.py b/tests/__init__.py
similarity index 100%
rename from kinto/tests/__init__.py
rename to tests/__init__.py
diff --git a/kinto/tests/core/__init__.py b/tests/core/__init__.py
similarity index 100%
rename from kinto/tests/core/__init__.py
rename to tests/core/__init__.py
diff --git a/kinto/tests/core/listeners.py b/tests/core/listeners.py
similarity index 100%
rename from kinto/tests/core/listeners.py
rename to tests/core/listeners.py
diff --git a/kinto/tests/core/resource/__init__.py b/tests/core/resource/__init__.py
similarity index 94%
rename from kinto/tests/core/resource/__init__.py
rename to tests/core/resource/__init__.py
--- a/kinto/tests/core/resource/__init__.py
+++ b/tests/core/resource/__init__.py
@@ -1,9 +1,10 @@
 import mock
+import unittest
 
+from kinto.core import resource
 from kinto.core.authorization import RouteFactory
 from kinto.core.storage import memory
-from kinto.tests.core.support import unittest, DummyRequest
-from kinto.core import resource
+from kinto.core.testing import DummyRequest
 
 
 class StrictSchema(resource.ResourceSchema):
diff --git a/kinto/tests/core/resource/test_base.py b/tests/core/resource/test_base.py
similarity index 97%
rename from kinto/tests/core/resource/test_base.py
rename to tests/core/resource/test_base.py
--- a/kinto/tests/core/resource/test_base.py
+++ b/tests/core/resource/test_base.py
@@ -3,7 +3,8 @@
 
 from kinto.core.resource import UserResource, ShareableResource
 from kinto.core.storage import exceptions as storage_exceptions
-from kinto.tests.core.resource import BaseTest
+
+from . import BaseTest
 
 
 class ResourceTest(BaseTest):
diff --git a/kinto/tests/core/resource/test_cache_expires.py b/tests/core/resource/test_cache_expires.py
similarity index 96%
rename from kinto/tests/core/resource/test_cache_expires.py
rename to tests/core/resource/test_cache_expires.py
--- a/kinto/tests/core/resource/test_cache_expires.py
+++ b/tests/core/resource/test_cache_expires.py
@@ -1,4 +1,4 @@
-from kinto.tests.core.resource import BaseTest
+from . import BaseTest
 
 
 class CacheExpires(BaseTest):
diff --git a/kinto/tests/core/resource/test_events.py b/tests/core/resource/test_events.py
similarity index 96%
rename from kinto/tests/core/resource/test_events.py
rename to tests/core/resource/test_events.py
--- a/kinto/tests/core/resource/test_events.py
+++ b/tests/core/resource/test_events.py
@@ -2,15 +2,15 @@
 import uuid
 from contextlib import contextmanager
 
-import webtest
 from pyramid.config import Configurator
 
+from kinto.core import statsd
 from kinto.core.events import (ResourceChanged, AfterResourceChanged,
                                ResourceRead, AfterResourceRead, ACTIONS)
 from kinto.core.storage.exceptions import BackendError
-from kinto.tests.core.testapp import main as make_testapp
-from kinto.tests.core.support import unittest, BaseWebTest, get_request_class
-from kinto.core import statsd
+from kinto.core.testing import unittest
+
+from ..support import BaseWebTest
 
 
 @contextmanager
@@ -43,16 +43,14 @@ def tearDown(self):
     def listener(self, event):
         self.events.append(event)
 
-    def make_app(self, settings=None):
+    def make_app(self, settings=None, config=None):
         settings = self.get_app_settings(settings)
-        self.config = Configurator(settings=settings)
+        config = Configurator(settings=settings)
         for event_cls in self.subscribed:
-            self.config.add_subscriber(self.listener, event_cls)
-        self.config.commit()
-        app = make_testapp(config=self.config)
-        app = webtest.TestApp(app)
-        app.RequestClass = get_request_class(self.api_prefix)
-        return app
+            config.add_subscriber(self.listener, event_cls)
+        config.commit()
+        return super(BaseEventTest, self).make_app(settings=settings,
+                                                   config=config)
 
 
 class ResourceReadTest(BaseEventTest, unittest.TestCase):
@@ -439,7 +437,7 @@ def get_app_settings(self, *args, **kwargs):
             return settings
 
         settings['statsd_url'] = 'udp://localhost:8125'
-        this_module = 'kinto.tests.core.resource.test_events'
+        this_module = 'tests.core.resource.test_events'
         settings['event_listeners'] = 'test'
         settings['event_listeners.test.use'] = this_module
         return settings
diff --git a/kinto/tests/core/resource/test_filter.py b/tests/core/resource/test_filter.py
similarity index 99%
rename from kinto/tests/core/resource/test_filter.py
rename to tests/core/resource/test_filter.py
--- a/kinto/tests/core/resource/test_filter.py
+++ b/tests/core/resource/test_filter.py
@@ -1,7 +1,7 @@
 from pyramid import httpexceptions
 
 from kinto.core.errors import ERRORS
-from kinto.tests.core.resource import BaseTest
+from . import BaseTest
 
 
 class FilteringTest(BaseTest):
diff --git a/kinto/tests/core/resource/test_model.py b/tests/core/resource/test_model.py
similarity index 98%
rename from kinto/tests/core/resource/test_model.py
rename to tests/core/resource/test_model.py
--- a/kinto/tests/core/resource/test_model.py
+++ b/tests/core/resource/test_model.py
@@ -1,6 +1,6 @@
 from pyramid import httpexceptions
 
-from kinto.tests.core.resource import BaseTest
+from . import BaseTest
 
 
 class ModelTest(BaseTest):
diff --git a/kinto/tests/core/resource/test_object_permissions.py b/tests/core/resource/test_object_permissions.py
similarity index 99%
rename from kinto/tests/core/resource/test_object_permissions.py
rename to tests/core/resource/test_object_permissions.py
--- a/kinto/tests/core/resource/test_object_permissions.py
+++ b/tests/core/resource/test_object_permissions.py
@@ -3,9 +3,10 @@
 from pyramid import httpexceptions
 
 from kinto.core.resource import ShareableResource
-from kinto.tests.core.resource import BaseTest
 from kinto.core.permission.memory import Permission
 
+from . import BaseTest
+
 
 class PermissionTest(BaseTest):
     resource_class = ShareableResource
diff --git a/kinto/tests/core/resource/test_pagination.py b/tests/core/resource/test_pagination.py
similarity index 99%
rename from kinto/tests/core/resource/test_pagination.py
rename to tests/core/resource/test_pagination.py
--- a/kinto/tests/core/resource/test_pagination.py
+++ b/tests/core/resource/test_pagination.py
@@ -5,9 +5,10 @@
 from six.moves.urllib.parse import parse_qs, urlparse
 from pyramid.httpexceptions import HTTPBadRequest
 
-from kinto.tests.core.resource import BaseTest
 from kinto.core.utils import json
 
+from . import BaseTest
+
 
 class PaginationTest(BaseTest):
     def setUp(self):
diff --git a/kinto/tests/core/resource/test_partial_response.py b/tests/core/resource/test_partial_response.py
similarity index 98%
rename from kinto/tests/core/resource/test_partial_response.py
rename to tests/core/resource/test_partial_response.py
--- a/kinto/tests/core/resource/test_partial_response.py
+++ b/tests/core/resource/test_partial_response.py
@@ -1,7 +1,8 @@
 from pyramid import httpexceptions
 
 from kinto.core.resource import ShareableResource
-from kinto.tests.core.resource import BaseTest
+
+from . import BaseTest
 
 
 class PartialResponseBase(BaseTest):
diff --git a/kinto/tests/core/resource/test_preconditions.py b/tests/core/resource/test_preconditions.py
similarity index 99%
rename from kinto/tests/core/resource/test_preconditions.py
rename to tests/core/resource/test_preconditions.py
--- a/kinto/tests/core/resource/test_preconditions.py
+++ b/tests/core/resource/test_preconditions.py
@@ -1,7 +1,8 @@
 from pyramid import httpexceptions
 
 from kinto.core.errors import ERRORS
-from kinto.tests.core.resource import BaseTest
+
+from . import BaseTest
 
 
 class NotModifiedTest(BaseTest):
diff --git a/kinto/tests/core/resource/test_record.py b/tests/core/resource/test_record.py
similarity index 99%
rename from kinto/tests/core/resource/test_record.py
rename to tests/core/resource/test_record.py
--- a/kinto/tests/core/resource/test_record.py
+++ b/tests/core/resource/test_record.py
@@ -5,7 +5,8 @@
 
 from kinto.core.resource import ResourceSchema
 from kinto.core.errors import ERRORS
-from kinto.tests.core.resource import BaseTest
+
+from . import BaseTest
 
 
 class GetTest(BaseTest):
diff --git a/kinto/tests/core/resource/test_schema.py b/tests/core/resource/test_schema.py
similarity index 98%
rename from kinto/tests/core/resource/test_schema.py
rename to tests/core/resource/test_schema.py
--- a/kinto/tests/core/resource/test_schema.py
+++ b/tests/core/resource/test_schema.py
@@ -1,7 +1,7 @@
 import colander
 import mock
 
-from kinto.tests.core.support import unittest
+from kinto.core.testing import unittest
 from kinto.core.resource import schema
 
 
diff --git a/kinto/tests/core/resource/test_sort.py b/tests/core/resource/test_sort.py
similarity index 99%
rename from kinto/tests/core/resource/test_sort.py
rename to tests/core/resource/test_sort.py
--- a/kinto/tests/core/resource/test_sort.py
+++ b/tests/core/resource/test_sort.py
@@ -3,7 +3,8 @@
 from pyramid import httpexceptions
 
 from kinto.core.errors import ERRORS
-from kinto.tests.core.resource import BaseTest
+
+from . import BaseTest
 
 
 class SortingTest(BaseTest):
diff --git a/kinto/tests/core/resource/test_sync.py b/tests/core/resource/test_sync.py
similarity index 97%
rename from kinto/tests/core/resource/test_sync.py
rename to tests/core/resource/test_sync.py
--- a/kinto/tests/core/resource/test_sync.py
+++ b/tests/core/resource/test_sync.py
@@ -5,12 +5,13 @@
 import six
 from pyramid import httpexceptions
 
-from kinto.tests.core.resource import BaseTest
-from kinto.tests.core.support import ThreadMixin
 from kinto.core.utils import decode_header
 
+from . import BaseTest
+from .. import support
 
-class SinceModifiedTest(ThreadMixin, BaseTest):
+
+class SinceModifiedTest(support.ThreadMixin, BaseTest):
 
     def setUp(self):
         super(SinceModifiedTest, self).setUp()
diff --git a/kinto/tests/core/resource/test_views.py b/tests/core/resource/test_views.py
similarity index 99%
rename from kinto/tests/core/resource/test_views.py
rename to tests/core/resource/test_views.py
--- a/kinto/tests/core/resource/test_views.py
+++ b/tests/core/resource/test_views.py
@@ -3,7 +3,9 @@
 
 from kinto.core.storage import exceptions as storage_exceptions
 from kinto.core.errors import ERRORS
-from kinto.tests.core.support import unittest, BaseWebTest
+from kinto.core.testing import unittest
+
+from ..support import BaseWebTest
 
 
 MINIMALIST_RECORD = {'name': 'Champignon'}
@@ -716,7 +718,7 @@ def test_backend_errors_original_error_is_logged(self):
 
 
 class PaginationNextURLTest(BaseWebTest, unittest.TestCase):
-    """Extra tests for `kinto.tests.core.resource.test_pagination`
+    """Extra tests for `tests.core.resource.test_pagination`
     """
 
     def setUp(self):
@@ -756,7 +758,7 @@ def test_next_page_url_relies_on_headers_information(self):
 
 
 class SchemaLessPartialResponseTest(BaseWebTest, unittest.TestCase):
-    """Extra tests for :mod:`kinto.tests.core.resource.test_partial_response`
+    """Extra tests for :mod:`tests.core.resource.test_partial_response`
     """
     collection_url = '/spores'
 
diff --git a/kinto/tests/core/resource/test_views_cors.py b/tests/core/resource/test_views_cors.py
similarity index 98%
rename from kinto/tests/core/resource/test_views_cors.py
rename to tests/core/resource/test_views_cors.py
--- a/kinto/tests/core/resource/test_views_cors.py
+++ b/tests/core/resource/test_views_cors.py
@@ -1,7 +1,9 @@
 import mock
 from pyramid import httpexceptions
 
-from kinto.tests.core.support import (unittest, BaseWebTest)
+from kinto.core.testing import unittest
+
+from ..support import BaseWebTest
 
 
 MINIMALIST_RECORD = {'name': 'Champignon'}
@@ -94,8 +96,7 @@ def test_present_on_invalid_record_creation(self):
         self.assertIn('Access-Control-Allow-Origin', response.headers)
 
     def test_present_on_readonly_update(self):
-        with mock.patch('kinto.tests.core.testapp.views.'
-                        'MushroomSchema.is_readonly',
+        with mock.patch('tests.core.testapp.views.MushroomSchema.is_readonly',
                         return_value=True):
             body = {'data': {'name': 'Amanite'}}
             response = self.app.patch_json(self.get_item_url(),
diff --git a/kinto/tests/core/resource/test_viewset.py b/tests/core/resource/test_viewset.py
similarity index 99%
rename from kinto/tests/core/resource/test_viewset.py
rename to tests/core/resource/test_viewset.py
--- a/kinto/tests/core/resource/test_viewset.py
+++ b/tests/core/resource/test_viewset.py
@@ -5,7 +5,7 @@
 
 from kinto.core import authorization, DEFAULT_SETTINGS
 from kinto.core.resource import ViewSet, ShareableViewSet, register_resource
-from kinto.tests.core.support import unittest
+from kinto.core.testing import unittest
 
 
 class FakeViewSet(ViewSet):
diff --git a/kinto/tests/core/schema/postgresql-storage-1.6.sql b/tests/core/schema/postgresql-storage-1.6.sql
similarity index 100%
rename from kinto/tests/core/schema/postgresql-storage-1.6.sql
rename to tests/core/schema/postgresql-storage-1.6.sql
diff --git a/tests/core/support.py b/tests/core/support.py
new file mode 100644
--- /dev/null
+++ b/tests/core/support.py
@@ -0,0 +1,101 @@
+import mock
+import threading
+import functools
+
+from pyramid.security import IAuthorizationPolicy, Authenticated, Everyone
+from zope.interface import implementer
+
+from kinto.core.authorization import PRIVATE
+from kinto.core import testing
+
+from .testapp import main as testapp
+
+
+# This is the principal a connected user should have (in the tests).
+USER_PRINCIPAL = 'basicauth:8a931a10fc88ab2f6d1cc02a07d3a81b5d4768f6f13e85c5' \
+                 'd8d4180419acb1b4'
+
+
+class BaseWebTest(testing.BaseWebTest):
+    """Base Web Test to test your cornice service.
+
+    It setups the database before each test and delete it after.
+    """
+
+    api_prefix = "v0"
+    entry_point = testapp
+    principal = USER_PRINCIPAL
+
+    authorization_policy = 'tests.core.support.AllowAuthorizationPolicy'
+    collection_url = '/mushrooms'
+
+    def __init__(self, *args, **kwargs):
+        super(BaseWebTest, self).__init__(*args, **kwargs)
+        self.headers.update(testing.get_user_headers('mat'))
+
+    def get_app_settings(self, extras=None):
+        if extras is None:
+            extras = {}
+        extras.setdefault('project_name', 'myapp')
+        extras.setdefault('project_version', '0.0.1')
+        extras.setdefault('project_docs', 'https://kinto.readthedocs.io/')
+        extras.setdefault('multiauth.authorization_policy',
+                          self.authorization_policy)
+        return super(BaseWebTest, self).get_app_settings(extras)
+
+    def get_item_url(self, id=None):
+        """Return the URL of the item using self.item_url."""
+        if id is None:
+            id = self.record['id']
+        return self.collection_url + '/' + str(id)
+
+
+class ThreadMixin(object):
+
+    def setUp(self):
+        super(ThreadMixin, self).setUp()
+        self._threads = []
+
+    def tearDown(self):
+        super(ThreadMixin, self).tearDown()
+
+        for thread in self._threads:
+            thread.join()
+
+    def _create_thread(self, *args, **kwargs):
+        thread = threading.Thread(*args, **kwargs)
+        self._threads.append(thread)
+        return thread
+
+
+@implementer(IAuthorizationPolicy)
+class AllowAuthorizationPolicy(object):
+    def permits(self, context, principals, permission):
+        if permission == PRIVATE:
+            return Authenticated in principals
+        if Everyone in principals:
+            return True
+        # Kinto-Core default authz policy uses prefixed_userid.
+        prefixed = [context.prefixed_userid]
+        return USER_PRINCIPAL in (principals + prefixed)
+
+    def principals_allowed_by_permission(self, context, permission):
+        raise NotImplementedError()  # PRAGMA NOCOVER
+
+
+def authorize(permits=True, authz_class=None):
+    """Patch the default authorization policy to return what is specified
+    in :param:permits.
+    """
+    if authz_class is None:
+        authz_class = 'tests.core.support.AllowAuthorizationPolicy'
+
+    def wrapper(f):
+        @functools.wraps(f)
+        def wrapped(*args, **kwargs):
+            with mock.patch(
+                    '%s.permits' % authz_class,
+                    return_value=permits):
+                return f(*args, **kwargs)
+        return wrapped
+    return wrapper
diff --git a/kinto/tests/core/test_authentication.py b/tests/core/test_authentication.py
similarity index 97%
rename from kinto/tests/core/test_authentication.py
rename to tests/core/test_authentication.py
--- a/kinto/tests/core/test_authentication.py
+++ b/tests/core/test_authentication.py
@@ -3,8 +3,9 @@
 
 from kinto.core import authentication
 from kinto.core import utils
+from kinto.core.testing import DummyRequest, unittest
 
-from .support import BaseWebTest, DummyRequest, unittest
+from .support import BaseWebTest
 
 
 class AuthenticationPoliciesTest(BaseWebTest, unittest.TestCase):
@@ -31,7 +32,7 @@ def test_views_are_forbidden_if_unknown_auth_method(self):
         app.get(self.collection_url, headers=self.headers, status=401)
 
     def test_principals_are_fetched_from_permission_backend(self):
-        patch = mock.patch(('kinto.tests.core.support.'
+        patch = mock.patch(('tests.core.support.'
                             'AllowAuthorizationPolicy.permits'))
         self.addCleanup(patch.stop)
         mocked = patch.start()
diff --git a/kinto/tests/core/test_authorization.py b/tests/core/test_authorization.py
similarity index 99%
rename from kinto/tests/core/test_authorization.py
rename to tests/core/test_authorization.py
--- a/kinto/tests/core/test_authorization.py
+++ b/tests/core/test_authorization.py
@@ -2,10 +2,10 @@
 
 from pyramid.request import Request
 
-from .support import DummyRequest, unittest
 from kinto.core import authentication
 from kinto.core.authorization import RouteFactory, AuthorizationPolicy
 from kinto.core.storage import exceptions as storage_exceptions
+from kinto.core.testing import DummyRequest, unittest
 
 
 class RouteFactoryTest(unittest.TestCase):
diff --git a/kinto/tests/core/test_cache.py b/tests/core/test_cache.py
similarity index 99%
rename from kinto/tests/core/test_cache.py
rename to tests/core/test_cache.py
--- a/kinto/tests/core/test_cache.py
+++ b/tests/core/test_cache.py
@@ -1,4 +1,5 @@
 import mock
+import unittest
 import time
 
 from pyramid import testing
@@ -10,7 +11,7 @@
                               postgresql as postgresql_backend,
                               heartbeat)
 
-from .support import unittest, skip_if_no_postgresql
+from kinto.core.testing import skip_if_no_postgresql
 
 
 class CacheBaseTest(unittest.TestCase):
diff --git a/kinto/tests/core/test_deprecation.py b/tests/core/test_deprecation.py
similarity index 96%
rename from kinto/tests/core/test_deprecation.py
rename to tests/core/test_deprecation.py
--- a/kinto/tests/core/test_deprecation.py
+++ b/tests/core/test_deprecation.py
@@ -2,9 +2,11 @@
 
 import mock
 
-from .support import BaseWebTest, unittest
 from kinto.core.errors import ERRORS
 from kinto.core.utils import json
+from kinto.core.testing import unittest
+
+from .support import BaseWebTest
 
 
 class DeprecationTest(BaseWebTest, unittest.TestCase):
diff --git a/kinto/tests/core/test_errors.py b/tests/core/test_errors.py
similarity index 95%
rename from kinto/tests/core/test_errors.py
rename to tests/core/test_errors.py
--- a/kinto/tests/core/test_errors.py
+++ b/tests/core/test_errors.py
@@ -1,9 +1,9 @@
 import json
+import unittest
 
 from kinto.core.errors import send_alert
 from kinto.core.utils import decode_header
-
-from .support import unittest, DummyRequest
+from kinto.core.testing import DummyRequest
 
 
 class SendAlertTest(unittest.TestCase):
diff --git a/kinto/tests/core/test_initialization.py b/tests/core/test_initialization.py
similarity index 99%
rename from kinto/tests/core/test_initialization.py
rename to tests/core/test_initialization.py
--- a/kinto/tests/core/test_initialization.py
+++ b/tests/core/test_initialization.py
@@ -8,7 +8,7 @@
 
 import kinto.core
 from kinto.core import initialization
-from .support import unittest
+from kinto.core.testing import unittest
 
 
 class InitializationTest(unittest.TestCase):
@@ -441,7 +441,7 @@ def make_app(self):
         config = Configurator(settings=kinto.core.DEFAULT_SETTINGS)
         config.add_settings({
             'permission_backend': 'kinto.core.permission.memory',
-            'includes': 'kinto.tests.core.testplugin'
+            'includes': 'tests.core.testplugin'
         })
         kinto.core.initialize(config, '0.0.1', 'name')
         return webtest.TestApp(config.make_wsgi_app())
diff --git a/kinto/tests/core/test_listeners.py b/tests/core/test_listeners.py
similarity index 87%
rename from kinto/tests/core/test_listeners.py
rename to tests/core/test_listeners.py
--- a/kinto/tests/core/test_listeners.py
+++ b/tests/core/test_listeners.py
@@ -6,8 +6,8 @@
 from kinto.core import initialization
 from kinto.core.events import ResourceChanged, ResourceRead, ACTIONS
 from kinto.core.listeners import ListenerBase
+from kinto.core.testing import unittest
 
-from .support import unittest
 
 UID = str(uuid.uuid4())
 
@@ -36,13 +36,13 @@ class Request(object):
 
 class ListenerSetupTest(unittest.TestCase):
     def setUp(self):
-        demo_patch = mock.patch('kinto.tests.core.listeners.load_from_config')
+        demo_patch = mock.patch('tests.core.listeners.load_from_config')
         self.addCleanup(demo_patch.stop)
         self.demo_mocked = demo_patch.start()
 
     def make_app(self, extra_settings={}):
         settings = {
-            'event_listeners': 'kinto.tests.core.listeners',
+            'event_listeners': 'tests.core.listeners',
         }
         settings.update(**extra_settings)
         config = testing.setUp(settings=settings)
@@ -53,21 +53,21 @@ def make_app(self, extra_settings={}):
     def test_listener_module_is_specified_via_settings(self):
         self.make_app({
             'event_listeners': 'demo',
-            'event_listeners.demo.use': 'kinto.tests.core.listeners',
+            'event_listeners.demo.use': 'tests.core.listeners',
         })
         self.assertTrue(self.demo_mocked.called)
 
     def test_listener_module_can_be_specified_via_listeners_list(self):
         self.make_app({
             'event_listeners': 'demo',
-            'event_listeners.demo.use': 'kinto.tests.core.listeners',
+            'event_listeners.demo.use': 'tests.core.listeners',
         })
         self.assertTrue(self.demo_mocked.called)
 
     def test_callback_called_when_action_is_not_filtered(self):
         config = self.make_app({
             'event_listeners': 'demo',
-            'event_listeners.demo.use': 'kinto.tests.core.listeners',
+            'event_listeners.demo.use': 'tests.core.listeners',
         })
         ev = ResourceChanged({'action': ACTIONS.CREATE.value}, [], Request())
         config.registry.notify(ev)
@@ -77,7 +77,7 @@ def test_callback_called_when_action_is_not_filtered(self):
     def test_callback_is_not_called_when_action_is_filtered(self):
         config = self.make_app({
             'event_listeners': 'demo',
-            'event_listeners.demo.use': 'kinto.tests.core.listeners',
+            'event_listeners.demo.use': 'tests.core.listeners',
             'event_listeners.demo.actions': 'delete',
         })
         ev = ResourceChanged({'action': ACTIONS.CREATE.value}, [], Request())
@@ -88,7 +88,7 @@ def test_callback_is_not_called_when_action_is_filtered(self):
     def test_callback_called_when_resource_is_not_filtered(self):
         config = self.make_app({
             'event_listeners': 'demo',
-            'event_listeners.demo.use': 'kinto.tests.core.listeners',
+            'event_listeners.demo.use': 'tests.core.listeners',
         })
         event = ResourceChanged({'action': ACTIONS.CREATE.value,
                                  'resource_name': 'mushroom'}, [], Request())
@@ -99,7 +99,7 @@ def test_callback_called_when_resource_is_not_filtered(self):
     def test_callback_is_not_called_when_resource_is_filtered(self):
         config = self.make_app({
             'event_listeners': 'demo',
-            'event_listeners.demo.use': 'kinto.tests.core.listeners',
+            'event_listeners.demo.use': 'tests.core.listeners',
             'event_listeners.demo.resources': 'toad',
         })
         event = ResourceChanged({'action': ACTIONS.CREATE.value,
@@ -111,7 +111,7 @@ def test_callback_is_not_called_when_resource_is_filtered(self):
     def test_callback_is_not_called_on_read_by_default(self):
         config = self.make_app({
             'event_listeners': 'demo',
-            'event_listeners.demo.use': 'kinto.tests.core.listeners',
+            'event_listeners.demo.use': 'tests.core.listeners',
         })
         event = ResourceRead({'action': ACTIONS.READ.value}, [], Request())
         config.registry.notify(event)
@@ -121,7 +121,7 @@ def test_callback_is_not_called_on_read_by_default(self):
     def test_callback_is_called_on_read_if_specified(self):
         config = self.make_app({
             'event_listeners': 'demo',
-            'event_listeners.demo.use': 'kinto.tests.core.listeners',
+            'event_listeners.demo.use': 'tests.core.listeners',
             'event_listeners.demo.actions': 'read',
         })
         event = ResourceRead({'action': ACTIONS.READ.value}, [], Request())
@@ -132,7 +132,7 @@ def test_callback_is_called_on_read_if_specified(self):
     def test_same_callback_is_called_for_read_and_write_specified(self):
         config = self.make_app({
             'event_listeners': 'demo',
-            'event_listeners.demo.use': 'kinto.tests.core.listeners',
+            'event_listeners.demo.use': 'tests.core.listeners',
             'event_listeners.demo.actions': 'read create delete',
         })
         ev = ResourceRead({'action': ACTIONS.READ.value}, [], Request())
@@ -145,7 +145,7 @@ def test_same_callback_is_called_for_read_and_write_specified(self):
     def test_loading_can_read_configuration_from_environment(self):
         environ = {
             "KINTO_EVENT_LISTENERS": "kvstore",
-            "KINTO_EVENT_LISTENERS_KVSTORE_USE": "kinto.tests.core.listeners",
+            "KINTO_EVENT_LISTENERS_KVSTORE_USE": "tests.core.listeners",
             "KINTO_EVENT_LISTENERS_KVSTORE_URL": "demo://demo:6379/0",
             "KINTO_EVENT_LISTENERS_KVSTORE_POOL_SIZE": "5",
             "KINTO_EVENT_LISTENERS_KVSTORE_LISTNAME": "queue",
diff --git a/kinto/tests/core/test_logging.py b/tests/core/test_logging.py
similarity index 99%
rename from kinto/tests/core/test_logging.py
rename to tests/core/test_logging.py
--- a/kinto/tests/core/test_logging.py
+++ b/tests/core/test_logging.py
@@ -11,8 +11,9 @@
 from kinto.core import initialization
 from kinto.core import logs as core_logs
 from kinto.core.utils import json
+from kinto.core.testing import unittest
 
-from .support import BaseWebTest, unittest
+from .support import BaseWebTest
 
 
 def logger_context():
diff --git a/kinto/tests/core/test_permission.py b/tests/core/test_permission.py
similarity index 99%
rename from kinto/tests/core/test_permission.py
rename to tests/core/test_permission.py
--- a/kinto/tests/core/test_permission.py
+++ b/tests/core/test_permission.py
@@ -1,4 +1,5 @@
 import mock
+import unittest
 
 from pyramid import testing
 
@@ -8,7 +9,7 @@
                                    memory as memory_backend,
                                    postgresql as postgresql_backend, heartbeat)
 
-from .support import unittest, skip_if_no_postgresql, DummyRequest
+from kinto.core.testing import skip_if_no_postgresql, DummyRequest
 
 
 class PermissionBaseTest(unittest.TestCase):
diff --git a/kinto/tests/core/test_scripts.py b/tests/core/test_scripts.py
similarity index 99%
rename from kinto/tests/core/test_scripts.py
rename to tests/core/test_scripts.py
--- a/kinto/tests/core/test_scripts.py
+++ b/tests/core/test_scripts.py
@@ -2,7 +2,7 @@
 
 from kinto.core import scripts
 from kinto.core.storage.exceptions import RecordNotFoundError
-from .support import unittest
+from kinto.core.testing import unittest
 
 
 class InitSchemaTest(unittest.TestCase):
diff --git a/kinto/tests/core/test_statsd.py b/tests/core/test_statsd.py
similarity index 98%
rename from kinto/tests/core/test_statsd.py
rename to tests/core/test_statsd.py
--- a/kinto/tests/core/test_statsd.py
+++ b/tests/core/test_statsd.py
@@ -2,9 +2,11 @@
 
 from pyramid import testing
 
-from kinto.tests.core.support import unittest, BaseWebTest
+from kinto.core.testing import unittest
 from kinto.core import statsd
 
+from .support import BaseWebTest
+
 
 class TestedClass(object):
     attribute = 3.14
diff --git a/kinto/tests/core/test_storage.py b/tests/core/test_storage.py
similarity index 99%
rename from kinto/tests/core/test_storage.py
rename to tests/core/test_storage.py
--- a/kinto/tests/core/test_storage.py
+++ b/tests/core/test_storage.py
@@ -10,9 +10,10 @@
     exceptions, Filter, generators, memory, postgresql,
     Sort, StorageBase, heartbeat
 )
+from kinto.core.testing import (unittest, DummyRequest,
+                                skip_if_travis, skip_if_no_postgresql)
 
-from .support import (unittest, ThreadMixin, DummyRequest,
-                      skip_if_travis, skip_if_no_postgresql)
+from .support import ThreadMixin
 
 
 RECORD_ID = '472be9ec-26fe-461b-8282-9c4e4b207ab3'
diff --git a/kinto/tests/core/test_storage_migrations.py b/tests/core/test_storage_migrations.py
similarity index 99%
rename from kinto/tests/core/test_storage_migrations.py
rename to tests/core/test_storage_migrations.py
--- a/kinto/tests/core/test_storage_migrations.py
+++ b/tests/core/test_storage_migrations.py
@@ -1,4 +1,5 @@
 import os
+import unittest
 
 import mock
 import six
@@ -8,8 +9,7 @@
 from kinto.core.permission import postgresql as postgresql_permission
 from kinto.core.storage import postgresql as postgresql_storage
 from kinto.core.utils import json
-
-from .support import unittest, skip_if_no_postgresql
+from kinto.core.testing import skip_if_no_postgresql
 
 
 @skip_if_no_postgresql
diff --git a/kinto/tests/core/test_storage_pool.py b/tests/core/test_storage_pool.py
similarity index 97%
rename from kinto/tests/core/test_storage_pool.py
rename to tests/core/test_storage_pool.py
--- a/kinto/tests/core/test_storage_pool.py
+++ b/tests/core/test_storage_pool.py
@@ -1,9 +1,10 @@
 import time
 import threading
+import unittest
 
 from pyramid import testing
 
-from .support import unittest, skip_if_no_postgresql
+from kinto.core.testing import skip_if_no_postgresql
 
 
 @skip_if_no_postgresql
diff --git a/kinto/tests/core/test_utils.py b/tests/core/test_utils.py
similarity index 99%
rename from kinto/tests/core/test_utils.py
rename to tests/core/test_utils.py
--- a/kinto/tests/core/test_utils.py
+++ b/tests/core/test_utils.py
@@ -1,5 +1,6 @@
 # -*- coding: utf-8 -*-
 from __future__ import unicode_literals
+import unittest
 import os
 
 import colander
@@ -16,8 +17,7 @@
     current_service, encode_header, decode_header, follow_subrequest,
     build_request, dict_subset
 )
-
-from .support import unittest, DummyRequest
+from kinto.core.testing import DummyRequest
 
 
 def build_real_request(wsgi_environ):
diff --git a/kinto/tests/core/test_views_batch.py b/tests/core/test_views_batch.py
similarity index 99%
rename from kinto/tests/core/test_views_batch.py
rename to tests/core/test_views_batch.py
--- a/kinto/tests/core/test_views_batch.py
+++ b/tests/core/test_views_batch.py
@@ -2,13 +2,16 @@
 import colander
 import mock
 import uuid
+import unittest
 
 from pyramid.response import Response
 
 from kinto.core.views.batch import BatchPayloadSchema, batch as batch_service
-from kinto.tests.core.support import BaseWebTest, unittest, DummyRequest
+from kinto.core.testing import DummyRequest
 from kinto.core.utils import json
 
+from .support import BaseWebTest
+
 
 class BatchViewTest(BaseWebTest, unittest.TestCase):
 
diff --git a/kinto/tests/core/test_views_errors.py b/tests/core/test_views_errors.py
similarity index 89%
rename from kinto/tests/core/test_views_errors.py
rename to tests/core/test_views_errors.py
--- a/kinto/tests/core/test_views_errors.py
+++ b/tests/core/test_views_errors.py
@@ -1,9 +1,12 @@
+import unittest
 import mock
+
 from pyramid import httpexceptions
 
 from kinto.core.errors import ERRORS, http_error
+from kinto.core.testing import FormattedErrorMixin
 
-from .support import BaseWebTest, unittest, authorize, FormattedErrorMixin
+from .support import BaseWebTest, authorize
 
 
 class ErrorViewTest(FormattedErrorMixin, BaseWebTest, unittest.TestCase):
@@ -52,9 +55,8 @@ def test_404_can_be_overridded(self):
         custom_404 = http_error(httpexceptions.HTTPNotFound(),
                                 errno=ERRORS.MISSING_RESOURCE,
                                 message="Customized.")
-        with mock.patch(
-                'kinto.tests.core.testapp.views.Mushroom._extract_filters',
-                side_effect=custom_404):
+        with mock.patch('tests.core.testapp.views.Mushroom._extract_filters',
+                        side_effect=custom_404):
             response = self.app.get(self.sample_url, headers=self.headers,
                                     status=404)
         self.assertFormattedError(
@@ -79,9 +81,8 @@ def test_403_can_be_overridded(self):
         custom_403 = http_error(httpexceptions.HTTPForbidden(),
                                 errno=ERRORS.FORBIDDEN,
                                 message="Customized.")
-        with mock.patch(
-                'kinto.tests.core.testapp.views.Mushroom._extract_filters',
-                side_effect=custom_403):
+        with mock.patch('tests.core.testapp.views.Mushroom._extract_filters',
+                        side_effect=custom_403):
             response = self.app.get(self.sample_url,
                                     headers=self.headers, status=403)
         self.assertFormattedError(
@@ -99,9 +100,8 @@ def test_405_can_have_custom_message(self):
         custom_405 = http_error(httpexceptions.HTTPMethodNotAllowed(),
                                 errno=ERRORS.METHOD_NOT_ALLOWED,
                                 message="Disabled from conf.")
-        with mock.patch(
-                'kinto.tests.core.testapp.views.Mushroom._extract_filters',
-                side_effect=custom_405):
+        with mock.patch('tests.core.testapp.views.Mushroom._extract_filters',
+                        side_effect=custom_405):
             response = self.app.get(self.sample_url,
                                     headers=self.headers, status=405)
         self.assertFormattedError(
@@ -109,9 +109,8 @@ def test_405_can_have_custom_message(self):
             "Disabled from conf.")
 
     def test_500_is_valid_formatted_error(self):
-        with mock.patch(
-                'kinto.tests.core.testapp.views.Mushroom._extract_filters',
-                side_effect=ValueError):
+        with mock.patch('tests.core.testapp.views.Mushroom._extract_filters',
+                        side_effect=ValueError):
             response = self.app.get(self.sample_url,
                                     headers=self.headers, status=500)
         self.assertFormattedError(
@@ -126,9 +125,8 @@ def test_400_with_invalid_url_path(self):
             "Invalid URL path.")
 
     def test_info_link_in_error_responses_can_be_configured(self):
-        with mock.patch(
-                'kinto.tests.core.testapp.views.Mushroom._extract_filters',
-                side_effect=ValueError):
+        with mock.patch('tests.core.testapp.views.Mushroom._extract_filters',
+                        side_effect=ValueError):
             link = "https://github.com/mozilla-services/kinto/issues/"
             app = self.make_app({'error_info_link': link,
                                  'readonly': False})
@@ -140,9 +138,8 @@ def test_info_link_in_error_responses_can_be_configured(self):
             link)
 
     def test_503_is_valid_formatted_error(self):
-        with mock.patch(
-                'kinto.tests.core.testapp.views.Mushroom._extract_filters',
-                side_effect=httpexceptions.HTTPServiceUnavailable):
+        with mock.patch('tests.core.testapp.views.Mushroom._extract_filters',
+                        side_effect=httpexceptions.HTTPServiceUnavailable):
             response = self.app.get(self.sample_url,
                                     headers=self.headers, status=503)
         self.assertFormattedError(
@@ -155,9 +152,8 @@ def test_503_can_have_custom_message(self):
         custom_503 = http_error(httpexceptions.HTTPServiceUnavailable(),
                                 errno=ERRORS.BACKEND,
                                 message="Unable to connect the server.")
-        with mock.patch(
-                'kinto.tests.core.testapp.views.Mushroom._extract_filters',
-                side_effect=custom_503):
+        with mock.patch('tests.core.testapp.views.Mushroom._extract_filters',
+                        side_effect=custom_503):
             response = self.app.get(self.sample_url,
                                     headers=self.headers, status=503)
         self.assertFormattedError(
@@ -166,9 +162,8 @@ def test_503_can_have_custom_message(self):
 
     def test_500_provides_traceback_on_server(self):
         mock_traceback = mock.patch('logging.traceback.print_exception')
-        with mock.patch(
-                'kinto.tests.core.testapp.views.Mushroom._extract_filters',
-                side_effect=ValueError):
+        with mock.patch('tests.core.testapp.views.Mushroom._extract_filters',
+                        side_effect=ValueError):
             with mock_traceback as mocked_traceback:
                 self.app.get(self.sample_url, headers=self.headers, status=500)
                 self.assertTrue(mocked_traceback.called)
diff --git a/kinto/tests/core/test_views_heartbeat.py b/tests/core/test_views_heartbeat.py
similarity index 96%
rename from kinto/tests/core/test_views_heartbeat.py
rename to tests/core/test_views_heartbeat.py
--- a/kinto/tests/core/test_views_heartbeat.py
+++ b/tests/core/test_views_heartbeat.py
@@ -1,6 +1,8 @@
 import mock
 
-from .support import BaseWebTest, unittest
+from kinto.core.testing import unittest
+
+from .support import BaseWebTest
 
 
 class SuccessTest(BaseWebTest, unittest.TestCase):
diff --git a/kinto/tests/core/test_views_hello.py b/tests/core/test_views_hello.py
similarity index 98%
rename from kinto/tests/core/test_views_hello.py
rename to tests/core/test_views_hello.py
--- a/kinto/tests/core/test_views_hello.py
+++ b/tests/core/test_views_hello.py
@@ -1,7 +1,9 @@
 import mock
 from pyramid import testing
 
-from .support import BaseWebTest, unittest
+from kinto.core.testing import unittest
+
+from .support import BaseWebTest
 
 
 class HelloViewTest(BaseWebTest, unittest.TestCase):
diff --git a/kinto/tests/core/test_views_transaction.py b/tests/core/test_views_transaction.py
similarity index 98%
rename from kinto/tests/core/test_views_transaction.py
rename to tests/core/test_views_transaction.py
--- a/kinto/tests/core/test_views_transaction.py
+++ b/tests/core/test_views_transaction.py
@@ -1,4 +1,5 @@
 import mock
+import unittest
 
 from pyramid import testing
 from pyramid import httpexceptions
@@ -6,8 +7,9 @@
 from kinto.core.storage.exceptions import BackendError
 from kinto.core.utils import sqlalchemy
 from kinto.core import events
-from .support import (BaseWebTest, unittest, skip_if_no_postgresql,
-                      USER_PRINCIPAL)
+from kinto.core.testing import skip_if_no_postgresql
+
+from .support import BaseWebTest, USER_PRINCIPAL
 
 
 class PostgreSQLTest(BaseWebTest):
diff --git a/kinto/tests/core/test_views_version.py b/tests/core/test_views_version.py
similarity index 92%
rename from kinto/tests/core/test_views_version.py
rename to tests/core/test_views_version.py
--- a/kinto/tests/core/test_views_version.py
+++ b/tests/core/test_views_version.py
@@ -1,5 +1,7 @@
 from kinto.core.views import version
-from .support import BaseWebTest, unittest
+from kinto.core.testing import unittest
+
+from .support import BaseWebTest
 
 
 class VersionViewTest(BaseWebTest, unittest.TestCase):
diff --git a/kinto/tests/core/testapp/__init__.py b/tests/core/testapp/__init__.py
similarity index 71%
rename from kinto/tests/core/testapp/__init__.py
rename to tests/core/testapp/__init__.py
--- a/kinto/tests/core/testapp/__init__.py
+++ b/tests/core/testapp/__init__.py
@@ -1,24 +1,29 @@
+import os
+
 from pyramid.config import Configurator
 from pyramid.static import static_view
+
 import kinto.core
 
+here = os.path.dirname(__file__)
+
 
 def includeme(config):
-    config.scan("kinto.tests.core.testapp.views")
+    config.scan("tests.core.testapp.views")
 
     # Add an example route with trailing slash (here to serve static files).
     # This is only used to test 404 redirection in ``test_views_errors.py``
-    static = static_view('kinto:tests/core/testapp/static', use_subpath=True)
+    abs_path = os.path.join(here, 'static')
+    static = static_view(abs_path, use_subpath=True)
     config.add_route('catchall_static', '/static/*subpath')
     config.add_view(static, route_name="catchall_static")
 
 
-def main(settings=None, config=None, *args, **additional_settings):
-    if settings is None:
-        settings = {}
-    settings.update(additional_settings)
+def main(global_config, config=None, **settings):
+    global_config.update(settings)
     if config is None:
         config = Configurator(settings=settings)
+
     kinto.core.initialize(config, version='0.0.1')
     config.include(includeme)
     app = config.make_wsgi_app()
diff --git a/kinto/tests/core/testapp/static/index.html b/tests/core/testapp/static/index.html
similarity index 100%
rename from kinto/tests/core/testapp/static/index.html
rename to tests/core/testapp/static/index.html
diff --git a/kinto/tests/core/testapp/views.py b/tests/core/testapp/views.py
similarity index 100%
rename from kinto/tests/core/testapp/views.py
rename to tests/core/testapp/views.py
diff --git a/kinto/tests/core/testplugin/__init__.py b/tests/core/testplugin/__init__.py
similarity index 100%
rename from kinto/tests/core/testplugin/__init__.py
rename to tests/core/testplugin/__init__.py
diff --git a/tests/plugins/__init__.py b/tests/plugins/__init__.py
new file mode 100644
diff --git a/kinto/plugins/default_bucket/test_plugin.py b/tests/plugins/test_default_bucket.py
similarity index 94%
rename from kinto/plugins/default_bucket/test_plugin.py
rename to tests/plugins/test_default_bucket.py
--- a/kinto/plugins/default_bucket/test_plugin.py
+++ b/tests/plugins/test_default_bucket.py
@@ -1,4 +1,5 @@
 import mock
+import unittest
 from six import text_type
 from uuid import UUID
 
@@ -6,15 +7,21 @@
 
 from kinto.core.errors import ERRORS, http_error
 from kinto.core.storage import exceptions as storage_exceptions
-from kinto.tests.core.support import FormattedErrorMixin
+from kinto.core.testing import get_user_headers, FormattedErrorMixin
 from kinto.core.utils import hmac_digest
 
-from kinto.tests.support import (BaseWebTest, unittest, get_user_headers,
-                                 MINIMALIST_RECORD)
+from ..support import BaseWebTest, MINIMALIST_RECORD
 
 
-class DefaultBucketViewTest(FormattedErrorMixin, BaseWebTest,
-                            unittest.TestCase):
+class DefaultBucketWebTest(BaseWebTest, unittest.TestCase):
+
+    def get_app_settings(self, extras=None):
+        settings = super(DefaultBucketWebTest, self).get_app_settings(extras)
+        settings['includes'] = 'kinto.plugins.default_bucket'
+        return settings
+
+
+class DefaultBucketViewTest(FormattedErrorMixin, DefaultBucketWebTest):
 
     bucket_url = '/buckets/default'
     collection_url = '/buckets/default/collections/tasks'
@@ -215,12 +222,12 @@ def test_formatted_error_are_passed_through(self):
             self.assertEqual(resp.body, response.body)
 
 
-class HelloViewTest(BaseWebTest, unittest.TestCase):
+class HelloViewTest(DefaultBucketWebTest):
 
     def test_returns_bucket_id_and_url_if_authenticated(self):
         response = self.app.get('/', headers=self.headers)
         self.assertEqual(response.json['user']['bucket'],
-                         '23bb0efc-e80d-829e-6757-79d41e16640f')
+                         'ddaf8694-fa9e-2949-ed0e-77198a7907fb')
 
     def test_flush_capability_if_enabled(self):
         resp = self.app.get('/')
@@ -238,16 +245,17 @@ def listener(event):
     return listener
 
 
-class EventsTest(BaseWebTest, unittest.TestCase):
+class EventsTest(DefaultBucketWebTest):
     def tearDown(self):
         super(EventsTest, self).tearDown()
         del _events[:]
 
-    def get_app_settings(self, extra=None):
-        settings = super(EventsTest, self).get_app_settings(extra)
+    def get_app_settings(self, extras=None):
+        settings = super(EventsTest, self).get_app_settings(extras)
+        settings = settings.copy()
         settings['event_listeners'] = 'testevent',
         settings['event_listeners.testevent.use'] = (
-            'kinto.plugins.default_bucket.test_plugin')
+            'tests.plugins.test_default_bucket')
         return settings
 
     def test_an_event_is_sent_on_implicit_bucket_creation(self):
@@ -299,7 +307,7 @@ def test_events_sent_on_bucket_and_collection_creation(self):
                                                                 bucket_id)
 
 
-class ReadonlyDefaultBucket(BaseWebTest, unittest.TestCase):
+class ReadonlyDefaultBucket(DefaultBucketWebTest):
 
     def get_app_settings(self, extras=None):
         settings = super(ReadonlyDefaultBucket, self).get_app_settings(extras)
@@ -310,7 +318,7 @@ def test_implicit_creation_is_rejected(self):
         self.app.get('/buckets/default', headers=self.headers, status=405)
 
 
-class BackendErrorTest(BaseWebTest, unittest.TestCase):
+class BackendErrorTest(DefaultBucketWebTest):
     def setUp(self):
         super(BackendErrorTest, self).setUp()
         self.patcher = mock.patch.object(
diff --git a/kinto/plugins/history/test_plugin.py b/tests/plugins/test_history.py
similarity index 93%
rename from kinto/plugins/history/test_plugin.py
rename to tests/plugins/test_history.py
--- a/kinto/plugins/history/test_plugin.py
+++ b/tests/plugins/test_history.py
@@ -1,13 +1,17 @@
+import unittest
+
 import mock
 import re
 
-from kinto.tests.support import (BaseWebTest, unittest, get_user_headers)
+from kinto.core.testing import get_user_headers
+
+from .. import support
 
 
-class HistoryWebTest(BaseWebTest, unittest.TestCase):
+class HistoryWebTest(support.BaseWebTest, unittest.TestCase):
 
-    def get_app_settings(self, extra=None):
-        settings = super(HistoryWebTest, self).get_app_settings(extra)
+    def get_app_settings(self, extras=None):
+        settings = super(HistoryWebTest, self).get_app_settings(extras)
         settings['includes'] = 'kinto.plugins.history'
         return settings
 
@@ -59,7 +63,7 @@ def test_only_collection_endpoint_is_available(self):
     def test_tracks_user_and_date(self):
         resp = self.app.get(self.history_uri, headers=self.headers)
         entry = resp.json['data'][-1]
-        assert entry['user_id'].startswith('basicauth:3a0c56')
+        assert entry['user_id'] == self.principal
         assert re.match('^\d{4}\-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{6}',
                         entry['date'])
 
@@ -347,8 +351,8 @@ def test_delete_on_collection(self):
 
 class DefaultBucketTest(HistoryWebTest):
 
-    def get_app_settings(self, extra=None):
-        settings = super(HistoryWebTest, self).get_app_settings(extra)
+    def get_app_settings(self, extras=None):
+        settings = super(HistoryWebTest, self).get_app_settings(extras)
         settings['includes'] = ('kinto.plugins.default_bucket '
                                 'kinto.plugins.history')
         return settings
@@ -379,14 +383,14 @@ def test_implicit_creations_are_listed(self):
         assert entries[2]['resource_name'] == 'bucket'
         assert entries[2]['bucket_id'] == self.bucket_id
         assert entries[2]['uri'] == bucket_uri
-        assert 'basicauth:3' in entries[2]['target']['permissions']['write'][0]
+        assert entries[2]['target']['permissions']['write'][0] == self.principal  # NOQA
 
         collection_uri = bucket_uri + '/collections/blah'
         assert entries[1]['resource_name'] == 'collection'
         assert 'bucket_id' not in entries[1]
         assert entries[1]['collection_id'] == 'blah'
         assert entries[1]['uri'] == collection_uri
-        assert 'basicauth:3' in entries[1]['target']['permissions']['write'][0]
+        assert entries[1]['target']['permissions']['write'][0] == self.principal  # NOQA
 
         record_uri = collection_uri + '/records/%s' % record['id']
         assert entries[0]['resource_name'] == 'record'
@@ -395,18 +399,23 @@ def test_implicit_creations_are_listed(self):
         assert entries[0]['record_id'] == record['id']
         assert entries[0]['uri'] == record_uri
         assert entries[0]['target']['data']['foo'] == 42
-        assert 'basicauth:3' in entries[0]['target']['permissions']['write'][0]
+        assert entries[0]['target']['permissions']['write'][0] == self.principal  # NOQA
 
 
 class PermissionsTest(HistoryWebTest):
 
     def setUp(self):
-        self.alice_headers = get_user_headers('alice:')
-        self.julia_headers = get_user_headers('julia:')
-        self.mike_headers = get_user_headers('mike:')
-        self.alice_principal = 'basicauth:845a151f1fbb0063738943a4531f8b7ef521fa488ed5ac7d077aa7ee1f349ef7'  # NOQA
-        self.julia_principal = 'basicauth:2f5fcddb299319097b9ae72f609d071d99aaf46ef9c3bc82bcc0212d14e35c4f'  # NOQA
-        self.mike_principal = 'basicauth:b3c35f5b528685cbd68b084a3aa3404f81c06e3d068000c2a109150356e45241'  # NOQA
+        self.alice_headers = get_user_headers('alice')
+        self.bob_headers = get_user_headers('bob')
+        self.julia_headers = get_user_headers('julia')
+
+        self.alice_principal = ('basicauth:d5b0026601f1b251974e09548d44155e16'
+                                '812e3c64ff7ae053fe3542e2ca1570')
+        self.bob_principal = ('basicauth:c031ced27503f788b102ca54269a062ec737'
+                              '94bb075154c74a0d4311e74ca8b6')
+        self.julia_principal = ('basicauth:d8bab8d9fe0510fcaf9b5ad5942c027fc'
+                                '2fdf80b6dc59cc3c48d12a2fcb18f1c')
+
         bucket = {
             'permissions': {
                 'read': [self.alice_principal]
@@ -419,7 +428,7 @@ def setUp(self):
         }
         record = {
             'permissions': {
-                'write': [self.mike_principal, self.alice_principal],
+                'write': [self.bob_principal, self.alice_principal],
             }
         }
         self.app.put('/buckets/author-only',
@@ -477,13 +486,13 @@ def test_collection_read_restricts_to_collection(self):
 
     def test_write_on_record_restricts_to_record(self):
         resp = self.app.get('/buckets/test/history',
-                            headers=self.mike_headers)
+                            headers=self.bob_headers)
         entries = resp.json['data']
         assert len(entries) == 2
         assert ('system.Authenticated' in
                 entries[0]['target']['permissions']['read'])
         assert entries[0]['resource_name'] == 'record'
-        assert (self.mike_principal in
+        assert (self.bob_principal in
                 entries[1]['target']['permissions']['write'])
         assert entries[1]['resource_name'] == 'record'
 
diff --git a/kinto/plugins/quotas/test_plugin.py b/tests/plugins/test_quotas.py
similarity index 95%
rename from kinto/plugins/quotas/test_plugin.py
rename to tests/plugins/test_quotas.py
--- a/kinto/plugins/quotas/test_plugin.py
+++ b/tests/plugins/test_quotas.py
@@ -1,14 +1,17 @@
 import transaction
 import pytest
+
 from kinto.core.errors import ERRORS
 from kinto.core.storage.exceptions import RecordNotFoundError
-from kinto.tests.core.support import FormattedErrorMixin, sqlalchemy
-from kinto.tests.support import BaseWebTest, unittest
-from .listener import QUOTA_RESOURCE_NAME, QUOTA_BUCKET_ID, QUOTA_COLLECTION_ID
-from .utils import record_size
+from kinto.core.testing import FormattedErrorMixin, sqlalchemy, unittest
+from kinto.plugins.quotas.listener import (
+    QUOTA_RESOURCE_NAME, QUOTA_BUCKET_ID, QUOTA_COLLECTION_ID)
+from kinto.plugins.quotas.utils import record_size
+
+from .. import support
 
 
-class QuotaWebTest(BaseWebTest, unittest.TestCase):
+class QuotaWebTest(support.BaseWebTest, unittest.TestCase):
 
     bucket_uri = '/buckets/test'
     collection_uri = '/buckets/test/collections/col'
@@ -38,8 +41,8 @@ def create_record(self):
         resp = self.app.put_json(self.record_uri, body, headers=self.headers)
         self.record = resp.json['data']
 
-    def get_app_settings(self, extra=None):
-        settings = super(QuotaWebTest, self).get_app_settings(extra)
+    def get_app_settings(self, extras=None):
+        settings = super(QuotaWebTest, self).get_app_settings(extras)
 
         # Setup the postgresql backend for transaction support.
         settings['storage_backend'] = 'kinto.core.storage.postgresql'
@@ -766,9 +769,9 @@ class QuotaMaxBytesExceededSettingsListenerTest(
 
     error_message = "Bucket maximum total size exceeded "
 
-    def get_app_settings(self, extra=None):
+    def get_app_settings(self, extras=None):
         settings = super(QuotaMaxBytesExceededSettingsListenerTest,
-                         self).get_app_settings(extra)
+                         self).get_app_settings(extras)
         settings['quotas.bucket_max_bytes'] = '150'
         return settings
 
@@ -779,9 +782,9 @@ class QuotaMaxBytesExceededBucketSettingsListenerTest(
 
     error_message = "Bucket maximum total size exceeded "
 
-    def get_app_settings(self, extra=None):
+    def get_app_settings(self, extras=None):
         settings = super(QuotaMaxBytesExceededBucketSettingsListenerTest,
-                         self).get_app_settings(extra)
+                         self).get_app_settings(extras)
         settings['quotas.bucket_test_max_bytes'] = '150'
         return settings
 
@@ -791,9 +794,9 @@ class QuotaMaxItemsExceededSettingsListenerTest(
 
     error_message = "Bucket maximum number of objects exceeded "
 
-    def get_app_settings(self, extra=None):
+    def get_app_settings(self, extras=None):
         settings = super(QuotaMaxItemsExceededSettingsListenerTest,
-                         self).get_app_settings(extra)
+                         self).get_app_settings(extras)
         settings['quotas.bucket_max_items'] = '1'
         return settings
 
@@ -803,9 +806,9 @@ class QuotaMaxItemsExceededBucketSettingsListenerTest(
 
     error_message = "Bucket maximum number of objects exceeded "
 
-    def get_app_settings(self, extra=None):
+    def get_app_settings(self, extras=None):
         settings = super(QuotaMaxItemsExceededBucketSettingsListenerTest,
-                         self).get_app_settings(extra)
+                         self).get_app_settings(extras)
         settings['quotas.bucket_test_max_items'] = '1'
         return settings
 
@@ -816,9 +819,9 @@ class QuotaMaxBytesPerItemExceededListenerTest(
 
     error_message = "Maximum bytes per object exceeded "
 
-    def get_app_settings(self, extra=None):
+    def get_app_settings(self, extras=None):
         settings = super(QuotaMaxBytesPerItemExceededListenerTest,
-                         self).get_app_settings(extra)
+                         self).get_app_settings(extras)
         settings['quotas.bucket_max_bytes_per_item'] = '55'
         return settings
 
@@ -829,9 +832,9 @@ class QuotaMaxBytesPerItemExceededBucketListenerTest(
 
     error_message = "Maximum bytes per object exceeded "
 
-    def get_app_settings(self, extra=None):
+    def get_app_settings(self, extras=None):
         settings = super(QuotaMaxBytesPerItemExceededBucketListenerTest,
-                         self).get_app_settings(extra)
+                         self).get_app_settings(extras)
         settings['quotas.bucket_test_max_bytes_per_item'] = '55'
         return settings
 
@@ -907,10 +910,10 @@ class QuotaMaxBytesExceededCollectionSettingsListenerTest(
 
     error_message = "Collection maximum size exceeded "
 
-    def get_app_settings(self, extra=None):
+    def get_app_settings(self, extras=None):
         settings = super(
             QuotaMaxBytesExceededCollectionSettingsListenerTest,
-            self).get_app_settings(extra)
+            self).get_app_settings(extras)
         settings['quotas.collection_max_bytes'] = '100'
         return settings
 
@@ -921,10 +924,10 @@ class QuotaMaxBytesExceededCollectionBucketSettingsListenerTest(
 
     error_message = "Collection maximum size exceeded "
 
-    def get_app_settings(self, extra=None):
+    def get_app_settings(self, extras=None):
         settings = super(
             QuotaMaxBytesExceededCollectionBucketSettingsListenerTest,
-            self).get_app_settings(extra)
+            self).get_app_settings(extras)
         settings['quotas.collection_test_max_bytes'] = '100'
         return settings
 
@@ -935,10 +938,10 @@ class QuotaMaxBytesExceededBucketCollectionSettingsListenerTest(
 
     error_message = "Collection maximum size exceeded "
 
-    def get_app_settings(self, extra=None):
+    def get_app_settings(self, extras=None):
         settings = super(
             QuotaMaxBytesExceededBucketCollectionSettingsListenerTest,
-            self).get_app_settings(extra)
+            self).get_app_settings(extras)
         settings['quotas.collection_test_col_max_bytes'] = '100'
         return settings
 
@@ -948,10 +951,10 @@ class QuotaMaxItemsExceededCollectionSettingsListenerTest(
 
     error_message = "Collection maximum number of objects exceeded "
 
-    def get_app_settings(self, extra=None):
+    def get_app_settings(self, extras=None):
         settings = super(
             QuotaMaxItemsExceededCollectionSettingsListenerTest,
-            self).get_app_settings(extra)
+            self).get_app_settings(extras)
         settings['quotas.collection_max_items'] = '1'
         return settings
 
@@ -961,10 +964,10 @@ class QuotaMaxItemsExceededCollectionBucketSettingsListenerTest(
 
     error_message = "Collection maximum number of objects exceeded "
 
-    def get_app_settings(self, extra=None):
+    def get_app_settings(self, extras=None):
         settings = super(
             QuotaMaxItemsExceededCollectionBucketSettingsListenerTest,
-            self).get_app_settings(extra)
+            self).get_app_settings(extras)
         settings['quotas.collection_test_max_items'] = '1'
         return settings
 
@@ -974,10 +977,10 @@ class QuotaMaxItemsExceededBucketCollectionSettingsListenerTest(
 
     error_message = "Collection maximum number of objects exceeded "
 
-    def get_app_settings(self, extra=None):
+    def get_app_settings(self, extras=None):
         settings = super(
             QuotaMaxItemsExceededBucketCollectionSettingsListenerTest,
-            self).get_app_settings(extra)
+            self).get_app_settings(extras)
         settings['quotas.collection_test_col_max_items'] = '1'
         return settings
 
@@ -987,10 +990,10 @@ class QuotaMaxBytesPerItemExceededCollectionSettingsListenerTest(
 
     error_message = "Maximum bytes per object exceeded "
 
-    def get_app_settings(self, extra=None):
+    def get_app_settings(self, extras=None):
         settings = super(
             QuotaMaxBytesPerItemExceededCollectionSettingsListenerTest,
-            self).get_app_settings(extra)
+            self).get_app_settings(extras)
         settings['quotas.collection_max_bytes_per_item'] = '80'
         return settings
 
@@ -1001,10 +1004,10 @@ class QuotaMaxBytesPerItemExceededCollectionBucketSettingsListenerTest(
 
     error_message = "Maximum bytes per object exceeded "
 
-    def get_app_settings(self, extra=None):
+    def get_app_settings(self, extras=None):
         settings = super(
             QuotaMaxBytesPerItemExceededCollectionBucketSettingsListenerTest,
-            self).get_app_settings(extra)
+            self).get_app_settings(extras)
         settings['quotas.collection_test_max_bytes_per_item'] = '80'
         return settings
 
@@ -1015,9 +1018,9 @@ class QuotaMaxBytesPerItemExceededBucketCollectionSettingsListenerTest(
 
     error_message = "Maximum bytes per object exceeded "
 
-    def get_app_settings(self, extra=None):
+    def get_app_settings(self, extras=None):
         settings = super(
             QuotaMaxBytesPerItemExceededBucketCollectionSettingsListenerTest,
-            self).get_app_settings(extra)
+            self).get_app_settings(extras)
         settings['quotas.collection_test_col_max_bytes_per_item'] = '80'
         return settings
diff --git a/tests/support.py b/tests/support.py
new file mode 100644
--- /dev/null
+++ b/tests/support.py
@@ -0,0 +1,43 @@
+from kinto.core import testing
+from kinto import main as kinto_main
+from kinto import DEFAULT_SETTINGS
+
+
+MINIMALIST_BUCKET = {}
+MINIMALIST_COLLECTION = {}
+MINIMALIST_GROUP = {'data': dict(members=['fxa:user'])}
+MINIMALIST_RECORD = {'data': dict(name="Hulled Barley",
+                                  type="Whole Grain")}
+USER_PRINCIPAL = 'basicauth:8a931a10fc88ab2f6d1cc02a07d3a81b5d4768f6f13e85c5' \
+                 'd8d4180419acb1b4'
+
+
+class BaseWebTest(testing.BaseWebTest):
+
+    api_prefix = "v1"
+    entry_point = kinto_main
+    principal = USER_PRINCIPAL
+
+    def __init__(self, *args, **kwargs):
+        super(BaseWebTest, self).__init__(*args, **kwargs)
+        self.headers.update(testing.get_user_headers('mat'))
+
+    def get_app_settings(self, extras=None):
+        settings = DEFAULT_SETTINGS.copy()
+        if extras is not None:
+            settings.update(extras)
+        settings = super(BaseWebTest, self).get_app_settings(extras=settings)
+        return settings
+
+    def create_group(self, bucket_id, group_id, members=None):
+        if members is None:
+            group = MINIMALIST_GROUP
+        else:
+            group = {'data': {'members': members}}
+        group_url = '/buckets/%s/groups/%s' % (bucket_id, group_id)
+        self.app.put_json(group_url, group,
+                          headers=self.headers, status=201)
+
+    def create_bucket(self, bucket_id):
+        self.app.put_json('/buckets/%s' % bucket_id, MINIMALIST_BUCKET,
+                          headers=self.headers, status=201)
diff --git a/kinto/tests/test_authorization.py b/tests/test_authorization.py
similarity index 99%
rename from kinto/tests/test_authorization.py
rename to tests/test_authorization.py
--- a/kinto/tests/test_authorization.py
+++ b/tests/test_authorization.py
@@ -1,8 +1,8 @@
+from kinto.core.testing import unittest
+
 from kinto.authorization import (get_object_type, build_permission_tuple,
                                  build_permissions_set)
 
-from .support import unittest
-
 
 class PermissionInheritanceTest(unittest.TestCase):
     record_uri = '/buckets/blog/collections/articles/records/article1'
diff --git a/kinto/tests/test_config.py b/tests/test_config.py
similarity index 100%
rename from kinto/tests/test_config.py
rename to tests/test_config.py
diff --git a/kinto/tests/test_configuration/test.ini b/tests/test_configuration/test.ini
similarity index 100%
rename from kinto/tests/test_configuration/test.ini
rename to tests/test_configuration/test.ini
diff --git a/kinto/tests/test_main.py b/tests/test_main.py
similarity index 100%
rename from kinto/tests/test_main.py
rename to tests/test_main.py
diff --git a/kinto/tests/test_views_buckets.py b/tests/test_views_buckets.py
similarity index 97%
rename from kinto/tests/test_views_buckets.py
rename to tests/test_views_buckets.py
--- a/kinto/tests/test_views_buckets.py
+++ b/tests/test_views_buckets.py
@@ -1,6 +1,10 @@
+import unittest
+
 from pyramid.security import Authenticated
 
-from .support import (BaseWebTest, unittest, get_user_headers,
+from kinto.core.testing import get_user_headers
+
+from .support import (BaseWebTest,
                       MINIMALIST_BUCKET, MINIMALIST_GROUP,
                       MINIMALIST_COLLECTION, MINIMALIST_RECORD)
 
@@ -138,9 +142,9 @@ def setUp(self):
                           bucket,
                           headers=self.headers)
 
-    def get_app_settings(self, extra=None):
+    def get_app_settings(self, extras=None):
         settings = super(BucketReadPermissionTest,
-                         self).get_app_settings(extra)
+                         self).get_app_settings(extras)
         # Give the right to list buckets (for self.principal and alice).
         settings['kinto.bucket_read_principals'] = Authenticated
         return settings
@@ -180,8 +184,8 @@ def setUp(self):
         # Delete the bucket.
         self.app.delete(self.bucket_url, headers=self.headers)
 
-    def get_app_settings(self, extra=None):
-        settings = super(BucketDeletionTest, self).get_app_settings(extra)
+    def get_app_settings(self, extras=None):
+        settings = super(BucketDeletionTest, self).get_app_settings(extras)
         # Give the permission to read, to get an explicit 404 once deleted.
         settings['kinto.bucket_read_principals'] = self.principal
         return settings
diff --git a/kinto/tests/test_views_collections.py b/tests/test_views_collections.py
similarity index 97%
rename from kinto/tests/test_views_collections.py
rename to tests/test_views_collections.py
--- a/kinto/tests/test_views_collections.py
+++ b/tests/test_views_collections.py
@@ -1,6 +1,9 @@
-from .support import (BaseWebTest, unittest, MINIMALIST_BUCKET,
-                      MINIMALIST_COLLECTION, MINIMALIST_RECORD,
-                      get_user_headers)
+import unittest
+
+from kinto.core.testing import get_user_headers
+
+from .support import (BaseWebTest, MINIMALIST_BUCKET,
+                      MINIMALIST_COLLECTION, MINIMALIST_RECORD)
 
 
 class CollectionViewTest(BaseWebTest, unittest.TestCase):
diff --git a/kinto/tests/test_views_collections_cache.py b/tests/test_views_collections_cache.py
similarity index 96%
rename from kinto/tests/test_views_collections_cache.py
rename to tests/test_views_collections_cache.py
--- a/kinto/tests/test_views_collections_cache.py
+++ b/tests/test_views_collections_cache.py
@@ -1,10 +1,12 @@
-from .support import (BaseWebTest, unittest, MINIMALIST_BUCKET,
+from kinto.core.testing import unittest
+
+from .support import (BaseWebTest, MINIMALIST_BUCKET,
                       MINIMALIST_COLLECTION, MINIMALIST_RECORD)
 
 
 class GlobalSettingsTest(BaseWebTest, unittest.TestCase):
-    def get_app_settings(self, extra=None):
-        settings = super(GlobalSettingsTest, self).get_app_settings(extra)
+    def get_app_settings(self, extras=None):
+        settings = super(GlobalSettingsTest, self).get_app_settings(extras)
         settings['kinto.record_cache_expires_seconds'] = 3600
         settings['kinto.record_read_principals'] = 'system.Everyone'
         return settings
@@ -32,8 +34,8 @@ def test_expires_and_cache_control_headers_are_set(self):
 
 
 class SpecificSettingsTest(BaseWebTest, unittest.TestCase):
-    def get_app_settings(self, extra=None):
-        settings = super(SpecificSettingsTest, self).get_app_settings(extra)
+    def get_app_settings(self, extras=None):
+        settings = super(SpecificSettingsTest, self).get_app_settings(extras)
         settings['kinto.blog_record_cache_expires_seconds'] = '30'
         settings['kinto.browser_top500_record_cache_expires_seconds'] = '60'
         return settings
@@ -132,7 +134,7 @@ def test_cache_control_is_set_no_cache_if_zero(self):
         self.assertEqual(r.headers['Pragma'], 'no-cache')
 
     def test_cache_control_on_collection_overrides_setting(self):
-        app = self._get_test_app({
+        app = self.make_app(settings={
             'kinto.record_cache_expires_seconds': 10,
             'kinto.record_read_principals': 'system.Everyone'
         })
diff --git a/kinto/tests/test_views_collections_schema.py b/tests/test_views_collections_schema.py
similarity index 98%
rename from kinto/tests/test_views_collections_schema.py
rename to tests/test_views_collections_schema.py
--- a/kinto/tests/test_views_collections_schema.py
+++ b/tests/test_views_collections_schema.py
@@ -1,4 +1,6 @@
-from .support import BaseWebTest, unittest
+from kinto.core.testing import unittest
+
+from .support import BaseWebTest
 
 
 BUCKET_URL = '/buckets/blog'
@@ -47,9 +49,8 @@ def test_records_are_not_invalid_if_do_not_match_schema(self):
 
 
 class BaseWebTestWithSchema(BaseWebTest):
-    def get_app_settings(self, additional_settings=None):
-        settings = super(BaseWebTestWithSchema, self).get_app_settings(
-            additional_settings)
+    def get_app_settings(self, extras=None):
+        settings = super(BaseWebTestWithSchema, self).get_app_settings(extras)
         settings['experimental_collection_schema_validation'] = 'True'
         return settings
 
diff --git a/kinto/tests/test_views_contribute.py b/tests/test_views_contribute.py
similarity index 82%
rename from kinto/tests/test_views_contribute.py
rename to tests/test_views_contribute.py
--- a/kinto/tests/test_views_contribute.py
+++ b/tests/test_views_contribute.py
@@ -1,4 +1,6 @@
-from .support import BaseWebTest, unittest
+from kinto.core.testing import unittest
+
+from .support import BaseWebTest
 
 
 class ContributeViewTest(BaseWebTest, unittest.TestCase):
diff --git a/kinto/tests/test_views_disable_default.py b/tests/test_views_disable_default.py
similarity index 73%
rename from kinto/tests/test_views_disable_default.py
rename to tests/test_views_disable_default.py
--- a/kinto/tests/test_views_disable_default.py
+++ b/tests/test_views_disable_default.py
@@ -1,4 +1,6 @@
-from .support import (BaseWebTest, unittest)
+from kinto.core.testing import unittest
+
+from .support import BaseWebTest
 
 
 class DisableDefaultBucketViewTest(BaseWebTest, unittest.TestCase):
@@ -7,10 +9,10 @@ class DisableDefaultBucketViewTest(BaseWebTest, unittest.TestCase):
 
     def test_returns_403_if_excluded_in_configuration(self):
         extra = {'includes': ''}
-        app = self._get_test_app(settings=extra)
+        app = self.make_app(settings=extra)
         app.get(self.test_url, headers=self.headers, status=403)
 
     def test_returns_200_if_included_in_configuration(self):
         extra = {'includes': 'kinto.plugins.default_bucket'}
-        app = self._get_test_app(settings=extra)
+        app = self.make_app(settings=extra)
         app.get(self.test_url, headers=self.headers, status=200)
diff --git a/kinto/tests/test_views_flush.py b/tests/test_views_flush.py
similarity index 74%
rename from kinto/tests/test_views_flush.py
rename to tests/test_views_flush.py
--- a/kinto/tests/test_views_flush.py
+++ b/tests/test_views_flush.py
@@ -1,12 +1,12 @@
 import os
-import webtest
+import unittest
+
 from pyramid.config import Configurator
 
-from kinto.tests.core import support as core_support
-from kinto import main
+from kinto.core.testing import get_user_headers
 from kinto.events import ServerFlushed
 
-from .support import (BaseWebTest, unittest, get_user_headers,
+from .support import (BaseWebTest,
                       MINIMALIST_BUCKET, MINIMALIST_COLLECTION,
                       MINIMALIST_RECORD)
 
@@ -24,8 +24,9 @@ def setUp(self):
 
         self.alice_headers = self.headers.copy()
         self.alice_headers.update(**get_user_headers('alice'))
-        alice_principal = ('basicauth:8df4b22019cc89d0bb679bc51373a9da56a'
-                           '7ae9978c52fbe684510c3d257c855')
+
+        resp = self.app.get('/', headers=self.alice_headers)
+        alice_principal = resp.json['user']['id']
         bucket['permissions'] = {'write': [alice_principal]}
 
         # Create shared bucket.
@@ -49,21 +50,19 @@ def tearDown(self):
         self.events = []
         super(FlushViewTest, self).tearDown()
 
-    def _get_test_app(self, settings=None):
-        app_settings = self.get_app_settings(settings)
-        self.config = Configurator(settings=app_settings)
-        self.config.add_subscriber(self.listener, ServerFlushed)
-        self.config.commit()
-        app = webtest.TestApp(main({}, config=self.config, **app_settings))
-        app.RequestClass = core_support.get_request_class(prefix="v1")
-
-        return app
-
-    def get_app_settings(self, extra=None):
-        if extra is None:
-            extra = {}
-        extra.setdefault('flush_endpoint_enabled', True)
-        settings = super(FlushViewTest, self).get_app_settings(extra)
+    def make_app(self, settings=None, config=None):
+        settings = self.get_app_settings(settings)
+        config = Configurator(settings=settings)
+        config.add_subscriber(self.listener, ServerFlushed)
+        config.commit()
+        return super(FlushViewTest, self).make_app(settings=settings,
+                                                   config=config)
+
+    def get_app_settings(self, extras=None):
+        if extras is None:
+            extras = {}
+        extras.setdefault('flush_endpoint_enabled', True)
+        settings = super(FlushViewTest, self).get_app_settings(extras)
         return settings
 
     def listener(self, event):
@@ -71,7 +70,7 @@ def listener(self, event):
 
     def test_returns_404_if_not_enabled_in_configuration(self):
         extra = {'flush_endpoint_enabled': False}
-        app = self._get_test_app(settings=extra)
+        app = self.make_app(settings=extra)
         app.post('/__flush__', headers=self.headers, status=404)
 
     def test_removes_every_records_of_everykind(self):
@@ -95,6 +94,6 @@ def test_event_triggered_post(self):
     def test_can_be_enabled_via_environment(self):
         os.environ['KINTO_FLUSH_ENDPOINT_ENABLED'] = 'true'
         extra = {'flush_endpoint_enabled': False}
-        app = self._get_test_app(settings=extra)
+        app = self.make_app(settings=extra)
         app.post('/__flush__', headers=self.headers)
         os.environ.pop('KINTO_FLUSH_ENDPOINT_ENABLED')
diff --git a/kinto/tests/test_views_groups.py b/tests/test_views_groups.py
similarity index 99%
rename from kinto/tests/test_views_groups.py
rename to tests/test_views_groups.py
--- a/kinto/tests/test_views_groups.py
+++ b/tests/test_views_groups.py
@@ -1,4 +1,6 @@
-from .support import (BaseWebTest, unittest, MINIMALIST_BUCKET,
+from kinto.core.testing import unittest
+
+from .support import (BaseWebTest, MINIMALIST_BUCKET,
                       MINIMALIST_GROUP)
 
 
diff --git a/kinto/tests/test_views_hello.py b/tests/test_views_hello.py
similarity index 74%
rename from kinto/tests/test_views_hello.py
rename to tests/test_views_hello.py
--- a/kinto/tests/test_views_hello.py
+++ b/tests/test_views_hello.py
@@ -1,6 +1,8 @@
 from kinto import __version__ as VERSION
 
-from .support import BaseWebTest, unittest
+from kinto.core.testing import unittest
+
+from .support import BaseWebTest
 
 
 class HelloViewTest(BaseWebTest, unittest.TestCase):
@@ -19,14 +21,11 @@ def test_hides_user_info_if_anonymous(self):
 
     def test_returns_user_id_if_authenticated(self):
         response = self.app.get('/', headers=self.headers)
-        self.assertEqual(response.json['user']['id'],
-                         ('basicauth:3a0c56d278def4113f38d0cfff6db1b06b'
-                          '84fcc4384ee890cf7bbaa772317e10'))
+        self.assertEqual(response.json['user']['id'], self.principal)
 
     def test_capability_is_exposed_if_setting_is_set(self):
-        settings = self.get_app_settings()
-        settings['experimental_collection_schema_validation'] = True
-        app = self._get_test_app(settings=settings)
+        settings = {'experimental_collection_schema_validation': True}
+        app = self.make_app(settings=settings)
         resp = app.get('/')
         capabilities = resp.json['capabilities']
         self.assertIn('schema', capabilities)
@@ -40,15 +39,14 @@ def test_capability_is_exposed_if_setting_is_set(self):
     def test_capability_is_exposed_if_setting_is_not_set(self):
         settings = self.get_app_settings()
         settings['experimental_collection_schema_validation'] = False
-        app = self._get_test_app(settings=settings)
+        app = self.make_app(settings=settings)
         resp = app.get('/')
         capabilities = resp.json['capabilities']
         self.assertNotIn('schema', capabilities)
 
     def test_flush_capability_if_enabled(self):
-        settings = self.get_app_settings()
-        settings['flush_endpoint_enabled'] = True
-        app = self._get_test_app(settings=settings)
+        settings = {'flush_endpoint_enabled': True}
+        app = self.make_app(settings=settings)
         resp = app.get('/')
         capabilities = resp.json['capabilities']
         self.assertIn('flush_endpoint', capabilities)
@@ -61,17 +59,15 @@ def test_flush_capability_if_enabled(self):
         self.assertEqual(expected, capabilities['flush_endpoint'])
 
     def test_flush_capability_if_not_enabled(self):
-        settings = self.get_app_settings()
-        settings['flush_endpoint_enabled'] = False
-        app = self._get_test_app(settings=settings)
+        settings = {'flush_endpoint_enabled': False}
+        app = self.make_app(settings=settings)
         resp = app.get('/')
         capabilities = resp.json['capabilities']
         self.assertNotIn('flush_endpoint', capabilities)
 
     def test_permissions_capability_if_enabled(self):
-        settings = self.get_app_settings()
-        settings['experimental_permissions_endpoint'] = True
-        app = self._get_test_app(settings=settings)
+        settings = {'experimental_permissions_endpoint': True}
+        app = self.make_app(settings=settings)
         resp = app.get('/')
         capabilities = resp.json['capabilities']
         self.assertIn('permissions_endpoint', capabilities)
@@ -84,9 +80,8 @@ def test_permissions_capability_if_enabled(self):
         self.assertEqual(expected, capabilities['permissions_endpoint'])
 
     def test_permissions_capability_if_not_enabled(self):
-        settings = self.get_app_settings()
-        settings['experimental_permissions_endpoint'] = False
-        app = self._get_test_app(settings=settings)
+        settings = {'experimental_permissions_endpoint': False}
+        app = self.make_app(settings=settings)
         resp = app.get('/')
         capabilities = resp.json['capabilities']
         self.assertNotIn('permissions_endpoint', capabilities)
diff --git a/kinto/tests/test_views_objects_permissions.py b/tests/test_views_objects_permissions.py
similarity index 94%
rename from kinto/tests/test_views_objects_permissions.py
rename to tests/test_views_objects_permissions.py
--- a/kinto/tests/test_views_objects_permissions.py
+++ b/tests/test_views_objects_permissions.py
@@ -1,4 +1,8 @@
-from .support import (BaseWebTest, unittest, get_user_headers,
+import unittest
+
+from kinto.core.testing import get_user_headers
+
+from .support import (BaseWebTest,
                       MINIMALIST_BUCKET, MINIMALIST_COLLECTION,
                       MINIMALIST_GROUP, MINIMALIST_RECORD)
 
@@ -9,12 +13,13 @@ def __init__(self, *args, **kwargs):
         super(PermissionsTest, self).__init__(*args, **kwargs)
         self.alice_headers = self.headers.copy()
         self.alice_headers.update(**get_user_headers('alice'))
-        self.alice_principal = ('basicauth:8df4b22019cc89d0bb679bc51373a9da56a'
-                                '7ae9978c52fbe684510c3d257c855')
         self.bob_headers = self.headers.copy()
         self.bob_headers.update(**get_user_headers('bob'))
-        self.bob_principal = ('basicauth:21364d1282b58b3af15a0d619d03122a318be'
-                              'e30c1a5a2d09c3b958e5f7fb71a')
+
+        self.alice_principal = ('basicauth:d5b0026601f1b251974e09548d44155e16'
+                                '812e3c64ff7ae053fe3542e2ca1570')
+        self.bob_principal = ('basicauth:c031ced27503f788b102ca54269a062ec737'
+                              '94bb075154c74a0d4311e74ca8b6')
 
 
 class BucketPermissionsTest(PermissionsTest):
diff --git a/kinto/tests/test_views_permissions.py b/tests/test_views_permissions.py
similarity index 92%
rename from kinto/tests/test_views_permissions.py
rename to tests/test_views_permissions.py
--- a/kinto/tests/test_views_permissions.py
+++ b/tests/test_views_permissions.py
@@ -1,6 +1,10 @@
-from .support import (BaseWebTest, unittest, MINIMALIST_RECORD,
+import unittest
+
+from kinto.core.testing import get_user_headers
+
+from .support import (BaseWebTest, MINIMALIST_RECORD,
                       MINIMALIST_GROUP, MINIMALIST_BUCKET,
-                      MINIMALIST_COLLECTION, get_user_headers)
+                      MINIMALIST_COLLECTION)
 
 
 RECORD_ID = 'd5db6e57-2c10-43e2-96c8-56602ef01435'
@@ -26,9 +30,8 @@ def setUp(self):
         self.app.put_json('/buckets/water', MINIMALIST_BUCKET,
                           headers=get_user_headers('alice'))
 
-    def get_app_settings(self, additional_settings=None):
-        settings = super(PermissionsViewTest, self).get_app_settings(
-            additional_settings)
+    def get_app_settings(self, extras=None):
+        settings = super(PermissionsViewTest, self).get_app_settings(extras)
         settings['experimental_permissions_endpoint'] = 'True'
         return settings
 
diff --git a/kinto/tests/test_views_records.py b/tests/test_views_records.py
similarity index 98%
rename from kinto/tests/test_views_records.py
rename to tests/test_views_records.py
--- a/kinto/tests/test_views_records.py
+++ b/tests/test_views_records.py
@@ -1,11 +1,14 @@
 import json
 import mock
 import re
+import unittest
 
 from kinto.core.utils import decode_header
-from .support import (BaseWebTest, unittest, MINIMALIST_RECORD,
+from kinto.core.testing import get_user_headers
+
+from .support import (BaseWebTest, MINIMALIST_RECORD,
                       MINIMALIST_GROUP, MINIMALIST_BUCKET,
-                      MINIMALIST_COLLECTION, get_user_headers)
+                      MINIMALIST_COLLECTION)
 
 
 class RecordsViewTest(BaseWebTest, unittest.TestCase):