from __future__ import print_function, unicode_literals
import inspect
import sys
import unittest
from collections import OrderedDict
import six
from django.core.urlresolvers import reverse
from django.test import override_settings
from .validators import BaseValidator
[docs]class TestStep(object):
"""
Test step for :py:class:`subui.test_runner.SubUITestRunner`.
The step is responsible for executing a self-contained task
such as submitting a form to a particular URL and then
make assertions regarding the server response.
:var unittest.TestCase TestStep.test: Test class instance
with which validators are going to run all their
assertions with. By default it is an instance of
:py:class:`unittest.TestCase` however can be changed
to any other class to add additional assertion methods.
:var str url_name: Name of the url as defined in ``urls.py``
by which the URL is going to be calculated.
:var tuple url_args: URL args to be used while calculating
the URL using Django's ``reverse``.
:var dict url_kwargs: URL kwargs to be used while calculating
the URL using Django's ``reverse``.
:var str request_method: HTTP method to use for the request.
Default is ``"post"``
:var str urlconf: Django URL Configuration.
:var str content_type: Content-Type of the request.
:var dict overriden_settings: Dictionary of settings to be overriden for the
test request.
:var dict data: Data to be sent to the server in the request
:var pycontext.context.Context state:
Reference to a global state from the test runner.
:var list TestStep.validators: List of response validators
:var response: Server response for the made request.
This attribute is only available after
:py:meth:`request` is called.
:param kwargs: A dictionary of values which will overwrite
any instance attributes. This allows to pass additional
data to the test step without necessarily subclassing
and manually instantiating step instance.
:type kwargs: dict
"""
test = unittest.TestCase('__init__')
url_name = None
url_args = None
url_kwargs = None
urlconf = None
content_type = None
overriden_settings = None
request_method = 'post'
state = None
data = None
validators = []
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
[docs] def init(self, client, steps, step_index, step_key, state):
"""
Initialize the step with necessary values from the
test runner.
:param client: Django test client to use to make server requests
:type client: django.test.client.Client
:param steps: All steps from the test runner.
This and step index allows to get previous and/or
next steps.
:type steps: collections.OrderedDict
:param step_index: Index of the step within all steps
test runner will execute.
:type step_index: int
:param step_key: Key of the state of how it was provided
to the test runner in case test step needs
to reference other steps within the runner by their.
:type step_key: str
:param state: Global state reference from the test runner.
:type state: platform_utils.utils.dt_context.BaseContext
"""
self.client = client
self.steps = steps
self.step_index = step_index
self.step_key = step_key
self.state = state
@property
def prev_steps(self):
"""
Get :py:class:`collections.OrderedDict` of previous steps
excluding itself, if any.
.. note:: The steps are returned in order of adjacency
from the current step. For example
(using list instead of OrederedDict in example)::
> step = TestStep()
> step.steps = [0, 1, 2, 3, 4]
> step.step_index = 3
> step.prev_steps
[2, 1, 0]
:rtype: :py:class:`collections.OrderedDict`
"""
return OrderedDict(list(self.steps.items())[:self.step_index][::-1])
@property
def next_steps(self):
"""
Get :py:class:`collections.OrderedDict` of next steps
excluding itself, if any.
:rtype: :py:class:`collections.OrderedDict`
"""
return OrderedDict(list(self.steps.items())[self.step_index + 1:])
@property
def prev_step(self):
"""
Get previous step instance, if any.
:rtype: :py:class:`TestStep`
"""
prev_steps = list(self.prev_steps.values())
return prev_steps[0] if prev_steps else None
@property
def next_step(self):
"""
Get previous step instance, if any.
:rtype: :py:class:`TestStep`
"""
next_steps = list(self.next_steps.values())
return next_steps[0] if next_steps else None
[docs] def get_urlconf(self):
"""
Get ``urlconf`` which will be used to compute the URL using ``reverse``
By default this returns :py:attr:`urlconf`, if defined,
else None
:rtype: str
"""
return self.urlconf or None
[docs] def get_override_settings(self):
"""
Get ``overriden_settings`` which will be used to decorate the request with
the defined settings to be overriden.
By default this returns :py:attr:`overriden_settings`, if defined,
else empty dict
:rtype: dict
"""
return self.overriden_settings or {}
[docs] def get_url_args(self):
"""
Get ``url_args`` which will be used to compute
the URL using ``reverse``.
By default this returns :py:attr:`url_args`, if defined,
else empty tuple.
:rtype: tuple
"""
return self.url_args or tuple()
[docs] def get_url_kwargs(self):
"""
Get ``url_kwargs`` which will be used to compute
the URL using ``reverse``.
By default this returns :py:attr:`url_kwargs`, if defined,
else empty dict.
:rtype: dict
"""
return self.url_kwargs or {}
[docs] def get_url(self):
"""
Compute the URL to request using Django's ``reverse``.
Reverse is called using :py:attr:`url_name`,
:py:meth:`get_url_args` and :py:meth:`get_url_args`.
:rtype: str
"""
return reverse(self.url_name,
args=self.get_url_args(),
kwargs=self.get_url_kwargs(),
urlconf=self.get_urlconf())
[docs] def get_content_type(self):
"""
Get ``content_type`` which will be used when making the test request.
By default this returns :py:attr:`content_type`, if defined,
else empty string.
:rtype: str
"""
return self.content_type or ''
[docs] def get_request_data(self, data=None):
"""
Get data dict to be sent to the server.
:param data: Data to be used while sending server request.
If not defined, :py:attr:`data` is returned.
:rtype: dict
"""
return (self.data if data is None else data) or {}
[docs] def get_request_kwargs(self):
"""
Get kwargs to be passed to the :py:attr:`client`.
By default this returns dict of format::
{
'path': ...,
'data': ...
}
Can be overwritten in case additional parameters need
to be passed to the client to make the request.
:rtype: dict
"""
kwargs = {
'path': self.get_url(),
'data': self.get_request_data(),
}
content_type = self.get_content_type()
if content_type:
kwargs['content_type'] = content_type
return kwargs
[docs] def get_validators(self):
"""
Get all validators.
By default returns :py:attr:`validators` however
can be used as a hook to returns additional validators
dynamically.
:rtype: list
"""
return self.validators
[docs] def request(self):
"""
Make the server request. Server response is then saved
in :py:attr:`request`.
Before making the request, :py:meth:`pre_request_hook`
is called and :py:meth:`post_request_hook` is called
after the request.
:returns: server response
"""
self.pre_request_hook()
try:
with override_settings(**self.get_override_settings()):
self.response = (getattr(self.client, self.request_method)
(**self.get_request_kwargs()))
except Exception:
validator = BaseValidator(self)
e_type, e, e_traceback = sys.exc_info()
msg = ('{} failed:\n\n{}'
''.format(validator._get_base_error_message(),
six.text_type(e)))
cls = type(e_type.__name__, (Exception,), {})
six.reraise(
cls,
cls(msg),
e_traceback
)
self.post_request_hook()
return self.response
[docs] def test_response(self):
"""
Test the server response by looping over all
validators as returned by :py:meth:`get_validators`.
Before assertions, :py:meth:`pre_test_response`
is called and :py:meth:`post_test_response` is called
after assertions.
"""
self.pre_test_response()
for validator in self.get_validators():
if inspect.isclass(validator):
validator(self).test(self)
else:
validator.test(self)
self.post_test_response()
[docs] def pre_test_response(self):
"""
Hook which is executed before validating the response.
"""
[docs] def post_test_response(self):
"""
Hook which is executed after validating the response.
"""
[docs] def pre_request_hook(self):
"""
Hook which is executed before server request is sent.
"""
[docs] def post_request_hook(self):
"""
Hook which is executed after server request is sent.
"""
[docs]class StatefulUrlParamsTestStep(TestStep):
"""
Test step same as :py:class:`TestStep`
except it references ``url_args`` and ``url_kwargs`` from the state.
Having url computed from the state, allows for a particular step
to change ``url_args`` or ``url_kwargs`` hence future
steps will fetch different resources.
"""
[docs] def get_url_args(self):
"""
Get URL args for Django's ``reverse``
Similar to :py:meth:`TestStep.get_url_args` except
url args are retrieved by default from state and if not
available get args from class attribute.
:returns: tuple of url_args
"""
args = super(StatefulUrlParamsTestStep, self).get_url_args()
return self.state.get('url_args', args)
[docs] def get_url_kwargs(self):
"""
Get URL kwargs fpr Django's ``reverse``
Similar to :py:meth:`TestStep.get_url_kwargs` except
url args are retrieved by default from state and if not
available get kwargs from class attribute.
:returns: dict of url_kwargs
"""
kwargs = super(StatefulUrlParamsTestStep, self).get_url_kwargs()
return self.state.get('url_kwargs', kwargs)