just another orm...
View the Project on GitHub tflori/orm
For development of this library we using Mockery and we suggest to use it for your test environment too.
What needs to be tested is up to you but we recommend to mock all the database access including quoting and querying table definitions.
For easier use we created a trait that helps you with emulating the behaviour of Entity
and EntityManager
. All
methods have the prefix orm
to not interfere with your methods.
If you are not using mockery and don’t want to have this dependency you will have to create your own helpers. This guide will just give some hints how to mock it.
For testing you need to be able to inject a mock object into your code. We assume a dependency injection available in
$di
with ->get('service')
and ->set('service', 'getter')
.
The production bootstrap could look like this:
$di = new DependencyInjector();
$di->set('config', function () {
return [
'dbConfig' => new DbConfig('mysql', 'any_db', 'any_user', 'any_password')
];
});
$di->set('entityManager', function () use ($di) {
return new EntityManager([
'connection' => $di->get('config')['dbConfig']
]);
});
// and what ever else
In the setUp()
method for your test case you have to mock the access to the database:
abstract class TestCase extends MockeryTestCase
{
use \ORM\Testing\MocksEntityManager;
/** @var \Mockery\Mock[] */
protected $mocks;
protected function setUp()
{
parent::setUp();
// load your dependency injection here
$di = $GLOBALS['di'];
// create the mock for EntityManager
$em = $this->mocks['em'] = $this->ormInitMock([], 'mysql');
// inject the entity manager mock
$di->set('entityManager', function () use ($em) {
return $em;
});
}
protected function tearDown()
{
parent::tearDown();
$this->closeMockery();
// cleanup your dependency injection
}
}
Note: The following methods (defined in MocksEntityManager
trait) rely on the EntityManager::getInstance()
to
retrieve the responsible EntityManagerMock
for the given $class
. If you have multiple database connections and
EntityManager
instances and you use a different method to get the responsible EntityManager
you have to adapt the
MocksEntityManager::ormGetInstance($class): EntityManagerMock
method.
Due to the fact that the entity constructor is final and calls an internal method (onInit()
) you can not just create
the mock with Mockery::mock(Article::class, [['id' => 42, 'title' => 'Hello World!']])->makePartial()
. Also creating
a passive mock with Mockery::mock(new Article(['id' => 42, 'title' => 'Hello world!']))
does not work because
the original object is wrapped but the magic getter not.
You have to initialize the object without using the original constructor, set the entity manager manually, set the original data and reset the entity. We created a helper for this to make it easier.
class ArticleControllerTest extends TestCase
{
public function testMockEntity()
{
$entity = \Mockery::mock(Article::class)->makePartial();
$entity->setEntityManager($this->mocks['em']);
$entity->setOriginalData(['id' => 42, 'title' => 'Hello World!']);
$entity->reset;
// now you can use the entity as usual and write expectations
}
public function testMockEntityUsingHelper()
{
$entity = $this->ormCreateMockEntity(Article::class, ['id' => 42, 'title' => 'Hello World!']);
// when onInit is required call it now: $entity->onInit(false);
// now you can use the entity as usual and write expectations
}
}
First we want to have a look on the basics- so lets have a look on CRUD-Operations.
When a function creates an Entity
it is mostly filling it with data and later call ->save()
to it. We can provide a
factory that we then can overwrite in the tests and instead of returning an Entity
we can return a partial mock for
this Entity
. How ever this will result in a function that we create only for tests.
The other way is that we mock the requests the Entity
will cause when you call ->save()
. This is some bit tricky
because save is also for updating and the entity calls more than just EntityManager::insert(Entity)
. The save method
makes two calls methods on EntiyManager
for a new Entiy
:
sync(Entity)
- the Entity
does not exist but when the primary key is incomplete this throws anywayinsert(Entity, boolean)
- when the Entity
has a primary key then it gets inserted without using auto incrementFor an easier use we provide a method to expect exactly this. You can use it as follows:
// the function that creates the entity
function createArticle($title)
{
$article = new Article();
$article->title = $title;
$article->save();
return json_encode($article);
}
class ArticleControllerTest extends TestCase
{
public function testSavesTheArticle()
{
$this->ormExpectInsert(Article::class, ['created' => date('c')]);
createArticle('Anything');
}
// to proof that the result is correct you may need to overwrite the id too (otherwise it is random)
public function testReturnsJsonEncodedArticle()
{
$defaults = ['id' => 42, 'created' => date('c')];
$this->ormExpectInsert(Article::class, $defaults);
$article = createArticle('Anything');
self::assertSame(array_merge($defaults, ['title' => 'Anything']), json_decode($article, true));
}
}
If you use (for performance reason)
$em->insert($entity)
directly in your code you can of course write your own expectation for it:$em->shouldReceive('insert')->with(anInstanceOf(Article::class))
.
Reading entities should be much easier. Basically you expect that EntityManager::fetch()
is called. If you expect a
specific primary key you can just create the entity and provide it through EntityManager::map(Entity)
or you just
expect fetch to be called with $class
and $primaryKey
and return $entity
. If you just want the EntityManager
to return the corresponding entity you don’t have to mock it at all. Every entity created with ormCreateMockedEntity
will be available through $entityManager->fetch($class, $primaryKey
. We think for fetching a specific entity by
primary key you don’t need a helper method.
// the function that reads one article
function getArticle($id)
{
$article = $GLOBALS['di']->get('entityManager')->fetch(Article::class, $id);
return $article;
}
class ArticleControllerTest extends TestCase
{
// expect a call
public function testGetsTheArticle()
{
$this->mocks['em']->shouldReceive('fetch')->with(Article::class, 42)->once();
getArticle(42);
}
// provide and use it (we don't need to mock this functionality)
public function testGetsTheArticleFromMap()
{
$article = $this->ormCreateMockedEntity(Article::class, [
'id' => 42,
'title' => 'Hello World!',
'created' => date('c')
]);
$result = getArticle(42);
self::assertSame($article, $result);
}
}
The EntityFetcher get’s mocked with an empty result automatically. You can provide results for an fetcher of an entity
with ormAddResult($class, Entity ...$entities)
. This returns an ORM\Testing\EntityFetcherMock\Result
and
can then be specified to met the expected where conditions, joins, grouping and ordering. Also the limit and offset
can be specified and if necessary you can force the query to match your regular expressions. Basically the Result
is
an EntityFetcher
. So you can use it like an EntityFetcher
to specify the conditions.
Expecting something specific can be more hard overall we recommend to just provide a basic result in the setUp
method
of your TestCase. A result without conditions will be used if no other result matches.
function getArticles($search = '')
{
$fetcher = $GLOBALS['di']->get('entityManager')->fetch(Article::class);
if (!empty($search)) {
$fetcher->where('title', 'LIKE', '%' . $search . '%');
}
return $fetcher->all();
}
class ArticleControllerTest extends TestCase
{
// expect fetch
public function testFetchesArticles()
{
$this->mocks['em']->shouldReceive('fetch')->with(Article::class)->once()->passthru();
getArticles();
}
// expect where condition
public function testSearchesForTitle()
{
$articles = [new Article(['title' => 'Hello World!']), new Article(['title' => 'Anything'])];
$this->ormAddResult(Article::class, ...$articles)
->where('title', 'LIKE', '%cambozola%');
$result = getArticles('cambozola');
self::assertSame($articles, $result);
}
// assert empty result
public function testReturnsEmptyResult()
{
$articles = [new Article(['title' => 'Hello World!']), new Article(['title' => 'Anything'])];
$this->ormAddResult(Article::class, ...$articles); // any non matching query
$this->ormAddResult(Article::class)->where('title', 'LIKE', '%cambozola%');
$result = getArticles('cambozola');
self::assertEmpty($result);
}
}
For mocking an EntityFetcher
the procedure will be more complicated. Therefore we created a helper method that act
like a real EntityFetcher
by default. This method returns the mock from EntityFetcher
this way you can expect
specific calls.
/*
function getArticles($search = '')
{
$fetcher = $GLOBALS['di']->get('entityManager')->fetch(Article::class);
if (!empty($search)) {
$fetcher->where('title', 'LIKE', '%' . $search . '%');
}
return $fetcher->all();
}
class ArticleControllerTest extends TestCase
{
// expect fetch
public function testFetchesArticles()
{
$this->ormExpectFetch(Article::class);
getArticles();
}
// expect where condition
public function testSearchesForTitle()
{
$fetcher = $this->ormExpectFetch(Article::class);
$fetcher->shouldReceive('where')->with('title', 'LIKE', '%cambozola%');
getArticles('cambozola');
}
// expect result (attention: the search is ignored)
public function testReturnsArticles()
{
$articles = [new Article(), new Article()];
$articles[0]->title = 'Hello World!';
$articles[1]->title = 'Anything';
$this->ormExpectFetch(Article::class, $articles);
$result = getArticles('cambozola');
self::assertSame($articles, $result);
}
}
*/
There is no need for complicated stuff:
But it might be necessary that the entity is not dirty afterwards, to emulate the data has changed in the database
and the entity got new data while updated (triggers like ON UPDATE CURRENT TIMESTAMP
). To make the save method act
like the original method act (without using the database) there is a helper method for it.
function updateArticle($id, $newTitle)
{
$article = $GLOBALS['di']->get('entityManager')->fetch(Article::class, $id);
$article->title = $newTitle;
$article->save();
}
class ArticleControllerTest extends TestCase
{
public function testCallsSave()
{
$article = $this->ormCreateMockedEntity(Article::class, ['id' => 42, 'title' => 'Hello World!']);
$article->shouldReceive('save')->once();
updateArticle(42, 'Don`t Panic!');
self::assertSame('Don`t Panic!', $article->title);
// ATTENTION! the entity is still dirty:
self::assertTrue($article->isDirty());
}
public function testWithHelper()
{
$article = $this->ormCreateMockedEntity(Article::class, [
'id' => 42,
'title' => 'Hello World!',
'changed' => date('c', strtotime('-1 Hour'))
]);
$this->ormExpectUpdate($article, ['changed' => date('c')]);
updateArticle(42, 'Don`t Panic!');
}
}
When you deleting an Entity you will have to know which entity will come or at least from which class. Then you just
expect delete with $em->shouldReceive('delete')->with($entity)->once()->andReturn(true)
. The helper for it just
removes the the original data like the delete method generally does.
function deleteArticle($id)
{
$em = $GLOBALS['di']->get('entityManager');
$article = $em->fetch(Article::class, $id);
$em->delete($article);
}
class ArticleControllerTest extends TestCase
{
public function testDeleteWithoutHelper()
{
$article = $this->omCreateMockedEntity(Article::class, ['id' => 42]);
$this->mocks['em']->shouldReceive('delete')->with($article)->once()->andReturn(true);
deleteArticle(42);
}
public function testDeleteWithHelper()
{
$article = $this->omCreateMockedEntity(Article::class, ['id' => 42]);
$this->ormExpectDelete($article);
deleteArticle(42);
}
}
Relations are always taken from $entity->fetch($relation[, true])
. You can easily mock this method with expecting
the relation name as first and true
as second parameter. It has to return an array or a single relation appropriate
to the definition of the relation. If you expecting fetch without the second parameter you can add the expected results
with ormAddResult($class, ...$entities)
.
function getArticleCategories($articleId)
{
$em = $GLOBALS['di']->get('entityManager');
$article = $em->fetch(Article::class, $id);
return $article->categories;
}
function getFirstArticleCategory($articleId)
{
$em = $GLOBALS['di']->get('entityManager');
$article = $em->fetch(Article::class, $id);
$fetcher = $article->fetch('categories');
return $fetcher->one();
}
class ArticleControllerTest extends TestCase
{
public function testFetchWithGetAll()
{
$categories = [new Category(), new Category()];
$article = $this->omCreateMockedEntity(Article::class, ['id' => 42]);
$article->expect('fetch')->with('categories', true)->once()->andReturn($categories);
$result = getArticleCategories(42);
self::assertSame($categories, $result);
}
public function testFetchUsingFetcher()
{
$categories = [new Category(), new Category()];
$article = $this->omCreateMockedEntity(Article::class, ['id' => 42]);
$this->ormAddResult(Category::class, ...$categories);
$result = getFirstArticleCategory(42);
self::assertSame($categories[0], $result);
}
}