12
12
# See the License for the specific language governing permissions and
13
13
# limitations under the License.
14
14
15
- import unittest
16
-
17
15
import bookshelf
18
16
import config
19
17
from flaky import flaky
20
18
from gcloud .exceptions import ServiceUnavailable
21
- from nose . plugins . attrib import attr
19
+ import pytest
22
20
23
21
24
- def flaky_filter (e , * args ):
25
- return isinstance (e , ServiceUnavailable )
22
+ @pytest .yield_fixture (params = ['datastore' , 'cloudsql' , 'mongodb' ])
23
+ def app (request ):
24
+ """This fixtures provides a Flask app instance configured for testing.
26
25
26
+ Because it's parametric, it will cause every test that uses this fixture
27
+ to run three times: one time for each backend (datastore, cloudsql, and
28
+ mongodb).
29
+
30
+ It also ensures the tests run within a request context, allowing
31
+ any calls to flask.request, flask.current_app, etc. to work."""
32
+ app = bookshelf .create_app (
33
+ config ,
34
+ testing = True ,
35
+ config_overrides = {
36
+ 'DATA_BACKEND' : request .param
37
+ })
38
+
39
+ with app .test_request_context ():
40
+ yield app
27
41
28
- @flaky (rerun_filter = flaky_filter )
29
- class IntegrationBase (unittest .TestCase ):
30
42
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 )})
43
+ @ pytest . yield_fixture
44
+ def model ( monkeypatch , app ):
45
+ """This fixture provides a modified version of the app's model that tracks
46
+ all created items and deletes them at the end of the test.
35
47
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
- )
48
+ Any tests that directly or indirectly interact with the database should use
49
+ this to ensure that resources are properly cleaned up.
44
50
45
- with self . app . app_context ():
46
- self . model = bookshelf . get_model ()
51
+ Monkeypatch is provided by pytest and used to patch the model's create
52
+ method.
47
53
48
- self .ids_to_delete = []
54
+ The app fixture is needed to provide the configuration and context needed
55
+ to get the proper model object.
56
+ """
57
+ model = bookshelf .get_model ()
49
58
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
59
+ ids_to_delete = []
53
60
54
- def tracking_create (* args , ** kwargs ):
55
- res = self .original_create (* args , ** kwargs )
56
- self .ids_to_delete .append (res ['id' ])
57
- return res
61
+ # Monkey-patch create so we can track the IDs of every item
62
+ # created and delete them after the test case.
63
+ original_create = model .create
58
64
59
- self .model .create = tracking_create
65
+ def tracking_create (* args , ** kwargs ):
66
+ res = original_create (* args , ** kwargs )
67
+ ids_to_delete .append (res ['id' ])
68
+ return res
60
69
61
- def tearDown ( self ):
70
+ monkeypatch . setattr ( model , 'create' , tracking_create )
62
71
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 ))
72
+ yield model
66
73
67
- self .model .create = self .original_create
74
+ # Delete all items that we created during tests.
75
+ list (map (model .delete , ids_to_delete ))
68
76
69
77
70
- @attr ('slow' )
71
- class CrudTestsMixin (object ):
72
- def testList (self ):
73
- self .createBooks (11 )
78
+ def flaky_filter (e , * args ):
79
+ """Used by flaky to determine when to re-run a test case."""
80
+ return isinstance (e , ServiceUnavailable )
81
+
82
+
83
+ # Mark all test cases in this class as flaky, so that if errors occur they
84
+ # can be retried. This is useful when databases are temporarily unavailable.
85
+ @flaky (rerun_filter = flaky_filter )
86
+ # Tell pytest to use both the app and model fixtures for all test cases.
87
+ # This ensures that configuration is properly applied and that all database
88
+ # resources created during tests are cleaned up.
89
+ @pytest .mark .usefixtures ('app' , 'model' )
90
+ class TestCrudActions (object ):
74
91
75
- with self .app .test_client () as c :
92
+ def test_list (self , app , model ):
93
+ for i in range (1 , 12 ):
94
+ model .create ({'title' : u'Book {0}' .format (i )})
95
+
96
+ with app .test_client () as c :
76
97
rv = c .get ('/books/' )
77
98
78
99
assert rv .status == '200 OK'
@@ -84,66 +105,45 @@ def testList(self):
84
105
assert 'Book 9' not in body , "Should not show more than 10 books"
85
106
assert 'More' in body , "Should have more than one page"
86
107
87
- def testAddAndView (self ):
108
+ def test_add (self , app ):
88
109
data = {
89
110
'title' : 'Test Book' ,
90
111
'author' : 'Test Author' ,
91
112
'publishedDate' : 'Test Date Published' ,
92
113
'description' : 'Test Description'
93
114
}
94
115
95
- with self . app .test_client () as c :
116
+ with app .test_client () as c :
96
117
rv = c .post ('/books/add' , data = data , follow_redirects = True )
97
118
98
119
assert rv .status == '200 OK'
99
-
100
120
body = rv .data .decode ('utf-8' )
101
121
assert 'Test Book' in body
102
122
assert 'Test Author' in body
103
123
assert 'Test Date Published' in body
104
124
assert 'Test Description' in body
105
125
106
- def testEditAndView (self ):
107
- with self .app .test_request_context ():
108
- existing = self .model .create ({'title' : "Temp Title" })
126
+ def test_edit (self , app , model ):
127
+ existing = model .create ({'title' : "Temp Title" })
109
128
110
- with self . app .test_client () as c :
129
+ with app .test_client () as c :
111
130
rv = c .post (
112
131
'/books/%s/edit' % existing ['id' ],
113
132
data = {'title' : 'Updated Title' },
114
133
follow_redirects = True )
115
134
116
135
assert rv .status == '200 OK'
117
-
118
136
body = rv .data .decode ('utf-8' )
119
137
assert 'Updated Title' in body
120
138
assert 'Temp Title' not in body
121
139
122
- def testDelete (self ):
123
- with self .app .test_request_context ():
124
- existing = self .model .create ({'title' : "Temp Title" })
140
+ def test_delete (self , app , model ):
141
+ existing = model .create ({'title' : "Temp Title" })
125
142
126
- with self . app .test_client () as c :
143
+ with app .test_client () as c :
127
144
rv = c .get (
128
145
'/books/%s/delete' % existing ['id' ],
129
146
follow_redirects = True )
130
147
131
148
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'
149
+ assert not model .read (existing ['id' ])
0 commit comments