Skip to content

Commit 743cb00

Browse files
Wrote part 6 section 2 instructions
1 parent c56a0f9 commit 743cb00

File tree

1 file changed

+238
-1
lines changed

1 file changed

+238
-1
lines changed

tutorial/6-api-testing.md

Lines changed: 238 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,10 +111,247 @@ def gh_project_name():
111111
Now, our tests can safely and easily fetch these variables.
112112
These fixtures have *session* scope so that pytest will read them only one time during the entire testing session.
113113

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+
114153

115154
## Writing a pure API test
116155

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.
118355

119356

120357
## Writing a hybrid UI/API test

0 commit comments

Comments
 (0)