1
\$\begingroup\$

I'm quite new to Zend and unit testing in general. I have come up with a small application that uses Zend Framework 2 and Doctrine. It has only one model and controller and I want to run some unit tests on them.

Here's what I have so far:

Base doctrine 'entity' class, containing methods I want to use in all of my entities:

<?php
/**
 * Base entity class containing some functionality that will be used by all
 * entities 
 */

namespace Perceptive\Database;

use Zend\Validator\ValidatorChain;

class Entity{

    //An array of validators for various fields in this entity
    protected $validators;

    /**
     * Returns the properties of this object as an array for ease of use. Will
     * return only properties with the ORM\Column annotation as this way we know
     * for sure that it is a column with data associated, and won't pick up any
     * other properties. 
     * @return array 
     */
    public function toArray(){
      //Create an annotation reader so we can read annotations
      $reader = new \Doctrine\Common\Annotations\AnnotationReader();

      //Create a reflection class and retrieve the properties
      $reflClass = new \ReflectionClass($this);
      $properties = $reflClass->getProperties();

      //Create an array in which to store the data
      $array = array();

      //Loop through each property. Get the annotations for each property
      //and add to the array to return, ONLY if it contains an ORM\Column
      //annotation.
      foreach($properties as $property){
        $annotations = $reader->getPropertyAnnotations($property);
        foreach($annotations as $annotation){
          if($annotation instanceof \Doctrine\ORM\Mapping\Column){
            $array[$property->name] = $this->{$property->name};
          }
        }
      }

      //Finally, return the data array to the user
      return $array;
    }

    /**
     * Updates all of the values in this entity from an array. If any property
     * does not exist a ReflectionException will be thrown.
     * @param array $data
     * @return \Perceptive\Database\Entity 
     */
    public function fromArray($data){
      //Create an annotation reader so we can read annotations
      $reader = new \Doctrine\Common\Annotations\AnnotationReader();

      //Create a reflection class and retrieve the properties
      $reflClass = new \ReflectionClass($this);

      //Loop through each element in the supplied array
      foreach($data as $key=>$value){
          //Attempt to get at the property - if the property doesn't exist an
          //exception will be thrown here.
          $property = $reflClass->getProperty($key);

          //Access the property's annotations
          $annotations = $reader->getPropertyAnnotations($property);

          //Loop through all annotations to see if this is actually a valid column
          //to update.
          $isColumn = false;
          foreach($annotations as $annotation){
            if($annotation instanceof \Doctrine\ORM\Mapping\Column){
              $isColumn = true;
            }
          }

          //If it is a column then update it using it's setter function. Otherwise,
          //throw an exception.
          if($isColumn===true){
            $func = 'set'.ucfirst($property->getName());
            $this->$func($data[$property->getName()]);
          }else{
            throw new \Exception('You cannot update the value of a non-column using fromArray.');
          }
      }

      //return this object to facilitate a 'fluent' interface.
      return $this;
    }

    /**
     * Validates a field against an array of validators. Returns true if the value is
     * valid or an error string if not.
     * @param string $fieldName The name of the field to validate. This is only used when constructing the error string
     * @param mixed $value
     * @param array $validators
     * @return boolean|string 
     */
    protected function setField($fieldName, $value){
      //Create a validator chain
      $validatorChain = new ValidatorChain();
      $validators = $this->getValidators();

      //Try to retrieve the validators for this field
      if(array_key_exists($fieldName, $this->validators)){
        $validators = $this->validators[$fieldName];
      }else{
        $validators = array();
      }

      //Add all validators to the chain
      foreach($validators as $validator){
        $validatorChain->attach($validator);
      }

      //Check if the value is valid according to the validators. Return true if so,
      //or an error string if not.
      if($validatorChain->isValid($value)){
        $this->{$fieldName} = $value;
        return $this;
      }else{
        $err = 'The '.$fieldName.' field was not valid: '.implode(',',$validatorChain->getMessages());
        throw new \Exception($err);
      }
    }
}

My 'config' entity, which represents a one-row table containing some configuration options:

<?php
/**
 * @todo: add a base entity class which handles validation via annotations
 * and includes toArray function. Also needs to get/set using __get and __set
 * magic methods. Potentially add a fromArray method?
 */
namespace Application\Entity;

use Doctrine\ORM\Mapping as ORM;
use Zend\Validator;
use Zend\I18n\Validator as I18nValidator;
use Perceptive\Database\Entity;


/** 
 * @ORM\Entity
 * @ORM\HasLifecycleCallbacks
 */
class Config extends Entity{
    /** 
     * @ORM\Id
     * @ORM\Column(type="integer")
     */
    protected $minLengthUserId;

    /** 
     * @ORM\Id
     * @ORM\Column(type="integer") 
     */
    protected $minLengthUserName;

    /** 
     * @ORM\Id
     * @ORM\Column(type="integer") 
     */
    protected $minLengthUserPassword;

    /** 
     * @ORM\Id
     * @ORM\Column(type="integer") 
     */
    protected $daysPasswordReuse;

    /** 
     * @ORM\Id
     * @ORM\Column(type="boolean") 
     */
    protected $passwordLettersAndNumbers;

    /** 
     * @ORM\Id
     * @ORM\Column(type="boolean") 
     */
    protected $passwordUpperLower;

    /** 
     * @ORM\Id
     * @ORM\Column(type="integer") 
     */
    protected $maxFailedLogins;

    /** 
     * @ORM\Id
     * @ORM\Column(type="integer") 
     */
    protected $passwordValidity;

    /** 
     * @ORM\Id
     * @ORM\Column(type="integer") 
     */
    protected $passwordExpiryDays;

    /** 
     * @ORM\Id
     * @ORM\Column(type="integer") 
     */
    protected $timeout;

    // getters/setters
    /**
     * Get the minimum length of the user ID
     * @return int 
     */
    public function getMinLengthUserId(){
        return $this->minLengthUserId;
    }

    /**
     * Set the minmum length of the user ID
     * @param int $minLengthUserId
     * @return \Application\Entity\Config This object
     */
    public function setMinLengthUserId($minLengthUserId){
        //Use the setField function, which checks whether the field is valid,
        //to set the value.
        return $this->setField('minLengthUserId', $minLengthUserId);
    }

    /**
     * Get the minimum length of the user name
     * @return int 
     */
    public function getminLengthUserName(){
      return $this->minLengthUserName;
    }

    /**
     * Set the minimum length of the user name
     * @param int $minLengthUserName
     * @return \Application\Entity\Config 
     */
    public function setMinLengthUserName($minLengthUserName){
      //Use the setField function, which checks whether the field is valid,
      //to set the value.
      return $this->setField('minLengthUserName', $minLengthUserName);
    }

    /**
     * Get the minimum length of the user password
     * @return int 
     */
    public function getMinLengthUserPassword(){
      return $this->minLengthUserPassword;
    }

    /**
     * Set the minimum length of the user password
     * @param int $minLengthUserPassword
     * @return \Application\Entity\Config 
     */
    public function setMinLengthUserPassword($minLengthUserPassword){
      //Use the setField function, which checks whether the field is valid,
      //to set the value.
      return $this->setField('minLengthUserPassword', $minLengthUserPassword);
    }

    /**
     * Get the number of days before passwords can be reused
     * @return int 
     */
    public function getDaysPasswordReuse(){
      return $this->daysPasswordReuse;
    }

    /**
     * Set the number of days before passwords can be reused
     * @param int $daysPasswordReuse
     * @return \Application\Entity\Config 
     */
    public function setDaysPasswordReuse($daysPasswordReuse){
      //Use the setField function, which checks whether the field is valid,
      //to set the value.
      return $this->setField('daysPasswordReuse', $daysPasswordReuse);
    }

    /**
     * Get whether the passwords must contain letters and numbers
     * @return boolean 
     */
    public function getPasswordLettersAndNumbers(){
      return $this->passwordLettersAndNumbers;
    }

    /**
     * Set whether passwords must contain letters and numbers
     * @param int $passwordLettersAndNumbers
     * @return \Application\Entity\Config 
     */
    public function setPasswordLettersAndNumbers($passwordLettersAndNumbers){
      //Use the setField function, which checks whether the field is valid,
      //to set the value.
      return $this->setField('passwordLettersAndNumbers', $passwordLettersAndNumbers);
    }

    /**
     * Get whether password must contain upper and lower case characters
     * @return type 
     */
    public function getPasswordUpperLower(){
      return $this->passwordUpperLower;
    }

    /**
     * Set whether password must contain upper and lower case characters
     * @param type $passwordUpperLower
     * @return \Application\Entity\Config 
     */
    public function setPasswordUpperLower($passwordUpperLower){
      //Use the setField function, which checks whether the field is valid,
      //to set the value.
      return $this->setField('passwordUpperLower', $passwordUpperLower);
    }

    /**
     * Get the number of failed logins before user is locked out
     * @return int 
     */
    public function getMaxFailedLogins(){
      return $this->maxFailedLogins;
    }

    /**
     * Set the number of failed logins before user is locked out
     * @param int $maxFailedLogins
     * @return \Application\Entity\Config 
     */
    public function setMaxFailedLogins($maxFailedLogins){
      //Use the setField function, which checks whether the field is valid,
      //to set the value.
      return $this->setField('maxFailedLogins', $maxFailedLogins);
    }

    /**
     * Get the password validity period in days
     * @return int 
     */
    public function getPasswordValidity(){
      return $this->passwordValidity;
    }

    /**
     * Set the password validity in days
     * @param int $passwordValidity
     * @return \Application\Entity\Config 
     */
    public function setPasswordValidity($passwordValidity){
      //Use the setField function, which checks whether the field is valid,
      //to set the value.
      return $this->setField('passwordValidity', $passwordValidity);
    }

    /**
     * Get the number of days prior to expiry that the user starts getting
     * warning messages
     * @return int 
     */
    public function getPasswordExpiryDays(){
      return $this->passwordExpiryDays;
    }

    /**
     * Get the number of days prior to expiry that the user starts getting
     * warning messages
     * @param int $passwordExpiryDays
     * @return \Application\Entity\Config 
     */
    public function setPasswordExpiryDays($passwordExpiryDays){
      //Use the setField function, which checks whether the field is valid,
      //to set the value.
      return $this->setField('passwordExpiryDays', $passwordExpiryDays);
    }

    /**
     * Get the timeout period of the application
     * @return int 
     */
    public function getTimeout(){
      return $this->timeout;
    }

    /**
     * Get the timeout period of the application
     * @param int $timeout
     * @return \Application\Entity\Config 
     */
    public function setTimeout($timeout){
      //Use the setField function, which checks whether the field is valid,
      //to set the value.
      return $this->setField('timeout', $timeout);
    }

    /**
     * Returns a list of validators for each column. These validators are checked
     * in the class' setField method, which is inherited from the Perceptive\Database\Entity class
     * @return array
     */
    public function getValidators(){
      //If the validators array hasn't been initialised, initialise it
      if(!isset($this->validators)){
        $validators = array(
            'minLengthUserId' => array(
                new I18nValidator\Int(),
                new Validator\GreaterThan(1),
            ),
            'minLengthUserName' => array(
                new I18nValidator\Int(),
                new Validator\GreaterThan(2),
            ),
            'minLengthUserPassword' => array(
                new I18nValidator\Int(),
                new Validator\GreaterThan(3),
            ),
            'daysPasswordReuse' => array(
                new I18nValidator\Int(),
                new Validator\GreaterThan(-1),
            ),
            'passwordLettersAndNumbers' => array(
                new I18nValidator\Int(),
                new Validator\GreaterThan(-1),
                new Validator\LessThan(2),
            ),
            'passwordUpperLower' => array(
                new I18nValidator\Int(),
                new Validator\GreaterThan(-1),
                new Validator\LessThan(2),
            ),
            'maxFailedLogins' => array(
                new I18nValidator\Int(),
                new Validator\GreaterThan(0),
            ),
            'passwordValidity' => array(
                new I18nValidator\Int(),
                new Validator\GreaterThan(1),
            ),
            'passwordExpiryDays' => array(
                new I18nValidator\Int(),
                new Validator\GreaterThan(1),
            ),
            'timeout' => array(
                new I18nValidator\Int(),
                new Validator\GreaterThan(0),
            )
        );
        $this->validators = $validators;
      }

      //Return the list of validators
      return $this->validators;
    }

    /**
     * @todo: add a lifecyle event which validates before persisting the entity.
     * This way there is no chance of invalid values being saved to the database.
     * This should probably be implemented in the parent class so all entities know
     * to validate.
     */
}

And my controller, which can read from and write to the entity:

<?php
/**
 * A restful controller that retrieves and updates configuration information
 */
namespace Application\Controller;

use Zend\Mvc\Controller\AbstractRestfulController;
use Zend\View\Model\JsonModel;

class ConfigController extends AbstractRestfulController
{
    /**
     * The doctrine EntityManager for use with database operations
     * @var \Doctrine\ORM\EntityManager
     */
    protected $em;

    /**
     * Constructor function manages dependencies
     * @param \Doctrine\ORM\EntityManager $em 
     */
    public function __construct(\Doctrine\ORM\EntityManager $em){
      $this->em = $em;
    }

    /**
     * Retrieves the configuration from the database 
     */
    public function getList(){
      //locate the doctrine entity manager
      $em = $this->em;

      //there should only ever be one row in the configuration table, so I use findAll
      $config = $em->getRepository("\Application\Entity\Config")->findAll();

      //return a JsonModel to the user. I use my toArray function to convert the doctrine
      //entity into an array - the JsonModel can't handle a doctrine entity itself.
      return new JsonModel(array(
        'data' => $config[0]->toArray(),
      ));
    }

    /**
     * Updates the configuration
     */
    public function replaceList($data){
      //locate the doctrine entity manager
      $em = $this->em;

      //there should only ever be one row in the configuration table, so I use findAll
      $config = $em->getRepository("\Application\Entity\Config")->findAll();

      //use the entity's fromArray function to update the data
      $config[0]->fromArray($data);

      //save the entity to the database
      $em->persist($config[0]);
      $em->flush();

      //return a JsonModel to the user. I use my toArray function to convert the doctrine
      //entity into an array - the JsonModel can't handle a doctrine entity itself.
      return new JsonModel(array(
        'data' => $config[0]->toArray(),
      )); 
    }
}

Because of character limits on I was unable to paste in my unit tests, but here are links to my unit tests so far:

For the entity: https://github.com/hputus/config-app/blob/master/module/Application/test/ApplicationTest/Entity/ConfigTest.php

For the controller: https://github.com/hputus/config-app/blob/master/module/Application/test/ApplicationTest/Controller/ConfigControllerTest.php

Some questions:

  • Am I doing anything obviously wrong here?
  • In the tests for the entity, I am repeating the same tests for many different fields? - Is there a way to minimise this? Like have a standard battery of tests to run on integer columns for instance?
  • In the controller I am trying to 'mock up' doctrine's entity manager so that changes aren't really saved into the database - am I doing this properly?
  • Is there anything else in the controller which I should test?

Thanks in advance!

\$\endgroup\$
  • 1
    \$\begingroup\$ Just a quick question: Are you, at any point, creating a mockup of the actual entities and/or entity manager? If you don't, there's a good chance you might accidentally alter actual data, which probably isn't what you want to do. And a quick tip: to avoid having to write test per field, use a dataprovider, through reflection, it'll keep calling a generic test function, that uses the arrays provided by the dataprovider function \$\endgroup\$ – Elias Van Ootegem Sep 18 '13 at 10:59
  • \$\begingroup\$ Hi, yes in my Controller test I create a mockup of the doctrine Entity Manager so the data is not really saved. So for the dataprovider, do you mean something like this? pastebin.com/cbNPBqsN Here I just have one test per field which will accept parameters from the data provider. \$\endgroup\$ – user1578653 Sep 18 '13 at 13:23
  • 1
    \$\begingroup\$ Well, sort of, you could also do this: pastebin.com/rWR5SvVN. Pass setter and getter methods to the test function, so you have 1 function to test all fields. You could even add an array instead of a value (for argument $a) and loop over that array, each time calling the assertTrue method. You could also pass the expected Exception per value, to be more specific in your test. This way, you'll test the entire model in 1 test method \$\endgroup\$ – Elias Van Ootegem Sep 18 '13 at 14:13
  • \$\begingroup\$ Thanks for that! I was just thinking, is there actually any point in 'unit testing' the controller since there is basically no logic in there, and I would need to write 'functional tests' for the controller to ensure that stuff was actually getting saved to the database anyway? I feel like I would be writing the same tests twice, once with a mocked up entity manager and once with a real one? So I'm kind of thinking of unit testing the models and functionally testing the controllers... \$\endgroup\$ – user1578653 Sep 20 '13 at 7:57
  • 1
    \$\begingroup\$ Well, there is a point in unit-testing controllers: You can test the entities, the entities, and all other objects, but unit-testing the controller (using a client) is the best way of testing how they all come together, because that's what the controller does: it decides which part/method of what object is called where \$\endgroup\$ – Elias Van Ootegem Sep 20 '13 at 8:03

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.