diff --git a/.gitignore b/.gitignore index c9226f9e..8f041f22 100644 --- a/.gitignore +++ b/.gitignore @@ -57,6 +57,12 @@ target/ .venv*/ qaseio.lock +qase.lock + +# Robot Framework outputs +output.xml +log.html +report.html #Ipython Notebook .ipynb_checkpoints diff --git a/examples/README.md b/examples/README.md index 4f5d86dd..a3655f90 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,31 +1,65 @@ -# Examples +# Qase Python Examples -This directory contains examples of how to use the Qase Python reporters. +This directory contains working examples for all Qase Python reporters. ## Directory Structure -- **[single](./single/)** - Examples for single-project reporting mode - - [pytest](./single/pytest/) - Pytest examples for single project - - [behave](./single/behave/) - Behave examples for single project - - [robot](./single/robot/) - Robot Framework examples for single project - - [tavern](./single/tavern/) - Tavern examples for single project - -- **[multiproject](./multiproject/)** - Examples for multi-project reporting mode - - [pytest](./multiproject/pytest/) - Pytest examples for multiple projects - - [behave](./multiproject/behave/) - Behave examples for multiple projects - - [robot](./multiproject/robot/) - Robot Framework examples for multiple projects - - [tavern](./multiproject/tavern/) - Tavern examples for multiple projects +| Directory | Mode | Description | +|-----------|------|-------------| +| [single/](./single/) | `testops` | Report results to a single Qase project | +| [multiproject/](./multiproject/) | `testops_multi` | Report results to multiple Qase projects | ## Quick Start -### Single Project Examples +1. Choose your framework and mode +2. Navigate to the example directory +3. Install dependencies: `pip install -r requirements.txt` +4. Configure `qase.config.json` with your API token and project code +5. Run the tests + +## Single Project Examples + +Report all test results to one Qase project using `mode: testops`. + +``` +single/ +├── pytest/ # Pytest examples (basic, attachments, steps, profilers) +├── behave/ # Behave BDD examples +├── robot/ # Robot Framework examples +└── tavern/ # Tavern API testing examples +``` + +See [single/README.md](./single/README.md) for details. + +## Multi-Project Examples + +Report test results to multiple Qase projects simultaneously using `mode: testops_multi`. + +``` +multiproject/ +├── pytest/ # @qase.project_id() decorator +├── behave/ # @qase.project_id.CODE:ID tags +├── robot/ # Q-PROJECT.CODE-ID tags +└── tavern/ # QaseProjectID.CODE=ID in test names +``` + +See [multiproject/README.md](./multiproject/README.md) for details. + +## Configuration + +All examples use `qase.config.json` for configuration. Before running: + +1. Copy the example config or edit existing one +2. Replace `` with your [Qase API token](https://app.qase.io/user/api/token) +3. Replace `` with your project code (from URL: `app.qase.io/project/CODE`) -For single-project examples, see the README files in each framework directory: -- [pytest](./single/pytest/Readme.md) -- [behave](./single/behave/README.md) -- [robot](./single/robot/Readme.md) -- [tavern](./single/tavern/Readme.md) +For complete configuration reference, see [qase-python-commons](../qase-python-commons/README.md). -### Multi-Project Examples +## Framework Documentation -For multi-project examples, see the [multiproject README](./multiproject/README.md). +| Framework | Package | Documentation | +|-----------|---------|---------------| +| Pytest | `qase-pytest` | [README](../qase-pytest/README.md) \| [Usage Guide](../qase-pytest/docs/usage.md) | +| Behave | `qase-behave` | [README](../qase-behave/README.md) \| [Usage Guide](../qase-behave/docs/usage.md) | +| Robot Framework | `qase-robotframework` | [README](../qase-robotframework/README.md) \| [Usage Guide](../qase-robotframework/docs/usage.md) | +| Tavern | `qase-tavern` | [README](../qase-tavern/README.md) \| [Usage Guide](../qase-tavern/docs/usage.md) | diff --git a/examples/multiproject/README.md b/examples/multiproject/README.md index 5c288b6f..226acffb 100644 --- a/examples/multiproject/README.md +++ b/examples/multiproject/README.md @@ -1,26 +1,37 @@ # Multi-Project Examples -This directory contains examples demonstrating how to use Qase TestOps with multiple projects. +Examples for reporting test results to multiple Qase projects simultaneously using `mode: testops_multi`. ## Overview -The multi-project feature allows you to send test results to multiple Qase projects simultaneously, with different test case IDs for each project. This is useful when: +The multi-project feature allows you to send test results to multiple Qase projects with different test case IDs for each project. This is useful when: + - You need to report the same test to different projects - Different projects track the same functionality with different test case IDs - You want to maintain separate test runs for different environments or teams -## Projects Used in Examples +## Frameworks -- **DEMO1**: Development/Staging project -- **DEMO2**: Demo/Production project +| Framework | Directory | Annotation Syntax | +|-----------|-----------|-------------------| +| Pytest | [pytest/](./pytest/) | `@qase.project_id("CODE", 1)` | +| Behave | [behave/](./behave/) | `@qase.project_id.CODE:1` | +| Robot Framework | [robot/](./robot/) | `Q-PROJECT.CODE-1` | +| Tavern | [tavern/](./tavern/) | `QaseProjectID.CODE=1` | ## Configuration -All examples use a `qase.config.json` configuration file with the following structure: +All examples use `qase.config.json` with `mode: testops_multi`: ```json -{ +{ "mode": "testops_multi", + "testops": { + "api": { + "token": "", + "host": "qase.io" + } + }, "testops_multi": { "default_project": "DEMO1", "projects": [ @@ -47,194 +58,145 @@ All examples use a `qase.config.json` configuration file with the following stru } ``` -## Running Examples - -### Pytest +## Quick Start -1. **Install packages in development mode** (required for local changes): +1. Install the reporter package: ```bash - cd qase-python - pip install -e qase-python-commons - pip install -e qase-pytest + pip install qase-pytest # or qase-behave, qase-robotframework, qase-tavern ``` - Or use the development requirements: +2. Navigate to the example directory: ```bash cd examples/multiproject/pytest - pip install -r requirements.dev.txt ``` -2. Update the API token in `qase.config.json` +3. Update `qase.config.json`: + - Replace `` with your API token + - Update project codes to match your Qase projects -3. Run the tests: +4. Run the tests: ```bash - cd examples/multiproject/pytest - pytest tests/test_multi_project.py --qase-mode=testops_multi - ``` + # Pytest + pytest tests/ -### Behave + # Behave + cd ../behave + behave --format=qase.behave.formatter:QaseFormatter -1. **Install packages in development mode**: - ```bash - cd qase-python - pip install -e qase-python-commons - pip install -e qase-behave - ``` + # Robot Framework + cd ../robot + robot --listener qase.robotframework.Listener tests/ -2. Update the API token in `qase.config.json` - -3. Run the tests: - ```bash - cd examples/multiproject/behave - behave tests/features/multi_project.feature --define qase-mode=testops_multi + # Tavern + cd ../tavern + pytest ``` -### Tavern - -1. **Install packages in development mode**: - ```bash - cd qase-python - pip install -e qase-python-commons - pip install -e qase-tavern - ``` - -2. Update the API token in `qase.config.json` - -3. Run the tests: - ```bash - cd examples/multiproject/tavern - pytest test_multi_project.tavern.yaml --qase-mode=testops_multi - ``` - -### Robot Framework - -1. **Install packages in development mode**: - ```bash - cd qase-python - pip install -e qase-python-commons - pip install -e qase-robotframework - ``` - -2. Update the API token in `qase.config.json` - -3. Run the tests: - ```bash - cd examples/multiproject/robot - robot --listener qase.robotframework.Listener tests/multi_project.robot - ``` - -## Test Examples +## Annotation Examples ### Single Project with Single ID **Pytest:** ```python -@qase.project_id("DEVX", 1) +@qase.project_id("DEMO1", 1) def test_example(): assert True ``` **Behave:** ```gherkin -@qase.project_id.DEVX:1 +@qase.project_id.DEMO1:1 Scenario: Example test ``` -**Tavern:** -```yaml -test_name: QaseProjectID.DEVX=1 Example test -``` - **Robot Framework:** ```robotframework -[Tags] Q-PROJECT.DEVX-1 +Test Case + [Tags] Q-PROJECT.DEMO1-1 +``` + +**Tavern:** +```yaml +test_name: QaseProjectID.DEMO1=1 Example test ``` ### Single Project with Multiple IDs **Pytest:** ```python -@qase.project_id("DEVX", [2, 3]) +@qase.project_id("DEMO1", [1, 2, 3]) def test_example(): assert True ``` **Behave:** ```gherkin -@qase.project_id.DEVX:2,3 +@qase.project_id.DEMO1:1,2,3 Scenario: Example test ``` -**Tavern:** -```yaml -test_name: QaseProjectID.DEVX=2,3 Example test -``` - **Robot Framework:** ```robotframework -[Tags] Q-PROJECT.DEVX-2,3 +Test Case + [Tags] Q-PROJECT.DEMO1-1,2,3 +``` + +**Tavern:** +```yaml +test_name: QaseProjectID.DEMO1=1,2,3 Example test ``` ### Multiple Projects **Pytest:** ```python -@qase.project_id("DEVX", 4) -@qase.project_id("DEMO", 10) +@qase.project_id("DEMO1", 1) +@qase.project_id("DEMO2", 10) def test_example(): assert True ``` **Behave:** ```gherkin -@qase.project_id.DEVX:4 -@qase.project_id.DEMO:10 +@qase.project_id.DEMO1:1 @qase.project_id.DEMO2:10 Scenario: Example test ``` -**Tavern:** -```yaml -test_name: QaseProjectID.DEVX=4 QaseProjectID.DEMO=10 Example test -``` - **Robot Framework:** ```robotframework -[Tags] Q-PROJECT.DEVX-4 Q-PROJECT.DEMO-10 +Test Case + [Tags] Q-PROJECT.DEMO1-1 Q-PROJECT.DEMO2-10 ``` -## What to Expect - -When you run the examples: - -1. **Test runs will be created** in both DEVX and DEMO projects -2. **Test results will be sent** to the appropriate projects based on the test case IDs specified -3. **Each project will have its own test run** with the configured title and description -4. **Results will appear** in both projects' dashboards +**Tavern:** +```yaml +test_name: QaseProjectID.DEMO1=1 QaseProjectID.DEMO2=10 Example test +``` -## Installation Notes +## Default Project -**Important:** Since multi-project support is a new feature, you need to install the packages in **development mode** (editable install) to use the local source code with new changes: +Tests without explicit project mapping are sent to `default_project`: -```bash -# From the root of qase-python repository -cd qase-python +```python +# No @qase.project_id decorator +# This test goes to DEMO1 (default_project) +def test_without_project(): + assert True +``` -# Install commons (required by all reporters) -pip install -e qase-python-commons +## What to Expect -# Install the specific reporter you want to use -pip install -e qase-pytest # For pytest -pip install -e qase-behave # For behave -pip install -e qase-tavern # For tavern -pip install -e qase-robotframework # For robotframework -``` +When you run the examples: -The `-e` flag installs packages in "editable" mode, which means changes to the source code will be immediately available without reinstalling. +1. **Separate test runs** are created in each configured project +2. **Test results** are routed to the appropriate project based on annotations +3. **Tests without annotations** go to the `default_project` +4. **Each project** gets its own run with configured title and description -## Notes +## Documentation -- Make sure the projects DEVX and DEMO exist in your Qase instance -- Update the API token in the configuration files before running -- Test case IDs used in examples (1, 2, 3, etc.) should exist in your projects or be created -- The `default_project` setting is used when a test doesn't specify a project mapping -- **You must install packages in development mode** to use the new multi-project features +- [Multi-Project Guide (Pytest)](../../qase-pytest/docs/MULTI_PROJECT.md) +- [Multi-Project Guide (Behave)](../../qase-behave/docs/MULTI_PROJECT.md) +- [Multi-Project Guide (Robot Framework)](../../qase-robotframework/docs/MULTI_PROJECT.md) +- [Multi-Project Guide (Tavern)](../../qase-tavern/docs/MULTI_PROJECT.md) +- [Configuration Reference](../../qase-python-commons/README.md) diff --git a/examples/single/README.md b/examples/single/README.md new file mode 100644 index 00000000..45797e8e --- /dev/null +++ b/examples/single/README.md @@ -0,0 +1,130 @@ +# Single Project Examples + +Examples for reporting test results to a single Qase project using `mode: testops`. + +## Frameworks + +| Framework | Directory | Run Command | +|-----------|-----------|-------------| +| Pytest | [pytest/](./pytest/) | `pytest tests/` | +| Behave | [behave/](./behave/) | `behave --format=qase.behave.formatter:QaseFormatter` | +| Robot Framework | [robot/](./robot/) | `robot --listener qase.robotframework.Listener tests/` | +| Tavern | [tavern/](./tavern/) | `pytest` | + +## Quick Start + +1. Navigate to the framework directory: + ```bash + cd examples/single/pytest + ``` + +2. Install dependencies: + ```bash + pip install -r requirements.txt + ``` + +3. Configure credentials (choose one method): + + **Option A: Edit config file** + ```bash + # Edit qase.config.json and replace and + ``` + + **Option B: Use environment variables** + ```bash + export QASE_TESTOPS_API_TOKEN=your_token + export QASE_TESTOPS_PROJECT=your_project_code + ``` + +4. Run tests (see framework-specific commands above) + +## Test Case Linking + +Use `@qase.id()` to link tests to existing test cases in Qase: + +**Pytest:** +```python +@qase.id(1) +def test_example(): + pass + +@qase.id([1, 2, 3]) # Multiple IDs +def test_multiple(): + pass +``` + +**Behave:** +```gherkin +@qase.id:1 +Scenario: Example test +``` + +**Robot Framework:** +```robotframework +Test Case Name + [Tags] Q-1 +``` + +**Tavern:** +```yaml +test_name: QaseID=1 Example test +``` + +## What's in Each Example + +### Pytest + +| File | Demonstrates | +|------|--------------| +| `test_basic.py` | `@qase.id`, `@qase.title`, `@qase.fields`, `@qase.description`, `@qase.priority`, `@qase.severity` | +| `test_attachments.py` | `qase.attach()` with files and byte data | +| `test_steps.py` | `@qase.step` decorator and `with qase.step()` context | +| `test_*_profiler.py` | Database profiling examples (SQLite, PostgreSQL, MySQL, MongoDB, Redis) | + +### Behave + +| File | Demonstrates | +|------|--------------| +| `simple.feature` | Basic test with `@qase.id` and `@qase.fields` | +| `attachments.feature` | File attachments in BDD tests | +| `suites.feature` | Test suite organization with `@qase.suite` | +| `parametrized.feature` | Scenario outlines with examples | + +### Robot Framework + +| File | Demonstrates | +|------|--------------| +| `simple.robot` | Basic tests with `Q-ID` tags | +| `parametrized.robot` | Data-driven tests with parameters | + +### Tavern + +| File | Demonstrates | +|------|--------------| +| `test_simple.tavern.yaml` | Basic API test without Qase ID | +| `test_with_id.tavern.yaml` | API test with `QaseID=N` in test name | + +## Configuration + +Each framework directory contains a `qase.config.json` with recommended settings. + +Example configuration: +```json +{ + "mode": "testops", + "fallback": "report", + "testops": { + "api": { + "token": "", + "host": "qase.io" + }, + "project": "", + "run": { + "title": "Example run", + "complete": true + } + } +} +``` + +For complete configuration options, see [qase-python-commons](../../qase-python-commons/README.md). diff --git a/examples/single/behave/README.md b/examples/single/behave/README.md index 3d147aa9..a2366e54 100644 --- a/examples/single/behave/README.md +++ b/examples/single/behave/README.md @@ -1,44 +1,127 @@ -### Behave Example +# Behave Examples -This is a sample project demonstrating how to write and execute tests using the Behave framework with integration to -Qase Test Management. +Examples demonstrating Qase Behave Reporter features. ---- +## Setup -## Prerequisites +1. Install dependencies: + ```bash + pip install -r requirements.txt + ``` -Ensure that the following tools are installed on your machine: +2. Configure credentials in `qase.config.json`: + - Replace `` with your [API token](https://app.qase.io/user/api/token) + - Replace `` with your project code -1. [Python](https://www.python.org/) (version 3.7 or higher is recommended) -2. [pip](https://pip.pypa.io/en/stable/) + Or use environment variables: + ```bash + export QASE_TESTOPS_API_TOKEN=your_token + export QASE_TESTOPS_PROJECT=your_project_code + ``` ---- +## Run Tests -## Setup Instructions +```bash +# Run all tests +behave --format=qase.behave.formatter:QaseFormatter -1. Clone this repository by running the following commands: - ```bash - git clone https://github.com/qase-tms/qase-python.git - cd qase-python/examples/single/behave - ``` +# Run specific feature +behave --format=qase.behave.formatter:QaseFormatter tests/features/simple.feature -2. Install the project dependencies: - ```bash - pip install -r requirements.txt - ``` +# Run with console output +behave --format=qase.behave.formatter:QaseFormatter --format=pretty +``` -3. Create a `qase.config.json` file in the root of the project. Follow the instructions on - [how to configure the file](https://github.com/qase-tms/qase-python/blob/main/qase-behave/docs/CONFIGURATION.md). +## Examples -4. To run tests and upload the results to Qase Test Management, use the following command: - ```bash - behave --format=qase.behave.formatter:QaseFormatter - ``` - This will execute the tests and display the results in the terminal. +| File | Description | +|------|-------------| +| `simple.feature` | Basic tests with `@qase.id` and `@qase.fields` tags | +| `attachments.feature` | File attachments in BDD scenarios | +| `suites.feature` | Test suite organization with `@qase.suite` | +| `parametrized.feature` | Scenario Outlines with Examples tables | + +## Code Examples + +### Link to Test Case + +```gherkin +@qase.id:1 +Scenario: Test linked to case 1 + Given some precondition + When action is performed + Then expected result occurs +``` + +### Add Metadata + +```gherkin +@qase.id:1 @qase.fields:{"severity":"critical","priority":"high"} +Scenario: Test with metadata + Given some precondition + When action is performed + Then expected result occurs +``` + +### Test Suites + +```gherkin +@qase.suite:Authentication +Feature: Login functionality + + @qase.id:1 + Scenario: Valid login + Given user is on login page + When user enters valid credentials + Then user is logged in +``` + +### Parametrized Tests + +```gherkin +@qase.id:10 +Scenario Outline: Login with different users + Given user "" exists + When user logs in with password "" + Then login result is "" + + Examples: + | username | password | result | + | admin | admin123 | success | + | user | wrong | failure | +``` + +### Ignore Test + +```gherkin +@qase.ignore +Scenario: This test will not be reported to Qase + Given some condition + When action happens + Then result is ignored +``` + +### Attachments + +```gherkin +Scenario: Test with attachment + Given I have a file to attach + When I attach the file "screenshot.png" + Then the attachment is added to the test result +``` + +Step implementation: +```python +from behave import when +from qase.behave import qase ---- +@when('I attach the file "{filename}"') +def attach_file(context, filename): + qase.attach(f"/path/to/{filename}") +``` -## Additional Resources +## Documentation -For more details on how to use this integration with Qase Test Management, visit -the [Qase Behave documentation](https://github.com/qase-tms/qase-python/tree/main/qase-behave). +- [Behave Reporter README](../../../qase-behave/README.md) +- [Usage Guide](../../../qase-behave/docs/usage.md) +- [Configuration Reference](../../../qase-python-commons/README.md) diff --git a/examples/single/behave/tests/features/attachments.feature b/examples/single/behave/tests/features/attachments.feature index fe931a03..b35a2873 100644 --- a/examples/single/behave/tests/features/attachments.feature +++ b/examples/single/behave/tests/features/attachments.feature @@ -3,37 +3,37 @@ Feature: Test Attachments I want to attach files and content to my test results So that I can provide additional context for test failures - @qase.id:10 + @qase.id:300 Scenario: Attach file to test Given I have a test with attachments When I attach a file to the test Then the attachments should be included in the test result - @qase.id:20 + @qase.id:301 Scenario: Attach text content Given I have a test with attachments When I attach content as text Then the attachments should be included in the test result - @qase.id:30 + @qase.id:302 Scenario: Attach JSON data Given I have a test with attachments When I attach JSON data Then the attachments should be included in the test result - @qase.id:40 + @qase.id:303 Scenario: Attach screenshot Given I want to attach a screenshot When I attach the screenshot Then the attachments should be included in the test result - @qase.id:50 + @qase.id:304 Scenario: Add comments to test Given I have a test with attachments When I add a comment about the test Then the attachments should be included in the test result - @qase.id:60 + @qase.id:305 Scenario: Add debug information Given I have a test with attachments When I add debug information diff --git a/examples/single/behave/tests/features/parametrized.feature b/examples/single/behave/tests/features/parametrized.feature index d28ccf7e..b5e86590 100644 --- a/examples/single/behave/tests/features/parametrized.feature +++ b/examples/single/behave/tests/features/parametrized.feature @@ -1,24 +1,111 @@ -Feature: Parametrized Tests - - Scenario Outline: Test with parameters success - Given I have a test with parameters "" and "" - When I run it - Then it should pass - - Examples: - | param1 | param2 | - | 1 | 2 | - | 3 | 4 | - | 5 | 6 | - - - Scenario Outline: Test with parameters failed - Given I have a test with parameters "" and "" - When I run it - Then it should fail - - Examples: - | param1 | param2 | - | 1 | 2 | - | 3 | 4 | - | 5 | 6 | +Feature: Parametrized Scenarios + Demonstrates Scenario Outlines with Examples tables. + Each row in Examples table creates a separate test result in Qase. + + # ============================================================================ + # Basic parametrization - Login with different user roles + # ============================================================================ + + @qase.id:200 + Scenario Outline: User login with different roles + Given user with role "" exists + When user logs in with email "" + Then user should have access to "" + + Examples: Standard users + | role | email | accessible_pages | + | viewer | viewer@example.com | dashboard,profile | + | editor | editor@example.com | dashboard,profile,content | + + Examples: Admin users + | role | email | accessible_pages | + | admin | admin@example.com | dashboard,profile,content,settings | + | super_admin | super@example.com | dashboard,profile,content,settings,users | + + # ============================================================================ + # Validation scenarios - Input validation testing + # ============================================================================ + + @qase.id:201 @qase.fields:{"severity":"normal","layer":"unit"} + Scenario Outline: Email validation + When email "" is validated + Then validation result should be "" + And error message should be "" + + Examples: Valid emails + | email | result | error_message | + | user@example.com | valid | none | + | test.user@mail.org | valid | none | + | admin+tag@site.net | valid | none | + + Examples: Invalid emails + | email | result | error_message | + | invalid | invalid | Missing @ symbol | + | @nodomain.com | invalid | Missing local part | + | user@ | invalid | Missing domain | + | user@.com | invalid | Invalid domain format | + + # ============================================================================ + # API testing - HTTP status codes + # ============================================================================ + + @qase.id:202 @qase.fields:{"layer":"api"} + Scenario Outline: API endpoint returns correct status + Given API endpoint "" exists + When "" request is sent + Then response status should be + And response should contain "" + + Examples: GET endpoints + | endpoint | method | status_code | expected_field | + | /api/users | GET | 200 | users | + | /api/posts | GET | 200 | posts | + | /api/invalid | GET | 404 | error | + + Examples: POST endpoints + | endpoint | method | status_code | expected_field | + | /api/users | POST | 201 | id | + | /api/unauthorized | POST | 401 | error | + + # ============================================================================ + # Business logic - Pricing calculations + # ============================================================================ + + @qase.id:203 @qase.fields:{"severity":"critical"} + Scenario Outline: Discount calculation + Given base price is + And discount type is "" + And discount value is + When discount is applied + Then final price should be + + Examples: Percentage discounts + | base_price | discount_type | discount_value | final_price | + | 100.00 | percentage | 10 | 90.00 | + | 250.00 | percentage | 20 | 200.00 | + | 500.00 | percentage | 50 | 250.00 | + + Examples: Fixed amount discounts + | base_price | discount_type | discount_value | final_price | + | 100.00 | fixed | 15 | 85.00 | + | 250.00 | fixed | 50 | 200.00 | + | 500.00 | fixed | 100 | 400.00 | + + # ============================================================================ + # Edge cases - Boundary value testing + # ============================================================================ + + @qase.id:204 + Scenario Outline: Password length validation + When password "" is checked + Then password should be "" + And message should be "" + + Examples: Boundary values + | password | validity | message | + | abc | invalid | Password must be at least 8 chars | + | abcdefg | invalid | Password must be at least 8 chars | + | abcdefgh | valid | Password meets requirements | + | abcdefghi | valid | Password meets requirements | + | a234567890123456789012345678901 | valid | Password meets requirements | + | a2345678901234567890123456789012 | invalid | Password exceeds 32 chars | diff --git a/examples/single/behave/tests/features/simple.feature b/examples/single/behave/tests/features/simple.feature index 237da32a..70b8dd63 100644 --- a/examples/single/behave/tests/features/simple.feature +++ b/examples/single/behave/tests/features/simple.feature @@ -1,35 +1,81 @@ -Feature: Simple tests +Feature: Basic Qase Integration + Demonstrates basic Qase Behave Reporter features including + test case linking, metadata fields, and test organization. - Scenario: Test without annotations success - Given I have a simple test - When I run it - Then it should pass - - Scenario: Test without annotations failed - Given I have a simple test - When I run it - Then it should fail + # ============================================================================ + # @qase.id - Link scenario to existing Qase test case + # ============================================================================ @qase.id:1 - Scenario: Test with QaseID success - Given I have a simple test - When I run it - Then it should pass + Scenario: User can login with valid credentials + Given a registered user exists with email "user@example.com" + When the user enters valid credentials + And clicks the login button + Then the user should be redirected to dashboard + And should see welcome message @qase.id:2 - Scenario: Test with QaseID failed - Given I have a simple test - When I run it - Then it should fail - - @qase.fields:{"description":"It_is_simple_test"} - Scenario: Test with Fields success - Given I have a simple test - When I run it - Then it should pass - - @qase.fields:{"description":"It_is_simple_test"} - Scenario: Test with Fields failed - Given I have a simple test - When I run it - Then it should fail + Scenario: User cannot login with invalid password + Given a registered user exists with email "user@example.com" + When the user enters wrong password + And clicks the login button + Then an error message should be displayed + And the user should remain on login page + + @qase.id:3,4,5 + Scenario: User session management + Given the user is logged in + When the user clicks logout + Then the session should be terminated + And the user should be redirected to login page + + # ============================================================================ + # @qase.fields - Add metadata to test case + # ============================================================================ + + @qase.fields:{"severity":"critical","priority":"high","layer":"e2e"} + Scenario: Payment processing completes successfully + Given a user has items in shopping cart + And valid payment method is configured + When the user initiates checkout + And confirms the payment + Then the order should be created + And confirmation email should be sent + + @qase.fields:{"description":"Verifies_password_reset_flow_works_correctly","preconditions":"User_must_have_registered_email"} + Scenario: Password reset email is sent + Given user exists with email "forgot@example.com" + When user requests password reset + Then reset email should be sent + And email should contain reset link + + # ============================================================================ + # Combined annotations + # ============================================================================ + + @qase.id:6 @qase.fields:{"severity":"blocker","description":"Critical_checkout_validation"} + Scenario: Cart total is calculated correctly + Given the following items in cart + | name | price | quantity | + | Laptop | 999.99 | 1 | + | Mouse | 29.99 | 2 | + | Keyboard | 79.99 | 1 | + When the cart total is calculated + Then the total should be 1139.96 + + # ============================================================================ + # Tests without Qase ID (auto-created in Qase) + # ============================================================================ + + Scenario: Search returns relevant results + Given products exist in the catalog + When user searches for "laptop" + Then search results should contain "laptop" + And results should be sorted by relevance + + Scenario: User profile can be updated + Given the user is logged in + And viewing profile page + When user updates their name to "John Doe" + And saves the changes + Then profile should be updated successfully diff --git a/examples/single/behave/tests/features/steps/steps.py b/examples/single/behave/tests/features/steps/steps.py index 09b0efb1..8e847120 100644 --- a/examples/single/behave/tests/features/steps/steps.py +++ b/examples/single/behave/tests/features/steps/steps.py @@ -1,83 +1,495 @@ -from behave import * +""" +Step definitions for Behave examples. -@given('I have a simple test') -def step_impl(context): - pass +These steps demonstrate realistic test scenarios while remaining +simple enough to run without external dependencies. +""" +from behave import given, when, then -@given('I have a test with parameters "{param1}" and "{param2}"') -def step_given_test_with_parameters(context, param1, param2): - pass -@when('I run it') -def step_impl(context): - pass +# ============================================================================ +# User Authentication Steps +# ============================================================================ +@given('a registered user exists with email "{email}"') +def step_user_exists(context, email): + context.user = {"email": email, "password": "SecurePass123"} -@then('it should pass') -def step_impl(context): - pass +@given('the user is logged in') +def step_user_logged_in(context): + context.session = {"active": True, "user_id": 1} -@then('it should fail') -def step_impl(context): - assert False +@given('user is logged in') +def step_user_is_logged_in(context): + context.session = {"active": True, "user_id": 1} -@given('I have a test with multiple IDs') -def step_impl(context): - pass +@given('user enters valid credentials') +def step_user_enters_valid_creds_given(context): + context.login_attempt = {"valid": True} -@when('I execute it') -def step_impl(context): - pass +@given('the login page is displayed') +def step_login_page(context): + context.current_page = "login" + + +@when('the user enters valid credentials') +def step_enter_valid_creds(context): + context.login_attempt = {"valid": True} + + +@when('the user enters wrong password') +def step_enter_wrong_password(context): + context.login_attempt = {"valid": False, "error": "Invalid password"} + + +@when('user enters valid credentials') +def step_user_enters_valid_creds(context): + context.login_attempt = {"valid": True} + + +@when('user enters email "{email}"') +def step_enter_email(context, email): + context.entered_email = email + + +@when('user enters password "{password}"') +def step_enter_password(context, password): + context.entered_password = password + + +@when('clicks the login button') +def step_click_login(context): + context.login_submitted = True -@then('it should succeed') -def step_impl(context): - pass +@when('clicks submit button') +def step_click_submit(context): + context.form_submitted = True -@given('I have a test for multiple projects') -def step_impl(context): + +@when('the user clicks logout') +def step_click_logout(context): + context.session = {"active": False} + + +@then('the user should be redirected to dashboard') +def step_redirect_dashboard(context): + assert context.login_attempt.get("valid", False), "Login should be valid" + + +@then('should see welcome message') +def step_see_welcome(context): + pass # Verified by UI in real scenario + + +@then('an error message should be displayed') +def step_error_displayed(context): + assert not context.login_attempt.get("valid", True), "Should have error" + + +@then('the user should remain on login page') +def step_remain_login(context): pass -@then('it should be reported to both projects') -def step_impl(context): +@then('user should remain on login page') +def step_user_remain_login(context): pass -@given('I have a complex multi-project test') -def step_impl(context): +@then('the session should be terminated') +def step_session_terminated(context): + assert not context.session.get("active", True) + + +@then('the user should be redirected to login page') +def step_redirect_login(context): pass -@then('it should work correctly') -def step_impl(context): +@then('user should be authenticated') +def step_user_authenticated(context): pass -@given('I have a test that will fail') -def step_impl(context): +@then('redirected to dashboard') +def step_redirected_dashboard(context): pass -@then('it should fail intentionally') -def step_impl(context): - assert False, "This test intentionally fails" +# ============================================================================ +# OAuth Steps +# ============================================================================ + +@when('user clicks "{button_text}"') +def step_click_button(context, button_text): + context.clicked_button = button_text + + +@when('completes Google authentication') +def step_google_auth(context): + context.oauth_result = {"provider": "google", "success": True} + + +@when('completes GitHub authentication') +def step_github_auth(context): + context.oauth_result = {"provider": "github", "success": True} + + +@when('Google returns authentication error') +def step_google_error(context): + context.oauth_result = {"provider": "google", "success": False, "error": "Access denied"} + +@then('profile should be synced from Google') +def step_profile_google(context): + assert context.oauth_result.get("provider") == "google" -@given('I have a test that will pass') -def step_impl(context): + +@then('profile should be synced from GitHub') +def step_profile_github(context): + assert context.oauth_result.get("provider") == "github" + + +@then('error message should be displayed') +def step_error_message(context): + assert context.oauth_result.get("error") is not None + + +# ============================================================================ +# Session and Security Steps +# ============================================================================ + +@when('checks "remember me" checkbox') +def step_check_remember_me(context): + context.remember_me = True + + +@then('session should persist for 30 days') +def step_session_persist(context): + assert context.remember_me + + +@given('user has account with email "{email}"') +def step_user_has_account(context, email): + context.user_account = {"email": email, "locked": False, "failed_attempts": 0} + + +@when('user enters wrong password {count} times') +def step_wrong_password_times(context, count): + context.user_account["failed_attempts"] = int(count) + if int(count) >= 5: + context.user_account["locked"] = True + + +@then('account should be locked') +def step_account_locked(context): + assert context.user_account.get("locked", False) + + +@then('lockout notification email should be sent') +def step_lockout_email(context): + pass # Email sending verified separately + + +@given('user has 2FA enabled') +def step_2fa_enabled(context): + context.two_factor = {"enabled": True} + + +@when('2FA code is requested') +def step_2fa_requested(context): + context.two_factor["code_sent"] = True + + +@when('user enters valid 2FA code') +def step_enter_2fa(context): + context.two_factor["verified"] = True + + +@then('user enters valid 2FA code') +def step_enter_2fa_then(context): + context.two_factor["verified"] = True + + +@given('user is inactive for 30 minutes') +def step_inactive(context): + context.session["inactive_minutes"] = 30 + + +@when('user performs an action') +def step_perform_action(context): + if context.session.get("inactive_minutes", 0) >= 30: + context.session["expired"] = True + + +@then('session should be expired') +def step_session_expired(context): + assert context.session.get("expired", False) + + +@then('user should be redirected to login') +def step_redirect_to_login(context): pass -@given('I have a test without ID') -def step_impl(context): +# ============================================================================ +# Shopping Cart Steps +# ============================================================================ + +@given('a user has items in shopping cart') +def step_cart_items(context): + context.cart = {"items": [{"id": 1, "price": 99.99}], "total": 99.99} + + +@given('valid payment method is configured') +def step_payment_configured(context): + context.payment = {"method": "credit_card", "valid": True} + + +@given('the following items in cart') +def step_items_table(context): + context.cart = {"items": [], "total": 0} + for row in context.table: + item = { + "name": row["name"], + "price": float(row["price"]), + "quantity": int(row["quantity"]) + } + context.cart["items"].append(item) + + +@when('the user initiates checkout') +def step_init_checkout(context): + context.checkout = {"initiated": True} + + +@when('confirms the payment') +def step_confirm_payment(context): + context.checkout["payment_confirmed"] = True + + +@when('the cart total is calculated') +def step_calculate_total(context): + total = sum(item["price"] * item["quantity"] for item in context.cart["items"]) + context.cart["total"] = round(total, 2) + + +@then('the order should be created') +def step_order_created(context): + context.order = {"id": "ORD-001", "status": "created"} + + +@then('confirmation email should be sent') +def step_confirmation_email(context): pass -@then('it should be sent to first project') -def step_impl(context): +@then('the total should be {expected_total}') +def step_verify_total(context, expected_total): + assert abs(context.cart["total"] - float(expected_total)) < 0.01 + + +# ============================================================================ +# Password Reset Steps +# ============================================================================ + +@given('user exists with email "{email}"') +def step_user_exists_email(context, email): + context.reset_user = {"email": email} + + +@when('user requests password reset') +def step_request_reset(context): + context.reset_requested = True + + +@then('reset email should be sent') +def step_reset_email_sent(context): + assert context.reset_requested + + +@then('email should contain reset link') +def step_email_has_link(context): pass + + +# ============================================================================ +# Search Steps +# ============================================================================ + +@given('products exist in the catalog') +def step_products_exist(context): + context.catalog = ["Laptop", "Desktop", "Tablet", "Phone"] + + +@when('user searches for "{query}"') +def step_search(context, query): + context.search_results = [p for p in context.catalog if query.lower() in p.lower()] + + +@then('search results should contain "{expected}"') +def step_results_contain(context, expected): + assert any(expected.lower() in r.lower() for r in context.search_results) + + +@then('results should be sorted by relevance') +def step_sorted_relevance(context): + pass # Sorting verified separately + + +# ============================================================================ +# Profile Steps +# ============================================================================ + +@given('viewing profile page') +def step_view_profile(context): + context.current_page = "profile" + + +@when('user updates their name to "{name}"') +def step_update_name(context, name): + context.profile_update = {"name": name} + + +@when('saves the changes') +def step_save_changes(context): + context.profile_update["saved"] = True + + +@then('profile should be updated successfully') +def step_profile_updated(context): + assert context.profile_update.get("saved", False) + + +# ============================================================================ +# Parametrized Test Steps +# ============================================================================ + +@given('user with role "{role}" exists') +def step_user_role(context, role): + context.user = {"role": role} + + +@when('user logs in with email "{email}"') +def step_login_email(context, email): + context.logged_in_email = email + + +@then('user should have access to "{pages}"') +def step_access_pages(context, pages): + expected_pages = pages.split(",") + # Simplified permission check + assert len(expected_pages) > 0 + + +@when('email "{email}" is validated') +def step_validate_email(context, email): + is_valid = "@" in email and "." in email.split("@")[-1] if "@" in email else False + has_local = email.split("@")[0] if "@" in email else "" + has_domain = email.split("@")[-1] if "@" in email else "" + + if not is_valid or not has_local: + context.validation = {"result": "invalid", "error": "Missing @ symbol" if "@" not in email else "Invalid format"} + elif email.startswith("@"): + context.validation = {"result": "invalid", "error": "Missing local part"} + elif email.endswith("@"): + context.validation = {"result": "invalid", "error": "Missing domain"} + elif ".com" in email and email.split("@")[-1].startswith("."): + context.validation = {"result": "invalid", "error": "Invalid domain format"} + else: + context.validation = {"result": "valid", "error": "none"} + + +@then('validation result should be "{result}"') +def step_validation_result(context, result): + assert context.validation["result"] == result + + +@then('error message should be "{message}"') +def step_validation_error(context, message): + assert context.validation["error"] == message + + +@given('API endpoint "{endpoint}" exists') +def step_api_endpoint(context, endpoint): + context.api_endpoint = endpoint + + +@when('"{method}" request is sent') +def step_send_request(context, method): + # Simulated API responses + responses = { + ("/api/users", "GET"): {"status": 200, "body": {"users": []}}, + ("/api/posts", "GET"): {"status": 200, "body": {"posts": []}}, + ("/api/invalid", "GET"): {"status": 404, "body": {"error": "Not found"}}, + ("/api/users", "POST"): {"status": 201, "body": {"id": 1}}, + ("/api/unauthorized", "POST"): {"status": 401, "body": {"error": "Unauthorized"}}, + } + key = (context.api_endpoint, method) + context.api_response = responses.get(key, {"status": 500, "body": {"error": "Unknown"}}) + + +@then('response status should be {status_code:d}') +def step_status_code(context, status_code): + assert context.api_response["status"] == status_code + + +@then('response should contain "{field}"') +def step_response_contains(context, field): + assert field in context.api_response["body"] + + +@given('base price is {price}') +def step_base_price(context, price): + context.pricing = {"base": float(price)} + + +@given('discount type is "{discount_type}"') +def step_discount_type(context, discount_type): + context.pricing["type"] = discount_type + + +@given('discount value is {value}') +def step_discount_value(context, value): + context.pricing["value"] = float(value) + + +@when('discount is applied') +def step_apply_discount(context): + base = context.pricing["base"] + value = context.pricing["value"] + if context.pricing["type"] == "percentage": + context.pricing["final"] = base - (base * value / 100) + else: + context.pricing["final"] = base - value + + +@then('final price should be {expected}') +def step_final_price(context, expected): + assert abs(context.pricing["final"] - float(expected)) < 0.01 + + +@when('password "{password}" is checked') +def step_check_password(context, password): + length = len(password) + if length < 8: + context.password_check = {"valid": "invalid", "message": "Password must be at least 8 chars"} + elif length > 32: + context.password_check = {"valid": "invalid", "message": "Password exceeds 32 chars"} + else: + context.password_check = {"valid": "valid", "message": "Password meets requirements"} + + +@then('password should be "{validity}"') +def step_password_validity(context, validity): + assert context.password_check["valid"] == validity + + +@then('message should be "{message}"') +def step_password_message(context, message): + assert context.password_check["message"] == message diff --git a/examples/single/behave/tests/features/suites.feature b/examples/single/behave/tests/features/suites.feature index 963f7fb0..54c7e58d 100644 --- a/examples/single/behave/tests/features/suites.feature +++ b/examples/single/behave/tests/features/suites.feature @@ -1,25 +1,81 @@ -Feature: Simple1 tests - - @qase.suite:MySuite - Scenario: Test with single suite success - Given I have a simple test - When I run it - Then it should pass - - @qase.suite:MySuite - Scenario: Test with single suite failed - Given I have a simple test - When I run it - Then it should fail - - @qase.suite:MySuite||SubSuite - Scenario: Test with multiple suite success - Given I have a simple test - When I run it - Then it should pass - - @qase.suite:MySuite||SubSuite - Scenario: Test with multiple suite failed - Given I have a simple test - When I run it - Then it should fail +@qase.suite:Authentication +Feature: User Authentication + Demonstrates test suite organization using @qase.suite tag. + All scenarios in this feature are organized under "Authentication" suite. + + # ============================================================================ + # Single-level suite organization + # ============================================================================ + + @qase.id:100 + Scenario: Login with email and password + Given the login page is displayed + When user enters email "user@example.com" + And user enters password "SecurePass123" + And clicks submit button + Then user should be authenticated + And redirected to dashboard + + @qase.id:101 + Scenario: Login with remember me option + Given the login page is displayed + When user enters valid credentials + And checks "remember me" checkbox + And clicks submit button + Then user should be authenticated + And session should persist for 30 days + + # ============================================================================ + # Nested suite organization + # ============================================================================ + + @qase.id:102 @qase.suite:Authentication||OAuth + Scenario: Login with Google OAuth + Given the login page is displayed + When user clicks "Sign in with Google" + And completes Google authentication + Then user should be authenticated + And profile should be synced from Google + + @qase.id:103 @qase.suite:Authentication||OAuth + Scenario: Login with GitHub OAuth + Given the login page is displayed + When user clicks "Sign in with GitHub" + And completes GitHub authentication + Then user should be authenticated + And profile should be synced from GitHub + + @qase.id:104 @qase.suite:Authentication||OAuth||Error_Handling + Scenario: OAuth provider returns error + Given the login page is displayed + When user clicks "Sign in with Google" + And Google returns authentication error + Then error message should be displayed + And user should remain on login page + + # ============================================================================ + # Security suite + # ============================================================================ + + @qase.id:105 @qase.suite:Authentication||Security + Scenario: Account lockout after failed attempts + Given user has account with email "user@example.com" + When user enters wrong password 5 times + Then account should be locked + And lockout notification email should be sent + + @qase.id:106 @qase.suite:Authentication||Security + Scenario: Two-factor authentication + Given user has 2FA enabled + And user enters valid credentials + When 2FA code is requested + And user enters valid 2FA code + Then user should be authenticated + + @qase.id:107 @qase.suite:Authentication||Security||Session + Scenario: Session timeout after inactivity + Given user is logged in + And user is inactive for 30 minutes + When user performs an action + Then session should be expired + And user should be redirected to login diff --git a/examples/single/pytest/Readme.md b/examples/single/pytest/Readme.md index 51087098..e3d41500 100644 --- a/examples/single/pytest/Readme.md +++ b/examples/single/pytest/Readme.md @@ -1,32 +1,143 @@ -# How to run these examples +# Pytest Examples -1. Clone the repository +Examples demonstrating Qase Pytest Reporter features. - ```bash - git clone https://github.com/qase-tms/qase-python.git - ``` +## Setup -2. Move to the directory with the examples +1. Install dependencies: + ```bash + pip install -r requirements.txt + ``` - ```bash - cd qase-python/examples/single/pytest - ``` +2. Configure credentials in `qase.config.json`: + - Replace `` with your [API token](https://app.qase.io/user/api/token) + - Replace `` with your project code -3. Install the required packages + Or use environment variables: + ```bash + export QASE_TESTOPS_API_TOKEN=your_token + export QASE_TESTOPS_PROJECT=your_project_code + ``` - ```bash - pip install -r requirements.txt - ``` +## Run Tests -4. Add the Qase token and project code to the ENV variables +```bash +# Run all tests +pytest tests/ - ```bash - export QASE_TESTOPS_API_TOKEN=your_token - export QASE_TESTOPS_PROJECT=your_project_code - ``` +# Run specific test file +pytest tests/test_basic.py -5. Run the tests +# Run with verbose output +pytest tests/ -v +``` - ```bash - pytest - ``` +## Examples + +| File | Description | +|------|-------------| +| `test_basic.py` | Decorators: `@qase.id`, `@qase.title`, `@qase.fields`, `@qase.description`, `@qase.priority`, `@qase.severity`, `@qase.layer`, `@qase.preconditions`, `@qase.postconditions` | +| `test_attachments.py` | Attachments: `qase.attach()` with files, bytes, and in steps | +| `test_steps.py` | Steps: `@qase.step` decorator and `with qase.step()` context manager | +| `test_sqlite_profiler.py` | Database profiling: SQLite operations with step tracking | +| `test_postgres_profiler.py` | Database profiling: PostgreSQL operations | +| `test_mysql_profiler.py` | Database profiling: MySQL operations | +| `test_mongodb_profiler.py` | Database profiling: MongoDB operations | +| `test_redis_profiler.py` | Database profiling: Redis operations | + +## Code Examples + +### Link to Test Case + +```python +from qase.pytest import qase + +@qase.id(1) +def test_with_id(): + assert True + +@qase.id([1, 2, 3]) # Link to multiple test cases +def test_with_multiple_ids(): + assert True +``` + +### Add Metadata + +```python +@qase.title("Login test") +@qase.description("Verify user can login with valid credentials") +@qase.severity("critical") +@qase.priority("high") +@qase.layer("e2e") +def test_login(): + assert True + +# Or use @qase.fields for multiple fields +@qase.fields( + ("severity", "critical"), + ("priority", "high"), + ("layer", "e2e"), +) +def test_with_fields(): + assert True +``` + +### Add Attachments + +```python +def test_with_attachment(): + # Attach file from filesystem + qase.attach("/path/to/file.txt") + + # Attach multiple files + qase.attach("/path/to/file1.txt", "/path/to/file2.png") + + # Attach with MIME type + qase.attach(("/path/to/file.json", "application/json")) + + # Attach bytes data + qase.attach((b"test data", "text/plain", "data.txt")) + + assert True +``` + +### Add Steps + +```python +@qase.step("Step as decorator") +def helper_function(): + pass + +def test_with_steps(): + helper_function() + + with qase.step("Step as context manager"): + # step code here + pass + + assert True +``` + +## Profiler Examples + +The `test_*_profiler.py` files demonstrate database profiling with automatic step tracking. Enable profiling in `qase.config.json`: + +```json +{ + "profilers": ["db"], + "framework": { + "pytest": { + "capture": { + "logs": true, + "http": true + } + } + } +} +``` + +## Documentation + +- [Pytest Reporter README](../../../qase-pytest/README.md) +- [Usage Guide](../../../qase-pytest/docs/usage.md) +- [Configuration Reference](../../../qase-python-commons/README.md) diff --git a/examples/single/pytest/tests/attachment_test.py b/examples/single/pytest/tests/attachment_test.py deleted file mode 100644 index 8945af89..00000000 --- a/examples/single/pytest/tests/attachment_test.py +++ /dev/null @@ -1,57 +0,0 @@ -import os - -from qase.pytest import qase - - -def test_with_bytes_attachment_success(): - qase.attach((str.encode("This is a simple string attachment"), "text/plain", "simple.txt")) - assert 1 == 1 - - -def test_with_bytes_attachment_failed(): - qase.attach((str.encode("This is a simple string attachment"), "text/plain", "simple.txt")) - assert 1 == 2 - - -def test_with_file_attachment_success(): - current_directory = os.getcwd() - qase.attach(f"{current_directory}/attachments/file.txt", - f"{current_directory}/attachments/image.png") - assert 1 == 1 - - -def test_with_file_attachment_failed(): - current_directory = os.getcwd() - qase.attach(f"{current_directory}/attachments/file.txt", - f"{current_directory}/attachments/image.png") - assert 1 == 2 - - -def test_with_file_attachment_and_mime_tipe_success(): - current_directory = os.getcwd() - qase.attach( - (f"{current_directory}/attachments/file.txt", "text/plain")) - assert 1 == 1 - - -def test_with_file_attachment_and_mime_tipe_failed(): - current_directory = os.getcwd() - qase.attach( - (f"{current_directory}/attachments/file.txt", "text/plain")) - assert 1 == 2 - - -@qase.step("Step with bytes attachment") -def step_with_bytes_attachment(): - qase.attach((str.encode("This is a simple string attachment"), "text/plain", "simple.txt")) - pass - - -def test_with_step_attachment_success(): - step_with_bytes_attachment() - assert 1 == 1 - - -def test_with_step_attachment_failed(): - step_with_bytes_attachment() - assert 1 == 2 diff --git a/examples/single/pytest/tests/simple_test.py b/examples/single/pytest/tests/simple_test.py deleted file mode 100644 index 34c0ada0..00000000 --- a/examples/single/pytest/tests/simple_test.py +++ /dev/null @@ -1,106 +0,0 @@ -from qase.pytest import qase - - -@qase.id(1) -def test_with_qase_id_success(): - assert 1 == 1 - - -@qase.id(2) -def test_with_qase_id_failed(): - assert 1 == 2 - - -@qase.title("Simple test success") -def test_with_title_success(): - assert 1 == 1 - - -@qase.title("Simple test failed") -def test_with_title_failed(): - assert 1 == 2 - - -@qase.description("Try to login to Qase TestOps using login and password 1") -def test_with_description_success(): - assert 1 == 1 - - -@qase.description("Try to login to Qase TestOps using login and password") -def test_with_description_failed(): - assert 1 == 2 - - -@qase.preconditions("*Precondition 1*. Markdown is supported.") -def test_with_preconditions_success(): - assert 1 == 1 - - -@qase.preconditions("*Precondition 1*. Markdown is supported.") -def test_with_preconditions_failed(): - assert 1 == 2 - - -@qase.postconditions("*Postcondition 1*. Markdown is supported.") -def test_with_postconditions_success(): - assert 1 == 1 - - -@qase.postconditions("*Postcondition 1*. Markdown is supported.1") -def test_with_postconditions_failed(): - assert 1 == 2 - - -@qase.severity("normal") -def test_with_severity_success(): - assert 1 == 1 - - -@qase.severity("normal") -def test_with_severity_failed(): - assert 1 == 2 - - -@qase.priority("high") -def test_with_priority_success(): - assert 1 == 1 - - -@qase.priority("high") -def test_with_priority_failed(): - assert 1 == 2 - - -@qase.layer("unit") -def test_with_layer_success(): - assert 1 == 1 - - -@qase.layer("unit") -def test_with_layer_failed(): - assert 1 == 2 - - -@qase.fields( - ("severity", "normal"), - ("custom_field", "value"), - ("priority", "high"), - ("layer", "unit"), - ("description", "Try to login to Qase TestOps using login and password"), - ("preconditions", "*Precondition 1*. Markdown is supported."), - ("postconditions", "*Postcondition 1*. Markdown is supported."), -) -def test_with_fields_success(): - assert 1 == 1 - - -@qase.fields( - ("severity", "normal"), - ("priority", "high"), - ("layer", "unit"), - ("description", "Try to login to Qase TestOps using login and password"), - ("preconditions", "*Precondition 1*. Markdown is supported."), - ("postconditions", "*Postcondition 1*. Markdown is supported."), -) -def test_with_fields_failed(): - assert 1 == 2 diff --git a/examples/single/pytest/tests/step_test.py b/examples/single/pytest/tests/step_test.py deleted file mode 100644 index 30758685..00000000 --- a/examples/single/pytest/tests/step_test.py +++ /dev/null @@ -1,26 +0,0 @@ -from qase.pytest import qase - - -@qase.step("Step 01") -def step01(): - step02() - pass - - -@qase.step("Step 02") -def step02(): - pass - - -def test_with_steps_success(): - step01() - with qase.step("Step 03"): - pass - assert 1 == 1 - - -def test_with_steps_failed(): - step01() - with qase.step("Step 03"): - pass - assert 1 == 2 diff --git a/examples/single/pytest/tests/test_attachments.py b/examples/single/pytest/tests/test_attachments.py new file mode 100644 index 00000000..63fc1126 --- /dev/null +++ b/examples/single/pytest/tests/test_attachments.py @@ -0,0 +1,347 @@ +""" +Attachment examples demonstrating Qase Pytest Reporter attachment functionality. + +Attachments allow you to include files, screenshots, logs, and other data +with your test results in Qase TestOps for better debugging and documentation. +""" +import json +import os +import tempfile + +from qase.pytest import qase + + +# ============================================================================ +# Basic file attachments +# ============================================================================ + +def test_attach_single_file(): + """Attach a single file from the filesystem.""" + current_dir = os.path.dirname(os.path.abspath(__file__)) + attachments_dir = os.path.join(current_dir, "..", "attachments") + + # Attach a text file + qase.attach(os.path.join(attachments_dir, "file.txt")) + + assert True, "File attached successfully" + + +def test_attach_multiple_files(): + """Attach multiple files at once.""" + current_dir = os.path.dirname(os.path.abspath(__file__)) + attachments_dir = os.path.join(current_dir, "..", "attachments") + + # Attach multiple files in a single call + qase.attach( + os.path.join(attachments_dir, "file.txt"), + os.path.join(attachments_dir, "image.png") + ) + + assert True, "Multiple files attached successfully" + + +def test_attach_file_with_mime_type(): + """Attach a file with explicit MIME type specification.""" + current_dir = os.path.dirname(os.path.abspath(__file__)) + attachments_dir = os.path.join(current_dir, "..", "attachments") + + # Specify MIME type as tuple (filepath, mime_type) + qase.attach((os.path.join(attachments_dir, "file.txt"), "text/plain")) + + assert True, "File with MIME type attached successfully" + + +# ============================================================================ +# Byte data attachments +# ============================================================================ + +def test_attach_text_content(): + """Attach text content directly as bytes.""" + log_content = """ + [2024-01-15 10:30:00] INFO: Test started + [2024-01-15 10:30:01] DEBUG: Connecting to database + [2024-01-15 10:30:02] INFO: Database connection established + [2024-01-15 10:30:03] DEBUG: Executing query + [2024-01-15 10:30:04] INFO: Test completed successfully + """ + + # Attach bytes with MIME type and filename: (bytes, mime_type, filename) + qase.attach((log_content.encode("utf-8"), "text/plain", "test_execution.log")) + + assert True, "Text content attached as bytes" + + +def test_attach_json_data(): + """Attach JSON data from a dictionary.""" + api_response = { + "status": "success", + "code": 200, + "data": { + "user_id": 12345, + "username": "testuser", + "email": "test@example.com", + "roles": ["user", "admin"] + }, + "metadata": { + "request_id": "req-abc-123", + "timestamp": "2024-01-15T10:30:00Z" + } + } + + json_bytes = json.dumps(api_response, indent=2).encode("utf-8") + qase.attach((json_bytes, "application/json", "api_response.json")) + + assert api_response["status"] == "success" + + +def test_attach_csv_report(): + """Attach CSV data as a report.""" + csv_content = """id,name,status,duration_ms +1,test_login,passed,120 +2,test_logout,passed,85 +3,test_profile,failed,250 +4,test_settings,passed,95 +5,test_dashboard,skipped,0 +""" + + qase.attach((csv_content.encode("utf-8"), "text/csv", "test_report.csv")) + + assert True, "CSV report attached" + + +def test_attach_html_report(): + """Attach HTML content as a report.""" + html_content = """ + + + Test Report + +

Test Execution Summary

+ + + + + +
TestStatusDuration
Login TestPASSED1.2s
Checkout TestPASSED3.5s
Search TestFAILED2.1s
+

Total: 3 tests, 2 passed, 1 failed

+ + + """ + + qase.attach((html_content.encode("utf-8"), "text/html", "summary_report.html")) + + assert True, "HTML report attached" + + +# ============================================================================ +# Dynamic content attachments +# ============================================================================ + +def test_attach_error_screenshot_simulation(): + """Simulate attaching a screenshot when an error occurs.""" + try: + # Simulated operation that might fail + result = perform_risky_operation() + assert result["success"], "Operation should succeed" + + except AssertionError: + # In real scenario, capture screenshot here + error_info = """ + ERROR DETAILS + ============= + Operation: perform_risky_operation + Error: AssertionError - Operation should succeed + Timestamp: 2024-01-15T10:30:00Z + + Stack trace: + File "test_attachments.py", line XX, in test_attach_error_screenshot_simulation + assert result["success"], "Operation should succeed" + """ + qase.attach((error_info.encode("utf-8"), "text/plain", "error_details.txt")) + raise + + +def perform_risky_operation(): + """Helper function that simulates an operation.""" + return {"success": True, "data": "result"} + + +def test_attach_performance_metrics(): + """Attach performance metrics collected during test.""" + # Simulate collecting performance data + metrics = { + "test_name": "API Response Time Test", + "iterations": 100, + "results": { + "min_ms": 45, + "max_ms": 320, + "avg_ms": 85, + "p50_ms": 78, + "p90_ms": 150, + "p99_ms": 280 + }, + "passed_threshold": True, + "threshold_ms": 200 + } + + # Attach as JSON for structured data + qase.attach(( + json.dumps(metrics, indent=2).encode("utf-8"), + "application/json", + "performance_metrics.json" + )) + + assert metrics["passed_threshold"], "Performance should be within threshold" + + +# ============================================================================ +# Attachments within steps +# ============================================================================ + +@qase.step("Execute API request and capture response") +def execute_api_request(endpoint: str) -> dict: + """Step that executes API request and attaches the response.""" + # Simulated API request + response = { + "endpoint": endpoint, + "method": "GET", + "status_code": 200, + "headers": { + "content-type": "application/json", + "x-request-id": "req-123" + }, + "body": { + "data": [{"id": 1}, {"id": 2}] + } + } + + # Attach request/response details + qase.attach(( + json.dumps(response, indent=2).encode("utf-8"), + "application/json", + f"api_response_{endpoint.replace('/', '_')}.json" + )) + + return response + + +@qase.step("Validate response data") +def validate_response(response: dict, expected_status: int) -> bool: + """Step that validates response and attaches validation report.""" + validation_results = { + "checks": [ + {"name": "status_code", "expected": expected_status, "actual": response["status_code"], "passed": response["status_code"] == expected_status}, + {"name": "has_body", "expected": True, "actual": "body" in response, "passed": "body" in response}, + {"name": "has_data", "expected": True, "actual": "data" in response.get("body", {}), "passed": "data" in response.get("body", {})} + ], + "all_passed": True + } + + validation_results["all_passed"] = all(c["passed"] for c in validation_results["checks"]) + + qase.attach(( + json.dumps(validation_results, indent=2).encode("utf-8"), + "application/json", + "validation_report.json" + )) + + return validation_results["all_passed"] + + +def test_api_flow_with_step_attachments(): + """Test demonstrating attachments within steps.""" + # Step 1: Make API request (attaches response) + response = execute_api_request("/api/users") + assert response["status_code"] == 200 + + # Step 2: Validate response (attaches validation report) + is_valid = validate_response(response, 200) + assert is_valid, "Response validation should pass" + + +# ============================================================================ +# Temporary file attachments +# ============================================================================ + +def test_attach_generated_file(): + """Generate a file during test and attach it.""" + # Create a temporary file with test results + with tempfile.NamedTemporaryFile(mode='w', suffix='.txt', delete=False) as f: + f.write("Generated Test Results\n") + f.write("=" * 40 + "\n") + f.write("Test: test_attach_generated_file\n") + f.write("Status: PASSED\n") + f.write("Duration: 0.5s\n") + temp_path = f.name + + try: + # Attach the generated file + qase.attach((temp_path, "text/plain")) + assert True, "Generated file attached" + finally: + # Clean up + os.unlink(temp_path) + + +# ============================================================================ +# Practical scenarios +# ============================================================================ + +@qase.id(50) +@qase.title("Database query execution with result attachment") +@qase.severity("normal") +def test_database_query_with_attachment(): + """Practical example: attach database query results.""" + # Simulated database query + query = "SELECT id, name, email FROM users WHERE status = 'active' LIMIT 10" + results = [ + {"id": 1, "name": "Alice", "email": "alice@example.com"}, + {"id": 2, "name": "Bob", "email": "bob@example.com"}, + {"id": 3, "name": "Charlie", "email": "charlie@example.com"}, + ] + + # Attach query for reference + qase.attach((query.encode("utf-8"), "text/plain", "executed_query.sql")) + + # Attach results as JSON + qase.attach(( + json.dumps(results, indent=2).encode("utf-8"), + "application/json", + "query_results.json" + )) + + assert len(results) > 0, "Query should return results" + + +@qase.id(51) +@qase.title("Configuration validation with config attachment") +def test_config_validation_with_attachment(): + """Practical example: validate and attach configuration.""" + config = { + "app": { + "name": "TestApp", + "version": "1.0.0", + "environment": "staging" + }, + "database": { + "host": "db.example.com", + "port": 5432, + "pool_size": 10 + }, + "features": { + "dark_mode": True, + "beta_features": False + } + } + + # Attach the configuration being tested + qase.attach(( + json.dumps(config, indent=2).encode("utf-8"), + "application/json", + "test_config.json" + )) + + # Validate configuration + assert config["app"]["name"], "App name must be set" + assert config["database"]["port"] > 0, "Database port must be positive" + assert "environment" in config["app"], "Environment must be specified" diff --git a/examples/single/pytest/tests/test_basic.py b/examples/single/pytest/tests/test_basic.py new file mode 100644 index 00000000..905e468f --- /dev/null +++ b/examples/single/pytest/tests/test_basic.py @@ -0,0 +1,352 @@ +""" +Basic examples demonstrating Qase Pytest Reporter decorators. + +This module shows how to use various decorators to enrich test metadata +that will be reported to Qase TestOps. +""" +from qase.pytest import qase + + +# ============================================================================ +# @qase.id - Link test to existing Qase test case +# ============================================================================ + +@qase.id(1) +def test_user_login_with_valid_credentials(): + """Test linked to Qase test case #1.""" + username = "admin" + password = "secure_password" + + # Simulate authentication + is_authenticated = len(username) > 0 and len(password) >= 8 + assert is_authenticated, "User should be authenticated with valid credentials" + + +@qase.id([2, 3]) +def test_user_permissions_check(): + """Test linked to multiple Qase test cases #2 and #3.""" + user_role = "admin" + required_permissions = ["read", "write", "delete"] + + # Admin has all permissions + user_permissions = ["read", "write", "delete"] if user_role == "admin" else ["read"] + + for permission in required_permissions: + assert permission in user_permissions, f"Missing permission: {permission}" + + +# ============================================================================ +# @qase.title - Custom test title in Qase +# ============================================================================ + +@qase.title("Verify shopping cart total calculation") +def test_cart_total(): + """Demonstrate custom title that appears in Qase instead of function name.""" + items = [ + {"name": "Laptop", "price": 999.99, "quantity": 1}, + {"name": "Mouse", "price": 29.99, "quantity": 2}, + {"name": "Keyboard", "price": 79.99, "quantity": 1}, + ] + + expected_total = 999.99 + (29.99 * 2) + 79.99 + actual_total = sum(item["price"] * item["quantity"] for item in items) + + assert abs(actual_total - expected_total) < 0.01, "Cart total should be calculated correctly" + + +# ============================================================================ +# @qase.description - Detailed test description +# ============================================================================ + +@qase.description(""" +This test verifies the password validation rules: +- Minimum 8 characters +- At least one uppercase letter +- At least one lowercase letter +- At least one digit +- At least one special character +""") +def test_password_validation(): + """Test with detailed description shown in Qase.""" + def validate_password(password: str) -> bool: + if len(password) < 8: + return False + has_upper = any(c.isupper() for c in password) + has_lower = any(c.islower() for c in password) + has_digit = any(c.isdigit() for c in password) + has_special = any(c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in password) + return has_upper and has_lower and has_digit and has_special + + valid_password = "SecureP@ss1" + assert validate_password(valid_password), "Password should meet all requirements" + + +# ============================================================================ +# @qase.preconditions / @qase.postconditions - Test conditions +# ============================================================================ + +@qase.preconditions(""" +**Prerequisites:** +- User account must exist in the system +- User must have verified email address +- Account must not be locked +""") +@qase.postconditions(""" +**After test completion:** +- User session token should be valid +- Last login timestamp should be updated +- Failed login counter should be reset to 0 +""") +def test_successful_login_flow(): + """Test with pre/postconditions documented in Qase.""" + user = { + "email": "user@example.com", + "email_verified": True, + "account_locked": False, + } + + # Check preconditions + assert user["email_verified"], "Email must be verified" + assert not user["account_locked"], "Account must not be locked" + + # Simulate login + session = {"token": "abc123", "valid": True} + login_timestamp = "2024-01-15T10:30:00Z" + failed_attempts = 0 + + # Verify postconditions + assert session["valid"], "Session should be valid" + assert login_timestamp is not None, "Login timestamp should be set" + assert failed_attempts == 0, "Failed attempts should be reset" + + +# ============================================================================ +# @qase.severity - Test severity level +# ============================================================================ + +@qase.severity("blocker") +def test_payment_processing(): + """Blocker severity - payment system must work.""" + payment = {"amount": 100.00, "currency": "USD", "status": "pending"} + + # Simulate payment processing + payment["status"] = "completed" + + assert payment["status"] == "completed", "Payment must be processed successfully" + + +@qase.severity("critical") +def test_user_data_encryption(): + """Critical severity - sensitive data must be encrypted.""" + sensitive_data = "user_password_123" + + # Simulate encryption (in real code, use proper encryption) + encrypted = sensitive_data[::-1] # Simple reversal for demo + + assert encrypted != sensitive_data, "Data must be encrypted" + + +@qase.severity("major") +def test_search_functionality(): + """Major severity - core feature but not critical.""" + products = ["Laptop", "Desktop", "Tablet", "Phone"] + query = "lap" + + results = [p for p in products if query.lower() in p.lower()] + + assert len(results) > 0, "Search should return matching results" + + +@qase.severity("normal") +def test_pagination(): + """Normal severity - standard functionality.""" + total_items = 100 + page_size = 10 + current_page = 3 + + start_index = (current_page - 1) * page_size + end_index = start_index + page_size + + assert start_index == 20, "Pagination start index should be correct" + assert end_index == 30, "Pagination end index should be correct" + + +@qase.severity("minor") +def test_ui_theme_switching(): + """Minor severity - nice to have feature.""" + current_theme = "light" + + new_theme = "dark" if current_theme == "light" else "light" + + assert new_theme == "dark", "Theme should switch correctly" + + +@qase.severity("trivial") +def test_tooltip_display(): + """Trivial severity - cosmetic feature.""" + element = {"has_tooltip": True, "tooltip_text": "Click to submit"} + + assert element["has_tooltip"], "Tooltip should be displayed" + + +# ============================================================================ +# @qase.priority - Test priority level +# ============================================================================ + +@qase.priority("high") +def test_authentication_service(): + """High priority test that should run first.""" + service_status = "healthy" + assert service_status == "healthy", "Auth service must be healthy" + + +@qase.priority("medium") +def test_notification_delivery(): + """Medium priority - important but can wait.""" + notification = {"type": "email", "sent": True} + assert notification["sent"], "Notification should be delivered" + + +@qase.priority("low") +def test_analytics_tracking(): + """Low priority - can be addressed later.""" + event = {"name": "page_view", "tracked": True} + assert event["tracked"], "Analytics event should be tracked" + + +# ============================================================================ +# @qase.layer - Test layer categorization +# ============================================================================ + +@qase.layer("unit") +def test_string_formatter(): + """Unit test - testing a single function.""" + def format_currency(amount: float, currency: str = "USD") -> str: + symbols = {"USD": "$", "EUR": "€", "GBP": "£"} + symbol = symbols.get(currency, currency) + return f"{symbol}{amount:.2f}" + + assert format_currency(99.5) == "$99.50" + assert format_currency(150, "EUR") == "€150.00" + + +@qase.layer("api") +def test_api_response_structure(): + """API test - testing API response format.""" + response = { + "status": "success", + "data": {"id": 1, "name": "Test"}, + "meta": {"page": 1, "total": 100} + } + + assert "status" in response + assert "data" in response + assert response["status"] == "success" + + +@qase.layer("e2e") +def test_user_registration_flow(): + """E2E test - testing complete user flow.""" + # Step 1: Fill registration form + form_data = {"email": "new@example.com", "password": "SecureP@ss1"} + + # Step 2: Submit form + submission_result = "success" + + # Step 3: Verify email sent + email_sent = True + + # Step 4: Confirm email + email_confirmed = True + + # Step 5: User can login + login_successful = True + + assert all([ + submission_result == "success", + email_sent, + email_confirmed, + login_successful + ]), "Complete registration flow should work" + + +# ============================================================================ +# @qase.fields - Multiple fields at once +# ============================================================================ + +@qase.id(10) +@qase.fields( + ("severity", "critical"), + ("priority", "high"), + ("layer", "e2e"), + ("description", "Complete checkout flow including cart, payment, and confirmation"), + ("preconditions", "User must be logged in with items in cart"), + ("postconditions", "Order should be created and confirmation email sent"), +) +def test_complete_checkout_flow(): + """Test with all metadata fields set using @qase.fields decorator.""" + # Cart validation + cart = {"items": [{"id": 1, "qty": 2}], "total": 199.98} + assert len(cart["items"]) > 0, "Cart should have items" + + # Payment processing + payment_result = {"status": "approved", "transaction_id": "TXN123"} + assert payment_result["status"] == "approved", "Payment should be approved" + + # Order creation + order = {"id": "ORD-001", "status": "confirmed"} + assert order["status"] == "confirmed", "Order should be confirmed" + + # Email confirmation + email_sent = True + assert email_sent, "Confirmation email should be sent" + + +# ============================================================================ +# @qase.ignore - Exclude test from Qase reporting +# ============================================================================ + +@qase.ignore() +def test_local_development_only(): + """This test is excluded from Qase reporting. + + Useful for tests that are: + - Only for local development + - Work in progress + - Environment-specific debugging + """ + debug_mode = True + assert debug_mode, "This test runs locally but won't be reported to Qase" + + +# ============================================================================ +# @qase.suite - Organize tests into suites +# ============================================================================ + +@qase.suite("Authentication") +def test_login_with_remember_me(): + """Test organized under 'Authentication' suite.""" + remember_me = True + session_duration = 30 if remember_me else 1 # days + + assert session_duration == 30, "Remember me should extend session" + + +@qase.suite("Authentication.OAuth") +def test_google_oauth_login(): + """Test organized under 'Authentication > OAuth' nested suite.""" + oauth_provider = "google" + oauth_token = "valid_token" + + assert oauth_provider == "google" + assert oauth_token is not None + + +@qase.suite("Authentication.OAuth.Error Handling") +def test_oauth_token_expiration(): + """Test organized under deeply nested suite structure.""" + token_expired = True + refresh_successful = True + + if token_expired: + assert refresh_successful, "Token refresh should work" diff --git a/examples/single/pytest/tests/test_steps.py b/examples/single/pytest/tests/test_steps.py new file mode 100644 index 00000000..e245d3f7 --- /dev/null +++ b/examples/single/pytest/tests/test_steps.py @@ -0,0 +1,300 @@ +""" +Step tracking examples demonstrating Qase Pytest Reporter step functionality. + +Steps are used to break down test execution into logical phases, +making test results more readable and easier to debug in Qase. +""" +from qase.pytest import qase + + +# ============================================================================ +# @qase.step decorator - Reusable step functions +# ============================================================================ + +@qase.step("Open the application home page") +def open_home_page(): + """Simulates opening the application.""" + app_loaded = True + return {"url": "https://app.example.com", "loaded": app_loaded} + + +@qase.step("Enter login credentials") +def enter_credentials(username: str, password: str): + """Simulates entering login credentials.""" + return {"username": username, "password_length": len(password)} + + +@qase.step("Click the login button") +def click_login_button(): + """Simulates clicking the login button.""" + return {"clicked": True, "timestamp": "2024-01-15T10:30:00Z"} + + +@qase.step("Verify user dashboard is displayed") +def verify_dashboard(expected_user: str): + """Simulates verifying the dashboard.""" + dashboard = { + "visible": True, + "username_displayed": expected_user, + "widgets_loaded": 5 + } + assert dashboard["visible"], "Dashboard should be visible" + return dashboard + + +def test_login_flow_with_step_decorator(): + """Demonstrates using @qase.step decorator for reusable steps.""" + # Step 1: Open home page + home = open_home_page() + assert home["loaded"], "Home page should load" + + # Step 2: Enter credentials + creds = enter_credentials("admin@example.com", "SecurePass123!") + assert creds["username"] == "admin@example.com" + + # Step 3: Click login + click_result = click_login_button() + assert click_result["clicked"], "Login button should be clicked" + + # Step 4: Verify dashboard + dashboard = verify_dashboard("admin@example.com") + assert dashboard["widgets_loaded"] > 0, "Dashboard should have widgets" + + +# ============================================================================ +# with qase.step() context manager - Inline steps +# ============================================================================ + +def test_checkout_flow_with_context_manager(): + """Demonstrates using 'with qase.step()' for inline step definitions.""" + cart_items = [] + order = {} + + with qase.step("Add products to shopping cart"): + cart_items.append({"id": 1, "name": "Wireless Mouse", "price": 29.99}) + cart_items.append({"id": 2, "name": "USB-C Hub", "price": 49.99}) + assert len(cart_items) == 2, "Cart should have 2 items" + + with qase.step("Apply discount coupon"): + coupon = "SAVE10" + discount_percent = 10 + subtotal = sum(item["price"] for item in cart_items) + discount_amount = subtotal * (discount_percent / 100) + total = subtotal - discount_amount + assert discount_amount > 0, "Discount should be applied" + + with qase.step("Enter shipping information"): + shipping = { + "name": "John Doe", + "address": "123 Main St", + "city": "San Francisco", + "zip": "94105", + "country": "USA" + } + shipping_valid = all(shipping.values()) + assert shipping_valid, "All shipping fields should be filled" + + with qase.step("Process payment"): + payment = { + "method": "credit_card", + "card_last_four": "4242", + "amount": total, + "status": "approved" + } + assert payment["status"] == "approved", "Payment should be approved" + + with qase.step("Confirm order creation"): + order = { + "id": "ORD-2024-001", + "items": cart_items, + "shipping": shipping, + "payment": payment, + "status": "confirmed" + } + assert order["status"] == "confirmed", "Order should be confirmed" + + +# ============================================================================ +# Nested steps - Steps within steps +# ============================================================================ + +@qase.step("Validate user registration form") +def validate_registration_form(form_data: dict): + """Parent step containing nested validation steps.""" + + @qase.step("Validate email format") + def validate_email(email: str) -> bool: + return "@" in email and "." in email.split("@")[1] + + @qase.step("Validate password strength") + def validate_password(password: str) -> dict: + return { + "length_ok": len(password) >= 8, + "has_upper": any(c.isupper() for c in password), + "has_lower": any(c.islower() for c in password), + "has_digit": any(c.isdigit() for c in password), + } + + @qase.step("Validate required fields") + def validate_required(data: dict, required: list) -> bool: + return all(data.get(field) for field in required) + + email_valid = validate_email(form_data.get("email", "")) + password_check = validate_password(form_data.get("password", "")) + required_valid = validate_required(form_data, ["email", "password", "name"]) + + return { + "email_valid": email_valid, + "password_valid": all(password_check.values()), + "required_valid": required_valid, + "overall_valid": email_valid and all(password_check.values()) and required_valid + } + + +def test_registration_with_nested_steps(): + """Demonstrates nested steps - steps that contain other steps.""" + form_data = { + "email": "newuser@example.com", + "password": "SecureP@ss123", + "name": "John Doe" + } + + with qase.step("User fills registration form"): + # Form data is already prepared above + assert form_data["email"], "Email should be filled" + + validation = validate_registration_form(form_data) + assert validation["overall_valid"], "Form should be valid" + + with qase.step("Submit registration"): + submission_result = {"success": True, "user_id": 12345} + assert submission_result["success"], "Registration should succeed" + + +# ============================================================================ +# Steps with expected results and data +# ============================================================================ + +@qase.step("Fetch user profile from API") +def fetch_user_profile(user_id: int) -> dict: + """Step that returns data for verification.""" + # Simulated API response + return { + "id": user_id, + "name": "Jane Smith", + "email": "jane.smith@example.com", + "role": "admin", + "created_at": "2023-01-15", + "last_login": "2024-01-14" + } + + +@qase.step("Update user profile") +def update_user_profile(user_id: int, updates: dict) -> dict: + """Step that performs an update operation.""" + # Simulated update result + return { + "success": True, + "updated_fields": list(updates.keys()), + "new_values": updates + } + + +@qase.step("Verify profile changes") +def verify_profile_changes(original: dict, updated: dict, expected_changes: dict) -> bool: + """Step that verifies expected changes were applied.""" + for field, expected_value in expected_changes.items(): + if updated.get(field) != expected_value: + return False + return True + + +def test_user_profile_update_flow(): + """Demonstrates steps with data flow and verification.""" + user_id = 42 + + # Step 1: Get current profile + original_profile = fetch_user_profile(user_id) + assert original_profile["id"] == user_id + + # Step 2: Prepare and apply updates + profile_updates = { + "name": "Jane Doe", + "role": "super_admin" + } + update_result = update_user_profile(user_id, profile_updates) + assert update_result["success"], "Update should succeed" + + # Step 3: Fetch updated profile + with qase.step("Fetch updated profile"): + updated_profile = {**original_profile, **profile_updates} + + # Step 4: Verify changes + changes_verified = verify_profile_changes( + original_profile, + updated_profile, + profile_updates + ) + assert changes_verified, "All changes should be verified" + + +# ============================================================================ +# Combined patterns - Complex test with mixed step styles +# ============================================================================ + +@qase.step("Initialize test database") +def init_test_database(): + """Setup step for database.""" + return {"connection": "active", "tables_created": True} + + +@qase.step("Create test user") +def create_test_user(db, user_data: dict): + """Step to create a user in database.""" + return {"id": 1, **user_data, "created": True} + + +@qase.step("Clean up test data") +def cleanup_test_data(db, user_id: int): + """Teardown step to clean up.""" + return {"deleted": True, "user_id": user_id} + + +@qase.id(100) +@qase.title("Complete E2E user management flow") +@qase.severity("critical") +def test_e2e_user_management(): + """Complex test demonstrating all step patterns combined.""" + user_id = None + + # Setup phase + db = init_test_database() + assert db["connection"] == "active" + + try: + # Create phase + user = create_test_user(db, { + "name": "Test User", + "email": "test@example.com" + }) + user_id = user["id"] + assert user["created"] + + # Verification phase with inline steps + with qase.step("Verify user exists in database"): + user_exists = True # Simulated check + assert user_exists, "User should exist" + + with qase.step("Verify user can authenticate"): + auth_result = {"success": True, "token": "xyz123"} + assert auth_result["success"], "User should be able to authenticate" + + with qase.step("Verify user permissions"): + permissions = ["read", "write"] + assert "read" in permissions, "User should have read permission" + + finally: + # Cleanup phase + if user_id: + cleanup_result = cleanup_test_data(db, user_id) + assert cleanup_result["deleted"] diff --git a/examples/single/robot/Readme.md b/examples/single/robot/Readme.md index 847240da..acb566d9 100644 --- a/examples/single/robot/Readme.md +++ b/examples/single/robot/Readme.md @@ -1,33 +1,94 @@ -# How to run these examples +# Robot Framework Examples -1. Clone the repository +Examples demonstrating Qase Robot Framework Reporter features. - ```bash - git clone https://github.com/qase-tms/qase-python.git - ``` +## Setup -2. Move to the directory with the examples +1. Install dependencies: + ```bash + pip install -r requirements.txt + ``` - ```bash - cd qase-python/examples/single/robot - ``` +2. Configure credentials: + ```bash + export QASE_MODE=testops + export QASE_TESTOPS_API_TOKEN=your_token + export QASE_TESTOPS_PROJECT=your_project_code + ``` -3. Install the required packages + Or edit `qase.config.json` and replace `` and ``. - ```bash - pip install -r requirements.txt - ``` +## Run Tests -4. Add the Qase token and project code to the ENV variables +```bash +# Run all tests +robot --listener qase.robotframework.Listener tests/ - ```bash - export QASE_MODE=testops - export QASE_TESTOPS_API_TOKEN=your_token - export QASE_TESTOPS_PROJECT=your_project_code - ``` +# Run specific test file +robot --listener qase.robotframework.Listener tests/simple.robot +``` -5. Run the tests +## Examples - ```bash - robot --listener qase.robotframework.Listener . - ``` +| File | Description | +|------|-------------| +| `simple.robot` | Basic tests with `Q-ID` tags, steps, and assertions | +| `parametrized.robot` | Data-driven tests with test templates | + +## Code Examples + +### Link to Test Case + +```robotframework +*** Test Cases *** +Test With Qase ID + [Tags] Q-1 + Log This test is linked to test case 1 + Should Be Equal 1 1 + +Test With Multiple IDs + [Tags] Q-1 Q-2 Q-3 + Log Linked to multiple test cases +``` + +### Parametrized Tests + +```robotframework +*** Test Cases *** Expression Expected +Addition 12 + 2 + 2 16 + [Tags] Q-10 + 2 + -3 -1 + +Subtraction 12 - 2 - 2 8 + [Tags] Q-11 + 2 - -3 5 +``` + +### Working with Steps + +Each keyword call is automatically reported as a step: + +```robotframework +*** Test Cases *** +Test With Steps + [Tags] Q-5 + Step One # Reported as step 1 + Step Two # Reported as step 2 + Verify Result # Reported as step 3 + +*** Keywords *** +Step One + Log Executing step one + +Step Two + Log Executing step two + +Verify Result + Should Be True ${TRUE} +``` + +## Documentation + +- [Robot Framework Reporter README](../../../qase-robotframework/README.md) +- [Usage Guide](../../../qase-robotframework/docs/usage.md) +- [Configuration Reference](../../../qase-python-commons/README.md) diff --git a/examples/single/robot/tests/parametrized.robot b/examples/single/robot/tests/parametrized.robot index bc76102a..71950875 100644 --- a/examples/single/robot/tests/parametrized.robot +++ b/examples/single/robot/tests/parametrized.robot @@ -1,25 +1,178 @@ *** Settings *** -Library steps.py +Documentation Examples demonstrating data-driven testing with Qase Robot Framework Reporter. +... Shows test templates and parameter tracking in Qase. +Library Collections *** Variables *** -${var1} 1 -${var2} 1 -${var3} 2 +# User credentials for different roles +@{VIEWER_ACCESS} dashboard profile +@{EDITOR_ACCESS} dashboard profile content +@{ADMIN_ACCESS} dashboard profile content settings users *** Test Cases *** -Parametrized Test success - [Tags] qase.params:[var1, var2] - Check numbers ${var1} ${var2} ${var3} - Passed Step -Parametrized Test failed - [Tags] qase.params:[var1, var2] - Check numbers ${var1} ${var2} ${var3} - Failed Step +# ============================================================================== +# Data-driven testing with Test Template +# ============================================================================== +Login With Different User Roles + [Documentation] Verify different user roles have appropriate access + [Tags] Q-40 + [Template] Verify User Role Access + viewer viewer@example.com @{VIEWER_ACCESS} + editor editor@example.com @{EDITOR_ACCESS} + admin admin@example.com @{ADMIN_ACCESS} + +Email Validation Test Cases + [Documentation] Test email validation with various inputs + [Tags] Q-41 qase.fields:{"layer":"unit"} + [Template] Validate Email Format + # Valid emails + user@example.com valid Email is valid + test.user@mail.org valid Email is valid + admin+tag@site.net valid Email is valid + # Invalid emails + invalid invalid Missing @ symbol + @nodomain.com invalid Missing local part + user@ invalid Missing domain + +HTTP Status Code Validation + [Documentation] Verify API endpoints return correct status codes + [Tags] Q-42 qase.fields:{"layer":"api"} + [Template] Verify API Response + /api/users GET 200 + /api/posts GET 200 + /api/invalid GET 404 + /api/users POST 201 + /api/protected GET 401 + +Discount Calculation Tests + [Documentation] Verify discount calculations for different types + [Tags] Q-43 qase.fields:{"severity":"critical"} + [Template] Calculate And Verify Discount + # Percentage discounts + 100.00 percentage 10 90.00 + 250.00 percentage 20 200.00 + 500.00 percentage 50 250.00 + # Fixed amount discounts + 100.00 fixed 15 85.00 + 250.00 fixed 50 200.00 + 500.00 fixed 100 400.00 + +Password Strength Validation + [Documentation] Test password validation with boundary values + [Tags] Q-44 + [Template] Validate Password Strength + # Too short + abc invalid Password must be at least 8 characters + abcdefg invalid Password must be at least 8 characters + # Valid length + abcdefgh valid Password meets requirements + Secure@Pass1 valid Password meets requirements + # Edge cases + 12345678 weak Password needs mixed characters + ABCDEFGH weak Password needs mixed characters + +Currency Conversion Tests + [Documentation] Test currency conversion calculations + [Tags] Q-45 qase.fields:{"layer":"unit"} + [Template] Convert And Verify Currency + 100.00 USD EUR 0.85 85.00 + 100.00 USD GBP 0.73 73.00 + 100.00 EUR USD 1.18 118.00 + 50.00 GBP EUR 1.16 58.00 + +# ============================================================================== +# Test with variables tracking +# ============================================================================== + +Shopping Cart Total With Tax + [Documentation] Calculate cart total with tax for different states + [Tags] Q-46 qase.params:[state, tax_rate] + [Template] Calculate Cart Total With Tax + CA 0.0725 107.25 + NY 0.08 108.00 + TX 0.0625 106.25 + OR 0.00 100.00 *** Keywords *** -Check numbers - [Arguments] ${var1} ${var2} ${var3} - Should Be Equal As Numbers ${var1} ${var2} - Should Be Equal As Numbers ${var3} ${var3} + +Verify User Role Access + [Arguments] ${role} ${email} @{expected_pages} + Log Testing user role: ${role} (${email}) + ${user}= Create Dictionary role=${role} email=${email} + Set Test Variable ${USER} ${user} + FOR ${page} IN @{expected_pages} + Log Verifying access to: ${page} + Should Not Be Empty ${page} + END + ${page_count}= Get Length ${expected_pages} + Log User has access to ${page_count} pages + +Validate Email Format + [Arguments] ${email} ${expected_result} ${expected_message} + Log Validating email: ${email} + ${is_valid}= Run Keyword And Return Status + ... Should Match Regexp ${email} ^[^@]+@[^@]+\\.[^@]+$ + ${result}= Set Variable If ${is_valid} valid invalid + ${message}= Set Variable If ${is_valid} Email is valid + ... ELSE IF '@' not in '${email}' Missing @ symbol + ... ELSE IF '${email}'.startswith('@') Missing local part + ... ELSE Missing domain + Should Be Equal ${result} ${expected_result} + +Verify API Response + [Arguments] ${endpoint} ${method} ${expected_status} + Log ${method} request to ${endpoint} + # Simulate API response based on endpoint + ${status}= Set Variable If + ... '${endpoint}' == '/api/invalid' 404 + ... '${endpoint}' == '/api/protected' 401 + ... '${method}' == 'POST' 201 + ... 200 + Should Be Equal As Numbers ${status} ${expected_status} + +Calculate And Verify Discount + [Arguments] ${base_price} ${discount_type} ${discount_value} ${expected_final} + Log Base price: $${base_price}, Discount: ${discount_value} (${discount_type}) + ${final_price}= Run Keyword If '${discount_type}' == 'percentage' + ... Evaluate ${base_price} - (${base_price} * ${discount_value} / 100) + ... ELSE + ... Evaluate ${base_price} - ${discount_value} + ${diff}= Evaluate abs(${final_price} - ${expected_final}) + Should Be True ${diff} < 0.01 Expected $${expected_final}, got $${final_price} + +Validate Password Strength + [Arguments] ${password} ${expected_validity} ${expected_message} + Log Checking password strength + ${length}= Get Length ${password} + ${has_upper}= Run Keyword And Return Status + ... Should Match Regexp ${password} [A-Z] + ${has_lower}= Run Keyword And Return Status + ... Should Match Regexp ${password} [a-z] + ${has_digit}= Run Keyword And Return Status + ... Should Match Regexp ${password} [0-9] + ${has_special}= Run Keyword And Return Status + ... Should Match Regexp ${password} [!@#$%^&*] + # Determine validity + ${validity}= Set Variable If + ... ${length} < 8 invalid + ... not (${has_upper} and ${has_lower} and ${has_digit}) weak + ... valid + Should Be Equal ${validity} ${expected_validity} + +Convert And Verify Currency + [Arguments] ${amount} ${from_currency} ${to_currency} ${rate} ${expected} + Log Converting ${amount} ${from_currency} to ${to_currency} at rate ${rate} + ${result}= Evaluate ${amount} * ${rate} + ${diff}= Evaluate abs(${result} - ${expected}) + Should Be True ${diff} < 0.01 + +Calculate Cart Total With Tax + [Arguments] ${state} ${tax_rate} ${expected_total} + Log Calculating cart total for ${state} with tax rate ${tax_rate} + ${subtotal}= Set Variable 100.00 + ${tax}= Evaluate ${subtotal} * ${tax_rate} + ${total}= Evaluate ${subtotal} + ${tax} + ${diff}= Evaluate abs(${total} - ${expected_total}) + Should Be True ${diff} < 0.01 diff --git a/examples/single/robot/tests/simple.robot b/examples/single/robot/tests/simple.robot index 2842eb71..8e01857a 100644 --- a/examples/single/robot/tests/simple.robot +++ b/examples/single/robot/tests/simple.robot @@ -1,70 +1,294 @@ *** Settings *** -Library steps.py +Documentation Basic examples demonstrating Qase Robot Framework Reporter features. +... Shows test case linking, metadata fields, and step tracking. +Library Collections +Library String + +*** Variables *** +${BASE_URL} https://api.example.com +${VALID_EMAIL} user@example.com +${VALID_PASS} SecurePass123 *** Test Cases *** -Test without metadata success - Step 01 - Step 02 - Passed step +# ============================================================================== +# Q-ID tag - Link test to existing Qase test case +# ============================================================================== + +User Can Login With Valid Credentials + [Documentation] Verifies successful login flow with valid credentials + [Tags] Q-1 + Open Login Page + Enter Email ${VALID_EMAIL} + Enter Password ${VALID_PASS} + Click Login Button + Verify Dashboard Is Displayed + +User Cannot Login With Invalid Password + [Documentation] Verifies error handling for invalid password + [Tags] Q-2 + Open Login Page + Enter Email ${VALID_EMAIL} + Enter Password wrong_password + Click Login Button + Verify Error Message Is Displayed Invalid credentials + +Test Linked To Multiple Cases + [Documentation] Test linked to multiple Qase test cases + [Tags] Q-3 Q-4 Q-5 + Open Login Page + Verify Login Page Elements + +# ============================================================================== +# qase.fields tag - Add metadata to test case +# ============================================================================== + +Payment Processing Test + [Documentation] Critical test for payment processing + [Tags] Q-10 qase.fields:{"severity":"blocker","priority":"high"} + Initialize Payment + Enter Payment Details 4242424242424242 12/25 123 + Submit Payment + Verify Payment Status approved + +Cart Total Calculation + [Tags] qase.fields:{"severity":"critical","layer":"unit","description":"Verifies shopping cart calculations"} + Create Empty Cart + Add Item To Cart Laptop 999.99 1 + Add Item To Cart Mouse 29.99 2 + Add Item To Cart Keyboard 79.99 1 + Verify Cart Total 1139.96 + +# ============================================================================== +# qase.ignore tag - Exclude test from Qase reporting +# ============================================================================== + +Debug Test For Local Development + [Documentation] This test is excluded from Qase reporting + [Tags] qase.ignore + Log This test runs locally but won't be reported to Qase + Should Be True ${TRUE} + +# ============================================================================== +# Automatic step tracking - Each keyword is reported as a step +# ============================================================================== + +E2E User Registration Flow + [Documentation] Complete user registration flow with step tracking + [Tags] Q-20 qase.fields:{"layer":"e2e"} + # Each keyword below is automatically reported as a step in Qase + Open Registration Page + Fill Registration Form newuser@example.com SecureP@ss1 John Doe + Accept Terms And Conditions + Submit Registration + Verify Confirmation Email Sent + Activate Account + Verify Account Is Active + +API Response Validation + [Documentation] API test with multiple validation steps + [Tags] Q-21 qase.fields:{"layer":"api"} + Send GET Request /api/users + Verify Status Code 200 + Verify Response Contains users + Verify Response Is JSON + +# ============================================================================== +# Combined test with all features +# ============================================================================== + +Complete Checkout Flow + [Documentation] Full checkout flow demonstrating all reporter features + [Tags] Q-30 qase.fields:{"severity":"critical","priority":"high","layer":"e2e","description":"Complete e-commerce checkout flow"} + # Setup + Create User Session + Create Empty Cart + # Add products + Add Item To Cart Wireless Mouse 29.99 2 + Add Item To Cart USB-C Hub 49.99 1 + # Apply discount + Apply Coupon Code SAVE10 + Verify Discount Applied 10 + # Shipping + Enter Shipping Address John Doe 123 Main St San Francisco 94105 + Select Shipping Method express + # Payment + Enter Payment Details 4242424242424242 12/25 123 + Submit Payment + # Verification + Verify Order Created + Verify Confirmation Email Sent + +*** Keywords *** + +# ============================================================================== +# Authentication Keywords +# ============================================================================== + +Open Login Page + Log Opening login page + ${page}= Set Variable login + Should Not Be Empty ${page} + +Enter Email + [Arguments] ${email} + Log Entering email: ${email} + Should Match Regexp ${email} .*@.*\\..* + +Enter Password + [Arguments] ${password} + Log Entering password (hidden) + Should Not Be Empty ${password} + +Click Login Button + Log Clicking login button + +Verify Dashboard Is Displayed + Log Verifying dashboard is displayed + Should Be True ${TRUE} + +Verify Error Message Is Displayed + [Arguments] ${message} + Log Verifying error message: ${message} + Should Not Be Empty ${message} + +Verify Login Page Elements + Log Verifying login page elements exist + +# ============================================================================== +# Payment Keywords +# ============================================================================== + +Initialize Payment + Log Initializing payment system + +Enter Payment Details + [Arguments] ${card_number} ${expiry} ${cvv} + Log Entering payment details + ${card_length}= Get Length ${card_number} + Should Be Equal As Numbers ${card_length} 16 + +Submit Payment + Log Submitting payment + +Verify Payment Status + [Arguments] ${expected_status} + Log Verifying payment status: ${expected_status} + Should Be Equal ${expected_status} approved + +# ============================================================================== +# Cart Keywords +# ============================================================================== + +Create Empty Cart + ${cart}= Create Dictionary items=@{EMPTY} total=0 + Set Test Variable ${CART} ${cart} + Log Created empty cart + +Add Item To Cart + [Arguments] ${name} ${price} ${quantity} + Log Adding to cart: ${name} x ${quantity} @ $${price} + ${item}= Create Dictionary name=${name} price=${price} quantity=${quantity} + ${items}= Get From Dictionary ${CART} items + Append To List ${items} ${item} + ${total}= Get From Dictionary ${CART} total + ${item_total}= Evaluate ${price} * ${quantity} + ${new_total}= Evaluate ${total} + ${item_total} + Set To Dictionary ${CART} total=${new_total} + +Verify Cart Total + [Arguments] ${expected} + ${actual}= Get From Dictionary ${CART} total + ${diff}= Evaluate abs(${actual} - ${expected}) + Should Be True ${diff} < 0.01 + +Apply Coupon Code + [Arguments] ${code} + Log Applying coupon: ${code} + Set Test Variable ${COUPON} ${code} + +Verify Discount Applied + [Arguments] ${percent} + Log Verifying ${percent}% discount applied + Should Be True ${percent} > 0 + +# ============================================================================== +# Registration Keywords +# ============================================================================== + +Open Registration Page + Log Opening registration page -Test without metadata failed - Step 01 - Step 02 - Failed Step +Fill Registration Form + [Arguments] ${email} ${password} ${name} + Log Filling registration form for ${name} + Should Match Regexp ${email} .*@.*\\..* + ${pass_length}= Get Length ${password} + Should Be True ${pass_length} >= 8 +Accept Terms And Conditions + Log Accepting terms and conditions -Test with QaseID success - [Tags] Q-10 - Step 01 - Step 02 - Passed step +Submit Registration + Log Submitting registration +Verify Confirmation Email Sent + Log Verification email sent -Test with QaseID failed - [Tags] Q-11 - Step 01 - Step 02 - Failed Step +Activate Account + Log Activating account +Verify Account Is Active + Log Account is active -Ignored test success - [Tags] qase.ignore - Step 01 - Step 02 - Passed step +# ============================================================================== +# API Keywords +# ============================================================================== +Send GET Request + [Arguments] ${endpoint} + Log Sending GET request to ${endpoint} + ${response}= Create Dictionary status=200 body={"users":[]} + Set Test Variable ${RESPONSE} ${response} -Ignored test failed - [Tags] qase.ignore - Step 01 - Step 02 - Failed Step +Verify Status Code + [Arguments] ${expected} + ${actual}= Get From Dictionary ${RESPONSE} status + Should Be Equal As Numbers ${actual} ${expected} +Verify Response Contains + [Arguments] ${field} + ${body}= Get From Dictionary ${RESPONSE} body + Should Contain ${body} ${field} -Test with fields success - [Tags] qase.fields:{ "preconditions": "Create object", "description": "It is simple test" } - Step 01 - Step 02 - Passed step +Verify Response Is JSON + Log Response is valid JSON +# ============================================================================== +# Shipping Keywords +# ============================================================================== -Test with fields failed - [Tags] qase.fields:{ "preconditions": "Create object", "description": "It is simple test" } - Step 01 - Step 02 - Failed Step +Enter Shipping Address + [Arguments] ${name} ${street} ${city} ${zip} + Log Entering shipping address for ${name} + ${address}= Create Dictionary name=${name} street=${street} city=${city} zip=${zip} + Set Test Variable ${SHIPPING} ${address} +Select Shipping Method + [Arguments] ${method} + Log Selecting shipping method: ${method} + Set Test Variable ${SHIPPING_METHOD} ${method} -Test with all metadata success - [Tags] Q-12 qase.fields:{ "preconditions": "Create object", "description": "It is simple test" } - Step 01 - Step 02 - Passed step +# ============================================================================== +# Order Keywords +# ============================================================================== +Create User Session + Log Creating user session + ${session}= Create Dictionary user_id=1 token=abc123 + Set Test Variable ${SESSION} ${session} -Test with all metadata failed - [Tags] Q-13 qase.fields:{ "preconditions": "Create object", "description": "It is simple test" } - Step 01 - Step 02 - Failed Step +Verify Order Created + Log Order created successfully + ${order}= Create Dictionary id=ORD-001 status=confirmed + Set Test Variable ${ORDER} ${order} diff --git a/examples/single/tavern/Readme.md b/examples/single/tavern/Readme.md index 32367223..d7d60dbc 100644 --- a/examples/single/tavern/Readme.md +++ b/examples/single/tavern/Readme.md @@ -1,33 +1,130 @@ -# How to run these examples +# Tavern Examples -1. Clone the repository +Examples demonstrating Qase Tavern Reporter features. - ```bash - git clone https://github.com/qase-tms/qase-python.git - ``` +## Setup -2. Move to the directory with the examples +1. Install dependencies: + ```bash + pip install -r requirements.txt + ``` - ```bash - cd qase-python/examples/single/tavern - ``` +2. Configure credentials: + ```bash + export QASE_MODE=testops + export QASE_TESTOPS_API_TOKEN=your_token + export QASE_TESTOPS_PROJECT=your_project_code + ``` -3. Install the required packages + Or edit `qase.config.json` and replace `` and ``. - ```bash - pip install -r requirements.txt - ``` +## Run Tests -4. Add the Qase token and project code to the ENV variables +```bash +# Run all tests +pytest - ```bash - export QASE_MODE=testops - export QASE_TESTOPS_API_TOKEN=your_token - export QASE_TESTOPS_PROJECT=your_project_code - ``` +# Run specific test file +pytest test_simple.tavern.yaml -5. Run the tests +# Run with verbose output +pytest -v +``` - ```bash - pytest - ``` +## Examples + +| File | Description | +|------|-------------| +| `test_simple.tavern.yaml` | Basic API tests without Qase IDs (auto-created test cases) | +| `test_with_id.tavern.yaml` | API tests linked to existing Qase test cases | + +## Code Examples + +### Basic Test (Auto-create) + +```yaml +--- +test_name: Get user list + +stages: + - name: Request users + request: + url: https://jsonplaceholder.typicode.com/users + method: GET + response: + status_code: 200 +``` + +### Link to Test Case + +```yaml +--- +test_name: QaseID=1 Get specific user + +stages: + - name: Request user by ID + request: + url: https://jsonplaceholder.typicode.com/users/1 + method: GET + response: + status_code: 200 + json: + id: 1 +``` + +### Multiple Test Cases + +```yaml +--- +test_name: QaseID=1,2 Test linked to multiple cases + +stages: + - name: Execute request + request: + url: https://api.example.com/endpoint + method: GET + response: + status_code: 200 +``` + +### Stages as Steps + +Each stage in your Tavern test is automatically reported as a step in Qase: + +```yaml +--- +test_name: QaseID=5 Multi-step test + +stages: + - name: Step 1 - Create resource # Reported as step 1 + request: + url: https://api.example.com/resource + method: POST + json: + name: test + response: + status_code: 201 + save: + json: + resource_id: id + + - name: Step 2 - Verify resource # Reported as step 2 + request: + url: https://api.example.com/resource/{resource_id} + method: GET + response: + status_code: 200 + + - name: Step 3 - Delete resource # Reported as step 3 + request: + url: https://api.example.com/resource/{resource_id} + method: DELETE + response: + status_code: 204 +``` + +## Documentation + +- [Tavern Reporter README](../../../qase-tavern/README.md) +- [Usage Guide](../../../qase-tavern/docs/usage.md) +- [Configuration Reference](../../../qase-python-commons/README.md) diff --git a/examples/single/tavern/test_simple.tavern.yaml b/examples/single/tavern/test_simple.tavern.yaml index daf81789..e6f53d2d 100644 --- a/examples/single/tavern/test_simple.tavern.yaml +++ b/examples/single/tavern/test_simple.tavern.yaml @@ -1,80 +1,141 @@ +# ============================================================================== +# Basic API Tests (auto-created test cases in Qase) +# ============================================================================== +# Tests without QaseID in the name will automatically create new test cases in Qase + --- -test_name: Simple test success +test_name: Get list of users from API stages: - - name: Step 1 + - name: Fetch all users request: - url: https://jsonplaceholder.typicode.com/posts/1 + url: https://jsonplaceholder.typicode.com/users method: GET response: status_code: 200 + headers: + content-type: application/json; charset=utf-8 json: - id: 1 - userId: 1 - title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit" - body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" + # Verify response is an array with expected structure + - id: 1 + name: Leanne Graham + username: Bret + email: Sincere@april.biz - - name: Step 2 +--- +test_name: Get single user by ID + +stages: + - name: Fetch user with ID 1 request: - url: https://jsonplaceholder.typicode.com/posts/1 + url: https://jsonplaceholder.typicode.com/users/1 method: GET response: status_code: 200 json: id: 1 - userId: 1 - title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit" - body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" + name: Leanne Graham + username: Bret + email: Sincere@april.biz + address: + street: Kulas Light + city: Gwenborough + +--- +test_name: Get posts for specific user - - name: Step 3 +stages: + - name: Fetch posts by user ID request: - url: https://jsonplaceholder.typicode.com/posts/1 + url: https://jsonplaceholder.typicode.com/posts method: GET + params: + userId: 1 response: status_code: 200 + # Verify response contains posts from the user json: - id: 1 - userId: 1 - title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit" - body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" + - userId: 1 +--- +test_name: Create new post via API + +stages: + - name: Submit new post + request: + url: https://jsonplaceholder.typicode.com/posts + method: POST + headers: + Content-Type: application/json + json: + title: Test Post Title + body: This is the test post body content. + userId: 1 + response: + status_code: 201 + json: + id: 101 + title: Test Post Title + body: This is the test post body content. + userId: 1 --- -test_name: Simple test failed +test_name: Update existing post stages: - - name: Step 1 + - name: Update post with PUT request: url: https://jsonplaceholder.typicode.com/posts/1 - method: GET + method: PUT + headers: + Content-Type: application/json + json: + id: 1 + title: Updated Post Title + body: Updated post body content. + userId: 1 response: status_code: 200 json: id: 1 - userId: 1 - title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit" - body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" + title: Updated Post Title - - name: Step 2 +--- +test_name: Partial update with PATCH + +stages: + - name: Patch post title only request: url: https://jsonplaceholder.typicode.com/posts/1 - method: GET + method: PATCH + headers: + Content-Type: application/json + json: + title: Patched Title response: - status_code: 300 + status_code: 200 json: id: 1 - userId: 1 - title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit" - body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" + title: Patched Title + +--- +test_name: Delete post from API - - name: Step 3 +stages: + - name: Delete post by ID request: url: https://jsonplaceholder.typicode.com/posts/1 - method: GET + method: DELETE response: status_code: 200 - json: - id: 1 - userId: 1 - title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit" - body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" + +--- +test_name: Handle non-existent resource + +stages: + - name: Request non-existent user + request: + url: https://jsonplaceholder.typicode.com/users/9999 + method: GET + response: + status_code: 404 diff --git a/examples/single/tavern/test_with_id.tavern.yaml b/examples/single/tavern/test_with_id.tavern.yaml index 4f6f97c4..ac6933a8 100644 --- a/examples/single/tavern/test_with_id.tavern.yaml +++ b/examples/single/tavern/test_with_id.tavern.yaml @@ -1,80 +1,253 @@ +# ============================================================================== +# API Tests Linked to Qase Test Cases +# ============================================================================== +# Use QaseID=N in test_name to link tests to existing Qase test cases. +# Each stage is automatically reported as a step in Qase. + --- -test_name: QaseID=1 Test with QaseID success +# Single Qase test case ID +test_name: QaseID=1 User authentication flow stages: - - name: Step 1 + - name: Step 1 - Get user credentials request: - url: https://jsonplaceholder.typicode.com/posts/1 + url: https://jsonplaceholder.typicode.com/users/1 method: GET response: status_code: 200 json: id: 1 - userId: 1 - title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit" - body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" + email: Sincere@april.biz + save: + json: + user_id: id + user_email: email - - name: Step 2 + - name: Step 2 - Verify user exists in system request: - url: https://jsonplaceholder.typicode.com/posts/1 + url: https://jsonplaceholder.typicode.com/users/{user_id} method: GET response: status_code: 200 json: - id: 1 + id: !int "{user_id}" + email: "{user_email}" + +--- +# Multiple Qase test case IDs +test_name: QaseID=2,3 User posts CRUD operations + +stages: + - name: Step 1 - Get user posts + request: + url: https://jsonplaceholder.typicode.com/posts + method: GET + params: userId: 1 - title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit" - body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" + response: + status_code: 200 + json: + - userId: 1 + save: + json: + first_post_id: "[0].id" - - name: Step 3 + - name: Step 2 - Verify specific post request: - url: https://jsonplaceholder.typicode.com/posts/1 + url: https://jsonplaceholder.typicode.com/posts/{first_post_id} method: GET response: status_code: 200 json: - id: 1 + id: !int "{first_post_id}" userId: 1 - title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit" - body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" + - name: Step 3 - Get post comments + request: + url: https://jsonplaceholder.typicode.com/posts/{first_post_id}/comments + method: GET + response: + status_code: 200 --- -test_name: QaseID=2 Test with QaseID failed +test_name: QaseID=10 Complete resource lifecycle stages: - - name: Step 1 + - name: Step 1 - Create new resource + request: + url: https://jsonplaceholder.typicode.com/posts + method: POST + headers: + Content-Type: application/json + json: + title: New Resource Title + body: Resource description and content. + userId: 1 + response: + status_code: 201 + json: + title: New Resource Title + save: + json: + created_id: id + + - name: Step 2 - Read created resource request: - url: https://jsonplaceholder.typicode.com/posts/1 + url: https://jsonplaceholder.typicode.com/posts/{created_id} method: GET response: status_code: 200 + + - name: Step 3 - Update resource + request: + url: https://jsonplaceholder.typicode.com/posts/{created_id} + method: PUT + headers: + Content-Type: application/json json: - id: 1 + id: !int "{created_id}" + title: Updated Resource Title + body: Updated description. userId: 1 - title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit" - body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" + response: + status_code: 200 + json: + title: Updated Resource Title + + - name: Step 4 - Delete resource + request: + url: https://jsonplaceholder.typicode.com/posts/{created_id} + method: DELETE + response: + status_code: 200 - - name: Step 2 +--- +test_name: QaseID=20 Nested resource retrieval + +stages: + - name: Step 1 - Get user details request: - url: https://jsonplaceholder.typicode.com/posts/1 + url: https://jsonplaceholder.typicode.com/users/1 method: GET response: - status_code: 300 + status_code: 200 json: id: 1 - userId: 1 - title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit" - body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" + company: + name: Romaguera-Crona + save: + json: + user_company: company.name - - name: Step 3 + - name: Step 2 - Get user albums request: - url: https://jsonplaceholder.typicode.com/posts/1 + url: https://jsonplaceholder.typicode.com/users/1/albums method: GET response: status_code: 200 + save: + json: + album_id: "[0].id" + + - name: Step 3 - Get album photos + request: + url: https://jsonplaceholder.typicode.com/albums/{album_id}/photos + method: GET + response: + status_code: 200 + +--- +test_name: QaseID=30 Data validation test + +stages: + - name: Step 1 - Get all todos + request: + url: https://jsonplaceholder.typicode.com/todos + method: GET + params: + _limit: 5 + response: + status_code: 200 + # Verify response structure json: - id: 1 + - id: 1 + userId: 1 + completed: !anybool + + - name: Step 2 - Filter completed todos + request: + url: https://jsonplaceholder.typicode.com/todos + method: GET + params: + completed: true + _limit: 3 + response: + status_code: 200 + json: + - completed: true + + - name: Step 3 - Filter by user + request: + url: https://jsonplaceholder.typicode.com/todos + method: GET + params: userId: 1 - title: "sunt aut facere repellat provident occaecati excepturi optio reprehenderit" - body: "quia et suscipit\nsuscipit recusandae consequuntur expedita et cum\nreprehenderit molestiae ut ut quas totam\nnostrum rerum est autem sunt rem eveniet architecto" + _limit: 3 + response: + status_code: 200 + json: + - userId: 1 + +--- +test_name: QaseID=40 Error handling scenarios + +stages: + - name: Step 1 - Request non-existent resource + request: + url: https://jsonplaceholder.typicode.com/posts/99999 + method: GET + response: + status_code: 404 + + - name: Step 2 - Verify empty object response + request: + url: https://jsonplaceholder.typicode.com/posts/99999 + method: GET + response: + status_code: 404 + json: {} + +--- +test_name: QaseID=50 Pagination and sorting + +stages: + - name: Step 1 - Get first page + request: + url: https://jsonplaceholder.typicode.com/posts + method: GET + params: + _page: 1 + _limit: 5 + response: + status_code: 200 + + - name: Step 2 - Get second page + request: + url: https://jsonplaceholder.typicode.com/posts + method: GET + params: + _page: 2 + _limit: 5 + response: + status_code: 200 + + - name: Step 3 - Sort by title descending + request: + url: https://jsonplaceholder.typicode.com/posts + method: GET + params: + _sort: title + _order: desc + _limit: 5 + response: + status_code: 200