2014-02-05 17:09:05 +01:00
|
|
|
Testing
|
|
|
|
=======
|
|
|
|
|
|
|
|
Dependencies
|
|
|
|
------------
|
|
|
|
|
2014-02-06 11:14:08 +01:00
|
|
|
Test suite is using +nose+, +httpretty+ and +mock+ for testing. You need these
|
|
|
|
three python modules installed to run tests. In openSUSE, you can do it using
|
|
|
|
the following command as a root:
|
2014-02-05 17:09:05 +01:00
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
2014-02-06 11:14:08 +01:00
|
|
|
zypper in python-nose python-httpretty python-mock
|
2014-02-05 17:09:05 +01:00
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
|
|
|
Running tests
|
|
|
|
-------------
|
|
|
|
|
|
|
|
To run the tests, you need to be in the topmost directory of your checkout and
|
|
|
|
run the following command there:
|
|
|
|
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
nosetests
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
2014-02-06 11:14:08 +01:00
|
|
|
Structure of the suite
|
|
|
|
----------------------
|
2014-02-05 17:09:05 +01:00
|
|
|
|
|
|
|
Each object is containing functions for the individual tests so split them per
|
|
|
|
area of interest.
|
|
|
|
|
|
|
|
In directory fixtures there are resulting xml files obtained from the OBS with
|
|
|
|
osc api calls.
|
|
|
|
|
2014-02-20 09:23:50 +01:00
|
|
|
Writing tests
|
|
|
|
-------------
|
|
|
|
|
|
|
|
There are few nice building stones available to implement test.
|
|
|
|
|
2014-02-24 16:27:28 +01:00
|
|
|
OBS class
|
|
|
|
~~~~~~~~~
|
|
|
|
|
|
|
|
+OBS+ class provides simulation of OBS including keeping internal states. It
|
|
|
|
supports only limited number of command, but that can be extended.
|
|
|
|
|
|
|
|
Extending OBS class via +responses+ dictionary
|
|
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
2014-02-20 09:23:50 +01:00
|
|
|
|
|
|
|
It contains dictionaries for 'GET', 'PUT', 'POST' and 'ALL'. All of them
|
|
|
|
correspond to the method used by program. 'ALL' is just a shortcut that covers
|
|
|
|
all methods and it is called only if no specific handler is found.
|
|
|
|
|
|
|
|
Dictionaries associated with each method contains as a key relative URL to the
|
|
|
|
top of the server and as data one of the following:
|
|
|
|
|
|
|
|
* path to the file in fixtures directory with XML to return
|
2014-02-24 16:27:28 +01:00
|
|
|
* XML itself starting with +<+
|
|
|
|
* function taking three arguments - 'request', 'uri' and 'headers'
|
2014-02-20 09:23:50 +01:00
|
|
|
* list which is combination of any of the above
|
|
|
|
|
|
|
|
If handling of +responses+ dictionary is enabled, URL is find in dictionary and
|
|
|
|
appropriate action is taken. If there is XML or path to the XML, XML is
|
|
|
|
returned directly. In case of function, function gets called and is expected to
|
|
|
|
return string that will be passed back. If there is array, first item is used
|
|
|
|
as described above and removed from the array.
|
|
|
|
|
2014-02-24 16:27:28 +01:00
|
|
|
To add new possible actions, define private method of OBS class that will
|
|
|
|
regiter handlers in this dictionary and add this method to be called by
|
|
|
|
+_clear_responses+ method.
|
2014-02-20 09:23:50 +01:00
|
|
|
|
|
|
|
example
|
2014-02-24 16:27:28 +01:00
|
|
|
^^^^^^^
|
2014-02-20 09:23:50 +01:00
|
|
|
|
2014-02-24 16:27:28 +01:00
|
|
|
First we create a template for our response. We will be using pythons string
|
|
|
|
Template class therefore XML has some values replaced with +${variable}+ and we
|
|
|
|
will assign those later.
|
2014-02-20 09:23:50 +01:00
|
|
|
|
|
|
|
.Template
|
|
|
|
[source,xml]
|
|
|
|
--------------------------------------------------------------------------------
|
2014-02-24 16:27:28 +01:00
|
|
|
<request id="${id}">
|
2014-02-20 09:23:50 +01:00
|
|
|
<action type="submit">
|
2014-02-24 16:27:28 +01:00
|
|
|
<source project="home:Admin" package="${package}" rev="59f0f46262d7b57b9cdc720c06d5e317"/>
|
|
|
|
<target project="openSUSE:Factory" package="${package}"/>
|
2014-02-20 09:23:50 +01:00
|
|
|
</action>
|
|
|
|
<state name="${request}" who="Admin" when="2014-02-17T12:38:52">
|
2014-02-24 16:27:28 +01:00
|
|
|
<comment>...</comment>
|
2014-02-20 09:23:50 +01:00
|
|
|
</state>
|
2014-02-24 16:27:28 +01:00
|
|
|
<review state="${review}" when="2014-02-17T12:34:10" who="${who}" by_${by}="${by_who}">
|
|
|
|
<comment>...</comment>
|
2014-02-20 09:23:50 +01:00
|
|
|
</review>
|
|
|
|
<description>test</description>
|
|
|
|
</request>
|
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
2014-02-24 16:27:28 +01:00
|
|
|
We can also define helpful local data structure representing actual state of OBS
|
2014-02-20 09:23:50 +01:00
|
|
|
|
|
|
|
[source,python]
|
|
|
|
--------------------------------------------------------------------------------
|
2014-02-24 16:27:28 +01:00
|
|
|
# Initial request data
|
|
|
|
requests_data = { '123': { 'request': 'new', 'review': 'accepted',
|
|
|
|
'who': 'Admin', 'by': 'group', 'id': '123',
|
|
|
|
'by_who': 'opensuse-review-team',
|
|
|
|
'package': 'gcc' },
|
|
|
|
'321': { 'request': 'review', 'review': 'new',
|
|
|
|
'who': 'Admin', 'by': 'group', 'id': '321',
|
|
|
|
'by_who': 'factory-staging',
|
|
|
|
'package': 'puppet' }
|
|
|
|
}
|
2014-02-20 09:23:50 +01:00
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
2014-02-24 16:27:28 +01:00
|
|
|
And the most important part is implementing OBS behaviour.
|
2014-02-20 09:23:50 +01:00
|
|
|
|
|
|
|
[source,python]
|
|
|
|
--------------------------------------------------------------------------------
|
2014-02-24 16:27:28 +01:00
|
|
|
def _request_review(self):
|
|
|
|
"""
|
|
|
|
Register requests methods
|
|
|
|
"""
|
|
|
|
|
|
|
|
# Load template
|
|
|
|
tmpl = Template(self._get_fixture_content('request_review.xml'))
|
|
|
|
|
|
|
|
# What happens when we try to change the review
|
|
|
|
def review_change(responses, request, uri):
|
|
|
|
rq_id = re.match( r'.*/([0-9]+)',uri).group(1)
|
|
|
|
args = self.requests_data[rq_id]
|
|
|
|
# Adding review
|
|
|
|
if request.querystring.has_key(u'cmd') and request.querystring[u'cmd'] == [u'addreview']:
|
|
|
|
self.requests_data[rq_id]['request'] = 'review'
|
|
|
|
self.requests_data[rq_id]['review'] = 'new'
|
|
|
|
# Changing review
|
|
|
|
if request.querystring.has_key(u'cmd') and request.querystring[u'cmd'] == [u'changereviewstate']:
|
|
|
|
self.requests_data[rq_id]['request'] = 'new'
|
|
|
|
self.requests_data[rq_id]['review'] = request.querystring[u'newstate'][0]
|
|
|
|
# Project review
|
|
|
|
if request.querystring.has_key(u'by_project'):
|
|
|
|
self.requests_data[rq_id]['by'] = 'project'
|
|
|
|
self.requests_data[rq_id]['by_who'] = request.querystring[u'by_project'][0]
|
|
|
|
# Group review
|
|
|
|
if request.querystring.has_key(u'by_group'):
|
|
|
|
self.requests_data[rq_id]['by'] = 'group'
|
|
|
|
self.requests_data[rq_id]['by_who'] = request.querystring[u'by_group'][0]
|
|
|
|
responses['GET']['/request/' + rq_id] = tmpl.substitute(self.requests_data[rq_id])
|
|
|
|
return responses['GET']['/request/' + rq_id]
|
|
|
|
|
|
|
|
# Register methods for all requests
|
|
|
|
for rq in self.requests_data:
|
|
|
|
# Static response for gets (just filling template from local data)
|
|
|
|
self.responses['GET']['/request/' + rq] = tmpl.substitute(self.requests_data[rq])
|
|
|
|
# Interpret other requests
|
|
|
|
self.responses['POST']['/request/' + rq] = review_change
|
2014-02-20 09:23:50 +01:00
|
|
|
--------------------------------------------------------------------------------
|
|
|
|
|
2014-02-24 16:27:28 +01:00
|
|
|
Method +_request_review+ will be called from +_clear_responses+ method and it
|
|
|
|
will fill responses for 'GET' requests from local data strusture we defined
|
|
|
|
before. It will also register +review_change+ function to handle 'POST'
|
|
|
|
requests.
|
|
|
|
|
|
|
|
Function +review_change+ will modify our local data structure to make sure we
|
|
|
|
remeber the state for next time, replaces the results of 'GET' requests and
|
|
|
|
returns whatever 'GET' should be returning now.
|
|
|
|
|
2014-02-20 09:23:50 +01:00
|
|
|
So whenever somebody sends 'addreview' command, XML that 'GET' on request
|
|
|
|
provides will be changed to reflect newly added review. And whenever somebody
|
|
|
|
sends 'changereviewstate', review will be closed with appropriate state.
|
|
|
|
|
|
|
|
So we have a simple testing framework with multiple states that reacts to the
|
|
|
|
API calls from functions we are testing following the behaviour we specified.
|
2014-02-24 16:27:28 +01:00
|
|
|
So tests itself can be pretty simple and depend on multiple function calls.
|
|
|
|
|
|
|
|
Registering +OBS+ class
|
|
|
|
~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
|
|
|
|
To take advantage of simulated OBS, you have to register it inside your test
|
|
|
|
and use it's api. To do so, just call +register_obs+ method at the beginning
|
|
|
|
of the test. If you are using staging plugin API, you should use +OBS.api+
|
|
|
|
object that is providing it for you convenience.
|
|
|
|
|
|
|
|
If you run request that wasn't implemented yet, exception will be raised
|
|
|
|
providing URL that program tried to access together with method used.
|
|
|
|
|
|
|
|
Simple tests given all behaviour you are expecting is already implemented can
|
|
|
|
be done for example like this:
|
2014-02-20 09:23:50 +01:00
|
|
|
|
|
|
|
[source,python]
|
|
|
|
--------------------------------------------------------------------------------
|
2014-02-24 16:27:28 +01:00
|
|
|
# Register OBS
|
|
|
|
self.obs.register_obs()
|
|
|
|
# Get rid of open requests
|
|
|
|
self.obs.api.dispatch_open_requests()
|
|
|
|
# Check that we tried to close it
|
2014-02-20 09:23:50 +01:00
|
|
|
self.assertEqual(httpretty.last_request().method, 'POST')
|
|
|
|
self.assertEqual(httpretty.last_request().querystring[u'cmd'], [u'changereviewstate'])
|
2014-02-24 16:27:28 +01:00
|
|
|
# Try it again
|
|
|
|
self.obs.api.dispatch_open_requests()
|
|
|
|
# This time there should be nothing to close
|
2014-02-20 09:23:50 +01:00
|
|
|
self.assertEqual(httpretty.last_request().method, 'GET')
|
|
|
|
--------------------------------------------------------------------------------
|