API tests for rest

Added tests for /stories endpoint

Added mock for sqlalchemy session.

Change-Id: I927b857b646ce014624d8fdd5eb2bc2803601e9e
This commit is contained in:
Nikita Konovalov 2014-01-17 16:48:24 +04:00
parent b02a396f3b
commit e5af375d59
7 changed files with 351 additions and 35 deletions

View File

@ -2,8 +2,7 @@
test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \ test_command=OS_STDOUT_CAPTURE=${OS_STDOUT_CAPTURE:-1} \
OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \ OS_STDERR_CAPTURE=${OS_STDERR_CAPTURE:-1} \
OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \ OS_TEST_TIMEOUT=${OS_TEST_TIMEOUT:-60} \
DJANGO_SETTINGS_MODULE=storyboard.settings \ ${PYTHON:-python} -m subunit.run discover storyboard/tests $LISTOPT $IDOPTION
${PYTHON:-python} -m subunit.run discover -t ./ ./ $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE test_id_option=--load-list $IDFILE
test_list_option=--list test_list_option=--list

View File

View File

@ -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"])

View File

@ -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

View File

@ -19,6 +19,8 @@ import os
import fixtures import fixtures
from oslo.config import cfg from oslo.config import cfg
import pecan
import pecan.testing
from storyboard.openstack.common import log as logging from storyboard.openstack.common import log as logging
import testtools import testtools
@ -75,3 +77,187 @@ class TestCase(testtools.TestCase):
group = kw.pop('group', None) group = kw.pop('group', None)
for k, v in kw.iteritems(): for k, v in kw.iteritems():
CONF.set_override(k, v, group) 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

View File

@ -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

View File

@ -8,11 +8,6 @@ usedevelop = True
install_command = pip install -U {opts} {packages} install_command = pip install -U {opts} {packages}
setenv = setenv =
VIRTUAL_ENV={envdir} 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 deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt -r{toxinidir}/test-requirements.txt
commands = python setup.py testr --slowest --testr-args='{posargs}' commands = python setup.py testr --slowest --testr-args='{posargs}'