diff --git a/.testr.conf b/.testr.conf
index 5ec63d8c..6236e319 100644
--- a/.testr.conf
+++ b/.testr.conf
@@ -2,8 +2,7 @@
 test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
              OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
              OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
-             DJANGO_SETTINGS_MODULE=storyboard.settings \
-             ${PYTHON:-python} -m subunit.run discover -t ./ ./ $LISTOPT $IDOPTION
+             ${PYTHON:-python} -m subunit.run discover storyboard/tests $LISTOPT $IDOPTION
 
 test_id_option=--load-list $IDFILE
 test_list_option=--list
diff --git a/storyboard/tests/api/__init__.py b/storyboard/tests/api/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/storyboard/tests/api/test_stories.py b/storyboard/tests/api/test_stories.py
new file mode 100644
index 00000000..9ccf2e97
--- /dev/null
+++ b/storyboard/tests/api/test_stories.py
@@ -0,0 +1,83 @@
+# -*- coding: utf-8 -*-
+
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
+"""
+test_storyboard
+----------------------------------
+
+Tests for `storyboard` module.
+"""
+import copy
+import json
+
+from mock import patch
+
+from storyboard.tests.api.utils import FakeSession
+from storyboard.tests import base
+
+SAMPLE_STORY = {
+    "title": "test_story",
+    "description": "some description"
+}
+
+SAMPLE_STORY_REQUEST = {
+    "story": SAMPLE_STORY
+}
+
+
+class TestStories(base.FunctionalTest):
+
+    @patch("storyboard.openstack.common.db.sqlalchemy.session.get_session")
+    def test_stories_endpoint(self, session_mock):
+        fake_session = FakeSession()
+        session_mock.return_value = fake_session
+
+        response = self.get_json(path="/stories")
+        self.assertEqual([], response)
+
+    @patch("storyboard.openstack.common.db.sqlalchemy.session.get_session")
+    def test_create(self, session_mock):
+        fake_session = FakeSession()
+        session_mock.return_value = fake_session
+
+        response = self.post_json("/stories", SAMPLE_STORY_REQUEST)
+        story = json.loads(response.body)
+
+        self.assertIn("id", story)
+        self.assertIn("created_at", story)
+        self.assertEqual(story["title"], SAMPLE_STORY["title"])
+        self.assertEqual(story["description"], SAMPLE_STORY["description"])
+
+    @patch("storyboard.openstack.common.db.sqlalchemy.session.get_session")
+    def test_update(self, session_mock):
+        fake_session = FakeSession()
+        session_mock.return_value = fake_session
+
+        response = self.post_json("/stories", SAMPLE_STORY_REQUEST)
+        old_story = json.loads(response.body)
+
+        update_request = copy.deepcopy(SAMPLE_STORY_REQUEST)
+        update_request["story_id"] = old_story["id"]
+        update_request["story"]["title"] = "updated_title"
+        update_request["story"]["description"] = "updated_description"
+
+        response = self.put_json("/stories", update_request)
+        updated_story = json.loads(response.body)
+
+        self.assertEqual(updated_story["id"], old_story["id"])
+        self.assertEqual(updated_story["created_at"], old_story["created_at"])
+
+        self.assertNotEqual(updated_story["title"], old_story["title"])
+        self.assertNotEqual(updated_story["description"],
+                            old_story["description"])
diff --git a/storyboard/tests/api/utils.py b/storyboard/tests/api/utils.py
new file mode 100644
index 00000000..ea62687d
--- /dev/null
+++ b/storyboard/tests/api/utils.py
@@ -0,0 +1,81 @@
+# Copyright (c) 2014 Mirantis Inc.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+# implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from datetime import datetime
+
+
+class FakeSession(object):
+    def __init__(self):
+        self.objects_by_type = dict()
+        self.max_id = 1
+
+    def add(self, obj):
+        obj_type = type(obj)
+        if obj_type not in self.objects_by_type:
+            self.objects_by_type[obj_type] = list()
+        if obj in self.objects_by_type[obj_type]:
+            return
+
+        setattr(obj, "id", self.max_id)
+        self.max_id += 1
+
+        setattr(obj, "created_at", datetime.now())
+
+        self.objects_by_type[obj_type].append(obj)
+
+    def query(self, obj_type):
+        return FakeQuery(self.objects_by_type.get(obj_type, []))
+
+    def begin(self):
+        return self
+
+    def __enter__(self):
+        pass
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        pass
+
+
+class FakeQuery(object):
+    def __init__(self, obj_list):
+        self.obj_list = obj_list
+
+    def filter_by(self, **kwargs):
+        filtered_list = []
+        for obj in self.obj_list:
+            matches = True
+            for k, v in kwargs.iteritems():
+                if getattr(obj, k) != v:
+                    matches = False
+                    break
+
+            if matches:
+                filtered_list.append(obj)
+
+        return FakeFilterResult(filtered_list)
+
+
+class FakeFilterResult(object):
+    def __init__(self, obj_list):
+        self.obj_list = obj_list
+
+    def first(self):
+        try:
+            return self.obj_list[0]
+        except Exception:
+            return None
+
+    def all(self):
+        return self.obj_list
diff --git a/storyboard/tests/base.py b/storyboard/tests/base.py
index f5160afe..5731d18f 100644
--- a/storyboard/tests/base.py
+++ b/storyboard/tests/base.py
@@ -19,6 +19,8 @@ import os
 
 import fixtures
 from oslo.config import cfg
+import pecan
+import pecan.testing
 from storyboard.openstack.common import log as logging
 import testtools
 
@@ -75,3 +77,187 @@ class TestCase(testtools.TestCase):
         group = kw.pop('group', None)
         for k, v in kw.iteritems():
             CONF.set_override(k, v, group)
+
+
+PATH_PREFIX = '/v1'
+
+
+class FunctionalTest(TestCase):
+    """Used for functional tests of Pecan controllers where you need to
+    test your literal application and its integration with the
+    framework.
+    """
+
+    def setUp(self):
+        super(FunctionalTest, self).setUp()
+        self.app = self._make_app()
+
+        self.addCleanup(self._reset_pecan)
+
+    def _make_app(self):
+        config = {
+            'app': {
+                'root': 'storyboard.api.root_controller.RootController',
+                'modules': ['storyboard.api']
+            }
+        }
+        return pecan.testing.load_test_app(config=config)
+
+    def _reset_pecan(self):
+        pecan.set_config({}, overwrite=True)
+
+    def _request_json(self, path, params, expect_errors=False, headers=None,
+                      method="post", extra_environ=None, status=None,
+                      path_prefix=PATH_PREFIX):
+        """Sends simulated HTTP request to Pecan test app.
+
+        :param path: url path of target service
+        :param params: content for wsgi.input of request
+        :param expect_errors: Boolean value; whether an error is expected based
+                              on request
+        :param headers: a dictionary of headers to send along with the request
+        :param method: Request method type. Appropriate method function call
+                       should be used rather than passing attribute in.
+        :param extra_environ: a dictionary of environ variables to send along
+                              with the request
+        :param status: expected status code of response
+        :param path_prefix: prefix of the url path
+        """
+        full_path = path_prefix + path
+        print('%s: %s %s' % (method.upper(), full_path, params))
+        response = getattr(self.app, "%s_json" % method)(
+            str(full_path),
+            params=params,
+            headers=headers,
+            status=status,
+            extra_environ=extra_environ,
+            expect_errors=expect_errors
+        )
+        print('GOT:%s' % response)
+        return response
+
+    def put_json(self, path, params, expect_errors=False, headers=None,
+                 extra_environ=None, status=None):
+        """Sends simulated HTTP PUT request to Pecan test app.
+
+        :param path: url path of target service
+        :param params: content for wsgi.input of request
+        :param expect_errors: Boolean value; whether an error is expected based
+                              on request
+        :param headers: a dictionary of headers to send along with the request
+        :param extra_environ: a dictionary of environ variables to send along
+                              with the request
+        :param status: expected status code of response
+        """
+        return self._request_json(path=path, params=params,
+                                  expect_errors=expect_errors,
+                                  headers=headers, extra_environ=extra_environ,
+                                  status=status, method="put")
+
+    def post_json(self, path, params, expect_errors=False, headers=None,
+                  extra_environ=None, status=None):
+        """Sends simulated HTTP POST request to Pecan test app.
+
+        :param path: url path of target service
+        :param params: content for wsgi.input of request
+        :param expect_errors: Boolean value; whether an error is expected based
+                              on request
+        :param headers: a dictionary of headers to send along with the request
+        :param extra_environ: a dictionary of environ variables to send along
+                              with the request
+        :param status: expected status code of response
+        """
+        return self._request_json(path=path, params=params,
+                                  expect_errors=expect_errors,
+                                  headers=headers, extra_environ=extra_environ,
+                                  status=status, method="post")
+
+    def patch_json(self, path, params, expect_errors=False, headers=None,
+                   extra_environ=None, status=None):
+        """Sends simulated HTTP PATCH request to Pecan test app.
+
+        :param path: url path of target service
+        :param params: content for wsgi.input of request
+        :param expect_errors: Boolean value; whether an error is expected based
+                              on request
+        :param headers: a dictionary of headers to send along with the request
+        :param extra_environ: a dictionary of environ variables to send along
+                              with the request
+        :param status: expected status code of response
+        """
+        return self._request_json(path=path, params=params,
+                                  expect_errors=expect_errors,
+                                  headers=headers, extra_environ=extra_environ,
+                                  status=status, method="patch")
+
+    def delete(self, path, expect_errors=False, headers=None,
+               extra_environ=None, status=None, path_prefix=PATH_PREFIX):
+        """Sends simulated HTTP DELETE request to Pecan test app.
+
+        :param path: url path of target service
+        :param expect_errors: Boolean value; whether an error is expected based
+                              on request
+        :param headers: a dictionary of headers to send along with the request
+        :param extra_environ: a dictionary of environ variables to send along
+                              with the request
+        :param status: expected status code of response
+        :param path_prefix: prefix of the url path
+        """
+        full_path = path_prefix + path
+        print('DELETE: %s' % (full_path))
+        response = self.app.delete(str(full_path),
+                                   headers=headers,
+                                   status=status,
+                                   extra_environ=extra_environ,
+                                   expect_errors=expect_errors)
+        print('GOT:%s' % response)
+        return response
+
+    def get_json(self, path, expect_errors=False, headers=None,
+                 extra_environ=None, q=[], path_prefix=PATH_PREFIX, **params):
+        """Sends simulated HTTP GET request to Pecan test app.
+
+        :param path: url path of target service
+        :param expect_errors: Boolean value;whether an error is expected based
+                              on request
+        :param headers: a dictionary of headers to send along with the request
+        :param extra_environ: a dictionary of environ variables to send along
+                              with the request
+        :param q: list of queries consisting of: field, value, op, and type
+                  keys
+        :param path_prefix: prefix of the url path
+        :param params: content for wsgi.input of request
+        """
+        full_path = path_prefix + path
+        query_params = {'q.field': [],
+                        'q.value': [],
+                        'q.op': [],
+                        }
+        for query in q:
+            for name in ['field', 'op', 'value']:
+                query_params['q.%s' % name].append(query.get(name, ''))
+        all_params = {}
+        all_params.update(params)
+        if q:
+            all_params.update(query_params)
+        print('GET: %s %r' % (full_path, all_params))
+        response = self.app.get(full_path,
+                                params=all_params,
+                                headers=headers,
+                                extra_environ=extra_environ,
+                                expect_errors=expect_errors)
+        if not expect_errors:
+            response = response.json
+        print('GOT:%s' % response)
+        return response
+
+    def validate_link(self, link):
+        """Checks if the given link can get correct data."""
+
+        # removes 'http://loicalhost' part
+        full_path = link.split('localhost', 1)[1]
+        try:
+            self.get_json(full_path, path_prefix='')
+            return True
+        except Exception:
+            return False
diff --git a/storyboard/tests/test_stories.py b/storyboard/tests/test_stories.py
deleted file mode 100644
index 22ead2cb..00000000
--- a/storyboard/tests/test_stories.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Licensed under the Apache License, Version 2.0 (the "License"); you may
-# not use this file except in compliance with the License. You may obtain
-# a copy of the License at
-#
-#      http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
-# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
-# License for the specific language governing permissions and limitations
-# under the License.
-
-"""
-test_storyboard
-----------------------------------
-
-Tests for `storyboard` module.
-"""
-
-from storyboard.tests import base
-
-
-class TestStories(base.TestCase):
-
-    def test_something(self):
-        pass
diff --git a/tox.ini b/tox.ini
index 1adbae0b..ccd11308 100644
--- a/tox.ini
+++ b/tox.ini
@@ -8,11 +8,6 @@ usedevelop = True
 install_command = pip install -U {opts} {packages}
 setenv =
     VIRTUAL_ENV={envdir}
-    LANG=en_US.UTF-8
-    LANGUAGE=en_US:en
-    LC_ALL=C
-    DJANGO_SETTINGS_MODULE=storyboard.settings
-
 deps = -r{toxinidir}/requirements.txt
        -r{toxinidir}/test-requirements.txt
 commands = python setup.py testr --slowest --testr-args='{posargs}'