blob: 7197a8149af2700030d1fb9f5ad341dc72172b87 [file] [log] [blame] [view]
# Testing in Infra.git
[TOC]
## The Bare Minimum
All operations on tests are performed using the [test.py](../test.py) script.
Here are some commands for the impatient:
| Command | Meaning |
| ---------------------------- | ------------------------------------------------------- |
| `./test.py test` | Run all tests in the repository and report results. |
| `./test.py list` | List all tests in the repository, without running them. |
| `./test.py test infra` | Run only tests found in the infra package. |
| `./test.py train` | Run all tests and write expectations. |
| `./test.py test infra:*foo*` | Run tests from infra with 'foo' in their name. |
By default, `test.py` collects coverage information, and not having 100%
coverage is an error.
## Writing Tests
`test.py` enforces some constraints so as to maintain a clear structure
in the repository:
* tests must be methods of subclasses of unittest.TestCase. test.py
will *not* look for standalone functions. In addition, the method
name must start with 'test'.
* tests classes must be contained in files named like `*_test.py`.
* the coverage information for file `foo.py` is only collected from
tests located in `test/foo_test.py` or `tests/foo_test.py`.
A test fails when an exception is raised, or if expectations don't match
(read on). Test methods can return a value. When run in train mode,
`test.py` stores these values on disk, in directories named like
`*.expected/` next to the file containing tests. When run in test mode,
the return values are compared to the ones previously stored, and the
test fails if they don't match.
Example
```python
import unittest
import
class FooTest(unittest.TestCase):
def test_sha1(self):
ret = hashlib.sha1("Unimportant text").hexdigest()
self.assertEqual(ret, '19c12dd68b216f1a7a26d5b0290355ceef8a35b2')
def test_sha1_expectations(self):
ret = hashlib.sha1("Unimportant text").hexdigest()
return ret
```
`test_sha1` and `test_sha1_expectations` performs the same task, in a
different way. To have both tests pass, you have to run:
./test.py train # record output of test_sha1_expectations
./test.py test
## Testing App Engine with Endpoints
Writing unit tests for code that uses Google Cloud Endpoints can be
difficult. More precisely, writing the unit tests is much like writing
any unit test, but ensuring that one's unit tests will run can be
painful.
Almost ubiquitously, one finds that testing App Engine involves testbed
and webtest. The former facilitates stubbing of various backend
services; the latter creates a mock application on which one can make
API calls and inspect the results. Some interactions between Endpoints
and webtest may prove turbid even to those used to testing App Engine
applications; what follows is a series of prescriptions concerning the
least obvious of these interactions.
For a more detailed description of the system, adapted to the novice and
with pointers to enlightening reading, see
[Testing novice](testing_novice.md). For high-level documentation
intended for the seasoned App Engine/Cloud Endpoints developer, read on.
### A Worked Example
`something.py` contains the API:
```python
class GoodRequest(messages.Message):
data = messages.IntegerField(1)
class GreatResponse(messages.Message):
data = messages.IntegerField(1)
@endpoints.api(name='someendpoint', version='v1')
class SomeEndpoint(remote.Service):
@endpoints.method(GoodRequest, GreatResponse,
path='/exalt', http_method='POST',
name='exalt')
def glorify(self, request):
glorious_number = request.data
if glorious_number < 0:
raise endpoints.BadRequestException(
'Perhaps you wanted to make a PessimisticRequest?')
response = GreatResponse(data=request.data ** 2)
```
`test/something_test.py` contains our test suite:
```python
# other imports
from something import SomeEndpoint
from support import test_case
class MyNiceTestSuite(test_case.EndpointsTestCase):
api_service_cls = SomeEndpoint
def setUp(self):
super(MyNiceTestSuite, self).setUp()
# testbed setup, stub initialization, etc. should go here
def testGlorifyPerformsWonderfulSquaring(self):
request = {'data': 4}
response = self.call_api('glorify', request).json_body
self.assertEquals(response, {'data': 16})
def testNegativeNumbersAreNotGloriousEnough(self):
request = {'data': -4}
with self.call_should_fail('400'):
self.call_api('glorify', request)
```
### test_case.EndpointsTestCase Is Balm to One Parched
`test_case` module (DEPSed as `/luci/appengine/components/support/test_case.py`)
hides some of the complexity of writing test cases for Endpoints code.
To explicate, `EndpointsTestCase` provides the following facilities:
* explicit creation of `endpoints.api_server` and `webtest.testApp`
with `setUp`
* correct routing to endpoints methods (the user no longer needs to write
`'/_ah/spi/IncredibleEndpointName.someLongMethodName'`) with
`call_api`
* error management (which will become error handling pending a fix for
[bug in `call_should_fail`](https://code.google.com/p/googleappengine/issues/detail?id=10544))
Much of the obscurity in Endpoints testing now evaporates. By using
`EndpointsTestCase`, we avoid the pitfalls that inhere in setting up and
posting to such an API in a test environment. A few final points:
* `api_service_cls`, a class member of the test suite, must be set;
otherwise, the test suite will not be able to create a test
application and will not have any knowledge of the API's methods
* `EndpointsTestCase.call_api` and `EndpointsTestCase.call_should_fail` are the
recommended ways to make an API call and to handle errors, respectively. Note
that the argument structure for `call_api` is
`(<method name>, <request body>)`; the method name is literally the name
to which a method is bound in the API code, not the name specified in the decorator
Happy testing!