Skip to content

Commit 015bb30

Browse files
committed
Add markers
1 parent 9bca755 commit 015bb30

File tree

8 files changed

+164
-5
lines changed

8 files changed

+164
-5
lines changed

.github/workflows/test-execution.yml

Lines changed: 27 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,17 @@ on:
77
description: Run Tests in Parallel
88
default: false
99

10+
test_name:
11+
description: Test File / Test Class / Test function Name
12+
required: false
13+
type: string
14+
15+
test_group:
16+
description: Test Group (e.g., smoke,regression or smoke)
17+
required: false
18+
type: string
19+
20+
1021
permissions:
1122
id-token: write
1223
contents: read
@@ -47,11 +58,22 @@ jobs:
4758
4859
- name: Execute Test
4960
run: |
50-
if [ "${{ github.event.inputs.parallel }}" = "true" ]; then
51-
pytest -n 5 --dist loadfile
52-
else
53-
pytest
54-
fi
61+
EXTRA_PARAMS=""
62+
63+
if ${{ inputs.parallel }} ; then
64+
EXTRA_PARAMS+=" -n 5 --dist loadfile";
65+
fi
66+
67+
if [ "${{ inputs.test_name }}" != "" ]; then
68+
EXTRA_PARAMS+=' -k ${{ inputs.test_name }}';
69+
fi
70+
71+
if [ "${{ inputs.test_group }}" != "" ]; then
72+
EXTRA_PARAMS+=' -m ${{ inputs.test_group }}';
73+
fi
74+
75+
echo "Running with: pytest ${EXTRA_PARAMS}"
76+
pytest ${EXTRA_PARAMS}
5577

5678
- name: Stop Book NodeJS App
5779
if: always()

README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,74 @@ class TestCreateBook(BaseTest):
111111
validator.validate_response_book(response, book)
112112
```
113113

114+
## Parallel Test Execution
115+
116+
This framework supports parallel test execution using pytest-xdist for faster test runs:
117+
118+
### Basic Parallel Execution
119+
```bash
120+
# Run tests in parallel with verbose output
121+
pytest -n auto --dist loadfile -v
122+
```
123+
124+
### Parallel Execution Modes
125+
- **`--dist loadfile`**: All tests in a file run sequentially, files run in parallel (recommended)
126+
- **`--dist loadscope`**: Tests grouped by scope (class/module) run together
127+
- **`--dist worksteal`**: Dynamic work distribution (fastest but less predictable)
128+
129+
### Parallel Safety Features
130+
- **Unique Test Data**: Test data is made unique per worker to avoid collisions
131+
- **Worker-Safe Fixtures**: API client and test setup fixtures are worker-safe
132+
- **Isolated Test Runs**: Each worker operates independently with separate test data
133+
134+
135+
## Pytest Markers
136+
137+
This project uses pytest markers to organize and categorize tests for flexible execution:
138+
139+
### Available Markers
140+
- **smoke**: Quick smoke tests for core functionality
141+
- **regression**: Comprehensive regression tests for business logic
142+
- **negative**: Error handling and negative scenario tests
143+
144+
### Usage Examples
145+
146+
Run only regression tests:
147+
```bash
148+
pytest -m regression
149+
```
150+
151+
Run both smoke and regression tests:
152+
```bash
153+
pytest -m "smoke or regression"
154+
```
155+
Run regression tests but exclude negative scenarios:
156+
```bash
157+
pytest -m "regression and not negative"
158+
```
159+
Run smoke tests in parallel:
160+
```bash
161+
pytest -m smoke -n auto --dist loadfile
162+
```
163+
164+
165+
### Marker Usage in Tests
166+
Each test function is decorated with appropriate markers:
167+
```python
168+
@pytest.mark.smoke
169+
@pytest.mark.regression
170+
@allure.title("Should create book successfully")
171+
def test_should_create_book_successfully(self):
172+
# Test implementation
173+
```
174+
175+
### Test Organization by Marker
176+
- **Smoke Tests**: Core functionality validation (create, get, update, delete basic scenarios)
177+
- **Regression Tests**: Complex business scenarios, pagination, search functionality, edge cases
178+
- **Negative Tests**: Error handling, invalid authentication, resource not found scenarios
179+
180+
This marker system enables flexible test execution strategies, from quick smoke tests during development to comprehensive regression testing in CI/CD pipelines.
181+
114182
## Troubleshooting
115183
- **Connection errors**: Ensure the Book API server is running at the correct URL.
116184
- **Parallel test issues**: Make sure test data is unique per worker or run tests serially.

github-pages/index.html

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ <h2><i class="fas fa-rocket"></i> Quick Start</h2>
5454
<li>Start your Book API server:<br><code>http://localhost:3000</code></li>
5555
<li>Run all tests:<br><code>pytest</code></li>
5656
<li>Run tests in parallel:<br><code>pytest -n auto --dist loadfile</code></li>
57+
<li>Run smoke tests quickly:<br><code>pytest -m smoke -n auto --dist loadfile</code></li>
5758
<li>Generate Allure report:<br><code>allure serve test-results/allure-results</code></li>
5859
<li>To generate a static Allure HTML report:<br><code>allure generate test-results/allure-results --clean -o test-results/allure-report</code></li>
5960
</ol>
@@ -81,6 +82,23 @@ <h2><i class="fas fa-tools"></i> Tech Stack</h2>
8182
<li><strong>pytest-xdist</strong> - Parallel test execution</li>
8283
</ul>
8384
</div>
85+
86+
<div class="card">
87+
<h2><i class="fas fa-tags"></i> Pytest Markers</h2>
88+
<ul>
89+
<li><strong>smoke</strong>: Quick smoke tests for core functionality</li>
90+
<li><strong>regression</strong>: Comprehensive regression tests for business logic</li>
91+
<li><strong>negative</strong>: Error handling and negative scenario tests</li>
92+
</ul>
93+
<p><strong>Run specific test types:</strong></p>
94+
<code>pytest -m regression</code><br>
95+
<p><strong>Combine markers for flexible execution:</strong></p>
96+
<code>pytest -m "smoke or regression"</code><br>
97+
<code>pytest -m "regression and not negative"</code><br>
98+
<p><strong>Parallel execution with markers:</strong></p>
99+
<code>pytest -m smoke -n auto --dist loadfile</code><br>
100+
</div>
101+
</div>
84102
</div>
85103

86104
<footer class="footer">

pytest.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ filterwarnings =
44

55
markers =
66
description: Adds a description to the test for documentation or reporting
7+
smoke: Quick smoke tests for core functionality
8+
regression: Comprehensive regression tests for business logic
9+
negative: Error handling and negative scenario tests
710

811
addopts = -v -s --html=test-results/report.html --self-contained-html --alluredir=test-results/allure-results
912
testpaths = tests

tests/test_create_book.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ def create_and_validate_book(
2929
validator.validate_error_message(response, expected_message)
3030
return response
3131

32+
@pytest.mark.smoke
33+
@pytest.mark.regression
3234
@allure.title("Should create book when title and author are valid")
3335
def test_should_create_book_when_title_and_author_are_valid(self):
3436
"""Test creating a book with valid title and author."""
@@ -38,6 +40,8 @@ def test_should_create_book_when_title_and_author_are_valid(self):
3840
}
3941
self.create_and_validate_book(book)
4042

43+
@pytest.mark.negative
44+
@pytest.mark.regression
4145
@allure.title("Should reject duplicate book creation")
4246
def test_should_reject_duplicate_book_creation(self):
4347
"""Test duplicate book creation returns 409."""
@@ -51,6 +55,7 @@ def test_should_reject_duplicate_book_creation(self):
5155
expected_message="A book with the same title and author already exists",
5256
)
5357

58+
@pytest.mark.regression
5459
@allure.title("Should create book when title is different for same author")
5560
def test_should_create_book_when_title_is_different_for_same_author(self):
5661
"""Test creating a book with a different title for the same author."""
@@ -60,6 +65,7 @@ def test_should_create_book_when_title_is_different_for_same_author(self):
6065
}
6166
self.create_and_validate_book(book)
6267

68+
@pytest.mark.regression
6369
@allure.title("Should create book when author is different for same book")
6470
def test_should_create_book_when_author_is_different_for_same_book(self):
6571
"""Test creating a book with a different author for the same title."""
@@ -69,6 +75,8 @@ def test_should_create_book_when_author_is_different_for_same_book(self):
6975
}
7076
self.create_and_validate_book(book)
7177

78+
@pytest.mark.negative
79+
@pytest.mark.regression
7280
@allure.title("Should return 401 when no auth token provided")
7381
def test_should_return_401_when_no_auth_token_provided(self):
7482
"""Test creating a book without authentication returns 401."""
@@ -77,6 +85,8 @@ def test_should_return_401_when_no_auth_token_provided(self):
7785
validator.validate_status_code(response, 401)
7886
validator.validate_error_message(response, "Unauthorized. No token provided.")
7987

88+
@pytest.mark.negative
89+
@pytest.mark.regression
8090
@pytest.mark.parametrize(
8191
"payload,expected_message",
8292
[
@@ -92,6 +102,8 @@ def test_should_reject_book_with_missing_fields(self, payload, expected_message)
92102
payload, expected_status=400, expected_message=expected_message
93103
)
94104

105+
@pytest.mark.negative
106+
@pytest.mark.regression
95107
@allure.title("Should reject book creation with client-provided ID")
96108
def test_should_reject_book_creation_with_client_provided_id(self):
97109
"""Test creating a book with a client-provided ID returns 400."""

tests/test_delete_book.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,17 @@ def test_create_book_before_delete_book_test(self, init_api_client):
3030
validator.validate_response_book(response, book)
3131
self.__class__.book_id = response.json()["id"]
3232

33+
@pytest.mark.negative
34+
@pytest.mark.regression
3335
@allure.title("Should return 401 when no auth token provided on delete")
3436
def test_should_return_401_when_no_auth_token_provided_on_delete(self):
3537
"""Test deleting a book without authentication returns 401."""
3638
response = self.client.delete(self.client.build_url(f"/{self.book_id}"))
3739
validator.validate_status_code(response, 401)
3840
validator.validate_error_message(response, "Unauthorized. No token provided.")
3941

42+
@pytest.mark.negative
43+
@pytest.mark.regression
4044
@allure.title("Should return 403 when user auth token is provided on delete")
4145
def test_should_return_403_when_user_auth_token_is_provided_on_delete(self):
4246
"""Test deleting a book with a user token returns 403."""
@@ -46,6 +50,8 @@ def test_should_return_403_when_user_auth_token_is_provided_on_delete(self):
4650
validator.validate_status_code(response, 403)
4751
validator.validate_error_message(response, "Forbidden. Admin access required.")
4852

53+
@pytest.mark.smoke
54+
@pytest.mark.regression
4955
@pytest.mark.dependency(name="delete_valid_book")
5056
@allure.title("Should delete book when book ID is valid")
5157
def test_should_delete_book_when_book_id_is_valid(self):
@@ -55,6 +61,8 @@ def test_should_delete_book_when_book_id_is_valid(self):
5561
)
5662
validator.validate_status_code(response, 204)
5763

64+
@pytest.mark.negative
65+
@pytest.mark.regression
5866
@pytest.mark.dependency(depends=["delete_valid_book"])
5967
@allure.title("Should return 404 when book is already deleted or does not exist")
6068
def test_should_return_404_when_book_is_already_deleted_or_not_exists(self):

tests/test_get_book.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,8 @@ def create_book_before_test(self, init_api_client, get_books):
5050
validator.validate_status_code(response, 201)
5151
validator.validate_response_book(response, book)
5252

53+
@pytest.mark.smoke
54+
@pytest.mark.regression
5355
@allure.title("Should return books for default page 1")
5456
def test_should_return_books_for_default_page1(self):
5557
"""Test Should return Book For Default page 1"""
@@ -59,6 +61,7 @@ def test_should_return_books_for_default_page1(self):
5961
len(response.json()) >= 10, "Number Of Books should greater or equal to 10"
6062
)
6163

64+
@pytest.mark.regression
6265
@allure.title("Should return books by page number")
6366
def test_should_return_books_by_page_number(self):
6467
"""Test Should return book by page number"""
@@ -69,6 +72,7 @@ def test_should_return_books_by_page_number(self):
6972
"Page 2 Book ids should greater than 10",
7073
)
7174

75+
@pytest.mark.regression
7276
@pytest.mark.parametrize(
7377
"params,expected_count,message",
7478
[
@@ -85,6 +89,8 @@ def test_pagination_cases(self, params, expected_count, message):
8589
validator.validate_status_code(response, 200)
8690
validator.assert_true(len(response.json()) == expected_count, message)
8791

92+
@pytest.mark.regression
93+
@pytest.mark.negative
8894
@allure.title("Should return books excluding last limit on negative limit")
8995
def test_should_return_books_excluding_last_limit_on_negative_limit(self):
9096
"""Test Should return book exlcuding last limit on negative limit"""
@@ -94,27 +100,34 @@ def test_should_return_books_excluding_last_limit_on_negative_limit(self):
94100
len(response.json()) != 0, "Books size with negative limt should be 0"
95101
)
96102

103+
@pytest.mark.smoke
104+
@pytest.mark.regression
97105
@allure.title("Should return single book by ID")
98106
def test_should_return_single_book_by_id(self):
99107
"""Test Should return Single Book by ID"""
100108
response = self.client.get(self.client.build_url("/10"))
101109
validator.validate_status_code(response, 200)
102110
validator.assert_equals(response.json()["id"], 10, "Retrieve Book by book ID")
103111

112+
@pytest.mark.negative
113+
@pytest.mark.regression
104114
@allure.title("Should not return book when book ID is invalid string")
105115
def test_should_not_return_book_when_book_id_is_invalid_string(self):
106116
"""Test should not return book when Book Id is invalid String"""
107117
response = self.client.get(self.client.build_url("/invalid-string"))
108118
validator.validate_status_code(response, 404)
109119
validator.validate_error_message(response, "Book not found")
110120

121+
@pytest.mark.negative
122+
@pytest.mark.regression
111123
@allure.title("Should not return book when book ID does not exist")
112124
def test_should_not_return_book_when_book_id_not_exists(self):
113125
"""Test Should not return book when book id not exists"""
114126
response = self.client.get(self.client.build_url("/999999"))
115127
validator.validate_status_code(response, 404)
116128
validator.validate_error_message(response, "Book not found")
117129

130+
@pytest.mark.regression
118131
@allure.title("Should return all books when book ID is empty")
119132
def test_should_return_all_books_when_book_id_is_empty(self):
120133
"""Test should return all books when book id is empty"""
@@ -124,6 +137,7 @@ def test_should_return_all_books_when_book_id_is_empty(self):
124137
len(response.json()) >= 10, "Number Of Books should greater or equal to 10"
125138
)
126139

140+
@pytest.mark.regression
127141
@allure.title("Should return books containing author")
128142
def test_should_return_books_contains_author(self):
129143
"""Test should return book contains author"""
@@ -133,6 +147,7 @@ def test_should_return_books_contains_author(self):
133147
)
134148
self.assert_search_results(response, author="Book Author")
135149

150+
@pytest.mark.regression
136151
@allure.title("Should return books containing title")
137152
def test_should_return_books_contains_title(self):
138153
"""Test should return book contains title"""
@@ -141,6 +156,7 @@ def test_should_return_books_contains_title(self):
141156
)
142157
self.assert_search_results(response, title="Book Title")
143158

159+
@pytest.mark.regression
144160
@allure.title("Should return books containing title and author")
145161
def test_should_return_books_contains_title_and_author(self):
146162
"""Test should return book contains title and author"""
@@ -150,6 +166,7 @@ def test_should_return_books_contains_title_and_author(self):
150166
)
151167
self.assert_search_results(response, title="Book Title", author="book author")
152168

169+
@pytest.mark.regression
153170
@allure.title("Should return single book with title and author")
154171
def test_should_return_single_book_with_title_and_author(self):
155172
"""Test should return single book for author and title"""
@@ -161,6 +178,8 @@ def test_should_return_single_book_with_title_and_author(self):
161178
response, title="Book Title 7", author="book author 7"
162179
)
163180

181+
@pytest.mark.negative
182+
@pytest.mark.regression
164183
@pytest.mark.parametrize(
165184
"params,status_code,expected_message",
166185
[

0 commit comments

Comments
 (0)