Merge pull request #2738 from coolo/add_test_docu
Refresh the documentation on how to add tests
This commit is contained in:
commit
a7a1aa83dc
61
README.md
61
README.md
@ -78,9 +78,9 @@ Then you can use the new `local` alias to access this new instance.
|
||||
|
||||
osc -A local api /about
|
||||
|
||||
Some tests will attempt to run against the local OBS, but not all.
|
||||
Some tests will attempt to run against the local OBS, but not all. It's still recommended to run this through docker-compose (see below)
|
||||
|
||||
nosetests
|
||||
pytest tests/*.py
|
||||
|
||||
## Running Continuous Integration
|
||||
|
||||
@ -130,3 +130,60 @@ To debug problems in the test suite or in the code, place a `breakpoint()` call
|
||||
You can access your testing OBS instance at `http://0.0.0.0:3000` and log in using "Admin" as username and "opensuse" as password. To prevent the data being removed while you are inspecting the OBS instance, you can put a call to the `breakpoint()` function.
|
||||
|
||||
Finally, if you miss anything for debugging, you can use `zypper` to install it.
|
||||
|
||||
### Adding tests
|
||||
|
||||
Testing the release tools isn't quite trivial as a lot of these tools rely on running openSUSE infrastructure. Some of the workflows we replay (not mock) in above described docker-compose setup. So each test will setup the required projects and e.g. staging workflows in a local containerized OBS installation and then do its assertions. If you want to add coverage, best check existing unit tests in tests/*.py. A generic test case looks similiar to this:
|
||||
|
||||
``` {.python title="Basic Test Example" }
|
||||
class TestExample(unittest.TestCase):
|
||||
|
||||
def test_basic(self):
|
||||
# Keep the workflow in local scope so that ending the test case will destroy it.
|
||||
# Destroying the workflow will also delete all created projects and packages. The
|
||||
# created workflow has a target project, but most of the test assets need to be created
|
||||
# as needed
|
||||
wf = OBSLocal.FactoryWorkflow()
|
||||
staging = wf.create_staging('A', freeze=True)
|
||||
wf.create_submit_request('devel:wine', 'wine')
|
||||
|
||||
ret = SelectCommand(wf.api, staging.name).perform(['wine'])
|
||||
self.assertEqual(True, ret)
|
||||
```
|
||||
|
||||
To ease having many such tests, we also have the `OBSLocal` class, which moves the creation of the workflow into `setUp` and the destruction in `tearDwon` functions of pytest. The principle stays the same though.
|
||||
|
||||
``` {.python title="OBSLocal Usage"}
|
||||
class TestExampleWithOBS(OBSLocal.TestCase):
|
||||
"""
|
||||
Tests for various api calls to ensure we return expected content
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super(TestExampleWithOBS, self).setUp()
|
||||
self.wf = OBSLocal.FactoryWorkflow()
|
||||
self.wf.setup_rings()
|
||||
self.staging_b = self.wf.create_staging('B')
|
||||
|
||||
def tearDown(self):
|
||||
del self.wf
|
||||
super(TestExampleWithOBS, self).tearDown()
|
||||
|
||||
def test_list_projects(self):
|
||||
"""
|
||||
List projects and their content
|
||||
"""
|
||||
staging_a = self.wf.create_staging('A')
|
||||
|
||||
# Prepare expected results
|
||||
data = [staging_a.name, self.staging_b.name]
|
||||
|
||||
# Compare the results
|
||||
self.assertEqual(data, self.wf.api.get_staging_projects())
|
||||
```
|
||||
|
||||
Note that we have some (older) test cases using httpretty, but those are very special cases and require you a lot of extra mocking as you can't mix httpretty and testing against the minimal OBS. So every extra
|
||||
call that osc libraries or our code do, will require changes in your test case. It can still be a viable option, especially if more than OBS is involved.
|
||||
|
||||
The method that you can combine with `OBSLocal` though is using MagicMock. This class is used to mock individual functions. So splitting the code to use helper functions to retrieve information and then
|
||||
mocking this inside the test case can be a good alternative to mocking the complete HTTP traffic.
|
@ -1,136 +0,0 @@
|
||||
Testing
|
||||
=======
|
||||
|
||||
Dependencies
|
||||
------------
|
||||
|
||||
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:
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
zypper in python-nose python-httpretty python-mock
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Running tests
|
||||
-------------
|
||||
|
||||
To run the tests, you need to be in the topmost directory of your checkout and
|
||||
run the following command there:
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
nosetests
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Structure of the suite
|
||||
----------------------
|
||||
|
||||
Each object contains 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.
|
||||
|
||||
Writing tests
|
||||
-------------
|
||||
|
||||
There are a few nice building stones available to implement test.
|
||||
|
||||
OBS class
|
||||
~~~~~~~~~
|
||||
|
||||
+OBS+ class provides simulation of OBS including keeping internal states. It
|
||||
supports only a limited number of commands, but that can be extended.
|
||||
|
||||
Extending OBS class
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can extend the OBS mockup class creating new method, and
|
||||
decorating it with one of the @GET, @PUT, @POST or @DELETE. The
|
||||
parameter of the decorator is the PATH of the URL or a regular
|
||||
expression that matches one of the possible paths.
|
||||
|
||||
If the new response can be implemented as a simple fixture, you can
|
||||
create the file in the +fixtures/+ directory in a place compatible in
|
||||
the expected path.
|
||||
|
||||
Because we are decorating methods, we can maintain an internal status
|
||||
inside the OBS mock-up instance.
|
||||
|
||||
example
|
||||
^^^^^^^
|
||||
|
||||
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.
|
||||
|
||||
.Template
|
||||
[source,xml]
|
||||
--------------------------------------------------------------------------------
|
||||
<request id="${id}">
|
||||
<action type="submit">
|
||||
<source project="home:Admin" package="${package}" rev="59f0f46262d7b57b9cdc720c06d5e317"/>
|
||||
<target project="openSUSE:Factory" package="${package}"/>
|
||||
</action>
|
||||
<state name="${request}" who="Admin" when="2014-02-17T12:38:52">
|
||||
<comment>...</comment>
|
||||
</state>
|
||||
<review state="${review}" when="2014-02-17T12:34:10" who="${who}" by_${by}="${by_who}">
|
||||
<comment>...</comment>
|
||||
</review>
|
||||
<description>test</description>
|
||||
</request>
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
We can also define helpful local data structure representing actual state of OBS
|
||||
|
||||
[source,python]
|
||||
--------------------------------------------------------------------------------
|
||||
# Initial request data
|
||||
self.requests = {
|
||||
'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',
|
||||
},
|
||||
}
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
And the most important part is implementing OBS behaviour.
|
||||
|
||||
[source,python]
|
||||
--------------------------------------------------------------------------------
|
||||
@GET(re.compile(r'/request/\d+'))
|
||||
def request(self, request, uri, headers):
|
||||
"""Return a request XML description."""
|
||||
request_id = re.search(r'(\d+)', uri).group(1)
|
||||
response = (404, headers, '<result>Not found</result>')
|
||||
try:
|
||||
template = string.Template(self._fixture(uri))
|
||||
response = (200, headers, template.substitute(self.requests[request_id]))
|
||||
except Exception as e:
|
||||
if DEBUG:
|
||||
print uri, e
|
||||
|
||||
if DEBUG:
|
||||
print 'REQUEST', uri, response
|
||||
|
||||
return response
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
The method +request+ will be called when a request to /request/NUMBER
|
||||
is made. The previous code will load the XML template and replace
|
||||
variables with the request dictionary content.
|
Loading…
x
Reference in New Issue
Block a user