Skip to content
Snippets Groups Projects
Unverified Commit 6865b715 authored by Thomas Flori's avatar Thomas Flori
Browse files

test and refactor migrate prototype

parent fc653edd
No related branches found
No related tags found
No related merge requests found
...@@ -43,6 +43,7 @@ class Migrations ...@@ -43,6 +43,7 @@ class Migrations
$this->db = $db; $this->db = $db;
$this->path = rtrim($path, '/'); $this->path = rtrim($path, '/');
/** @codeCoverageIgnore the default resolver is a) trivial and b) not testable */
$this->resolver = $resolver ?? function ($class, ...$args) { $this->resolver = $resolver ?? function ($class, ...$args) {
if ($class === AdapterInterface::class) { if ($class === AdapterInterface::class) {
return new BasicAdapter(...$args); return new BasicAdapter(...$args);
...@@ -72,16 +73,10 @@ class Migrations ...@@ -72,16 +73,10 @@ class Migrations
public function migrate(string $file = null): bool public function migrate(string $file = null): bool
{ {
$status = $this->getStatus(); $status = $this->getStatus();
$toExecute = array_filter($status->migrations, function (Model\Migration $migration) { $toExecute = array_filter($status->migrations, function (Model\Migration $migration) use ($file) {
return $migration->status !== 'done'; return $migration->status !== 'done' && (!$file || strpos($migration->file, $file) !== false);
}); });
if ($file && !isset($toExecute[$file])) {
return true; // nothing to migrate?
} elseif ($file) {
$toExecute = [$toExecute[$file]];
}
/** /**
* @var string $file * @var string $file
* @var Model\Migration $migration * @var Model\Migration $migration
...@@ -97,9 +92,9 @@ class Migrations ...@@ -97,9 +92,9 @@ class Migrations
$this->saveMigration($migration, 'done', microtime(true) - $start); $this->saveMigration($migration, 'done', microtime(true) - $start);
$this->db->commit(); $this->db->commit();
} catch (\Exception $exception) { } catch (\PDOException $exception) {
$this->db->rollBack(); $this->db->rollBack();
$this->saveMigration($migration, 'failed', microtime(true) - $start); // $this->saveMigration($migration, 'failed', microtime(true) - $start);
return false; return false;
} }
} }
...@@ -129,7 +124,7 @@ class Migrations ...@@ -129,7 +124,7 @@ class Migrations
]); ]);
} }
protected function loadMigrations(): array protected function loadMigrations()
{ {
if (!$this->migrations) { if (!$this->migrations) {
$this->migrations = $this->findMigrations(); $this->migrations = $this->findMigrations();
...@@ -151,8 +146,6 @@ class Migrations ...@@ -151,8 +146,6 @@ class Migrations
// the table does not exist - so nothing to do here // the table does not exist - so nothing to do here
} }
} }
return $this->migrations;
} }
protected function findMigrations(): array protected function findMigrations(): array
...@@ -235,19 +228,19 @@ class Migrations ...@@ -235,19 +228,19 @@ class Migrations
return $migrations; return $migrations;
} }
protected function executeStatement(Model\Statement $statement) // protected function executeStatement(Model\Statement $statement)
{ // {
$start = microtime(true); // $start = microtime(true);
try { // try {
$statement->result = $this->db->exec($statement); // $statement->result = $this->db->exec($statement);
$statement->exception = null; // $statement->exception = null;
} catch (\PDOException $exception) { // } catch (\PDOException $exception) {
$statement->exception = $exception; // $statement->exception = $exception;
throw $exception; // throw $exception;
} finally { // } finally {
$statement->executionTime = microtime(true) - $start; // $statement->executionTime = microtime(true) - $start;
} // }
} // }
protected function getAdapter(): AdapterInterface protected function getAdapter(): AdapterInterface
{ {
...@@ -256,8 +249,8 @@ class Migrations ...@@ -256,8 +249,8 @@ class Migrations
$this->resolver, $this->resolver,
AdapterInterface::class, AdapterInterface::class,
function (Model\Statement $statement) { function (Model\Statement $statement) {
$this->statements[] = $statement; // $this->statements[] = $statement;
$this->executeStatement($statement); // $this->executeStatement($statement);
} }
); );
} }
......
...@@ -2,8 +2,12 @@ ...@@ -2,8 +2,12 @@
namespace Breyta\Test\Migrations; namespace Breyta\Test\Migrations;
use Breyta\AdapterInterface;
use Breyta\BasicAdapter;
use Breyta\Migration\CreateMigrationTable;
use Breyta\Migrations; use Breyta\Migrations;
use Breyta\Model\Migration; use Breyta\Model\Migration;
use Breyta\Test\Example\CreateAnimalsTable;
use Breyta\Test\TestCase; use Breyta\Test\TestCase;
use Mockery as m; use Mockery as m;
...@@ -25,26 +29,209 @@ class MigrateTest extends TestCase ...@@ -25,26 +29,209 @@ class MigrateTest extends TestCase
$resolver = $this->resolver = m::spy(function ($class, ...$args) { $resolver = $this->resolver = m::spy(function ($class, ...$args) {
return new $class(...$args); return new $class(...$args);
}); });
$migrations = $this->migrations = m::mock(Migrations::class, [$this->pdo, __DIR__ . '/../Example', $resolver]) $this->migrations = m::mock(Migrations::class, [$this->pdo, __DIR__ . '/../Example', $resolver])
->makePartial(); ->makePartial();
$resolver->shouldReceive('__invoke')->with(AdapterInterface::class, m::type(\Closure::class))
->andReturn(m::mock(BasicAdapter::class))->byDefault();
$this->mockPreparedStatement('/^insert into migrations/i', true);
$this->mockPreparedStatement('/^delete from migrations/i', true, 0);
}
/** @test */
public function returnsSuccessWhenNoMigrationsNeedToBeExecuted()
{
$this->mockStatus(Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'done'
]))->once();
$result = $this->migrations->migrate();
self::assertTrue($result);
}
/** @test */
public function executesNewMigrations()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$this->mockStatus(Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'new'
]))->once()->ordered();
$migration->shouldReceive('up')->with()->once()->ordered();
$result = $this->migrations->migrate();
self::assertTrue($result);
}
/** @test */
public function executesFailedMigrations()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$this->mockStatus(Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'failed'
]))->once()->ordered();
$migration->shouldReceive('up')->with()->once()->ordered();
$result = $this->migrations->migrate();
self::assertTrue($result);
}
/** @test */
public function executesOnlyMatchingMigrations()
{
$migrationTableMigration = $this->mockMigration(
'@breyta/CreateMigrationTable.php',
CreateMigrationTable::class
);
$animalsTableMigration = $this->mockMigration('CreateAnimalsTable.php', CreateAnimalsTable::class);
$this->mockStatus(Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'new'
]), Migration::createInstance([
'file' => 'CreateAnimalsTable.php',
'status' => 'new'
]))->once()->ordered();
$migrationTableMigration->shouldReceive('up')->with()->once()->ordered();
$animalsTableMigration->shouldNotReceive('up');
$result = $this->migrations->migrate('MigrationTable');
self::assertTrue($result);
}
$migrations->shouldReceive('getStatus')->with() /** @test */
->andReturn((object)[ public function executesMigrationsInSeparateTransactions()
'migrations' => [ {
Migration::createInstance([ $migrationTableMigration = $this->mockMigration(
'file' => '@breyta/CreateMigrationTable.php', '@breyta/CreateMigrationTable.php',
'status' => 'done' CreateMigrationTable::class
]), );
], $animalsTableMigration = $this->mockMigration('CreateAnimalsTable.php', CreateAnimalsTable::class);
'count' => 0, $this->mockStatus(Migration::createInstance([
])->byDefault(); 'file' => '@breyta/CreateMigrationTable.php',
'status' => 'new'
]), Migration::createInstance([
'file' => 'CreateAnimalsTable.php',
'status' => 'new'
]))->once()->ordered();
$this->pdo->shouldReceive('beginTransaction')->once()->ordered();
$migrationTableMigration->shouldReceive('up')->once()->ordered();
$this->pdo->shouldReceive('commit')->once()->ordered();
$this->pdo->shouldReceive('beginTransaction')->once()->ordered();
$animalsTableMigration->shouldReceive('up')->once()->ordered();
$this->pdo->shouldReceive('commit')->once()->ordered();
$result = $this->migrations->migrate();
self::assertTrue($result);
}
/** @test */
public function savesTheMigrationStatus()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$this->mockStatus(Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'new'
]))->once()->ordered();
$migration->shouldReceive('up')->with()->once()->ordered();
$this->mockPreparedStatement('/^insert into migrations/i')
->shouldReceive('execute')->withArgs(function (array $values) {
self::assertCount(5, $values);
self::assertSame('@breyta/CreateMigrationTable.php', array_shift($values));
self::assertSame(date('c'), array_shift($values));
self::assertSame('done', array_shift($values));
self::assertSame('[]', array_shift($values));
self::assertInternalType('double', array_shift($values));
return true;
})->once()->andReturn(1)->ordered();
$result = $this->migrations->migrate();
self::assertTrue($result);
} }
/** @test */ /** @test */
public function returnsSuccessAsBoolean() public function removesPreviousStatus()
{ {
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$this->mockStatus(Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'failed'
]))->once()->ordered();
$migration->shouldReceive('up')->with()->once()->ordered();
$this->mockPreparedStatement('/^delete from migrations/i')
->shouldReceive('execute')->with(['@breyta/CreateMigrationTable.php'])
->once()->andReturn(1)->ordered();
$result = $this->migrations->migrate(); $result = $this->migrations->migrate();
self::assertTrue($result); self::assertTrue($result);
} }
/** @test */
public function pdoExceptionCausesARollback()
{
$migration = $this->mockMigration('@breyta/CreateMigrationTable.php', CreateMigrationTable::class);
$this->mockStatus(Migration::createInstance([
'file' => '@breyta/CreateMigrationTable.php',
'status' => 'new'
]))->once()->ordered();
$migration->shouldReceive('up')->with()->once()->andThrows(\PDOException::class)->ordered();
$this->pdo->shouldReceive('rollback')->once()->ordered();
$result = $this->migrations->migrate();
self::assertFalse($result);
}
/**
* @param Migration ...$migrations
* @return m\CompositeExpectation|m\Expectation
*/
protected function mockStatus(Migration ...$migrations): m\CompositeExpectation
{
$status = (object)[
'migrations' => array_combine(array_map(function (Migration $migration) {
return $migration->file;
}, $migrations), $migrations),
'count' => count(array_filter($migrations, function (Migration $migration) {
return $migration->status !== 'done';
}))
];
return $this->migrations->shouldReceive('getStatus')->with()->andReturn($status);
}
/**
* @param string $file
* @param string $class
* @return m\MockInterface
*/
protected function mockMigration(string $file, string $class): m\MockInterface
{
// add the file -> class mapping
$reflection = new \ReflectionClass($this->migrations);
$property = $reflection->getProperty('classes');
$property->setAccessible(true);
$classes = $property->getValue($this->migrations);
$classes[$file] = $class;
$property->setValue($this->migrations, $classes);
$this->resolver->shouldReceive('__invoke')->with($class, m::type(AdapterInterface::class))
->andReturn($migrationInstance = m::mock(CreateMigrationTable::class));
return $migrationInstance;
}
} }
...@@ -7,16 +7,34 @@ use Mockery as m; ...@@ -7,16 +7,34 @@ use Mockery as m;
abstract class TestCase extends MockeryTestCase abstract class TestCase extends MockeryTestCase
{ {
/** @var m\Mock|\PDO */ /** @var m\Mock */
protected $pdo; protected $pdo;
protected function setUp() protected function setUp()
{ {
parent::setUp(); date_default_timezone_set('UTC');
$pdo = $this->pdo = m::mock(\PDO::class); $pdo = $this->pdo = m::mock(\PDO::class);
$pdo->shouldReceive('setAttribute')->with(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION) $pdo->shouldReceive('setAttribute')->with(\PDO::ATTR_ERRMODE, \PDO::ERRMODE_EXCEPTION)
->andReturn(true)->byDefault(); ->andReturn(true)->byDefault();
$pdo->shouldReceive('beginTransaction')->byDefault();
$pdo->shouldReceive('commit')->byDefault();
$pdo->shouldReceive('rollback')->byDefault();
$pdo->shouldReceive('query')->andReturn(false)->byDefault(); $pdo->shouldReceive('query')->andReturn(false)->byDefault();
} }
protected function mockPreparedStatement(string $pattern, $byDefault = false, $defaultResult = 1)
{
$statement = m::mock(\PDOStatement::class);
$statement->shouldReceive('execute')->byDefault()->andReturn($defaultResult);
$expectation = $this->pdo->shouldReceive('prepare')->with(m::pattern($pattern));
if ($byDefault) {
$expectation->byDefault()->andReturn($statement);
} else {
$expectation->once()->andReturn($statement);
}
return $statement;
}
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment