Skip to content

Commit f12e2a2

Browse files
committed
Add test results report azure integration
1 parent 579d89b commit f12e2a2

File tree

10 files changed

+333
-75
lines changed

10 files changed

+333
-75
lines changed

.github/workflows/test-execution.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,15 @@ jobs:
9494
path: test-results
9595
retention-days: 3
9696

97+
- name: Post Test Results to Azure DevOps
98+
if: ${{ (success() || failure()) && inputs.publish_report }}
99+
uses: NayeemJohnY/actions/post-test-results-to-azure-devops@main
100+
with:
101+
test-results-json: test-results/test-results-report.json
102+
org-url: ${{ vars.AZURE_ORG_URL }}
103+
project: ${{ vars.AZURE_PROJECT }}
104+
azure-token: ${{ secrets.AZURE_TOKEN }}
105+
97106
- name: Prepare GitHub Pages Content
98107
if: success() || failure()
99108
uses: NayeemJohnY/actions/prepare-github-pages@main

README.md

Lines changed: 32 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -189,10 +189,35 @@ This marker system enables flexible test execution strategies, from quick smoke
189189
- **Parallel test issues**: Make sure test data is unique per worker or run tests serially.
190190
- **HTML report not generated**: Ensure `pytest-html` is installed and use the `--html` option.
191191
---
192-
## CI/CD & Automation
193-
194-
This project uses GitHub Actions for:
195-
- **Code Analysis**: Automated linting and static analysis on every push/PR.
196-
- **Test Execution**: Runs all tests and generates reports for every push/PR.
197-
198-
See the badges at the top of this README for the latest status of these workflows.
192+
## CI/CD & GitHub Actions
193+
194+
This project uses GitHub Actions for automated workflows:
195+
- **Code Analysis**: Runs linting and static analysis on every push and pull request.
196+
- **Test Execution**: Runs all tests and generates reports for every push and pull request, including parallel execution and publishing Allure/HTML reports.
197+
- **Badges**: See the top of this README for live status of these workflows.
198+
199+
Workflow files are located in `.github/workflows/`.
200+
201+
## Running the Test Execution Workflow (GitHub Actions)
202+
203+
### How to Trigger
204+
- The workflow runs automatically on every push and pull request to the repository.
205+
- You can also trigger it manually from the GitHub Actions tab by selecting the `Test Execution & Publish Report` workflow and clicking 'Run workflow'.
206+
207+
### What the Workflow Does
208+
- Installs Python and all dependencies from `requirements.txt`.
209+
- Runs all tests using pytest (including parallel execution with xdist).
210+
- Publishes test results as HTML and Allure reports (if configured).
211+
- Uploads the reports as workflow artifacts for download and review.
212+
- Updates status badges at the top of the README to reflect the latest run.
213+
214+
### New: Post Results to Azure
215+
- After test execution, the workflow now includes a step to post the test results to Azure (e.g., Azure DevOps, Azure Storage, or a custom API endpoint).
216+
- This enables centralized reporting, dashboard integration, or further automation in your Azure environment.
217+
- The step uses secure credentials and API endpoints configured in your repository secrets.
218+
- You can customize the target Azure service and payload format as needed for your organization.
219+
220+
### Viewing Results
221+
- Go to the 'Actions' tab in your GitHub repository.
222+
- Select the latest run of `Test Execution & Publish Report`.
223+
- Download the HTML/Allure report artifacts for detailed results.

conftest.py

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -38,18 +38,34 @@ def api_client():
3838
return APIClient(BASE_URI, BASE_PATH)
3939

4040

41-
def collect_test_results(test_name: str, test_params: str, report: TestReport):
41+
def collect_test_results(test_name: str, test_params: str, report: TestReport, call=None):
4242
"""
4343
Collects and aggregates test results for each test case.
4444
Stores outcome, duration (in ms), and iteration details if test parameters are present.
4545
"""
46+
def get_outcome():
47+
if report.outcome == "skipped":
48+
return "Error"
49+
return report.outcome.capitalize()
50+
51+
def get_error_message():
52+
if call and call.excinfo:
53+
# Get simple exception message from call.excinfo
54+
exception_type = call.excinfo.typename
55+
exception_value = str(call.excinfo.value)
56+
return f"{exception_type}: {exception_value}"
57+
return ""
58+
4659
test_case_id: str = test_case_mappings[test_name]["testCaseId"]
4760
# Convert duration to milliseconds
4861
duration_ms = int(float(report.duration) * 1000)
62+
error_message = get_error_message()
63+
new_outcome = get_outcome()
64+
4965
result = test_results.setdefault(
5066
test_case_id,
5167
{
52-
"outcome": report.outcome.capitalize(),
68+
"outcome": new_outcome,
5369
"durationInMs": 0,
5470
"comment": f"Test Name: {test_name}",
5571
"iterationDetails": [],
@@ -58,17 +74,31 @@ def collect_test_results(test_name: str, test_params: str, report: TestReport):
5874

5975
if test_params:
6076
iteration_id = len(result["iterationDetails"]) + 1
77+
error_message = f"Iteration {iteration_id}: {error_message}" if error_message else error_message
6178
iteration_result = {
6279
"id": iteration_id,
63-
"outcome": report.outcome.capitalize(),
80+
"outcome": new_outcome,
6481
"durationInMs": duration_ms,
65-
"comment": f"Test Parameters: {json.dumps(test_params)}",
82+
"errorMessage": error_message,
83+
"comment": f"DataDriven: Test Parameters: {json.dumps(test_params)}",
6684
}
6785
result["iterationDetails"].append(iteration_result)
6886

6987
result["durationInMs"] += duration_ms
70-
if result["outcome"] == "Failed" or report.outcome.capitalize() == "Failed":
71-
result["outcome"] = "Failed"
88+
89+
# Update outcome based on current and new results
90+
current_outcome = result["outcome"]
91+
# Determine final outcome: same = keep, different = Inconclusive
92+
if current_outcome != new_outcome:
93+
result["outcome"] = "Inconclusive"
94+
95+
# Append error messages from all iterations
96+
if error_message:
97+
existing_error = result.get("errorMessage", "")
98+
if existing_error:
99+
result["errorMessage"] = f"{existing_error}\n{error_message}"
100+
else:
101+
result["errorMessage"] = error_message
72102

73103

74104
@pytest.hookimpl(hookwrapper=True)
@@ -79,10 +109,16 @@ def pytest_runtest_makereport(item: Function, call):
79109
"""
80110
outcome: Result = yield
81111
report: TestReport = outcome.get_result()
112+
test_name: str = item.originalname
113+
test_params = item.callspec.params if hasattr(item, "callspec") else None
114+
115+
# Collect results for call phase (actual test execution)
82116
if report.when == "call":
83-
test_name: str = item.originalname
84-
test_params = item.callspec.params if hasattr(item, "callspec") else None
85-
collect_test_results(test_name, test_params, report)
117+
collect_test_results(test_name, test_params, report, call)
118+
119+
# Handle setup failures - mark as error
120+
elif report.when == "setup" and report.outcome in ["failed", "skipped"]:
121+
collect_test_results(test_name, test_params, report, call)
86122

87123

88124
def pytest_sessionfinish(session: Session, exitstatus):
@@ -115,9 +151,14 @@ def pytest_sessionfinish(session: Session, exitstatus):
115151
with open(temp_file_path, "r", encoding="utf-8") as temp_results_file:
116152
worker_data = json.load(temp_results_file)
117153
test_results.update(worker_data)
154+
report = {
155+
"testPlanName": test_plan_suite['testPlanName'],
156+
"testSuiteName": test_plan_suite['testSuiteName'],
157+
"testResults": test_results
158+
}
118159

119160
with open(TEST_RESULTS_PATH, "w", encoding="utf-8") as out:
120-
json.dump(test_results, out, indent=4)
161+
json.dump(report, out, indent=4)
121162
logger.info(
122163
"Test results report generated successfully: %s", TEST_RESULTS_PATH
123164
)
198 KB
Loading
295 KB
Loading
283 KB
Loading

github-pages/index.html

Lines changed: 107 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,64 @@
11
<!DOCTYPE html>
22
<html lang="en">
3+
34
<head>
45
<meta charset='utf-8'>
56
<meta http-equiv='X-UA-Compatible' content='IE=edge'>
67
<title>Book API Testing Framework</title>
78
<meta name='viewport' content='width=device-width, initial-scale=1'>
89
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
910
<meta property="og:title" content="Book API Testing Framework">
10-
<meta property="og:description" content="Comprehensive API Testing Framework for Book system with Python, pytest, and requests.">
11+
<meta property="og:description"
12+
content="Comprehensive API Testing Framework for Book system with Python, pytest, and requests.">
1113
<meta property="og:type" content="website">
1214
<meta name="twitter:card" content="summary_large_image">
1315
<link rel="icon" href="favicon.ico" type="image/x-icon">
1416
<link rel="stylesheet" href="style.css">
1517
</head>
18+
1619
<body>
17-
<a href="https://github.com/NayeemJohnY/book-api-python-requests-pytest-automation" class="github-corner" target="_blank" title="View on GitHub">
20+
<a href="https://github.com/NayeemJohnY/book-api-python-requests-pytest-automation" class="github-corner"
21+
target="_blank" title="View on GitHub">
1822
<img src="https://github.githubassets.com/images/modules/logos_page/GitHub-Mark.png" alt="GitHub Repository">
1923
</a>
20-
24+
2125
<div class="main-container">
2226
<header>
2327
<h1><i class="fas fa-book"></i> Book API Testing Framework</h1>
24-
<p class="subtitle">A robust, maintainable API test automation framework built with Python, pytest, and requests</p>
28+
<p class="subtitle">A robust, maintainable API test automation framework built with Python, pytest, and
29+
requests</p>
2530
</header>
26-
31+
2732
<nav class="quick-links">
2833
<a href="./allure-report/index.html" class="nav-link">
2934
<i class="fa-solid fa-chart-line"></i>
3035
View Allure Report
3136
</a>
37+
<a href="./test-results-report.json" class="nav-link">
38+
<i class="fa-solid fa-file-code"></i>
39+
Test Results JSON
40+
</a>
3241
</nav>
33-
42+
3443
<div class="content-grid">
3544
<div class="card">
3645
<h2><i class="fas fa-star"></i> Key Features</h2>
3746
<ul>
3847
<li><strong>Custom APIClient</strong> with built-in retry logic and comprehensive logging</li>
39-
<li><strong>Reusable Validators</strong> for status codes, error messages, and response validation</li>
48+
<li><strong>Reusable Validators</strong> for status codes, error messages, and response validation
49+
</li>
4050
<li><strong>DRY Architecture</strong> using pytest fixtures and base classes</li>
4151
<li><strong>Parallel Execution</strong> with pytest-xdist and worker-safe test data</li>
4252
<li><strong>Rich Reporting</strong> with HTML and Allure integration</li>
4353
<li><strong>Comprehensive Logging</strong> for all requests, responses, and assertions</li>
4454
</ul>
45-
<ul>
46-
<li><strong>CI/CD Workflows:</strong> Automated <code> Code Analysis</code> and <code>Test Execution</code> via GitHub Actions</li>
47-
</ul>
55+
<ul>
56+
<li><strong>CI/CD Workflows:</strong> Automated <code> Code Analysis</code> and
57+
<code>Test Execution</code> via GitHub Actions
58+
</li>
59+
</ul>
4860
</div>
49-
61+
5062
<div class="card">
5163
<h2><i class="fas fa-rocket"></i> Quick Start</h2>
5264
<ol>
@@ -56,10 +68,12 @@ <h2><i class="fas fa-rocket"></i> Quick Start</h2>
5668
<li>Run tests in parallel:<br><code>pytest -n auto --dist loadfile</code></li>
5769
<li>Run smoke tests quickly:<br><code>pytest -m smoke -n auto --dist loadfile</code></li>
5870
<li>Generate Allure report:<br><code>allure serve test-results/allure-results</code></li>
59-
<li>To generate a static Allure HTML report:<br><code>allure generate test-results/allure-results --clean -o test-results/allure-report</code></li>
71+
<li>To generate a static Allure HTML
72+
report:<br><code>allure generate test-results/allure-results --clean -o test-results/allure-report</code>
73+
</li>
6074
</ol>
6175
</div>
62-
76+
6377
<div class="card">
6478
<h2><i class="fas fa-cogs"></i> Best Practices</h2>
6579
<ul>
@@ -70,7 +84,7 @@ <h2><i class="fas fa-cogs"></i> Best Practices</h2>
7084
<li><strong>Parallel Safety:</strong> Unique test data per worker prevents collisions</li>
7185
</ul>
7286
</div>
73-
87+
7488
<div class="card">
7589
<h2><i class="fas fa-tools"></i> Tech Stack</h2>
7690
<ul>
@@ -102,24 +116,90 @@ <h2><i class="fas fa-tags"></i> Pytest Markers</h2>
102116
<div class="card">
103117
<h2><i class="fas fa-link"></i> Test Case Mapping & Result Collection</h2>
104118
<ul>
105-
<li><strong>Test Case Mapping:</strong> Each test function is mapped to a unique test case ID using <code>test-plan-suite.json</code> for traceability and reporting.</li>
106-
<li><strong>Result Collection:</strong> Test results are collected for each test case, including outcome, duration (ms), and iteration details. Results are aggregated and written to <code>test-results/test-results-report.json</code> after each run, supporting both serial and parallel execution.</li>
119+
<li><strong>Test Case Mapping:</strong> Each test function is mapped to a unique test case ID using
120+
<code>test-plan-suite.json</code> for traceability and reporting.
121+
</li>
122+
<li><strong>Result Collection:</strong> Test results are collected for each test case, including
123+
outcome, duration (ms), and iteration details. Results are aggregated and written to
124+
<code>test-results/test-results-report.json</code> after each run, supporting both serial and
125+
parallel execution.
126+
</li>
107127
</ul>
108128
</div>
129+
130+
<div class="card">
131+
<h2><i class="fa-brands fa-github"></i> CI/CD & GitHub Actions</h2>
132+
<ul>
133+
<li><strong>Code Analysis:</strong> Automated linting and static analysis on every push and pull
134+
request.</li>
135+
<li><strong>Test Execution:</strong> Runs all tests and generates reports for every push and pull
136+
request, including parallel execution and publishing Allure/HTML reports.</li>
137+
</ul>
109138
</div>
110139
</div>
111-
112-
<footer class="footer">
113-
<div>
114-
<a href="https://www.linkedin.com/in/nayeemjohny/" target="_blank" class="linkedin-link">
115-
<i class="fa-brands fa-linkedin"></i>Connect with me on LinkedIn
116-
</a>
140+
<div class="ado-images-container">
141+
<h2>Azure DevOps Test Plan Integration</h2>
142+
<p>Below are sample screenshots showing how test results are posted and visualized in Azure DevOps Test
143+
Plans:</p>
144+
<div class="ado-images-grid">
145+
<figure>
146+
<img src="images/ado-progress-report.png" alt="ADO Progress Report" class="ado-img"
147+
onclick="enlargeImage(this)">
148+
<figcaption>Progress Report</figcaption>
149+
</figure>
150+
<figure>
151+
<img src="images/ado-run-summary.png" alt="ADO Run Summary" class="ado-img"
152+
onclick="enlargeImage(this)">
153+
<figcaption>Run Summary</figcaption>
154+
</figure>
155+
<figure>
156+
<img src="images/ado-test-result-summary.png" alt="ADO Test Result Summary" class="ado-img"
157+
onclick="enlargeImage(this)">
158+
<figcaption>Test Result Summary</figcaption>
159+
</figure>
160+
<figure>
161+
<img src="images/ado-test-results.png" alt="ADO Test Results" class="ado-img"
162+
onclick="enlargeImage(this)">
163+
<figcaption>Test Results in Azure DevOps</figcaption>
164+
</figure>
117165
</div>
118-
<div class="copilot-credit">
119-
<i class="fa-solid fa-robot"></i>
120-
Thanks to <span class="copilot-highlight">GitHub Copilot</span> for assisting with documentation
166+
<div id="adoModal" class="ado-modal" onclick="closeModal(event)">
167+
<div class="ado-modal-content">
168+
<div class="ado-modal-image-container">
169+
<img id="adoModalImg" src="" alt="Enlarged Azure DevOps Screenshot">
170+
</div>
171+
<button class="ado-modal-close" onclick="closeModal(event)">Close</button>
172+
</div>
121173
</div>
122-
</footer>
174+
</div>
175+
<script>
176+
function enlargeImage(img) {
177+
var modal = document.getElementById('adoModal');
178+
var modalImg = document.getElementById('adoModalImg');
179+
modalImg.src = img.src;
180+
modal.classList.add('active');
181+
}
182+
function closeModal(event) {
183+
var modal = document.getElementById('adoModal');
184+
if (event.target.classList.contains('ado-modal') || event.target.classList.contains('ado-modal-close')) {
185+
modal.classList.remove('active');
186+
}
187+
}
188+
</script>
189+
</div>
190+
191+
<footer class="footer">
192+
<div>
193+
<a href="https://www.linkedin.com/in/nayeemjohny/" target="_blank" class="linkedin-link">
194+
<i class="fa-brands fa-linkedin"></i>Connect with me on LinkedIn
195+
</a>
196+
</div>
197+
<div class="copilot-credit">
198+
<i class="fa-solid fa-robot"></i>
199+
Thanks to <span class="copilot-highlight">GitHub Copilot</span> for assisting with documentation
200+
</div>
201+
</footer>
123202
</div>
124203
</body>
125-
</html>
204+
205+
</html>

0 commit comments

Comments
 (0)