diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..b1b23cc --- /dev/null +++ b/tests/README.md @@ -0,0 +1,184 @@ +# Testing & Quality Assurance Documentation + +This document outlines the testing strategy, tools and workflows used to ensure the reliability and quality of Perry's Odoo custom addons. + +Our testing architecture relies heavily on **Pytest** for both unit and integration tests, custom Odoo module mocking, strict pre-commit hooks and automated CI/CD pipelines. + +--- + +## Testing stack + +We use a Python testing stack defined in `requirements.txt`: + +* **[`pytest`](https://docs.pytest.org/)**: The core testing framework. +* **[`pytest-asyncio`](https://pytest-asyncio.readthedocs.io/)**: For testing asynchronous code (`async`/`await`). +* **[`pytest-mock`](https://pytest-mock.readthedocs.io/)**: For mocking dependencies and isolating tests (using `monkeypatch` and `Mock`). +* **[`pytest-cov`](https://pytest-cov.readthedocs.io/)**: To measure code coverage. +* **[`pytest-dotenv`](https://pypi.org/project/pytest-dotenv/)**: To automatically load `.env` variables during tests. +* **[`pytest-testdox`](https://pypi.org/project/pytest-testdox/)**: To generate human-readable terminal output for test results. + +Additionally, **code quality** is enforced by **[`Ruff`](https://docs.astral.sh/ruff/)** (linter/formatter) and **[`Safety`](https://github.com/pyupio/safety)** (dependency vulnerability scanner). + +--- + +## Directory structure + +The `tests` directory is organized by Odoo module to mirror the structure of the `custom_addons` folder. This makes it easy to locate the tests for any given feature. + +
+ Test Folder Structure +

Structure of the tests/ directory

+
+ +* **`perry_auto_reply/`**: Tests for the automated reply logic and user configurations (`test_res_users_perry.py`). No used anymore for main functionalities. +* **`perry_human_loop/`**: Tests for pending actions, summary computations, logfire configuration and action execution (e.g., creating leads, payments, calendar events). +* **`perry_webhook/`**: Tests for the webhook ingestion endpoints and their respective telemetry. +* **`conftest.py`**: Global test configuration and Odoo environment mocking. + +--- + +## Configuration and Fixtures + +### `pytest.ini` +Our Pytest configuration ensures correct test discovery and handles asynchronous modes automatically: +```ini +[pytest] +asyncio_mode = auto +testpaths = tests +python_files = test_*.py +pythonpath = . +``` + +### Global Odoo Mocking (`conftest.py`) +Testing Odoo modules usually requires a full database and server environment. To make our tests fast, reliable and isolated, we use a **custom mocking strategy** in `conftest.py`: + +* **`install_odoo_mock()`**: Intercepts the `import odoo` statements during the test collection phase. It dynamically creates fake Python modules (`odoo.models`, `odoo.fields`, `odoo.api`, `odoo.exceptions`) so our addon files can be imported without raising a `ModuleNotFoundError`. +* **`mock_odoo_modules` (autouse fixture)**: Re-installs the clean mock environment before every single test, ensuring absolute isolation. +* **Fake classes**: We use `FakeModel`, `FakeRecord` and `FakePendingAction` inside our test files to simulate Odoo's Active Record pattern (like `.create()`, `.sudo()`, and `.search()`) directly in memory. + +--- + +## Running the tests + +We use a `Makefile` to simplify the execution of test commands from the terminal. + +> **Important:** All `make` commands must be executed from the root of the repository. + +1. **Run all tests (with readable output):** + ```bash + make test + ``` + *Executes: `pytest --testdox tests/`* + + ![Make Test Output](./img/make-test-odoo.png) + *Example output of a successful `make test` run* + +2. **Run tests with Coverage Report:** + ```bash + make test-cov + ``` + *Executes: `pytest --cov=custom_addons --testdox tests/`* + + ![Make Test Coverage Output](./img/make-test-cov-odoo.png) + *Terminal output displaying the test coverage summary* + +--- + +## Pre-commit hooks + +Before any code is committed, we enforce quality checks using `.pre-commit-config.yaml`. The pipeline ensures that broken code never reaches the repository. + +The hooks run in this order: +1. **Ruff Check**: Lints the code for syntax and style errors. +2. **Ruff Format**: Automatically formats the Python code. +3. **Safety Scan**: Checks for known vulnerabilities in the installed dependencies. +4. **Run Tests**: Executes the full Pytest suite (`bash -c "PYTHONPATH=. pytest"`). The commit will be aborted if any test fails. + +--- + +## Continuous Integration (GitHub Actions) + +We use GitHub Actions to run our test suite automatically on the cloud whenever code is pushed to or merged into the `main` branch. This guarantees that integrations don't break existing features. + +![GitHub Actions Workflow](./img/github-actions-odoo.png) +*Successful test workflow execution in GitHub Actions* + +The workflow (`.github/workflows/tests.yml`) sets up Python 3.10, installs the requirements and runs Pytest: + +```yaml +name: Run tests + +on: + push: + branches: ["main", "master"] + pull_request: + branches: ["main", "master"] + +jobs: + test: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.10" + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Run tests + run: | + PYTHONPATH=. pytest +``` + +--- + +## Guide: How to add tests for a new Odoo feature + +If you are creating a new feature or model inside `custom_addons/`, you must ensure it is covered by tests. Because we mock the Odoo environment, you will rely on custom Fake classes. Follow these steps: + +### Step 1: Create the test file +Navigate to the corresponding folder inside `tests/` (e.g., `tests/perry_human_loop/`) and create a new file. **The file name must start with `test_`**: +`test_new_feature.py` + +### Step 2: Import dependencies and test utilities +Import `pytest`, `Mock` (if needed), and the fake Odoo helpers you need from your test suite context. + +### Step 3: Write the tests using memory models +* Start the function name with `test_`. +* Use standard Python dictionaries and instances of `FakeRecord` to represent Odoo records. +* Pass these fake objects into the logic you want to test. + +**Example (Testing an action execution):** +```python +import pytest +from custom_addons.perry_human_loop.models.perry_pending_action import PerryPendingAction + +# Assuming FakeRecord and FakePendingAction are defined locally or imported +def test_action_cancel_marks_records_as_cancelled(): + """Test that cancelling an action updates the state of multiple records.""" + + # 1. Arrange (Setup fake records in memory) + first = FakeRecord(state="draft") + second = FakeRecord(state="pending") + action = FakePendingAction([first, second]) + + # 2. Act (Trigger the method) + result = action.action_cancel() + + # 3. Assert (Verify Odoo-like behavior) + assert result == {"type": "ir.actions.client", "tag": "reload"} + assert first.state == "cancelled" + assert second.state == "cancelled" +``` + +### Step 4: Run and verify +Run `make test` or `make test-cov` locally to ensure the new tests pass. + +Once you try to commit, the pre-commit hook will also validate the work automatically. diff --git a/tests/img/github-actions-odoo.png b/tests/img/github-actions-odoo.png new file mode 100644 index 0000000..6f5e5c4 Binary files /dev/null and b/tests/img/github-actions-odoo.png differ diff --git a/tests/img/make-test-cov-odoo.png b/tests/img/make-test-cov-odoo.png new file mode 100644 index 0000000..2e632ac Binary files /dev/null and b/tests/img/make-test-cov-odoo.png differ diff --git a/tests/img/make-test-odoo.png b/tests/img/make-test-odoo.png new file mode 100644 index 0000000..98b6032 Binary files /dev/null and b/tests/img/make-test-odoo.png differ diff --git a/tests/img/tests_structure-odoo.png b/tests/img/tests_structure-odoo.png new file mode 100644 index 0000000..9122b6b Binary files /dev/null and b/tests/img/tests_structure-odoo.png differ