Skip to content

Commit

Permalink
Add better tests for HasOne.
Browse files Browse the repository at this point in the history
Use fixtures and add integration tests for the association. When we
first built the Association classes we focused on unit tests. Over time
I've found them harder to maintain and update as they are tightly
coupled to the implementation. Fixture based integration tests seem to
have aged better.
  • Loading branch information
markstory committed Feb 27, 2017
1 parent 3faa0c2 commit 0fc7f57
Show file tree
Hide file tree
Showing 2 changed files with 177 additions and 96 deletions.
57 changes: 57 additions & 0 deletions tests/Fixture/ProfilesFixture.php
@@ -0,0 +1,57 @@
<?php
/**
* CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
* Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
*
* Licensed under The MIT License
* For full copyright and license information, please see the LICENSE.txt
* Redistributions of files must retain the above copyright notice
*
* @copyright Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
* @link http://cakephp.org CakePHP(tm) Project
* @since 3.5.0
* @license http://www.opensource.org/licenses/mit-license.php MIT License
*/
namespace Cake\Test\Fixture;

use Cake\TestSuite\Fixture\TestFixture;

/**
* ProfileFixture
*/
class ProfilesFixture extends TestFixture
{

/**
* fields property
*
* @var array
*/
public $fields = [
'id' => ['type' => 'integer'],
'user_id' => ['type' => 'integer', 'null' => false],
'first_name' => ['type' => 'string', 'null' => true],
'last_name' => ['type' => 'string', 'null' => true],
'is_active' => ['type' => 'boolean', 'null' => false, 'default' => true],
'_constraints' => [
'primary' => ['type' => 'primary', 'columns' => ['id']],
'user_idx' => [
'type' => 'foreign',
'columns' => ['user_id'],
'references' => ['users', 'id']
]
]
];

/**
* records property
*
* @var array
*/
public $records = [
['user_id' => 1, 'first_name' => 'mariano', 'last_name' => 'iglesias', 'is_active' => false],
['user_id' => 2, 'first_name' => 'nate', 'last_name' => 'abele', 'is_active' => false],
['user_id' => 3, 'first_name' => 'larry', 'last_name' => 'masters', 'is_active' => true],
['user_id' => 4, 'first_name' => 'garrett', 'last_name' => 'woodworth', 'is_active' => false],
];
}
216 changes: 120 additions & 96 deletions tests/TestCase/ORM/Association/HasOneTest.php
Expand Up @@ -27,6 +27,13 @@
*/
class HasOneTest extends TestCase
{
/**
* Fixtures to load
*
* @var array
*/
public $fixtures = ['core.users', 'core.profiles'];

/**
* Set up
*
Expand All @@ -35,36 +42,8 @@ class HasOneTest extends TestCase
public function setUp()
{
parent::setUp();
$this->user = TableRegistry::get('Users', [
'schema' => [
'id' => ['type' => 'integer'],
'username' => ['type' => 'string'],
'_constraints' => [
'primary' => ['type' => 'primary', 'columns' => ['id']]
]
]
]);
$this->profile = TableRegistry::get('Profiles', [
'schema' => [
'id' => ['type' => 'integer'],
'first_name' => ['type' => 'string'],
'user_id' => ['type' => 'integer'],
'_constraints' => [
'primary' => ['type' => 'primary', 'columns' => ['id']]
]
]
]);
$this->profilesTypeMap = new TypeMap([
'Profiles.id' => 'integer',
'id' => 'integer',
'Profiles.first_name' => 'string',
'first_name' => 'string',
'Profiles.user_id' => 'integer',
'user_id' => 'integer',
'Profiles__first_name' => 'string',
'Profiles__user_id' => 'integer',
'Profiles__id' => 'integer',
]);
$this->user = TableRegistry::get('Users');
$this->profile = TableRegistry::get('Profiles');
}

/**
Expand Down Expand Up @@ -112,40 +91,21 @@ public function testCanBeJoined()
*/
public function testAttachTo()
{
$query = $this->getMockBuilder('\Cake\ORM\Query')
->setMethods(['join', 'select'])
->setConstructorArgs([null, null])
->getMock();
$config = [
'foreignKey' => 'user_id',
'sourceTable' => $this->user,
'targetTable' => $this->profile,
'property' => 'profile',
'joinType' => 'INNER',
'conditions' => ['Profiles.is_active' => true]
];
$association = new HasOne('Profiles', $config);
$field = new IdentifierExpression('Profiles.user_id');
$query->expects($this->once())->method('join')->with([
'Profiles' => [
'conditions' => new QueryExpression([
'Profiles.is_active' => true,
['Users.id' => $field],
], $this->profilesTypeMap),
'type' => 'LEFT',
'table' => 'profiles'
]
]);
$query->expects($this->once())->method('select')->with([
'Profiles__id' => 'Profiles.id',
'Profiles__first_name' => 'Profiles.first_name',
'Profiles__user_id' => 'Profiles.user_id'
]);
$query = $this->user->find();
$association->attachTo($query);

$this->assertEquals(
'string',
$query->typeMap()->type('Profiles__first_name'),
'Associations should map types.'
);
$results = $query->order('Users.id')->toArray();
$this->assertCount(1, $results, 'Only one record because of conditions & join type');
$this->assertSame('masters', $results[0]->Profiles['last_name']);
}

/**
Expand All @@ -155,29 +115,15 @@ public function testAttachTo()
*/
public function testAttachToNoFields()
{
$query = $this->getMockBuilder('\Cake\ORM\Query')
->setMethods(['join', 'select'])
->setConstructorArgs([null, null])
->getMock();
$config = [
'sourceTable' => $this->user,
'targetTable' => $this->profile,
'conditions' => ['Profiles.is_active' => true]
];
$association = new HasOne('Profiles', $config);
$field = new IdentifierExpression('Profiles.user_id');
$query->expects($this->once())->method('join')->with([
'Profiles' => [
'conditions' => new QueryExpression([
'Profiles.is_active' => true,
['Users.id' => $field],
], $this->profilesTypeMap),
'type' => 'LEFT',
'table' => 'profiles'
]
]);
$query->expects($this->never())->method('select');
$query = $this->user->query();
$association->attachTo($query, ['includeFields' => false]);
$this->assertEmpty($query->clause('select'));
}

/**
Expand All @@ -188,26 +134,45 @@ public function testAttachToNoFields()
*/
public function testAttachToMultiPrimaryKey()
{
$query = $this->getMockBuilder('\Cake\ORM\Query')
->setMethods(['join', 'select'])
->setConstructorArgs([null, null])
->getMock();
$selectTypeMap = new TypeMap([
'Profiles.id' => 'integer',
'id' => 'integer',
'Profiles.first_name' => 'string',
'first_name' => 'string',
'Profiles.user_id' => 'integer',
'user_id' => 'integer',
'Profiles__first_name' => 'string',
'Profiles__user_id' => 'integer',
'Profiles__id' => 'integer',
'Profiles__last_name' => 'string',
'Profiles.last_name' => 'string',
'last_name' => 'string',
'Profiles__is_active' => 'boolean',
'Profiles.is_active' => 'boolean',
'is_active' => 'boolean',
]);
$config = [
'sourceTable' => $this->user,
'targetTable' => $this->profile,
'conditions' => ['Profiles.is_active' => true],
'foreignKey' => ['user_id', 'user_site_id']
];

$this->user->primaryKey(['id', 'site_id']);
$association = new HasOne('Profiles', $config);

$query = $this->getMockBuilder('\Cake\ORM\Query')
->setMethods(['join', 'select'])
->setConstructorArgs([null, null])
->getMock();
$field1 = new IdentifierExpression('Profiles.user_id');
$field2 = new IdentifierExpression('Profiles.user_site_id');
$query->expects($this->once())->method('join')->with([
'Profiles' => [
'conditions' => new QueryExpression([
'Profiles.is_active' => true,
['Users.id' => $field1, 'Users.site_id' => $field2],
], $this->profilesTypeMap),
], $selectTypeMap),
'type' => 'LEFT',
'table' => 'profiles'
]
Expand All @@ -224,7 +189,7 @@ public function testAttachToMultiPrimaryKey()
* @expectedExceptionMessage Cannot match provided foreignKey for "Profiles", got "(user_id)" but expected foreign key for "(id, site_id)"
* @return void
*/
public function testAttachToMultiPrimaryKeyMistmatch()
public function testAttachToMultiPrimaryKeyMismatch()
{
$query = $this->getMockBuilder('\Cake\ORM\Query')
->setMethods(['join', 'select'])
Expand Down Expand Up @@ -289,12 +254,9 @@ public function testPropertyOption()
*/
public function testPropertyNoPlugin()
{
$mock = $this->getMockBuilder('Cake\ORM\Table')
->disableOriginalConstructor()
->getMock();
$config = [
'sourceTable' => $this->user,
'targetTable' => $mock,
'targetTable' => $this->profile,
];
$association = new HasOne('Contacts.Profiles', $config);
$this->assertEquals('profile', $association->property());
Expand All @@ -308,28 +270,24 @@ public function testPropertyNoPlugin()
*/
public function testAttachToBeforeFind()
{
$query = $this->getMockBuilder('\Cake\ORM\Query')
->setMethods(['join', 'select'])
->setConstructorArgs([null, null])
->getMock();
$config = [
'foreignKey' => 'user_id',
'sourceTable' => $this->user,
'targetTable' => $this->profile,
];
$listener = $this->getMockBuilder('stdClass')
->setMethods(['__invoke'])
->getMock();
$this->profile->eventManager()->attach($listener, 'Model.beforeFind');
$query = $this->user->query();

$this->listenerCalled = false;
$this->profile->eventManager()->on('Model.beforeFind', function($event, $query, $options, $primary) {
$this->listenerCalled = true;
$this->assertInstanceOf('\Cake\Event\Event', $event);
$this->assertInstanceOf('\Cake\ORM\Query', $query);
$this->assertInstanceOf('\ArrayObject', $options);
$this->assertFalse($primary);
});
$association = new HasOne('Profiles', $config);
$listener->expects($this->once())->method('__invoke')
->with(
$this->isInstanceOf('\Cake\Event\Event'),
$this->isInstanceOf('\Cake\ORM\Query'),
$this->isInstanceOf('\ArrayObject'),
false
);
$association->attachTo($query);
$this->assertTrue($this->listenerCalled, 'beforeFind event not fired.');
}

/**
Expand Down Expand Up @@ -366,4 +324,70 @@ public function testAttachToBeforeFindExtraOptions()
return $q->applyOptions(['something' => 'more']);
}]);
}

/**
* Test cascading deletes.
*
* @return void
*/
public function testCascadeDelete()
{
$config = [
'dependent' => true,
'sourceTable' => $this->user,
'targetTable' => $this->profile,
'conditions' => ['Profiles.is_active' => true],
'cascadeCallbacks' => false,
];
$association = new HasOne('Profiles', $config);

$this->profile->eventManager()->on('Model.beforeDelete', function () {
$this->fail('Callbacks should not be triggered when callbacks do not cascade.');
});

$entity = new Entity(['id' => 1]);
$association->cascadeDelete($entity);

$query = $this->profile->query()->where(['user_id' => 1]);
$this->assertEquals(1, $query->count(), 'Left non-matching row behind');

$query = $this->profile->query()->where(['user_id' => 3]);
$this->assertEquals(1, $query->count(), 'other records left behind');

$user = new Entity(['id' => 3]);
$this->assertTrue($association->cascadeDelete($user));
$query = $this->profile->query()->where(['user_id' => 3]);
$this->assertEquals(0, $query->count(), 'Matching record was deleted.');
}

/**
* Test cascading delete with has many.
*
* @return void
*/
public function testCascadeDeleteCallbacks()
{
$config = [
'dependent' => true,
'sourceTable' => $this->user,
'targetTable' => $this->profile,
'conditions' => ['Profiles.is_active' => true],
'cascadeCallbacks' => true,
];
$association = new HasOne('Profiles', $config);

$user = new Entity(['id' => 1]);
$this->assertTrue($association->cascadeDelete($user));

$query = $this->profile->query()->where(['user_id' => 1]);
$this->assertEquals(1, $query->count(), 'Left non-matching row behind');

$query = $this->profile->query()->where(['user_id' => 3]);
$this->assertEquals(1, $query->count(), 'other records left behind');

$user = new Entity(['id' => 3]);
$this->assertTrue($association->cascadeDelete($user));
$query = $this->profile->query()->where(['user_id' => 3]);
$this->assertEquals(0, $query->count(), 'Matching record was deleted.');
}
}

0 comments on commit 0fc7f57

Please sign in to comment.