This document assumes familiarity with writing Endpoints APIs in Python and with App Engine testing. The intention is to follow one path toward baseline App Engine and Endpoints testing knowledge, saving one hours/days searching for solutions to the non-trivial problems which may arise. Edifying links can be found throughout.
Users already comfortable with Endpoints (or just in need of plug-and-play example code) can skip to Conclusion: Use test_case.EndpointsTestCase
or return to Testing.
Assume that an Endpoints service is defined as
class QuietRequest(messages.Message): whispered = messages.StringField(1) class LoudResponse(messages.Message): yelled = messages.StringField(1) @endpoints.api(name='someendpoint', version='v1') class UpToElevenEndpoint(remote.Service): @endpoints.method(QuietRequest, LoudResponse, path='/fortissimo', http_method='POST', name='fortissimo') def yell(self, request): response = LoudResponse(yelled=request.whispered.upper())
This service will be deployed with an invocation like
app = endpoints.api_server([UpToElevenEndpoint])
In test code, one would then have to wrap such an app as a webtest.TestApp
, taking care to set restricted=False
, e.g.
class VolumeTestSuite(unittest.TestCase): def setUp(self): super(VolumeTestSuite, self).setUp() # testbed setup, stub initialization, etc. should go here app = endpoints.api_server([UpToElevenEndpoint], restricted=False) self.source_ip = 'some.ip.address' self.test_app = webtest.TestApp( app, extra_environ={'REMOTE_ADDR', self.source_ip}) def testYellMethod(self): response = self.test_app.post_json( '/_ah/spi/UpToElevenEndpoint.yell', {'whispered': 'yes, I am data'}) self.assertEqual( json.loads(response.body).get('yelled', ''), 'YES, I AM DATA')
With restricted=False
, test clients (and other clients from outside Google's serving infrastructure) can access the server. self.source_ip
is an IP at which an App Engine instance can be run.
If questions have arisen for you about any of the above, it may be beneficial to read on; if not, the punchline lies at Conclusion: Use test_case.EndpointsTestCase
.
Let us assume that one has defined the following service and messages in file something.py
:
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): response = GreatResponse(data=request.data ** 2)
We define SomeEndpoint
as a subclass of remote.Service
and we give it a single method, glorify
, which takes GoodRequest
and GreatResponse
as its request and response types, respectively. It reads the numeric data provided by the request and squares it (the most glorious of arithmetic operations).
We include the pattern in app.yaml
(assuming our Endpoints methods from above are in something.py
and that an api_server
called app
is defined therein):
- url: /_ah/spi/.* script: something.app
When we try to test on a local dev server, we find that neither path
nor name
will avail us, nor can we make calls to /_ah/api/<method>
. Instead, the URLs to be called in our tests will be of the form /_ah/spi/<ClassName>.<methodname>
. In this example, to test glorify
, we would make POST requests to /_ah/spi/SomeEndpoint.glorify
, completely disregarding the path
and name
fields. A little more on this topic can be found here.
One final oddity: all API calls will be converted to POST. Thus, whatever http_method
one declares in the API method definition, one must call self.test_app.post
(or its analogs, e.g. post_json
) when testing the API.
Details can be found here. Essentially, any exceptions raised and not handled during Endpoints code execution will be converted within endpoints/apiserving to a JSON response and served to the client. However, this code does not yet update the Content-Length
header, so a different error (AssertionError
from some place in the code that tries to assert that Content-Length
is correct) will almost certainly be raised.
This is easy to fix locally, but, until the bug report is addressed, there will be no way to accomplish proper error handling in Endpoints code.
Let's add error cases to something.py
:
@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?') if glorious_number == 109: raise endpoints.ForbiddenException( 'That number is deprecated. Use 28 instead.') response = GreatResponse(data=request.data ** 2)
We raise two kinds of exceptions: endpoints.BadRequestException
, which will set the response‘s status to 400
, and endpoints.ForbiddenException
, which corresponds to HTTP status 403
. One can check this page for a mapping of Endpoints’ exception types to the HTTP status codes they use.
We would expect to be able to add code like
with self.assertRaises(endpoints.ForbiddenException): response = self.test_app.post_json( '/_ah/spi/SomeEndpoint.glorify', {'data': 109})
or manually check that the status code in the response is 403
; however, either method will yield the rather cryptic AppError: Bad response: 500 Internal Server Error (not 200)
due to the content-length problem described above.
It is thus recommended to inherit from EndpointsTestCase, instead of unittest.TestCase
. Among other things (e.g. properly wrapping the endpoints.api_service
as a webtest.TestApp
), this class exposes the call_should_fail
context manager. While this method is still subject to the mentioned bug, it will be more fully implemented pending the bug's resolution. One simply asserts that the correct error status is raised, e.g.
with self.call_should_fail('403'): response = self.test_app.post_json( '/_ah/spi/SomeEndpoint.glorify', {'data': 109})
test_case.EndpointsTestCase
The previous section gestured toward a test module that addresses some of the shortcomings concerning Endpoints testing. In fact, much of the above complexity can be circumvented by using test_case
module. For more on EndpointsTestCase, return to testing.