Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions tests/README.md
Original file line number Diff line number Diff line change
@@ -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.

<div align="center">
<img src="./img/tests_structure-odoo.png" alt="Test Folder Structure">
<p><em>Structure of the <code>tests/</code> directory</em></p>
</div>

* **`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.
Binary file added tests/img/github-actions-odoo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/img/make-test-cov-odoo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/img/make-test-odoo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added tests/img/tests_structure-odoo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading