@@ -197,7 +197,7 @@ and can easily be run from project's root:
197197- Run the full test suite:
198198
199199 ` ` ` bash
200- composer run-tests
200+ composer run-all- tests
201201 ` ` `
202202
203203# # Coding practices
@@ -253,3 +253,194 @@ class ArrayAppend extends BaseFunction
253253⚠️ ** Beware:** you cannot use **?** (e.g. the ` ?? ` operator) as part of any
254254function prototype in Doctrine.
255255It causes query parsing failures.
256+
257+
258+ # # Testing: Patterns and Guidelines
259+
260+ This project has a rich, well-structured test suite consisting of fast unit tests and database-backed integration tests. Please follow the conventions below when adding or modifying tests.
261+
262+ # ## Tools and how to run tests
263+ - Framework: PHPUnit 10 (PHP attributes like # [Test], #[DataProvider])
264+ - Static analysis: PHPStan (+ doctrine + phpunit extensions)
265+ - Architecture checks: deptrac
266+ - Code style and refactoring: PHP-CS-Fixer, Rector
267+
268+ Composer scripts:
269+ - Run unit tests: ` composer run-unit-tests` (uses ci/phpunit/config-unit.xml)
270+ - Run integration tests: ` composer run-integration-tests` (uses ci/phpunit/config-integration.xml)
271+ - Run both suites: ` composer run-all-tests`
272+ - Static analysis: ` composer run-static-analysis`
273+
274+ Integration tests require a PostgreSQL with PostGIS:
275+ - Easiest: Docker Compose
276+ - Start: ` docker-compose up -d`
277+ - Stop: ` docker-compose down -v`
278+ - Alternatively (dev shell): ` devenv up`
279+ - See tests/Integration/README.md for environment variables and details
280+
281+ Coverage reports are written to var/logs/test-coverage/{unit| integration}/.
282+
283+ # ## Choosing unit vs. integration tests
284+ - Prefer unit tests for:
285+ - Pure value objects and small utilities (no DB/ORM)
286+ - Doctrine DBAL Type conversions (PHP < -> database string) using an AbstractPlatform mock
287+ - DQL AST function SQL generation (no DB round-trip)
288+ - Prefer integration tests for:
289+ - Verifying DBAL types round-trip correctly against a real PostgreSQL
290+ - DQL functions/operators evaluated end-to-end against PostgreSQL
291+ - Scenarios relying on PostGIS or PostgreSQL-specific behavior
292+
293+ Keep unit tests fast and deterministic; use integration tests to validate behavior against the real database.
294+
295+ # ## Unit test patterns and conventions
296+ - Location: tests/Unit/...
297+ - Naming:
298+ - Class names end with ` Test` (e.g., ` PointTest` , ` CidrTest` )
299+ - One file/class per subject
300+ - Concrete tests may be ` final`
301+ - Structure:
302+ - Extend ` PHPUnit\F ramework\T estCase` or an existing base test class in Unit when available
303+ - Use `setUp ()` to create an ` AbstractPlatform` mock and the subject under test when testing DBAL Types
304+ - Use ` # [DataProvider]` for bidirectional transformation scenarios (one provider used for both PHP->DB and DB->PHP tests)
305+ - Assertions:
306+ - Use domain-specific exceptions in negative tests (e.g., ` InvalidCidrForPHPException` , ` InvalidRangeForDatabaseException` )
307+ - Prefer dedicated assertion helpers provided by base classes (e.g., range equality helpers) when available
308+ - Value Object Range tests:
309+ - Reuse base classes:
310+ - ` tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/BaseRangeTestCase`
311+ - ` tests/Unit/MartinGeorgiev/Doctrine/DBAL/Types/ValueObject/BaseTimestampRangeTestCase`
312+ - Implement the abstract factory/expectation methods and provide concise data providers
313+ - DBAL Type unit tests:
314+ - Follow patterns from ` PointTest` , ` CidrTest` , ` JsonbTest`
315+ - Test name retrieval (` getName()` ), conversions in both directions, and invalid inputs
316+ - DQL AST unit tests:
317+ - Use ` tests/Unit/.../ORM/Query/AST/Functions/TestCase` to assert DQL -> SQL transformation
318+
319+ Anti-patterns to avoid in unit tests:
320+ - No echo/print statements
321+ - Avoid Reflection; test through public APIs
322+ - Avoid raw/native SQL when verifying DBAL Types; prefer conversion tests with platform mock
323+ - Avoid excessive PHPStan suppression; prefer PHPDoc over ` @phpstan-ignore`
324+
325+ # ## Integration test patterns and organization
326+ - Location: tests/Integration/...
327+ - Base infrastructure:
328+ - Extend ` tests/Integration/MartinGeorgiev/TestCase`
329+ - Sets up Doctrine ORM config, connection, schema ` test` , caches, and ensures PostGIS
330+ - Provides helpers to create/drop tables, run DQL, and assert results
331+ - Use specialized base classes based on what you test:
332+ - Array types: ` ArrayTypeTestCase`
333+ - Scalar types: ` ScalarTypeTestCase`
334+ - Range types: ` RangeTypeTestCase` (includes operator tests and ` assertRangeEquals` )
335+ - Spatial arrays: ` SpatialArrayTypeTestCase` (ARRAY[...] insertion for WKT)
336+ - Per-type organization:
337+ - One integration test class per DBAL Type (e.g., ` MacaddrTypeTest` , ` JsonbTypeTest` , ` IntegerArrayTypeTest` )
338+ - Implement:
339+ - ` protected function getTypeName(): string` (Doctrine type name)
340+ - ` protected function getPostgresTypeName(): string` (column type)
341+ - Data-driven tests:
342+ - Provide a ` provideValidTransformations()` where applicable
343+ - Use ` runDbalBindingRoundTrip($typeName , $columnType , $value )` for round-trips
344+ - Range integration tests:
345+ - Extend ` RangeTypeTestCase`
346+ - Provide a data provider for range values and add ` # [DataProvider('provideValidTransformations')]` to `can_handle_range_values()`
347+ - Optionally add operator scenarios via ` provideOperatorScenarios()` returning [name, DQL, expectedIds]
348+
349+ Anti-patterns to avoid in integration tests:
350+ - Do not modify or rely on global/public schema; tests use the dedicated ` test` schema created per test run
351+ - Avoid changing existing shared fixtures/data unless strictly necessary; prefer adding new, focused fixtures
352+ - No echo/print statements
353+
354+ # ## Base test classes and shared utilities
355+ Commonly used bases and what they provide:
356+ - Unit (Value Objects):
357+ - ` BaseRangeTestCase` : creation/formatting/boundary tests with abstract factory methods
358+ - ` BaseTimestampRangeTestCase` : extends the above with timestamp-specific boundary and helpers
359+ - Unit (DBAL Types):
360+ - ` tests/Unit/.../DBAL/Types/BaseRangeTestCase` : negative cases and conversions for range DBAL types
361+ - Unit (ORM functions):
362+ - ` tests/Unit/.../ORM/Query/AST/Functions/TestCase` : DQL to SQL transformation checks
363+ - Integration (DBAL Types):
364+ - ` TestCase` : connection/schema setup, round-trip helper, assertions
365+ - ` ArrayTypeTestCase` , ` ScalarTypeTestCase` , ` RangeTypeTestCase` , ` SpatialArrayTypeTestCase`
366+
367+ Prefer extending these base classes over duplicating setup/utility code.
368+
369+ # ## Exception handling and error testing
370+ - Use domain-specific exceptions consistently:
371+ - ` convertToPHPValue()` should throw ` ...ForPHPException`
372+ - ` convertToDatabaseValue()` should throw ` ...ForDatabaseException`
373+ - In tests, assert the exact exception class and include ` expectExceptionMessage()` when the message is part of the contract
374+ - For range equality in integration, use the provided ` assertRangeEquals()` which compares string representation and emptiness
375+
376+ # ## Test data and fixtures
377+ - Entities for integration live under ` fixtures/MartinGeorgiev/Doctrine/Entity` and are registered via attributes
378+ - If a new fixture is necessary, add it under the same namespace and keep it minimal and reusable
379+ - Range/operator tests seed their own tables (see ` RangeTypeTestCase` ’s `createRangeOperatorsTable ()` and insert helpers)
380+
381+ # ## Code style in test files
382+ - Use PHP attributes ` # [Test]` and `#[DataProvider]` (PHPUnit 10)
383+ - Descriptive dataset names in data providers improve failure readability
384+ - Prefer clear method names starting with verbs: ` can_...` , ` throws_...` , ` dql_is_...`
385+ - Keep tests small and focused; avoid commentary that restates code; write comments only to explain intent or PostgreSQL-specific behavior
386+ - Maintain alphabetical order in documentation blocks and lists when applicable
387+
388+ # ## Minimal examples
389+ Unit test for a DBAL Type (mock platform, bidirectional conversions):
390+
391+ ` ` ` php
392+ final class InetTest extends TestCase
393+ {
394+ /**
395+ * @var AbstractPlatform& MockObject
396+ * /
397+ private MockObject $platform ;
398+ private Inet $fixture ;
399+
400+ protected function setUp(): void
401+ {
402+ $this -> platform = $this -> createMock(AbstractPlatform::class);
403+ $this -> fixture = new Inet ();
404+ }
405+
406+ # [DataProvider('provideValidTransformations')]
407+ # [Test]
408+ public function can_transform_from_php_value(?string $php , ? string $pg ): void
409+ {
410+ $this -> assertEquals($pg , $this -> fixture-> convertToDatabaseValue($php , $this -> platform));
411+ }
412+
413+ # [DataProvider('provideValidTransformations')]
414+ # [Test]
415+ public function can_transform_to_php_value(?string $php , ? string $pg ): void
416+ {
417+ $this -> assertEquals($php , $this -> fixture-> convertToPHPValue($pg , $this -> platform));
418+ }
419+ }
420+ ` ` `
421+
422+ Integration test for an array type:
423+
424+ ` ` ` php
425+ final class TextArrayTypeTest extends ArrayTypeTestCase
426+ {
427+ protected function getTypeName(): string { return ' text[]' ; }
428+ protected function getPostgresTypeName(): string { return ' TEXT[]' ; }
429+ # [DataProvider('provideValidTransformations')] #[Test]
430+ public function can_handle_array_values(string $name , array $value ): void { parent::can_handle_array_values($name , $value ); }
431+ }
432+ ` ` `
433+
434+ Range integration test:
435+
436+ ` ` ` php
437+ final class Int4RangeTypeTest extends RangeTypeTestCase
438+ {
439+ protected function getTypeName(): string { return ' int4range' ; }
440+ protected function getPostgresTypeName(): string { return ' INT4RANGE' ; }
441+ # [DataProvider('provideValidTransformations')] #[Test]
442+ public function can_handle_range_values(string $name , RangeValueObject $range ): void { parent::can_handle_range_values($name , $range ); }
443+ }
444+ ` ` `
445+
446+ If unsure which base to extend or how to structure a new test, mirror a nearby, similar test and keep changes minimal and consistent with the patterns above.
0 commit comments