Add tests to kill escaped mutants (#529)#545
Add tests to kill escaped mutants (#529)#545dbuhonov wants to merge 55 commits intoyiisoft:masterfrom
Conversation
dbuhonov
commented
Mar 13, 2026
| Q | A |
|---|---|
| Is bugfix? | ❌ |
| New feature? | ❌ |
| Breaks BC? | ❌ |
| Fixed issues | part of #529 |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #545 +/- ##
=============================================
- Coverage 100.00% 99.93% -0.07%
Complexity 655 655
=============================================
Files 43 43
Lines 1629 1605 -24
=============================================
- Hits 1629 1604 -25
- Misses 0 1 +1 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
ec926d0 to
27f1743
Compare
… in EventsTraitTest.
…multiple test files.
…nd `RelationPopulator`
… behavior in RelationPopulator
There was a problem hiding this comment.
Pull request overview
This PR aims to increase mutation testing score (Issue #529) by adding/adjusting unit tests around ActiveRecord/ActiveQuery edge-cases (relations population/reset, upsert behavior, event dispatching, array access, etc.) and updating Infection configuration.
Changes:
- Added new tests covering previously untested branches/edge-cases in ActiveRecord, ActiveQuery, EventsTrait, Arrayable/ArrayAccess traits, RepositoryTrait, and MagicActiveRecord behavior.
- Introduced additional test stubs to exercise deep
via()relations, event-enabled models, and ArrayAccess edge-cases involving name collisions between properties and relations. - Updated
infection.json.distto ignore several specific mutators/locations.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Stubs/MagicActiveRecord/CategoryWithNameRelationArrayAccess.php | New stub to test ArrayAccess behavior when a relation name collides with a column/property name. |
| tests/Stubs/ActiveRecord/OrderItemWithDeepViaProfile.php | New stub to test deep via() chains and relation population behavior. |
| tests/Stubs/ActiveRecord/CustomerEventsModel.php | New stub enabling EventsTrait on a Customer derivative for event dispatch tests. |
| tests/Stubs/ActiveRecord/OrderItemWithConstructor.php | Doc comment fix to match the class name. |
| tests/RepositoryTraitTest.php | Adds coverage for find() behavior when condition is null (params should be ignored). |
| tests/MagicActiveRecordTest.php | Adds coverage for new-record save paths and property capability checks. |
| tests/EventsTraitTest.php | Adds coverage for delete prevention and “after” event dispatching + handler behavior. |
| tests/Driver/Pgsql/ActiveQueryTest.php | Adds PG-specific assertions for ModelRelationFilter condition types (array/json/scalar). |
| tests/ArrayableTraitTest.php | Adds coverage for extraFields() behavior when relations are populated. |
| tests/ArrayAccessTraitTest.php | Expands coverage for ArrayAccess edge-cases (property vs relation, magic properties, dependent-relation reset). |
| tests/ActiveRecordTest.php | Adds multiple tests for relation reset, populateProperties behavior, cloning semantics, linking/unlinking edge-cases, counters, overrides, and upsert defaults. |
| tests/ActiveQueryTest.php | Adds broad coverage around helper internals, joinWith parsing/behavior, relation population, via behavior, cloning, and filter application. |
| infection.json.dist | Adds multiple mutator ignore rules / regex-based ignores. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| "ConcatOperandRemoval": { | ||
| "ignore": [ | ||
| "Yiisoft\\ActiveRecord\\Trait\\MagicRelationsTrait::relationQuery", | ||
| "Yiisoft\\ActiveRecord\\Trait\\MagicRelationsTrait::relationNames" | ||
| ] | ||
| }, | ||
| "FalseValue": { | ||
| "ignore": [ | ||
| "Yiisoft\\ActiveRecord\\Event\\Handler\\SetValueOnUpdate::beforeUpsert" | ||
| ] | ||
| }, | ||
| "LogicalAnd": { | ||
| "ignore": [ | ||
| "Yiisoft\\ActiveRecord\\Internal\\JoinsWithBuilder::joinWithRelations" | ||
| ] | ||
| }, | ||
| "MatchArmRemoval": { | ||
| "ignore": [ | ||
| "Yiisoft\\ActiveRecord\\Event\\Handler\\SetValueOnUpdate::beforeUpsert" | ||
| ] | ||
| }, | ||
| "MethodCallRemoval": { | ||
| "ignore": [ | ||
| "Yiisoft\\ActiveRecord\\Internal\\JoinsWithBuilder::build" | ||
| ] | ||
| }, | ||
| "TrueValue": { | ||
| "ignore": [ | ||
| "Yiisoft\\ActiveRecord\\ActiveRecord::upsertInternal" | ||
| ], | ||
| "ignoreSourceCodeByRegex": [ | ||
| "protected function upsertInternal\\(\\?array \\$insertProperties = null, array\\|bool \\$updateProperties = true\\): void" | ||
| ] | ||
| }, | ||
| "UnwrapArrayValues": { | ||
| "ignore": [ | ||
| "Yiisoft\\ActiveRecord\\Internal\\JoinsWithBuilder::build" | ||
| ] | ||
| } |
There was a problem hiding this comment.
These exclusions are for survivors that are not obvious from the test diff itself, so I moved them into infection.json.dist intentionally to make them explicit and visible rather than leaving them hidden in the report only.
…ActiveRecordTest`.
There was a problem hiding this comment.
Pull request overview
This PR adds/extends PHPUnit coverage across ActiveRecord, ActiveQuery, events, array access/arrayable behavior, and several internal helpers to improve mutation score (Issue #529) by exercising previously untested branches and edge-cases.
Changes:
- Added many new tests around relation population/reset, upsert/link/unlink edge cases, event dispatching, and query/join/via behaviors.
- Introduced multiple new test stub ActiveRecord/ActiveQuery classes to support targeted scenarios (deep via relations, composite PKs, overridden internals, etc.).
- Updated Infection configuration with explicit mutator ignores for specific survivors.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/Stubs/MagicActiveRecord/CategoryWithNameRelationArrayAccess.php | New stub for ArrayAccess + magic relation name collision scenarios. |
| tests/Stubs/ActiveRecord/OrderWithCustomerProfileViaCustomerRelation.php | New stub to exercise nested “via” relation dependency handling. |
| tests/Stubs/ActiveRecord/OrderItemWithDeepViaProfile.php | New stub to cover deep via chain relation population. |
| tests/Stubs/ActiveRecord/OrderItemWithConstructor.php | Docblock fix for stub class name. |
| tests/Stubs/ActiveRecord/EmployeeWithPrototypeDossierRelation.php | New stub for prototype-cloned relation target and stricter PK checks. |
| tests/Stubs/ActiveRecord/CustomerWithUpdateInternalOverride.php | New stub to test overridable updateInternal(). |
| tests/Stubs/ActiveRecord/CustomerWithRefreshInternalOverride.php | New stub to test overridable refreshInternal(). |
| tests/Stubs/ActiveRecord/CustomerWithOverriddenRelationQuery.php | New stub to test overridable relation query creation. |
| tests/Stubs/ActiveRecord/CustomerWithDeleteInternalOverride.php | New stub to test overridable deleteInternal(). |
| tests/Stubs/ActiveRecord/CustomerSetValueOnUpdateUpsert.php | New stub for SetValueOnUpdate behavior during upsert. |
| tests/Stubs/ActiveRecord/CustomerEventsModel.php | New stub enabling EventsTrait on Customer. |
| tests/Stubs/ActiveRecord/CompositePrimaryKeyDossier.php | New stub for composite PK relation/linking scenarios. |
| tests/Stubs/ActiveQuery/SingleModelArrayActiveQuery.php | New stub to ensure one() path doesn’t fall back to all(). |
| tests/Stubs/ActiveQuery/OverriddenPrimaryTableNameActiveQuery.php | New stub to cover overriding primary table resolution. |
| tests/Stubs/ActiveQuery/OverriddenCreateModelsActiveQuery.php | New stub to cover overriding model creation from rows. |
| tests/Stubs/ActiveQuery/MissingLinkValuesActiveQuery.php | New stub for missing link values during relation population. |
| tests/Stubs/ActiveQuery/CreateModelsExceptionOnEmptyRowsActiveQuery.php | New stub ensuring empty populate doesn’t call createModels(). |
| tests/Stubs/ActiveQuery/AlternativeActiveQuery.php | New stub for alternate ActiveQuery implementation. |
| tests/RepositoryTraitTest.php | Added coverage for find() behavior with null condition/params handling. |
| tests/MagicActiveRecordTest.php | Added tests for save/update behavior and property accessor edge-cases. |
| tests/EventsTraitTest.php | Expanded event dispatch/handler behavior coverage (after-events, soft delete, defaults, etc.). |
| tests/Driver/Pgsql/ActiveQueryTest.php | Added PG-specific assertions for ModelRelationFilter conditions. |
| tests/ArrayableTraitTest.php | Added extraFields() coverage. |
| tests/ArrayAccessTraitTest.php | Expanded ArrayAccess tests for properties vs relations and magic properties. |
| tests/ActiveRecordTest.php | Added extensive relation reset/dependency, PK error message, upsert/link/unlink/counters, override hooks tests. |
| tests/ActiveQueryTest.php | Added extensive coverage for populate/dedup, joinWith validation, viaTable callable, relation population internals. |
| infection.json.dist | Added explicit mutator ignore configuration for remaining survivors. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
tests/Stubs/ActiveRecord/CustomerWithRefreshInternalOverride.php
Outdated
Show resolved
Hide resolved
tests/Stubs/ActiveRecord/CustomerWithOverriddenRelationQuery.php
Outdated
Show resolved
Hide resolved
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…arity and coverage.
|
Up review pls |
|
|
||
| $dossier = clone $dossierPrototype; | ||
| $dossier->set('summary', 'Strict primary key validation'); | ||
| if ($this->db()->getDriverName() !== 'sqlsrv') { |
|
|
||
| $this->assertSame(2, $dossier->get('department_id')); | ||
| $this->assertSame(2, $dossier->get('employee_id')); | ||
| $this->assertNotNull($dossier->get('id')); |
| public function testMarkPropertyChangedWithEmptyNameDoesNotModifyOldValues(): void | ||
| { | ||
| $customer = new Customer(); | ||
| $customer->assignOldValues(['' => 'sentinel', 'name' => 'user1']); | ||
|
|
||
| $customer->markPropertyChanged(''); | ||
|
|
||
| $this->assertSame(['' => 'sentinel', 'name' => 'user1'], $customer->oldValues()); | ||
| } |
There was a problem hiding this comment.
Perhaps this is a bug and $customer->markPropertyChanged(''); should change it.
| public function testCreateRelationQueryCanBeOverridden(): void | ||
| { | ||
| $customer = new CustomerWithOverriddenRelationQuery(); | ||
|
|
||
| $query = $customer->relationQuery('profile'); | ||
|
|
||
| $this->assertNotSame(ActiveQuery::class, $query::class); | ||
| } | ||
|
|
||
| public function testCreateRelationQueryIsProtected(): void | ||
| { | ||
| $method = new ReflectionMethod(AbstractActiveRecord::class, 'createRelationQuery'); | ||
|
|
||
| $this->assertTrue($method->isProtected()); | ||
| } | ||
|
|
||
| public function testDeleteInternalCanBeOverridden(): void | ||
| { | ||
| $this->reloadFixtureAfterTest(); | ||
|
|
||
| $customer = CustomerWithDeleteInternalOverride::query()->findByPk(1); | ||
|
|
||
| $this->assertSame(77, $customer->delete()); | ||
| $this->assertNotNull(Customer::query()->findByPk(1)); | ||
| } | ||
|
|
||
| public function testRefreshInternalCanBeOverridden(): void | ||
| { | ||
| $customer = CustomerWithRefreshInternalOverride::query()->findByPk(1); | ||
|
|
||
| $this->assertTrue($customer->refresh()); | ||
| $this->assertSame('refreshed-via-override', $customer->getName()); | ||
| } | ||
|
|
||
| public function testUpdateInternalCanBeOverridden(): void | ||
| { | ||
| $this->reloadFixtureAfterTest(); | ||
|
|
||
| $customer = CustomerWithUpdateInternalOverride::query()->findByPk(1); | ||
|
|
||
| $customer->setName('should-not-hit-db'); | ||
|
|
||
| $this->assertSame(91, $customer->update()); | ||
| $this->assertSame('user1', Customer::query()->findByPk(1)->getName()); | ||
| } |
There was a problem hiding this comment.
This is where the fundamentals of OOP (inheritance) are tested. This looks redundant, but if it is necessary, perhaps it would be better to create a separate class in which to define all methods which can be overridden.
| public function testOffsetExistsDoesNotTreatPropertyAsRelation(): void | ||
| { | ||
| $model = new CustomerArrayAccessModel(); | ||
| $model->name = 'test'; | ||
|
|
||
| $this->assertTrue(isset($model['name'])); | ||
| $this->assertFalse($model->isRelationPopulated('name')); | ||
| } |
There was a problem hiding this comment.
The test looks like testOffsetGetReturnsPropertyValue()
| public function testOffsetExistsDoesNotTreatPropertyAsRelation(): void | |
| { | |
| $model = new CustomerArrayAccessModel(); | |
| $model->name = 'test'; | |
| $this->assertTrue(isset($model['name'])); | |
| $this->assertFalse($model->isRelationPopulated('name')); | |
| } |
| $this->assertSame('customer c!', $tableName); | ||
| $this->assertSame('customer c!', $alias); |
| $this->assertSame($from, $tableName); | ||
| $this->assertSame($from, $alias); |
| { | ||
| $query = Customer::query(); | ||
| $method = new ReflectionMethod(ActiveQuery::class, 'removeDuplicatedRows'); | ||
| $method->setAccessible(true); |
There was a problem hiding this comment.
This is not required
https://www.php.net/manual/en/reflectionmethod.setaccessible.php
Note: As of PHP 8.1.0, calling this method has no effect; all methods are invokable by default.
| $method->setAccessible(true); |
| public function testCreateModelsCanBeOverridden(): void | ||
| { | ||
| $query = new OverriddenCreateModelsActiveQuery(Customer::class); | ||
|
|
||
| $this->assertSame( | ||
| [['overridden' => true, 'rows' => [['id' => 1]]]], | ||
| $query->populate([['id' => 1]]), | ||
| ); | ||
| } | ||
|
|
||
| public function testGetPrimaryTableNameCanBeOverridden(): void | ||
| { | ||
| $query = new OverriddenPrimaryTableNameActiveQuery(Customer::class); | ||
|
|
||
| $this->assertSame(['{{profile}}' => '{{profile}}'], $query->getTablesUsedInFrom()); | ||
| } |
There was a problem hiding this comment.
This is where the fundamentals of OOP (inheritance) are tested. This looks redundant, but if it is necessary, perhaps it would be better to create a separate class in which to define all methods which can be overridden.
| public function testModelRelationFilterCompositeArrayModelsUseQualifiedColumnNames(): void | ||
| { | ||
| $query = Dossier::query() | ||
| ->from(['d' => 'dossier']) | ||
| ->join('INNER JOIN', 'employee e', '1=1') | ||
| ->link(['department_id' => 'department_id', 'employee_id' => 'employee_id']); | ||
|
|
||
| ModelRelationFilter::apply($query, [ | ||
| ['department_id' => 2], | ||
| ]); | ||
|
|
||
| $where = $query->getWhere(); | ||
|
|
||
| $this->assertInstanceOf(In::class, $where); | ||
| $this->assertSame( | ||
| [['d.department_id' => 2, 'd.employee_id' => null]], | ||
| array_values($where->values), | ||
| ); | ||
| } |
There was a problem hiding this comment.
It looks like testModelRelationFilterCompositeKeysFillMissingValuesWithNull()