php - Zend Framework 和 Doctrine 2 - 我的单元测试是否足够?

标签 php unit-testing zend-framework2 phpunit

我对 Zend 和一般的单元测试还很陌生。我想出了一个使用 Zend Framework 2 和 Doctrine 的小应用程序。它只有一个模型和 Controller ,我想对它们运行一些单元测试。

这是我目前所拥有的:

基本原则“实体”类,包含我想在所有实体中使用的方法:

<?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);
      }
    }
}

我的“配置”实体,代表一个包含一些配置选项的单行表:

<?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.
     */
}

还有我的 Controller ,它可以读取和写入实体:

<?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(),
      )); 
    }
}

由于字符限制,我无法粘贴到我的单元测试中,但到目前为止,这里是我的单元测试的链接:

对于实体: https://github.com/hputus/config-app/blob/master/module/Application/test/ApplicationTest/Entity/ConfigTest.php

对于 Controller : https://github.com/hputus/config-app/blob/master/module/Application/test/ApplicationTest/Controller/ConfigControllerTest.php

一些问题:

  • 我在这里做错了什么吗?
  • 在实体测试中,我对许多不同的字段重复相同的测试 - 有没有办法将这种情况最小化?例如,有一组标准的测试可以在整数列上运行吗?
  • 在 Controller 中,我试图“模拟”学说的实体管理器,以便更改不会真正保存到数据库中 - 我这样做是否正确?
  • Controller 中还有什么我应该测试的吗?

提前致谢!

最佳答案

虽然您的代码看起来足够可靠,但它存在一些设计疏忽。

首先,Doctrine 建议将实体视为简单、愚蠢的值对象,并声明它们持有的数据始终被假定为有效。

这意味着任何业务逻辑,如水合作用、过滤和验证,都应该从实体外部移到一个单独的层。

说到水合作用,与其自己实现 fromArraytoArray 方法,不如使用提供的 DoctrineModule\Stdlib\Hydrator\DoctrineObject hydrator,它还可以与 Zend\InputFilter 完美融合,以处理过滤和验证。这将使实体测试变得不那么冗长,并且可以说不需要,因为您将单独测试过滤器。

来自 Doctrine 开发人员的另一个重要建议是不要将 ObjectManager 直接注入(inject)到 Controller 中。这是出于封装目的:希望将持久层的实现细节隐藏到 Controller 中,并且同样只公开一个中间层。

在你的情况下,所有这一切都可以通过一个 ConfigService 类来完成,该类由契约(Contract)设计,它将只提供你真正需要的方法(即 findAll()persist() 和其他方便的代理),并将隐藏 Controller 并非严格需要的依赖项,如 EntityManager、输入过滤器等。它还将有助于更轻松地模拟。

这样,如果有一天你想在你的持久层做一些改变,你只需要改变你的实体服务实现它的契约的方式:考虑添加一个自定义缓存适配器,或者使用 Doctrine 的 ODM 而不是ORM,甚至根本不使用 Doctrine。

除此之外,您的单元测试方法看起来还不错。

长话短说

  • 您不应将业务逻辑嵌入到 Doctrine 实体中。
  • 您应该将水化器与输入过滤器一起使用。
  • 您不应在 Controller 中注入(inject) EntityManager。
  • 中间层将有助于实现这些变化,同时保持模型和 Controller 的解耦。

关于php - Zend Framework 和 Doctrine 2 - 我的单元测试是否足够?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/18789468/

相关文章:

php - 如何获取 `world_id` = '.$world_id.' 的 mysql 数据库结果;在 PHP 上?

php - 代码点火器 : how to get process data with ajax and csrf

php - REST API 中的用户注册

unit-testing - 在 XUnit 测试中使用 AutoData 和 MemberData 属性

powershell - Pester 如何模拟 "test"模式中的 "test existence (not found) - create - test again to confirm creation"函数?

php - 在isset($_GET ['post_id' ]) === true)之后,需要调用Javascript将<div>可见性变为隐藏

ruby-on-rails - Date 与 ActiveSupport::TimeWithZone 的比较失败

zend-framework - 如何验证 ZF2 中的复选框

php - 从 Zend Framework 2 中的模块发布 Assets

php - fatal error : Call to a member function getId() on null in doctrine