Differences
This shows you the differences between two versions of the page.
| Next revision | Previous revision | ||
| en:entwickler:regression_test_suite [2026/08/24 13:40] – created kainhofer | en:entwickler:regression_test_suite [2026/08/24 18:12] (current) – [Continuous integration] kainhofer | ||
|---|---|---|---|
| Line 1: | Line 1: | ||
| ====== Regression test suite ====== | ====== Regression test suite ====== | ||
| - | The Admidio regression test suite is intended to protect | + | The Admidio regression test suite protects |
| - | Once set up, running | + | The suite does not only test isolated PHP classes. Depending on the layer it exercises the real Admidio database abstraction, |
| + | |||
| + | Once the environment is set up, running | ||
| <code bash> | <code bash> | ||
| Line 34: | Line 36: | ||
| </ | </ | ||
| - | It is useful both for developers contributing to Admidio itself and for third-party developers who build modules, plugins, integrations or other extensions against Admidio. | + | The test count in this example output |
| - | The suite does not only test isolated PHP classes. Depending on the test layer it also exercises the real Admidio | + | **The suite is intentionally destructive. Never point it at a production |
| - | This page describes how to set up, run and extend | + | The suite is not a browser UI test suite. Many tests deliberately stop at the Entity, Service or CLI acceptance boundary. Historic version-to-version database upgrade coverage is a separate lifecycle concern: a green regression |
| For the general setup of an Admidio development installation also see [[en: | For the general setup of an Admidio development installation also see [[en: | ||
| - | ===== Important safety warning | + | ===== Quick start ===== |
| - | **Never | + | ==== First run ==== |
| - | The regression environment is intentionally destructive. | + | From the root of your Admidio checkout: |
| - | Database-backed test runs recreate the Admidio schema using the production installer. Existing Admidio tables in the configured test database may therefore be removed. | + | <code bash> |
| + | # 1. install | ||
| + | composer install | ||
| - | The test harness contains additional safeguards. Among other checks, | + | # 2. start the test services (MariaDB, PostgreSQL, Mailpit) |
| + | docker compose -f docker-compose.test.yml up -d | ||
| - | Use a dedicated database name such as: | + | # 3. create the test configuration |
| + | cp .env.test.example .env.test | ||
| - | < | + | # 4. prepare and verify the environment |
| - | admidio_test | + | php tests/bin/setup-test-env.php |
| - | </code> | + | |
| - | Do not use a shared database and do not try to bypass the safety checks. | + | # 5. run everything |
| - | + | composer test:all | |
| - | Filesystem tests have a similar safeguard. They are only allowed to perform destructive operations below: | + | |
| - | + | ||
| - | < | + | |
| - | tests/ | + | |
| </ | </ | ||
| - | and require | + | Step 2 takes about 30 seconds until the databases accept connections. The setup script |
| - | ===== Test architecture ===== | + | On Windows use '' |
| - | The suite is divided into several layers. A test should be placed in the lowest layer that can reliably | + | The defaults in '' |
| - | ^ Layer ^ Typical location ^ Purpose ^ | + | ==== Everyday commands ==== |
| - | | Unit tests | '' | + | |
| - | | Integration tests | '' | + | |
| - | | CLI tests | '' | + | |
| - | | Filesystem integration tests | '' | + | |
| - | | Mail integration tests | '' | + | |
| - | | Installation tests | '' | + | |
| - | The suite should not be interpreted as a browser UI test suite. Many tests deliberately stop at the Entity, Service or CLI acceptance boundary. | + | ^ Command ^ What it runs ^ Database needed ^ |
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| - | Historic version-to-version database upgrade coverage is a separate lifecycle concern. A green regression | + | While developing, |
| - | ===== Core testing principle: test Admidio, not the test suite ===== | + | <code bash> |
| + | # one file | ||
| + | vendor/ | ||
| - | The most important rule when adding regression | + | # one method, in the whole suite or in one file |
| + | vendor/ | ||
| + | vendor/ | ||
| - | **The action being tested must be performed by production Admidio | + | # one suite with readable descriptions |
| + | vendor/ | ||
| + | </code> | ||
| - | A test must not reproduce | + | '' |
| - | For example, a test for a Service operation should normally: | + | '' |
| - | - create only the prerequisites needed by the test; | + | After a focused |
| - | - call the real Admidio Service; | + | |
| - | - let that Service call the normal Admidio Entities and database abstraction; | + | |
| - | - verify the resulting state independently, | + | |
| - | A test should **not** implement the same database writes itself and then claim that the Service was tested. | + | ==== What a run changes on your machine ==== |
| - | This distinction | + | * **The test database |
| + | * **Files are written below '' | ||
| + | * **Mail does not leave your machine.** The mail test talks to the local Mailpit SMTP listener. | ||
| + | * **Nothing else is touched.** Each database test runs inside a transaction that is rolled back afterwards. The exception are CLI subprocess tests, which commit and therefore clean up after themselves, see [[# | ||
| - | - creating reciprocal user relations; | + | ===== Setting up the test environment ===== |
| - | - creating or updating related records; | + | |
| - | - enforcing organization boundaries; | + | |
| - | - updating changelog information; | + | |
| - | - maintaining sequence values; | + | |
| - | - applying permissions; | + | |
| - | - sending messages or emails; | + | |
| - | - creating thumbnails or archive files. | + | |
| - | Fixtures are there to create prerequisites. They are not substitutes for the production workflow being tested. | + | ==== Requirements ==== |
| - | ===== Requirements ===== | + | * a checkout of the Admidio source tree; |
| + | * PHP as required by '' | ||
| + | * Composer, to install PHPUnit and '' | ||
| + | * a dedicated MariaDB, MySQL or PostgreSQL test database and the matching PDO driver ('' | ||
| + | * Mailpit or another local SMTP sink for the mail test; | ||
| + | * Docker, if you want the supplied test services instead of your own servers. | ||
| - | Before running | + | '' |
| - | The exact PHP version and required PHP extensions are defined by the current '' | + | Install the dependencies from the Admidio root directory: |
| - | + | ||
| - | You need: | + | |
| - | + | ||
| - | - a checkout of the Admidio source tree; | + | |
| - | - Composer; | + | |
| - | - the PHP extensions required by Admidio; | + | |
| - | - a dedicated MariaDB, PostgreSQL or MySQL test database; | + | |
| - | - the matching PDO database driver; | + | |
| - | - Mailpit when running the mail integration test; | + | |
| - | - GD and ZIP support for the photo/ | + | |
| - | + | ||
| - | Install the Composer | + | |
| <code bash> | <code bash> | ||
| Line 136: | Line 130: | ||
| </ | </ | ||
| - | ===== Configure | + | ==== Recommended: |
| - | Copy the supplied example configuration: | + | The repository contains '' |
| + | |||
| + | ^ Service ^ Image ^ Published ports ^ Database ^ User ^ Password ^ | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| <code bash> | <code bash> | ||
| - | cp .env.test.example .env.test | + | # start |
| - | </ | + | docker compose -f docker-compose.test.yml up -d |
| - | On Windows, copy the file using Explorer or PowerShell instead. | + | # status and logs |
| + | docker compose -f docker-compose.test.yml ps | ||
| + | docker compose -f docker-compose.test.yml logs -f mariadb | ||
| - | The process environment takes precedence over values stored in '' | + | # stop, keeping the data volumes |
| + | docker compose -f docker-compose.test.yml down | ||
| - | A typical configuration looks like this: | + | # stop and throw the databases away |
| + | docker compose -f docker-compose.test.yml down -v | ||
| + | </ | ||
| - | <code ini> | + | The '' |
| - | TEST_DATABASE_ENGINE=mariadb | + | |
| - | TEST_FILES_PATH=./ | + | |
| - | TEST_DB_MARIADB_HOST=127.0.0.1 | + | The Mailpit web UI is at [[http:// |
| - | TEST_DB_MARIADB_PORT=3306 | + | |
| - | TEST_DB_MARIADB_USER=admidio | + | |
| - | TEST_DB_MARIADB_PASS=admidio_test | + | |
| - | TEST_DB_MARIADB_NAME=admidio_test | + | |
| - | TEST_DB_POSTGRES_HOST=127.0.0.1 | + | The Compose file has no MySQL service, because MariaDB already occupies port 3306. MySQL 8 is a scheduled CI target. To test it locally, point the '' |
| - | TEST_DB_POSTGRES_PORT=5432 | + | |
| - | TEST_DB_POSTGRES_USER=admidio | + | |
| - | TEST_DB_POSTGRES_PASS=admidio_test | + | |
| - | TEST_DB_POSTGRES_NAME=admidio_test | + | |
| - | TEST_DB_MYSQL_HOST=127.0.0.1 | + | ==== Alternative: |
| - | TEST_DB_MYSQL_PORT=3306 | + | |
| - | TEST_DB_MYSQL_USER=admidio | + | |
| - | TEST_DB_MYSQL_PASS=admidio_test | + | |
| - | TEST_DB_MYSQL_NAME=admidio_test | + | |
| - | TEST_MAIL_HOST=127.0.0.1 | + | If you already run a database server, you only need a disposable database and a user that may create and drop tables in it. |
| - | TEST_MAIL_PORT=1025 | + | |
| - | TEST_MAILPIT_API_HOST=127.0.0.1 | + | === MariaDB and MySQL === |
| - | TEST_MAILPIT_API_PORT=8025 | + | |
| + | <code sql> | ||
| + | CREATE DATABASE admidio_test CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; | ||
| + | CREATE USER ' | ||
| + | GRANT ALL PRIVILEGES ON admidio_test.* TO ' | ||
| + | FLUSH PRIVILEGES; | ||
| </ | </ | ||
| - | The checked-in | + | The user needs '' |
| - | Only configure credentials for disposable test databases. | + | === PostgreSQL === |
| - | ===== Using the supplied Docker environment ===== | + | <code sql> |
| + | CREATE ROLE admidio LOGIN PASSWORD ' | ||
| + | CREATE DATABASE admidio_test OWNER admidio ENCODING ' | ||
| + | \c admidio_test | ||
| + | GRANT ALL ON SCHEMA public TO admidio; | ||
| + | </ | ||
| - | The easiest way to provide | + | The last statement matters from PostgreSQL 15 on, where the '' |
| - | From the directory containing the Compose configuration, start the test services: | + | === A local SMTP sink === |
| + | |||
| + | Without Docker, run Mailpit directly. It is a single binary, or: | ||
| <code bash> | <code bash> | ||
| - | docker | + | docker |
| </ | </ | ||
| - | The current regression environment provides database services for the normal | + | The mail test needs both the SMTP port and the HTTP API, because it verifies the delivered message through the API rather than trusting that the connection succeeded. |
| - | If you use your own database servers instead, simply | + | Whatever |
| - | MySQL can also be tested against an external MySQL instance when one is not part of the local Compose | + | ==== The test configuration |
| - | ==== Mailpit ==== | + | Copy the supplied example and edit it if your services differ from the defaults: |
| - | The default regression configuration expects: | + | <code bash> |
| + | cp .env.test.example .env.test | ||
| + | </ | ||
| - | ^ Service ^ Default endpoint ^ | + | '' |
| - | | SMTP | '' | + | |
| - | | HTTP API | '' | + | |
| - | The Mailpit regression | + | ^ Variable ^ Meaning ^ Default ^ |
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| - | Some Docker environments may show the Mailpit container as '' | + | ''< |
| - | - whether Admidio can send the message through Mailpit's SMTP listener; | + | Only '' |
| - | - whether the delivered message appears through | + | |
| - | Therefore a Docker '' | + | Two details that regularly cost time: |
| - | ===== Running | + | * **Use '' |
| + | * **A variable that is already set in the process environment wins over the file.** That is how CI configures a run without any '' | ||
| - | ==== Complete regression suite ==== | + | <code bash> |
| + | TEST_DATABASE_ENGINE=postgres composer test: | ||
| + | </ | ||
| - | Run the complete suite from the Admidio root directory: | + | <code powershell> |
| + | $env:TEST_DATABASE_ENGINE = ' | ||
| + | </ | ||
| + | |||
| + | ==== The setup script ==== | ||
| <code bash> | <code bash> | ||
| - | composer | + | php tests/ |
| </ | </ | ||
| - | This is the normal command before submitting a change | + | Run it once after creating '' |
| - | ==== Unit tests only ==== | + | - loads '' |
| + | - creates the subdirectories of the test data directory ('' | ||
| + | - connects to the database, retrying for up to a minute, because a container answers on its port before the server accepts connections; | ||
| + | - reports whether the mail sink is reachable, which is optional and does not stop the setup; | ||
| + | - writes the '' | ||
| - | For fast feedback while working on pure PHP logic: | + | The script accepts '' |
| <code bash> | <code bash> | ||
| - | composer | + | php tests/ |
| </ | </ | ||
| - | Unit tests deliberately do not initialize | + | **This option only affects |
| - | ==== Run one test file ==== | + | ==== Switching the database engine |
| - | During development it is often useful to run only the test currently being worked on: | + | To run the suite against another engine, change the engine in '' |
| <code bash> | <code bash> | ||
| - | vendor/ | + | # in .env.test: |
| + | php tests/bin/setup-test-env.php | ||
| + | composer test:all | ||
| </ | </ | ||
| - | or, for example: | + | Each engine has its own connection block, so both configurations can stay in the file side by side. The schema is rebuilt at the start of every run, so no manual cleanup is needed when you switch back and forth. |
| - | <code bash> | + | Before submitting a change that touches SQL, Entities or the installer, run the suite at least against MariaDB and PostgreSQL. Those are the two engines every pull request is tested on. |
| - | vendor/ | + | |
| - | </ | + | |
| - | ==== Run one test method | + | ==== Safety mechanisms |
| - | PHPUnit filtering can be used for an individual regression: | + | The suite refuses to run in an environment that does not look like a test environment. The guards are: |
| - | <code bash> | + | ^ Guard ^ Enforced in ^ Rule ^ |
| - | vendor/bin/ | + | | Database name | '' |
| - | </code> | + | | Data directory | '' |
| + | | Table prefix | '' | ||
| + | | Filesystem root | '' | ||
| - | Replace | + | There are two marker files in '' |
| - | After the focused | + | * '' |
| + | * '' | ||
| - | ===== Database initialization | + | Never point '' |
| - | Database-backed PHPUnit runs use the current Admidio production installer to create the schema. | + | ==== Continuous integration ==== |
| - | This is intentional: | + | '' |
| - | Normal database integration | + | ^ Job ^ When ^ What it does ^ |
| + | | '' | ||
| + | | MariaDB 10.11 | always, after fast-checks | setup script, '' | ||
| + | | PostgreSQL 15 | always, | ||
| + | | MySQL 8.0 | Mondays 02:00 UTC, or on demand | same as the other database jobs | | ||
| - | Tests should therefore not depend on execution order. | + | The workflow can also be started manually through // |
| - | Do not assume that data created by another | + | CI runs no mail job. The Mailpit test is a local integration |
| - | ===== A special case: CLI subprocess tests ===== | + | ===== How the suite is built ===== |
| - | CLI subprocess tests are different from ordinary transaction-based integration tests. | + | ==== Test layers ==== |
| - | They start the real Admidio executable as a separate process. That process has: | + | The suite is divided into layers. A test belongs in the lowest layer that can reliably test the behaviour in question. |
| - | - its own production | + | ^ Layer ^ Location ^ Purpose ^ |
| - | - its own database | + | | Unit tests | '' |
| - | - its own exit status; | + | | Integration tests | '' |
| - | - its own standard output | + | | Filesystem integration tests | '' |
| + | | Mail integration tests | '' | ||
| + | | CLI tests | '' | ||
| + | | Installation coverage | '' | ||
| - | This makes subprocess | + | The '' |
| - | A typical mutating CLI regression should follow this pattern: | + | ==== Directory layout ==== |
| - | - process A creates or changes an object; | + | < |
| - | - process B reads the object | + | tests/ |
| - | - another command removes the object again; | + | ├── bootstrap.php |
| - | - the test verifies that cleanup | + | ├── env.php |
| + | ├── bootstrap-admidio.php | ||
| + | ├── bin/ | ||
| + | │ | ||
| + | ├── Support/ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | │ | ||
| + | ├── Unit/ no database, no filesystem, no network | ||
| + | ├── Integration/ | ||
| + | ├── Cli/ CLI contract and workflow tests | ||
| + | └── adm_my_files/ | ||
| + | </ | ||
| - | Do not verify | + | The three bootstraps are separate on purpose. '' |
| - | Also remember that a CLI subprocess cannot see uncommitted data from the PHPUnit transaction of its parent process. | + | ==== Base classes and helpers ==== |
| - | Therefore prerequisites for a mutating CLI scenario must either already exist in the committed baseline or be created through subprocess commands as part of the scenario. | + | The base classes build on each other: |
| - | Mutating subprocess tests must clean up after themselves, normally in a '' | + | < |
| + | PHPUnit\Framework\TestCase | ||
| + | └── AdmidioTestCase | ||
| + | └── DatabaseTestCase | ||
| + | └── AdministratorTestCase | ||
| + | └── FilesystemTestCase | ||
| + | </ | ||
| - | The existing | + | ^ Base class ^ Gives you ^ Choose it when ^ |
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| + | | '' | ||
| - | ===== Filesystem tests ===== | + | '' |
| - | Tests that exercise documents, photos, imports, exports or other file operations must use the protected filesystem test base class. | + | Two traits add capabilities to any of those classes: |
| - | The regression filesystem root is: | + | ^ Trait ^ Provides ^ |
| + | | '' | ||
| + | | '' | ||
| - | < | + | '' |
| - | tests/ | + | |
| + | < | ||
| + | protected function getFixture(): | ||
| + | { | ||
| + | return new AdmidioTestFixture($this->getDatabase()); | ||
| + | } | ||
| </ | </ | ||
| - | The '' | + | It offers, among others, |
| - | If either check fails, destructive filesystem operations are refused. | + | ==== Schema creation and test isolation ==== |
| - | When adding | + | Database-backed runs do not load an SQL dump. The first '' |
| - | - use '' | + | The installation |
| - | - create files only below the test data root; | + | |
| - | - register created files and directories for cleanup; | + | |
| - | - call the actual Admidio Service or Entity that performs the filesystem operation; | + | |
| - | - verify both filesystem and database state where applicable; | + | |
| - | - verify cleanup explicitly. | + | |
| - | Never point '' | + | * one organization, |
| + | * the default categories, roles and profile fields of a fresh installation; | ||
| + | * one administrator with the login '' | ||
| - | ===== Mail tests ===== | + | Every individual test then runs inside a transaction that '' |
| - | Mail regression | + | * a test must not depend on data another test created; |
| + | * a test must not depend on execution order — '' | ||
| + | * anything a test writes through | ||
| - | The existing Mailpit test follows this path: | + | The two exceptions are CLI subprocesses, |
| - | < | + | ==== Core principle: test Admidio, not the test suite ==== |
| - | PreferencesService | + | |
| - | -> Admidio | + | |
| - | -> PHPMailer | + | |
| - | -> SMTP | + | |
| - | -> Mailpit | + | |
| - | -> Mailpit HTTP API | + | |
| - | </ | + | |
| - | A good mail regression | + | The most important rule when adding a regression test is: |
| - | The test should verify delivery through Mailpit, not merely whether a TCP port is reachable. | + | **The action being tested must be performed by production Admidio code.** |
| - | ===== Choosing | + | A test must not reproduce the expected Admidio behaviour inside a fixture, helper or mock and then verify |
| - | ^ Test requirement ^ Base class / pattern ^ | + | - create only the prerequisites the test needs; |
| - | | Pure production logic, no external state | '' | + | - call the real Admidio Service; |
| - | | Database, | + | - let that Service use the normal |
| - | | Managed files below the test data directory | '' | + | - verify the resulting state independently, |
| - | | Real command-line bootstrap | existing '' | + | |
| - | Do not make a pure Unit test extend '' | + | This matters most for operations that do more than one thing: reciprocal user relations, related records, organization boundaries, changelog entries, sequence values, permissions, |
| - | ===== Adding an Integration or Service regression ===== | + | Fixtures create prerequisites. They are not substitutes for the production workflow under test. If production code is expected to write two reciprocal relationship records, the test must not write those two records itself and then assert that both exist. |
| - | When fixing | + | ===== Adding |
| - | **Arrange** | + | ==== Where does the test belong? ==== |
| - | Create | + | ^ The behaviour you want to protect ^ Layer ^ Base class ^ |
| + | | A pure function or value object, no state | '' | ||
| + | | An Entity, a Service, a query, a cascade | '' | ||
| + | | The same, but the production path checks administrator rights | '' | ||
| + | | Visibility, role rights, organization isolation | '' | ||
| + | | Anything that writes, reads or deletes files | '' | ||
| + | | Sending mail | '' | ||
| + | | A command of the Admidio CLI | '' | ||
| - | Prefer normal Admidio Entities and Services for fixtures. | + | When in doubt, pick the lowest layer that can actually fail when the feature breaks. |
| - | **Act** | + | ==== Recipe: a regression test for a bug fix ==== |
| - | Call the real production method whose behavior | + | A regression test for a bug should fail before |
| - | Examples in the current suite include | + | - **Reproduce the defect** and find the production |
| + | - **Write the smallest test** that calls that production code and asserts the correct behaviour. Put it in the area directory of the module, name it after the behaviour, not after the ticket. | ||
| + | - **Run it and watch it fail** — and check //why// it fails. A test that errors out because a fixture is missing has not reproduced the bug. | ||
| + | - **Implement the production fix.** | ||
| + | - **Run the focused test again**, it must now pass: '' | ||
| + | - **Run the neighbouring tests**, for instance the whole area directory or '' | ||
| + | - **Run '' | ||
| - | **Assert independently** | + | If the fix is in code that only PostgreSQL or only MySQL reaches, also run the suite with that engine, see [[# |
| - | Read the result again independently. | + | Avoid writing |
| - | Depending on the feature, use: | + | ==== Recipe: a test for a new feature ==== |
| - | - a new production Entity object; | + | Structure the test as arrange, act, assert independently. The skeleton below follows |
| - | - '' | + | |
| - | - another Service read operation; | + | |
| - | - the physical file written by production code; | + | |
| - | - a second CLI subprocess; | + | |
| - | - the Mailpit HTTP API. | + | |
| - | The assertion should not simply inspect an array or object populated by the test fixture. | + | <code php> |
| + | <?php | ||
| - | ===== Fixtures ===== | + | namespace Admidio\Tests\Integration\Inventory; |
| - | Reusable fixtures are useful for common prerequisites such as: | + | use Admidio\Inventory\Service\ItemFieldService; |
| + | use Admidio\Tests\Support\AdministratorTestCase; | ||
| - | - organizations; | + | class ItemFieldServiceTest extends AdministratorTestCase |
| - | - users; | + | { |
| - | - roles; | + | /** |
| - | - memberships; | + | * @testdox ItemFieldService stores a new inventory field for the current organization |
| - | - categories. | + | */ |
| + | public function testSaveDataStoresTheFieldForTheCurrentOrganization(): | ||
| + | { | ||
| + | global $gCurrentOrgId; | ||
| - | Whenever possible, fixtures should create these objects | + | // Arrange: only the prerequisites, created |
| + | $db = $this-> | ||
| + | $fieldName = ' | ||
| - | A fixture may prepare state, but must not implement | + | // Act: the real production Service does the work |
| + | $this-> | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ' | ||
| + | ))); | ||
| - | For example, if production code is expected to create two reciprocal relationship records, a test fixture must not create those same two records and then assert that both exist. | + | // Assert: read the state back independently of the Service |
| + | $row = $db-> | ||
| + | ' | ||
| + | FROM ' | ||
| + | WHERE inf_org_id = ? | ||
| + | AND inf_name = ?', | ||
| + | array($gCurrentOrgId, | ||
| + | )-> | ||
| - | The production relationship operation must create them. | + | $this-> |
| + | $this-> | ||
| + | } | ||
| + | } | ||
| + | </ | ||
| - | ===== Database assertions ===== | + | Points worth copying: |
| - | Direct SQL is useful for independent | + | * the namespace mirrors the directory, and the class name ends in '' |
| + | * a random suffix in names keeps the test independent of leftovers and of parallel data; | ||
| + | * the Service is called exactly the way a module calls it; | ||
| + | * the assertion queries the database itself instead of trusting the object the Service returned; | ||
| + | * '' | ||
| - | Use Admidio' | + | ==== Using fixtures ==== |
| + | |||
| + | For prerequisites that Admidio | ||
| + | |||
| + | <code php> | ||
| + | $fixture = $this-> | ||
| + | |||
| + | $org = $fixture-> | ||
| + | $user = $fixture-> | ||
| + | $role = $fixture-> | ||
| + | $fixture-> | ||
| + | $fixture-> | ||
| + | </ | ||
| + | |||
| + | Each method returns an array with the generated ids and UUIDs. Fixtures create their objects through the same Entities production code uses. | ||
| + | |||
| + | The short name of an organization is at most ten characters and unique, and '' | ||
| + | |||
| + | A fixture may prepare state, but it must never implement the behaviour under test. | ||
| + | |||
| + | ==== Asserting persistence ==== | ||
| + | |||
| + | Verify the result through a path that is independent of the code you just called: | ||
| + | |||
| + | ^ Verification ^ Use it for ^ | ||
| + | | A new production Entity, read by id or UUID | the normal case for Entity and Service tests | | ||
| + | | '' | ||
| + | | A second Service read operation | read models, lists, exports | | ||
| + | | The file on disk | filesystem tests, next to the database check | | ||
| + | | A second CLI process | proving a CLI command really committed | | ||
| + | | The Mailpit HTTP API | proving a message was really delivered | | ||
| <code php> | <code php> | ||
| Line 422: | Line 543: | ||
| </ | </ | ||
| - | Direct SQL is appropriate | + | Direct SQL is the right tool for **asserting** what production code wrote. It is the wrong tool for reproducing the business operation the test claims to exercise. |
| - | It should not be used to reproduce | + | Keep the SQL portable across the engines Admidio supports. Use the '' |
| - | Tests must also remain portable across the database engines supported by Admidio. Avoid database-specific SQL unless the test explicitly verifies database-specific abstraction behavior. | + | ==== Naming and scope ==== |
| - | ===== Testing permissions | + | * A test file ends in '' |
| + | * The namespace mirrors the directory, for example '' | ||
| + | * Test method names and '' | ||
| + | * One regression test has one clear reason to fail. It may perform several steps when those steps form one production workflow, such as create, update and delete of the same object. | ||
| - | Permission and organization-isolation | + | ===== Special kinds of tests ===== |
| - | A weak test can accidentally prove only that the test author knows how to write a secure SQL query. | + | ==== Permissions and organization boundaries ==== |
| - | For example, manually writing: | + | Permission tests are the easiest to get wrong. A test that writes |
| <code sql> | <code sql> | ||
| Line 440: | Line 564: | ||
| </ | </ | ||
| - | inside | + | itself proves only that the test author knows how to write a safe query. It says nothing about the production |
| - | Whenever the regression concerns visibility, | + | Whenever the regression concerns visibility, |
| - | Then verify that inaccessible data is really absent. | + | The '' |
| - | ===== Testing the CLI ===== | + | <code php> |
| + | $visible | ||
| + | // production code that resolves rights for that user | ||
| + | }); | ||
| + | </ | ||
| - | CLI tests cover two different areas. | + | Use '' |
| - | ==== Contract | + | ==== Filesystem |
| - | Contract | + | Tests that exercise documents, photos, imports, exports or any other file operation extend '' |
| - | - command name; | + | The base class verifies that Admidio' |
| - | - description; | + | |
| - | - usage information; | + | |
| - | - arguments; | + | |
| - | - options; | + | |
| - | - callback availability. | + | |
| - | When adding a new command, make sure it satisfies the generic CLI contract tests instead of adding exceptions for incomplete metadata. | + | When adding a filesystem test: |
| - | ==== Workflow tests ==== | + | * create files only below the test data root, using '' |
| + | * register everything production code creates with '' | ||
| + | * call the actual Admidio Service or Entity that performs the file operation; | ||
| + | * verify both the filesystem and the database state; | ||
| + | * verify the cleanup path of the feature explicitly, where deleting is part of the behaviour. | ||
| - | Workflow tests exercise actual administration operations. | + | Cleanup runs in '' |
| - | For mutating workflows, use the real executable and verify committed state through another process. | + | ==== CLI tests ==== |
| - | Use machine-readable output such as JSON where the command supports it, rather than parsing human-oriented console formatting. | + | CLI coverage has two halves. |
| - | The test must also verify exit codes and error output where appropriate. | + | === Contract tests === |
| - | ===== Test naming | + | Contract tests inspect command registration: |
| - | Test file names should end in '' | + | === Workflow tests === |
| - | Test names and '' | + | Workflow tests start the real Admidio executable through the '' |
| - | Prefer a description such as: | + | * **A subprocess cannot see the PHPUnit transaction.** Prerequisites must either exist in the committed baseline created by the installer, or be created through subprocess commands |
| + | * **A subprocess commits.** Its changes survive the rollback, so a mutating test must clean up after itself, normally in a '' | ||
| + | * **File-writing commands are deliberately not covered here**, because the real CLI bootstrap uses the '' | ||
| - | < | + | The established pattern is create, verify from a second process, delete, verify the deletion: |
| - | PreferencesService sends a real email through Mailpit | + | |
| + | < | ||
| + | $login = ' | ||
| + | $created = false; | ||
| + | |||
| + | try { | ||
| + | $create = $this-> | ||
| + | $this-> | ||
| + | $created = true; | ||
| + | |||
| + | // A second process has a different connection. Seeing the record here proves | ||
| + | // the production CLI command committed an actual Admidio database write. | ||
| + | $show = $this-> | ||
| + | $this-> | ||
| + | $this-> | ||
| + | } finally { | ||
| + | if ($created) { | ||
| + | $delete = $this-> | ||
| + | $this-> | ||
| + | } | ||
| + | } | ||
| </ | </ | ||
| - | over: | + | Assert the exit code and the error output, and parse machine-readable output such as '' |
| + | |||
| + | ==== Mail tests ==== | ||
| + | |||
| + | Mail regressions exercise the real Admidio email stack instead of mocking the sender. The existing test follows this path: | ||
| < | < | ||
| - | Email works | + | PreferencesService |
| + | -> Admidio | ||
| + | -> PHPMailer | ||
| + | -> SMTP | ||
| + | -> Mailpit | ||
| + | -> Mailpit HTTP API | ||
| </ | </ | ||
| - | One regression test should have one clear reason to fail. | + | Guidelines: |
| - | A test may perform several steps when those steps form one production workflow. | + | * use a unique recipient address per run, so the assertion cannot match a message from an earlier run; |
| + | * verify the delivered message through the Mailpit API, not merely that a TCP port answered; | ||
| + | * restore the mail settings and any user data you changed, even though the transaction is rolled back, if the code under test caches them. | ||
| - | ===== How to test a bug fix ===== | + | The mail test deliberately ignores Docker' |
| - | A regression test for a bug should ideally fail before the production fix and pass after it. | + | ==== Installation coverage ==== |
| - | A useful workflow | + | Every database-backed run installs Admidio from scratch with the production installer, so the installer |
| - | - reproduce the defect; | + | '' |
| - | - add the smallest test that demonstrates the incorrect production behavior; | + | |
| - | - run the test and confirm that it fails for the expected reason; | + | |
| - | - implement the production fix; | + | |
| - | - run the focused test again; | + | |
| - | - run related Integration or CLI tests; | + | |
| - | - finally run '' | + | |
| - | Avoid writing the assertion only after changing the production code if doing so makes it impossible to prove that the test actually detects the regression. | + | ===== Checklists ===== |
| - | ===== Guidance for third-party developers ===== | + | ==== Before you open a pull request |
| - | Third-party modules and plugins benefit from following | + | * Does '' |
| + | * If the change touches SQL, Entities or the installer: does it also pass on the other engine? | ||
| + | * Does every new behaviour have a test that would fail without your production change? | ||
| + | * Do new tests clean up subprocess and filesystem changes? | ||
| + | * Are new language strings, database changes and update steps covered by the change itself? | ||
| - | Use a checkout of the Admidio version against which the extension is developed | + | A green run does not replace code review. Reviewers should still check whether |
| - | For extension tests: | + | ==== Reviewing a new regression test ==== |
| - | | + | |
| - | | + | * Does it verify the resulting state independently? |
| - | | + | * Does it verify a real database write where persistence is part of the feature? |
| - | | + | * Does it avoid duplicating |
| - | | + | |
| - | | + | |
| - | | + | |
| + | * Is filesystem | ||
| + | | ||
| + | | ||
| + | * Are permissions | ||
| + | | ||
| + | * **Would the test fail if the production behaviour it protects were removed?** | ||
| - | If a third-party change exposes a regression or missing contract in Admidio Core itself, consider contributing | + | If the answer |
| - | ===== What not to do ===== | + | ==== Anti-patterns |
| Do not add a test that: | Do not add a test that: | ||
| - | | + | |
| - | | + | |
| - | | + | |
| - | | + | |
| - | | + | |
| - | | + | |
| - | | + | |
| - | | + | |
| - | | + | |
| - | | + | |
| - | A regression | + | A test that cannot fail when the corresponding feature is broken provides false confidence and should be corrected. |
| - | ===== Reviewing a new regression test ===== | + | ===== Third-party modules and plugins |
| - | Before merging | + | Third-party modules and plugins benefit from the same principles even when their tests live outside the Admidio Core repository. Use a checkout of the Admidio version you develop against, and a dedicated test database. |
| - | | + | |
| - | - Does it independently verify the resulting state? | + | |
| - | - Does it verify a real database write when persistence is part of the feature? | + | |
| - | - Does it avoid duplicating | + | |
| - | | + | |
| - | | + | |
| - | | + | |
| - | - Is filesystem | + | |
| - | | + | |
| - | | + | |
| - | - Are permissions | + | |
| - | | + | |
| - | - Would the test fail if the production behavior it protects were removed? | + | |
| - | If the answer | + | If a third-party change exposes a regression or a missing contract in Admidio Core itself, consider contributing |
| ===== Troubleshooting ===== | ===== Troubleshooting ===== | ||
| - | ==== Safety check rejects the database ==== | + | ==== Safety check failed: |
| - | Use a dedicated database whose name contains | + | The name must contain |
| - | + | ||
| - | < | + | |
| - | admidio_test | + | |
| - | </code> | + | |
| - | Check the selected '' | + | Note that the setup script checks the name more loosely than the test run does. If '' |
| ==== Database connection fails ==== | ==== Database connection fails ==== | ||
| - | Check that: | + | Check that the service is running ('' |
| - | - the selected database service is running; | + | On MySQL and MariaDB, use '' |
| - | - hostname | + | |
| - | - the PDO driver is installed; | + | |
| - | - the test database exists; | + | |
| - | - the configured user has sufficient rights to create and remove | + | |
| - | ==== Unit tests work but Integration tests fail immediately | + | ==== " |
| - | Unit tests deliberately do not initialize | + | The bootstrap connects lazily, so a wrong configuration only surfaces when the first database-backed test runs. The message repeats the underlying connection error. Run the setup script, it reports the same problem with more context. |
| - | Check '' | + | ==== Unit tests pass but integration tests fail immediately ==== |
| - | ==== Filesystem | + | Unit tests deliberately never touch the database. A failure that appears only in the integration suite is almost always the environment: |
| - | Verify: | + | ==== The engine did not change ==== |
| - | < | + | '' |
| - | TEST_FILES_PATH=./ | + | |
| + | ==== Port 3306 is already in use ==== | ||
| + | |||
| + | The MariaDB container publishes 3306. A local MySQL or MariaDB service on the same port will conflict. Stop the local service, or change the published port in '' | ||
| + | |||
| + | ==== PostgreSQL: permission denied for schema public ==== | ||
| + | |||
| + | From PostgreSQL 15 on, the '' | ||
| + | |||
| + | < | ||
| + | GRANT ALL ON SCHEMA public TO admidio; | ||
| </ | </ | ||
| - | and make sure the checked-in file: | + | ==== A filesystem test refuses to run ==== |
| + | |||
| + | Verify that '' | ||
| < | < | ||
| - | tests/ | + | TEST_FILES_PATH=./ |
| </ | </ | ||
| - | still exists. | + | and that the committed marker file '' |
| - | + | ||
| - | Do not create | + | |
| ==== Mailpit is shown as unhealthy ==== | ==== Mailpit is shown as unhealthy ==== | ||
| - | Ignore the Docker health label initially | + | Ignore the Docker health label and check the endpoints the test actually |
| - | + | ||
| - | The regression | + | |
| < | < | ||
| Line 623: | Line 781: | ||
| </ | </ | ||
| - | If those endpoints | + | If those work, the mail test can pass even while Docker reports |
| - | ==== CLI test cannot see a fixture | + | ==== A CLI test cannot see its fixture ==== |
| - | A real CLI subprocess | + | A real CLI subprocess |
| - | + | ||
| - | Create the prerequisite through the CLI subprocess itself or use data that belongs to the committed | + | |
| ==== A test leaves data behind ==== | ==== A test leaves data behind ==== | ||
| - | Normal | + | Changes made through |
| - | Mutating subprocess changes are committed independently and must therefore be explicitly removed by the test. | + | ==== Code coverage is empty ==== |
| - | Filesystem changes must be registered for cleanup through | + | '' |
| - | ===== Before submitting a pull request ===== | + | ==== Windows notes ==== |
| - | Run the focused tests while developing, then run: | + | '' |
| - | <code bash> | + | ===== See also ===== |
| - | composer test:all | + | |
| - | </ | + | |
| - | A successful regression run does not replace code review. Reviewers should still check whether the new tests exercise the correct Admidio production path and whether important error, permission | + | * [[en: |
| + | * '' | ||
| + | * '' | ||
| - | The goal of the regression suite is not to maximize | + | The goal of the regression suite is not to maximise |