I am setting up new project based on Yii2 and Codeception. I am using advanced app template (backend, common, frontend).
I want most of frontend ActiveRecords to be "ReadOnly" so I made special Trait where I am blocking appropriate methods like save, insert, delete, update, ...
trait ReadOnlyActiveRecord {
/**
* #throws \yii\web\MethodNotAllowedHttpException
*/
public function save($runValidation = true, $attributeNames = null)
{
return self::throwNotAllowedException(__FUNCTION__);
}
//...
It simply throws an MethodNotAllowedHttpException.
Now I use this Trait in multiple frontend ARs and want to test them using Codeception like following
use \Codeception\Specify;
/**
* #expectedException \yii\web\MethodNotAllowedHttpException
*/
public function testSaveMethod()
{
$model = new AppLanguage();
$this->specify('model should be unsaveable', function () use ($model) {
expect('save function is not allowed', $model->save())->false();
});
}
/**
* #expectedException \yii\web\MethodNotAllowedHttpException
*/
public function testInsertMethod()
{
$model = new AppLanguage();
$this->specify('model should be unisertable', function () use ($model) {
expect('insert function is not allowed', $model->save())->false();
});
}
// ...
I now I am figuring out how to use these tests in multiple TestCests so I won't rewrite the code again and again in each of Cest.
So I am thinking about something like
/**
* Tests ActiveRecord is read only
*/
public function testReadOnly()
{
$model = new AppLanguage();
$this->processReadOnlyTests($model);
}
So my question is:
Where to put the test methods and how to include and call them in specific Cests?
Any suggestions?
Thank you.
Related
If we have the following class that uses the Builder design pattern:
class CourseListingBuilder extends Component
{
/**
* #var yii\db\Query
*/
private $query;
private $data = [];
public function init()
{
parent::init();
$this->query = new yii\db\Query();
}
/**
* Return a new instance of the builder
* #return CourseListingBuilder
*/
public static function create()
{
return new CourseListingBuilder();
}
public function selectColumns(array $columns)
{
// #TODO validate and format $columns
$this->query->select($columns);
return $this;
}
public function applyFilters($filters = [])
{
// #TODO validate and parse filters
$this->query->andWhere($filters);
return $this;
}
public function build()
{
// Make the actual DB query
$this->data = $this->query->all();
}
public function getData()
{
return $this->data;
}
}
We use it like this:
$data = CourseListingBuilder::create()
->selectColumns(['id', 'name'])
->applyFilters(['active'=>1])
->build()
->getData();
In time, these classes grow quite large because we have multiple developers working on them. Developers extend the selectColumns() or applyFilters() and occasionally, the code breaks. We've recently started introducing Unit Tests in the company and we would like to make classes that follow the above pattern - unit testable, preferrably through a small refactoring.
How do you properly unit test the above builder design pattern class, given its internal dependency to yii\db\Query (which comes from the Yii2 framework by the way, but this is not relevant to the example). What's relevant to the example is that we DO NOT want to test the internal behaviors of yii\db\Query. It comes from the framework. We know that it's working. In other words - we want to "mock" it as much as possible, while effectively testing the actual methods inside our Builder class and how they will affect the outcome.
The second thing to point out is that we know how to write unit tests in general. This question is not about "how to write unit tests" in general, but "how to write unit tests for a builder class that has an internal dependency to a third party DAO class".
Are we doing something wrong?
Such code is not unit testable but instead goes in the integration tests territory?
The simplest way would be to replace yii\db\Query with some simple class and test its state after method call.
class MockQuery extends \yii\db\Query {
public $select;
public $selectOption;
public function select($columns, $option = null) {
$this->select = $columns;
$this->selectOption = $option;
}
// ...
}
class CourseListingBuilder extends \yii\base\Component {
private $query;
public function selectColumns(array $columns) {
// #TODO validate and format $columns
$this->query->select($columns);
return $this;
}
// ...
}
And test:
public function testQuery() {
$builder = new CourseListingBuilder();
// use reflection to access private property
$reflection = new \ReflectionObject($builder);
$property = $reflection->getProperty('query');
$property->setAccessible(true);
$property->setValue($builder, new MockQuery());
$builder->selectColumns(['id', 'name']);
$query = $property->getValue($builder);
$this->assertSame(['id', 'name'], $query->select);
$this->assertNull($query->selectOption);
}
I learn TDD and I've started to use xSpec tool (language does not matter, but it's phpspec2 in my case). I write my first specification:
<?php
namespace spec\Mo\SpeechBundle\Controller;
use Doctrine\Common\Collections\Collection;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Symfony\Component\HttpFoundation\Response;
use Mo\SpeechBundle\Repository\IdeaRepository;
use Mo\SpeechBundle\Repository\SpeechRepository;
use Mo\SpeechBundle\Entity\Idea;
use Mo\SpeechBundle\Entity\Speech;
class SpeechControllerSpec extends ObjectBehavior
{
function let(SpeechRepository $speechRepository, IdeaRepository $ideaRepository, EngineInterface $templating)
{
$this->beConstructedWith($speechRepository, $ideaRepository, $templating);
}
function it_is_initializable()
{
$this->shouldHaveType('Mo\SpeechBundle\Controller\SpeechController');
}
function it_responds_to_show_action(EngineInterface $templating, Speech $speech, Response $response)
{
$templating
->renderResponse('MoSpeechBundle:Speech:show.html.twig', ['speech' => $speech])
->willReturn($response)
;
$this->showAction($speech)->shouldBeAnInstanceOf('Symfony\Component\HttpFoundation\Response');
}
function it_responds_to_list_action(
SpeechRepository $speechRepository,
IdeaRepository $ideaRepository,
EngineInterface $templating,
Response $response
)
{
$speeches = [new Speech()];
$ideas = [new Idea()];
$speechRepository->findAll()->willReturn($speeches);
$ideaRepository->findAll()->willReturn($ideas);
$templating
->renderResponse('MoSpeechBundle:Speech:list.html.twig', compact('speeches', 'ideas'))
->willReturn($response)
;
$this->listAction()->shouldBeAnInstanceOf('Symfony\Component\HttpFoundation\Response');
}
function it_responds_list_by_idea_action(
Idea $idea,
SpeechRepository $speechRepository,
IdeaRepository $ideaRepository,
EngineInterface $templating,
Response $response
)
{
$speeches = [new Speech()];
$ideas = [new Idea()];
$speechRepository->findByIdea($idea)->willReturn($speeches);
$ideaRepository->findAll()->willReturn($ideas);
$templating
->renderResponse('MoSpeechBundle:Speech:list.html.twig', compact('speeches', 'idea', 'ideas'))
->willReturn($response)
;
$this->listByIdeaAction($idea)->shouldBeAnInstanceOf('Symfony\Component\HttpFoundation\Response');
}
}
For controller:
<?php
namespace Mo\SpeechBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Templating\EngineInterface;
use Mo\SpeechBundle\Repository\IdeaRepository;
use Mo\SpeechBundle\Repository\SpeechRepository;
use Mo\SpeechBundle\Entity\Idea;
use Mo\SpeechBundle\Entity\Speech;
/**
* Manages speeches.
*/
class SpeechController
{
/**
* #var \Mo\SpeechBundle\Repository\SpeechRepository
*/
private $speechRepository;
/**
* #var \Mo\SpeechBundle\Repository\IdeaRepository
*/
private $ideaRepository;
/**
* #var \Symfony\Bundle\FrameworkBundle\Templating\EngineInterface
*/
private $templating;
/**
* #param \Mo\SpeechBundle\Repository\SpeechRepository $speechRepository
* #param \Mo\SpeechBundle\Repository\IdeaRepository $ideaRepository
* #param \Symfony\Bundle\FrameworkBundle\Templating\EngineInterface $templating
*/
public function __construct(SpeechRepository $speechRepository, IdeaRepository $ideaRepository, EngineInterface $templating)
{
$this->speechRepository = $speechRepository;
$this->ideaRepository = $ideaRepository;
$this->templating = $templating;
}
/**
* Shows speech.
*
* #param \Mo\SpeechBundle\Entity\Speech $speech
*
* #return \Symfony\Component\HttpFoundation\Response
*/
public function showAction(Speech $speech)
{
return $this->templating->renderResponse('MoSpeechBundle:Speech:show.html.twig', compact('speech'));
}
/**
* Shows list of speeches filtered by idea.
*
* #param \Mo\SpeechBundle\Entity\Idea $idea
*
* #return \Symfony\Component\HttpFoundation\Response
*/
public function listByIdeaAction(Idea $idea)
{
$speeches = $this->speechRepository->findByIdea($idea);
$ideas = $this->ideaRepository->findAll();
return $this->templating->renderResponse('MoSpeechBundle:Speech:list.html.twig', compact('speeches', 'idea', 'ideas'));
}
/**
* Shows list of all speeches.
*
* #return \Symfony\Component\HttpFoundation\Response
*/
public function listAction()
{
$speeches = $this->speechRepository->findAll();
$ideas = $this->ideaRepository->findAll();
return $this->templating->renderResponse('MoSpeechBundle:Speech:list.html.twig', compact('speeches', 'ideas'));
}
}
OK, now I'm sure that behavior of my controller is specified and I can move forward. But I have another problem.
I've used mock of repository for controller spec and now I want to write spec for repository itself:
<?php
namespace Mo\SpeechBundle\Repository;
use Doctrine\ORM\EntityRepository;
use Mo\SpeechBundle\Entity\Idea;
class SpeechRepository extends EntityRepository
{
/**
* Finds all speeches by specified idea.
*
* #param \Mo\SpeechBundle\Entity\Idea $idea
*
* #return array
*/
public function findByIdea(Idea $idea)
{
return $this
->createQueryBuilder('s')
->leftJoin('s.ideas', 'i')
->where('i = :idea')
->setParameters(compact('idea'))
->getQuery()
->getResult()
;
}
}
But specs describe behavior, as I understood. How correctly test repository that it really returns what I need, in my case speeches by ideas.
Should I consider to create functional test with xUnit tool (PHPUnit in PHP world)? Or I wri
te spec which describes that my repository correctly creates query? Or can I just use Behat for all app and don't pay attention to this problem.
I have spent a week for analysis of this question and found satisfactory answer.
The phpspec only specify behavior of our objects. Nothing more. We can't create functional tests with them.
So, we have two ways to test our functionality:
Use PHPUnit to write functional tests for modules and system itself.
Use Behat to describe features of our application.
PHPUnit, other similar framework and Behat have their pitfalls and strong sides.
What to use, can decide only a developer.
I understand your predicament completely and I have done exactly the same thing in the past. I think that the basic answer to your question is that your business logic should be separate from any framework (infrastructure code), and so you should not be testing objects of the type 'Doctrine\ORM\EntityRepository'.
I think the best approach is the have another layer within your application which will hold your business logic, and this in turn could use adapters to pass messages back and forth from the 'Doctrine\ORM\EntityRepository' type objects. That way you can fully spec your business rules (including any adapters) without having to test doctrine type objects which should not be tested anyway as for the most part this is 3rd party code.
Doing things this way also makes it easier for you to change your business rules in future.
In my zf2 project, I have doctrine 2 entities that reference the user entity as created by as the following:
/**
* #ORM\ManyToOne(targetEntity="User")
* #ORM\JoinColumn(name="created_by", referencedColumnName="id")
**/
protected $createdBy;
and I want to set this reference in the PrePersist, how can I do that?
I tried the following (I don't know if it is right):
/** #ORM\PrePersist */
public function prePersist() {
if ($this->createdBy === null) {
$session = new \Zend\Authentication\Storage\Session;
$userId = $session->read();
if ($userId !== null) {
$this->createdBy = $userId;
} else {
throw new \Exception("Invalid User");
}
}
}
but the main problem is that the $userId is an integer, and createdBy must hold the reference of the user not the user ID.
is there a better way to do that? if no, how can I get the reference instead of the user ID?
Instead of directly accessing your session storage, you might configuring a Zend\Authentication\AuthenticationService to handle your authenticated identity.
Then you could set your Namespace\For\Entity\User as your AuthenticationService identity and inject the authentication service via setter injection (see this post about hooking into Doctrine lifecycle events).
Then you should be able to do this:
/** #ORM\PrePersist */
public function prePersist() {
if (empty($this->createdBy)) {
$this->setCreatedBy($this->getAuthenticationService()->getIdentity());
}
}
...or you could add a $loggedInUser property to your entity, and inject the logged in User directly, instead of creating a dependency on the AuthenticationService (or session storage). This is probably the better way, because it simplifies your tests:
/** #ORM\PrePersist */
public function prePersist() {
if (empty($this->createdBy)) {
$this->setCreatedBy($this->getLoggedInUser());
}
}
Note that I got rid of the type-checking in your prePersist method by using setters, because then you can handle that via type-hinting in your setters like this:
public function setAuthenticationService(\Zend\Authentication\AuthenticationService $authenticationService){/** do stuff */};
public function setLoggedInUser(\Namespace\For\Entity\User $user){/** do stuff */};
public function setCreatedBy(\Namespace\For\Entity\User $user){/** do stuff */};
I'm doing test unit of my entity:
namespace PathtomyBundle\Tests;
require_once dirname(__DIR__).'/../../../app/AppKernel.php';
use Doctrine\ORM\Tools\SchemaTool;
abstract class TestCase extends \PHPUnit_Framework_TestCase
{
/**
* #var Symfony\Component\HttpKernel\AppKernel
*/
protected $kernel;
/**
* #var Doctrine\ORM\EntityManager
*/
protected $entityManager;
/**
* #var Symfony\Component\DependencyInjection\Container
*/
protected $container;
public function setUp()
{
// Boot the AppKernel in the test environment and with the debug.
$this->kernel = new \AppKernel('test', true);
$this->kernel->boot();
// Store the container and the entity manager in test case properties
$this->container = $this->kernel->getContainer();
$this->entityManager = $this->container->get('doctrine')->getEntityManager();
// Build the schema for sqlite
//$this->generateSchema();
parent::setUp();
}
public function tearDown()
{
// Shutdown the kernel.
$this->kernel->shutdown();
parent::tearDown();
}
protected function generateSchema()
{
// Get the metadatas of the application to create the schema.
$metadatas = $this->getMetadatas();
if ( ! empty($metadatas)) {
// Create SchemaTool
$tool = new SchemaTool($this->entityManager);
$tool->createSchema($metadatas);
} else {
throw new Doctrine\DBAL\Schema\SchemaException('No Metadata Classes to process.');
}
}
/**
* Overwrite this method to get specific metadatas.
*
* #return Array
*/
protected function getMetadatas()
{
return $this->entityManager->getMetadataFactory()->getAllMetadata();
}
}
and also:
namespace pathtomybundle\Tests\Entity;
use pathtomybundle\Tests\TestCase;
use pathtomybundle\Entity\Calendars;
require_once dirname(__DIR__).'/TestCase.php';
class CalendarsDbTest extends TestCase
{
protected $Calendars;
public function setUp()
{
parent::setUp();
$this->Calendars = new Calendars();
}
public function testGenerateCalendars()
{
$this->Calendars->setBeginDate(new \DateTime('now'));
$this->Calendars->setDescription('Description');
$this->Calendars->setEndDate(new \DateTime('now'));
$this->Calendars->setType('sur titre');
// Save the ExCalendars
$this->entityManager->persist($this->Calendars);
$this->entityManager->flush();
}
public function testUser(){
$this->assertEquals('Description', $this->Calendars->getDescription() );
}
So my questions are:
Why does it raise this error "Failed asserting that null matches expected"?
Why getDescription() returns NULL?
How to test two table with One-to-Many relationship for example my Table Calendars with another table in database?
Edit
For the third question :
For example I have two Tables Job and Calenders with Many-to-One relationship so I will have a Job_Id field in Calendars Table,so how I will do my test Unit with a foreign key "job_id"
In Calendars Entity :
/**
* #var Job
*
* #ORM\ManyToOne(targetEntity="Job")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="job_id", referencedColumnName="job_id")
* })
*/
private $jobId;
Edit-2-
when I run my phpunit test "phpunit -c app" to test setters function and persist in database so I have a with every test a new data insered in databse, my question is it possible to do a lot of test but I insert data in database just for one time because actually I must remove data from database with every test.
2 - another question : to create a database_test i use "$this->generateSchema();
" so after create a database for the first time and when the test call "TestCase"class (the code above) again so he tried to create the database_test again then I must remove the line after the first time and it's not good,so what I can do to run this line just for one time in the first time when i run my test?
Edit-3
/**
* #var Job
*
* #ORM\ManyToOne(targetEntity="Job")
* #ORM\JoinColumns({
* #ORM\JoinColumn(name="job_id", referencedColumnName="id")
* })
*/
private $job;
it's normal?
Every test in test case creates his own CalendarsDbTest object. So, in fact, $this->Calendar is different object in each test (if you want share it between tests you need create it in setUp method)
Is the same as above (there is null because you never call setDescription with $this->Calendars - it's different object than it is in first test)
I'm not sure what exactly you mean. Can you show more precise (for example method you want test) what you mean?
edit:
The answer is: you don't test it. Why? Because unit test is UNIT test - you should test here only your entity. Persistence, keeping relations etc. are Doctrine resposibility and should be tested there - you don't worry about it.
The only thing you should test is setter/getter for $jobId property (btw. it should be "$job" rather than "$jobId" because it's object of Job class - not an integer), eg.:
class CalendarTest extends \PHPUnit_Framework_TestCase
{
(...)
public function testSetGetJob()
{
$job = new Job();
$job->setSomeProperty('someValue');
$expectedJob = clone $job; // you clone this because in setter you pass object by reference
$calendar = new Calendar();
$calendar->setJob($job);
$this->assertEquals($expectedJob, $calendar->getJob());
}
(...)
}
I have the following setup "Many Users can have Many Projects (Collaborators)"
/**
* #Entity #HasLifeCycleCallbacks
* #Table(name="projects")
*/
class Project implements \Zend_Acl_Resource_Interface {
/**
* #ManyToMany(targetEntity="User", mappedBy="projects")
* #OrderBy({"displayName" = "ASC", "username" = "ASC"})
*/
protected $collaborators;
..
}
/**
* #Entity
* #Table(name="users")
*/
class User implements \Zend_Acl_Role_Interface {
/**
* #ManyToMany(targetEntity="Project", inversedBy="collaborators")
*/
protected $projects;
...
}
I tried to remove a collaborator using the following
$user = Application_DAO_User::findById($this->_getParam('userid'));
$proj = Application_DAO_Project::getProjectById($this->_getParam('id'));
Application_DAO_Project::removeCollaborator($proj, $user); // <---
// Application_DAO_User
public static function findById($id) {
return self::getStaticEm()->find('Application\Models\User', $id);
}
// Application_DAO_Project
public static function getProjectById($id) {
return self::getStaticEm()->find('Application\Models\Project', $id);
}
public static function removeCollaborator(Project $proj, User $collaborator) { // <---
$proj->getCollaborators()->remove($collaborator);
$collaborator->getProjects()->remove($proj);
self::getStaticEm()->flush();
}
And there isn't any errors but the database stays the same ...
This may be well over due but was just experiencing the same problem myself... According to the doctrine 2 documents, the function ArrayCollection->remove($i) is for removing by array index.
What you are after is:
getCollaborators()->removeElement($collaborator);
I went round in circles trying to figure this out until I realised that for this to work:
getCollaborators()->removeElement($collaborator);
$collaborator would have to be the actual object from the collaborators ArrayCollection. That is, if you pass in a new Collaborator object with the same parameters it won't remove it. That's because ArrayCollection uses array_search to look for the object you want to remove.
Hope that saves someone else a few hours...