@@ -111,10 +111,247 @@ def gh_project_name():
111
111
Now, our tests can safely and easily fetch these variables.
112
112
These fixtures have * session* scope so that pytest will read them only one time during the entire testing session.
113
113
114
+ The last thing we need is a Playwright request context object tailored to GitHub API requests.
115
+ We could build individual requests for each endpoint call,
116
+ but then we would need to explicitly set things like the base URL and the authentication token on each request.
117
+ Instead, with Playwright, we can build an
118
+ [ ` APIRequestContext ` ] ( https://playwright.dev/python/docs/api/class-apirequestcontext )
119
+ tailored to GitHub API requests.
120
+ Add the following fixture to ` tests/conftest.py ` to build a request context object for the GitHub API:
121
+
122
+ ``` python
123
+ @pytest.fixture (scope = ' session' )
124
+ def gh_context (playwright , gh_access_token ):
125
+ headers = {
126
+ " Accept" : " application/vnd.github.v3+json" ,
127
+ " Authorization" : f " token { gh_access_token} " }
128
+
129
+ request_context = playwright.request.new_context(
130
+ base_url = " https://api.github.com" ,
131
+ extra_http_headers = headers)
132
+
133
+ yield request_context
134
+ request_context.dispose()
135
+ ```
136
+
137
+ Let's break down the code:
138
+
139
+ 1 . The ` gh_context ` fixture has * session* scope because the context object can be shared by all tests.
140
+ 2 . It requires the ` playwright ` fixture for creating a new context object,
141
+ and it requires the ` gh_access_token ` fixture we just wrote for getting your personal access token.
142
+ 3 . GitHub API requests require two headers:
143
+ 1 . An ` Accept ` header for proper JSON formatting
144
+ 2 . An ` Authorization ` header that uses the access token
145
+ 4 . ` playwright.request.new_context(...) ` creates a new ` APIRequestContext ` object
146
+ with the base URL for the GitHub API and the headers.
147
+ 5 . The fixture yields the new context object and disposes of it after testing is complete.
148
+
149
+ Now, any test or other fixture can call ` gh_context ` for building GitHub API requests!
150
+ All requests created using ` gh_context ` will contain this base URL and these headers by default.
151
+
152
+
114
153
115
154
## Writing a pure API test
116
155
117
- ?
156
+ Our first test will create a new project card exclusively using the [ GitHub API] ( https://docs.github.com/en/rest ) .
157
+ The main part of the test has only two steps:
158
+
159
+ 1 . Create a new card on the project board.
160
+ 2 . Retrieve the newly created card to verify that it was created successfully.
161
+
162
+ However, this test will need more than just two API calls.
163
+ Here is the endpoint for creating a new project card:
164
+
165
+ ```
166
+ POST /projects/columns/{column_id}/cards
167
+ ```
168
+
169
+ Notice that to create a new card with this endpoint,
170
+ we need the ID of the target column in the desired project.
171
+ The column IDs come from the project data.
172
+ Thus, we need to make the following chain of calls:
173
+
174
+ 1 . [ Retrieve a list of user projects] ( https://docs.github.com/en/rest/reference/projects#list-user-projects )
175
+ to find the target project by name.
176
+ 2 . [ Retrieve a list of project columns] ( https://docs.github.com/en/rest/reference/projects#list-project-columns )
177
+ for the target project to find column IDs.
178
+ 3 . [ Create a project card] ( https://docs.github.com/en/rest/reference/projects#create-a-project-card )
179
+ in one of the columns using its IDs.
180
+ 4 . [ Retrieve the project card] ( https://docs.github.com/en/rest/reference/projects#get-a-project-card )
181
+ using the card's ID.
182
+
183
+ > The links provided above for each request document how to make each call.
184
+ > They also include example requests and responses.
185
+
186
+ The first two requests should be handled by fixtures
187
+ because they could (and, for our case, * will* ) be used for multiple tests.
188
+ Furthermore, all of these requests require
189
+ [ authentication] ( https://docs.github.com/en/rest/overview/other-authentication-methods )
190
+ using your personal access token.
191
+
192
+ Let's write a fixture for the first request to find the target project.
193
+ Add this code to ` conftest.py ` :
194
+
195
+ ``` python
196
+ @pytest.fixture (scope = ' session' )
197
+ def gh_project (gh_context , gh_username , gh_project_name ):
198
+ resource = f ' /users/ { gh_username} /projects '
199
+ response = gh_context.get(resource)
200
+ assert response.ok
201
+
202
+ name_match = lambda x : x[' name' ] == gh_project_name
203
+ filtered = filter (name_match, response.json())
204
+ project = list (filtered)[0 ]
205
+ assert project
206
+
207
+ return project
208
+ ```
209
+
210
+ The ` gh_project ` fixture has * session* scope
211
+ because we will treat the project's existence and name as immutable during test execution.
212
+ It uses the ` gh_context ` fixture to build requests with authentication,
213
+ and it uses the ` gh_username ` and ` gh_project_name ` fixtures for finding the target project.
214
+ To get a list of all your projects,
215
+ it makes a ` GET ` request to ` /users/{gh_username}/projects ` using ` gh_context ` ,
216
+ which automatically includes the base URL, headers, and authentication.
217
+ The subsequent ` assert response.ok ` command makes sure the request was successful.
218
+ If anything went wrong, tests would abort immediately.
219
+
220
+ The resulting response will contain a list of * all* user projects.
221
+ This fixture then filters the list to find the project with the target project name.
222
+ Once found, it asserts that the project object exists and then returns it.
223
+
224
+ Let's write a fixture for the next request in the call chain to get the list of columns for our project.
225
+ Add the following code to ` conftest.py ` :
226
+
227
+ ``` python
228
+ @pytest.fixture ()
229
+ def project_columns (gh_context , gh_project ):
230
+ response = gh_context.get(gh_project[' columns_url' ])
231
+ assert response.ok
232
+
233
+ columns = response.json()
234
+ assert len (columns) >= 2
235
+ return columns
236
+ ```
237
+
238
+ The ` project_columns ` fixture uses * function* scope.
239
+ In theory, columns could change during testing,
240
+ so each test should fetch a fresh column list.
241
+ It uses the ` gh_context ` fixture for making requests,
242
+ and it uses the ` gh_project ` fixture to get project data.
243
+ Thankfully, the project data includes a full endpoint URL to fetch the project's columns: ` columns_url ` .
244
+ This fixture makes a ` GET ` request on that URL.
245
+ Then, it verifies that the project has at least two columns before returning the column data.
246
+
247
+ This fixture returns the full column data.
248
+ However, for testing card creation, we only need a column ID.
249
+ Let's make it simple to get column IDs directly with yet another fixture:
250
+
251
+ ``` python
252
+ @pytest.fixture ()
253
+ def project_column_ids (project_columns ):
254
+ return list (map (lambda x : x[' id' ], project_columns))
255
+ ```
256
+
257
+ The ` project_column_ids ` fixture uses the ` map ` function to get a list of IDs from the list of columns.
258
+ We could have fetched the columns and mapped IDs in one fixture,
259
+ but it is better to separate them into two fixtures because they represent separate concerns.
260
+ Furthermore, while our current test only requires column IDs,
261
+ other tests may need other values from column data.
262
+
263
+ Now that all the setup is out of the way, let's automate the test!
264
+ Create a new file named ` tests/test_github_project.py ` ,
265
+ and add the following import statement:
266
+
267
+ ``` python
268
+ import time
269
+ ```
270
+
271
+ We'll need the ` time ` module to grab timestamps.
272
+
273
+ Define a test function for our card creation test:
274
+
275
+ ``` python
276
+ def test_create_project_card (gh_context , project_column_ids ):
277
+ ```
278
+
279
+ Our test will need ` gh_context ` to make requests and ` project_column_ids ` to pick a project column.
280
+
281
+ Every new card should have a note with a unique message
282
+ so that we can find cards when we need to interact with them.
283
+ One easy way to create unique messages is to append a timestamp value, like this:
284
+
285
+ ``` python
286
+ now = time.time()
287
+ note = f ' A new task at { now} '
288
+ ```
289
+
290
+ Then, we can create a new card in our project via an API call like this:
291
+
292
+ ``` python
293
+ c_response = gh_context.post(
294
+ f ' /projects/columns/ { project_column_ids[0 ]} /cards ' ,
295
+ data = {' note' : note})
296
+ assert c_response.ok
297
+ assert c_response.json()[' note' ] == note
298
+ ```
299
+
300
+ We use ` gh_context ` to make the ` POST ` request to the resource.
301
+ The column for the card doesn't matter,
302
+ so we can choose the first column for simplicity.
303
+ Immediately after receiving the response,
304
+ we should make sure the response is okay and that the card's note is correct.
305
+
306
+ Finally, we should verify that the card was actually created successfully
307
+ by attempting to ` GET ` the card using its ID:
308
+
309
+ ``` python
310
+ card_id = c_response.json()[' id' ]
311
+ r_response = gh_context.get(f ' /projects/columns/cards/ { card_id} ' )
312
+ assert r_response.ok
313
+ assert r_response.json() == c_response.json()
314
+ ```
315
+
316
+ The card's ID comes from the previous response.
317
+ Again, we use ` gh_context ` to make the request,
318
+ and we immediately verify the correctness of the response.
319
+ The response data should be identical to the response data from the creation request.
320
+
321
+ That completes our API-only card creation test!
322
+ Here's the complete code for the ` test_create_project_card ` test function:
323
+
324
+ ``` python
325
+ import time
326
+
327
+ def test_create_project_card (gh_context , project_column_ids ):
328
+
329
+ # Prep test data
330
+ now = time.time()
331
+ note = f ' A new task at { now} '
332
+
333
+ # Create a new card
334
+ c_response = gh_context.post(
335
+ f ' /projects/columns/ { project_column_ids[0 ]} /cards ' ,
336
+ data = {' note' : note})
337
+ assert c_response.ok
338
+ assert c_response.json()[' note' ] == note
339
+
340
+ # Retrieve the newly created card
341
+ card_id = c_response.json()[' id' ]
342
+ r_response = gh_context.get(f ' /projects/columns/cards/ { card_id} ' )
343
+ assert r_response.ok
344
+ assert r_response.json() == c_response.json()
345
+ ```
346
+
347
+ Run the new test module directly:
348
+
349
+ ``` bash
350
+ $ python3 -m pytest tests/test_github_project.py
351
+ ```
352
+
353
+ Make sure all your environment variables are set correctly.
354
+ The test should pass very quickly.
118
355
119
356
120
357
## Writing a hybrid UI/API test
0 commit comments