Skip to content

Commit 39b4d5d

Browse files
author
Jon Wayne Parrott
committed
Merge pull request GoogleCloudPlatform#37 from GoogleCloudPlatform/pytest-refactor
Pytest refactor
2 parents 852eedc + 52aba06 commit 39b4d5d

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

69 files changed

+1883
-1377
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ pip-delete-this-directory.txt
1414
# Unit test / coverage reports
1515
htmlcov/
1616
.tox/
17+
.nox/
1718
.coverage
1819
.coverage.*
1920
.cache

.travis.yml

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,7 @@ env:
66
before_install:
77
- openssl aes-256-cbc -K $encrypted_b4c8e1c51f6e_key -iv $encrypted_b4c8e1c51f6e_iv -in secrets.tar.enc -out secrets.tar -d
88
- tar xvf secrets.tar
9-
- pip install tox
9+
install:
10+
- pip install nox-automation tox
1011
script:
11-
- tox
12-
- cd $TRAVIS_BUILD_DIR/1-hello-world && tox
13-
- cp $TRAVIS_BUILD_DIR/config.py $TRAVIS_BUILD_DIR/2-structured-data && cd $TRAVIS_BUILD_DIR/2-structured-data
14-
&& tox
15-
- cp $TRAVIS_BUILD_DIR/config.py $TRAVIS_BUILD_DIR/3-binary-data && cd $TRAVIS_BUILD_DIR/3-binary-data
16-
&& tox
17-
- cp $TRAVIS_BUILD_DIR/config.py $TRAVIS_BUILD_DIR/4-auth && cd $TRAVIS_BUILD_DIR/4-auth
18-
&& tox
19-
- cp $TRAVIS_BUILD_DIR/config.py $TRAVIS_BUILD_DIR/5-logging && cd $TRAVIS_BUILD_DIR/5-logging
20-
&& tox
21-
- cp $TRAVIS_BUILD_DIR/config.py $TRAVIS_BUILD_DIR/6-pubsub && cd $TRAVIS_BUILD_DIR/6-pubsub
22-
&& tox
23-
- cp $TRAVIS_BUILD_DIR/config.py $TRAVIS_BUILD_DIR/7-gce && cd $TRAVIS_BUILD_DIR/7-gce
24-
&& tox
12+
- nox --session lint travis

2-structured-data/bookshelf/model_cloudsql.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
from flask import Flask
1616
from flask.ext.sqlalchemy import SQLAlchemy
17-
import six
1817

1918

2019
builtin_list = list
@@ -29,11 +28,10 @@ def init_app(app):
2928

3029
def from_sql(row):
3130
"""Translates a SQLAlchemy model instance into a dictionary"""
32-
d = {}
33-
for column in row.__table__.columns:
34-
d[column.name] = six.text_type(getattr(row, column.name))
35-
36-
return d
31+
data = row.__dict__.copy()
32+
data['id'] = row.id
33+
data.pop('_sa_instance_state')
34+
return data
3735

3836

3937
# [START model]
Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
tox==2.3.1
2-
unittest2==1.1.0
3-
nose==1.3.7
42
flake8==2.5.4
5-
coverage==4.1b2
6-
BeautifulSoup4==4.4.1
7-
mock==1.3.0
8-
flaky==3.0.3
3+
flaky==3.1.0
4+
pytest==2.8.7
5+
pytest-cov==2.2.1
6+
retrying==1.3.3

2-structured-data/requirements.txt

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
Flask==0.10.1
2-
gcloud==0.9.0
2+
gcloud==0.11.0
33
gunicorn==19.4.5
4-
oauth2client==1.5.2
4+
oauth2client==2.0.1
55
Flask-SQLAlchemy==2.1
6-
PyMySQL==0.7.1
7-
Flask-PyMongo==0.4.0
8-
PyMongo==3.2.1
6+
PyMySQL==0.7.2
7+
Flask-PyMongo==0.4.1
8+
PyMongo==3.2.2
99
six==1.10.0

2-structured-data/tests/conftest.py

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Copyright 2015 Google Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""conftest.py is used to define common test fixtures for pytest."""
16+
17+
import bookshelf
18+
import config
19+
from gcloud.exceptions import ServiceUnavailable
20+
from oauth2client.client import HttpAccessTokenRefreshError
21+
import pytest
22+
from retrying import retry
23+
24+
25+
@pytest.yield_fixture(params=['datastore', 'cloudsql', 'mongodb'])
26+
def app(request):
27+
"""This fixtures provides a Flask app instance configured for testing.
28+
29+
Because it's parametric, it will cause every test that uses this fixture
30+
to run three times: one time for each backend (datastore, cloudsql, and
31+
mongodb).
32+
33+
It also ensures the tests run within a request context, allowing
34+
any calls to flask.request, flask.current_app, etc. to work."""
35+
app = bookshelf.create_app(
36+
config,
37+
testing=True,
38+
config_overrides={
39+
'DATA_BACKEND': request.param
40+
})
41+
42+
with app.test_request_context():
43+
yield app
44+
45+
46+
@pytest.yield_fixture
47+
def model(monkeypatch, app):
48+
"""This fixture provides a modified version of the app's model that tracks
49+
all created items and deletes them at the end of the test.
50+
51+
Any tests that directly or indirectly interact with the database should use
52+
this to ensure that resources are properly cleaned up.
53+
54+
Monkeypatch is provided by pytest and used to patch the model's create
55+
method.
56+
57+
The app fixture is needed to provide the configuration and context needed
58+
to get the proper model object.
59+
"""
60+
model = bookshelf.get_model()
61+
62+
# Ensure no books exist before running. This typically helps if tests
63+
# somehow left the database in a bad state.
64+
delete_all_books(model)
65+
66+
yield model
67+
68+
# Delete all books that we created during tests.
69+
delete_all_books(model)
70+
71+
72+
# The backend data stores can sometimes be flaky. It's useful to retry this
73+
# a few times before giving up.
74+
@retry(
75+
stop_max_attempt_number=3,
76+
wait_exponential_multiplier=100,
77+
wait_exponential_max=2000)
78+
def delete_all_books(model):
79+
while True:
80+
books, _ = model.list(limit=50)
81+
if not books:
82+
break
83+
for book in books:
84+
model.delete(book['id'])
85+
86+
87+
def flaky_filter(info, *args):
88+
"""Used by flaky to determine when to re-run a test case."""
89+
_, e, _ = info
90+
return isinstance(e, (ServiceUnavailable, HttpAccessTokenRefreshError))

2-structured-data/tests/test_crud.py

Lines changed: 25 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -12,67 +12,26 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import unittest
16-
17-
import bookshelf
18-
import config
15+
from conftest import flaky_filter
1916
from flaky import flaky
20-
from gcloud.exceptions import ServiceUnavailable
21-
from nose.plugins.attrib import attr
22-
23-
24-
def flaky_filter(e, *args):
25-
return isinstance(e, ServiceUnavailable)
17+
import pytest
2618

2719

20+
# Mark all test cases in this class as flaky, so that if errors occur they
21+
# can be retried. This is useful when databases are temporarily unavailable.
2822
@flaky(rerun_filter=flaky_filter)
29-
class IntegrationBase(unittest.TestCase):
30-
31-
def createBooks(self, n=1):
32-
with self.app.test_request_context():
33-
for i in range(1, n + 1):
34-
self.model.create({'title': u'Book {0}'.format(i)})
35-
36-
def setUp(self):
37-
self.app = bookshelf.create_app(
38-
config,
39-
testing=True,
40-
config_overrides={
41-
'DATA_BACKEND': self.backend
42-
}
43-
)
44-
45-
with self.app.app_context():
46-
self.model = bookshelf.get_model()
47-
48-
self.ids_to_delete = []
49-
50-
# Monkey-patch create so we can track the IDs of every item
51-
# created and delete them during tearDown.
52-
self.original_create = self.model.create
53-
54-
def tracking_create(*args, **kwargs):
55-
res = self.original_create(*args, **kwargs)
56-
self.ids_to_delete.append(res['id'])
57-
return res
58-
59-
self.model.create = tracking_create
60-
61-
def tearDown(self):
62-
63-
# Delete all items that we created during tests.
64-
with self.app.test_request_context():
65-
list(map(self.model.delete, self.ids_to_delete))
66-
67-
self.model.create = self.original_create
68-
69-
70-
@attr('slow')
71-
class CrudTestsMixin(object):
72-
def testList(self):
73-
self.createBooks(11)
74-
75-
with self.app.test_client() as c:
23+
# Tell pytest to use both the app and model fixtures for all test cases.
24+
# This ensures that configuration is properly applied and that all database
25+
# resources created during tests are cleaned up. These fixtures are defined
26+
# in conftest.py
27+
@pytest.mark.usefixtures('app', 'model')
28+
class TestCrudActions(object):
29+
30+
def test_list(self, app, model):
31+
for i in range(1, 12):
32+
model.create({'title': u'Book {0}'.format(i)})
33+
34+
with app.test_client() as c:
7635
rv = c.get('/books/')
7736

7837
assert rv.status == '200 OK'
@@ -84,66 +43,45 @@ def testList(self):
8443
assert 'Book 9' not in body, "Should not show more than 10 books"
8544
assert 'More' in body, "Should have more than one page"
8645

87-
def testAddAndView(self):
46+
def test_add(self, app):
8847
data = {
8948
'title': 'Test Book',
9049
'author': 'Test Author',
9150
'publishedDate': 'Test Date Published',
9251
'description': 'Test Description'
9352
}
9453

95-
with self.app.test_client() as c:
54+
with app.test_client() as c:
9655
rv = c.post('/books/add', data=data, follow_redirects=True)
9756

9857
assert rv.status == '200 OK'
99-
10058
body = rv.data.decode('utf-8')
10159
assert 'Test Book' in body
10260
assert 'Test Author' in body
10361
assert 'Test Date Published' in body
10462
assert 'Test Description' in body
10563

106-
def testEditAndView(self):
107-
with self.app.test_request_context():
108-
existing = self.model.create({'title': "Temp Title"})
64+
def test_edit(self, app, model):
65+
existing = model.create({'title': "Temp Title"})
10966

110-
with self.app.test_client() as c:
67+
with app.test_client() as c:
11168
rv = c.post(
11269
'/books/%s/edit' % existing['id'],
11370
data={'title': 'Updated Title'},
11471
follow_redirects=True)
11572

11673
assert rv.status == '200 OK'
117-
11874
body = rv.data.decode('utf-8')
11975
assert 'Updated Title' in body
12076
assert 'Temp Title' not in body
12177

122-
def testDelete(self):
123-
with self.app.test_request_context():
124-
existing = self.model.create({'title': "Temp Title"})
78+
def test_delete(self, app, model):
79+
existing = model.create({'title': "Temp Title"})
12580

126-
with self.app.test_client() as c:
81+
with app.test_client() as c:
12782
rv = c.get(
12883
'/books/%s/delete' % existing['id'],
12984
follow_redirects=True)
13085

13186
assert rv.status == '200 OK'
132-
133-
with self.app.test_request_context():
134-
assert not self.model.read(existing['id'])
135-
136-
137-
@attr('datastore')
138-
class TestDatastore(CrudTestsMixin, IntegrationBase):
139-
backend = 'datastore'
140-
141-
142-
@attr('cloudsql')
143-
class TestCloudSql(CrudTestsMixin, IntegrationBase):
144-
backend = 'cloudsql'
145-
146-
147-
@attr('mongodb')
148-
class TestMongo(CrudTestsMixin, IntegrationBase):
149-
backend = 'mongodb'
87+
assert not model.read(existing['id'])

2-structured-data/tox.ini

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,9 @@ deps =
88
-rrequirements.txt
99
-rrequirements-dev.txt
1010
commands =
11-
nosetests \
12-
--with-flaky \
13-
--no-success-flaky-report \
14-
--with-coverage \
15-
--cover-package bookshelf \
16-
{posargs:-a '!e2e'}
17-
passenv = GOOGLE_APPLICATION_CREDENTIALS
11+
py.test --cov=bookshelf --no-success-flaky-report {posargs} tests
12+
passenv = GOOGLE_APPLICATION_CREDENTIALS DATASTORE_HOST
13+
setenv = PYTHONPATH={toxinidir}
1814

1915
[testenv:py34]
2016
basepython = python3.4

3-binary-data/bookshelf/model_cloudsql.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414

1515
from flask import Flask
1616
from flask.ext.sqlalchemy import SQLAlchemy
17-
import six
1817

1918

2019
builtin_list = list
@@ -29,11 +28,10 @@ def init_app(app):
2928

3029
def from_sql(row):
3130
"""Translates a SQLAlchemy model instance into a dictionary"""
32-
d = {}
33-
for column in row.__table__.columns:
34-
d[column.name] = six.text_type(getattr(row, column.name))
35-
36-
return d
31+
data = row.__dict__.copy()
32+
data['id'] = row.id
33+
data.pop('_sa_instance_state')
34+
return data
3735

3836

3937
class Book(db.Model):

3-binary-data/requirements-dev.txt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
tox==2.3.1
2-
unittest2==1.1.0
3-
nose==1.3.7
42
flake8==2.5.4
5-
coverage==4.1b2
6-
BeautifulSoup4==4.4.1
7-
mock==1.3.0
8-
flaky==3.0.3
3+
flaky==3.1.0
4+
pytest==2.8.7
5+
pytest-cov==2.2.1
6+
retrying==1.3.3

0 commit comments

Comments
 (0)