Zend Framework和Doctrine 2-我的单元测试是否足够? [英] Zend Framework and Doctrine 2 - are my unit tests sufficient?

查看:48
本文介绍了Zend Framework和Doctrine 2-我的单元测试是否足够?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一般对Zend和单元测试都不陌生.我提出了一个使用Zend Framework 2和Doctrine的小型应用程序.它只有一个模型和控制器,我想对它们进行一些单元测试.

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

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

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:

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

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

一些问题:

  • 我在这里明显做错了什么吗?
  • 在针对实体的测试中,我对许多不同的字段重复进行相同的测试-有没有办法将其最小化?例如,是否有一组标准的测试可以在整数列上运行?
  • 在控制器中,我试图模仿"该学说的实体管理器,以便将更改未真正保存到数据库中-我这样做是否正确?
  • 控制器中还有其他我应该测试的东西吗?

提前谢谢!

推荐答案

虽然您的代码看起来足够牢固,但它提出了一些设计疏忽.

While your code appears to be solid enough, it presents a couple of design oversights.

首先,Doctrine建议将实体视为简单的,愚蠢的值对象,并指出它们所持有的数据始终被认为是有效的.

First of all, Doctrine advise to treat entities like simple, dumb value objects, and states that the data they hold is always assumed to be valid.

这意味着任何业务逻辑(如水合作用,过滤和验证)都应移至实体外部,并移到单独的层.

This means that any business logic, like hydration, filtering and validation, should be moved outside entities to a separate layer.

说到水合,而不是自己实现fromArraytoArray方法,您可以使用提供的DoctrineModule\Stdlib\Hydrator\DoctrineObject水合器(也可以与Zend\InputFilter完美混合)来处理过滤和验证.这将使实体测试的详细程度降低很多,并且可以说不需要,因为您将单独测试过滤器.

Speaking of hydration, rather than implementing by yourself fromArray and toArray methods, you could use the supplied DoctrineModule\Stdlib\Hydrator\DoctrineObject hydrator, which can also blend flawlessly with Zend\InputFilter, to handle filtering and validation. This would make entity testing much much less verbose, and arguably not so needed, since you would test the filter separately.

Doctrine开发人员提出的另一个重要建议是不要在控制器内部直接插入ObjectManager.这是出于封装的目的:希望将持久层的实现细节隐藏到Controller,并且再次仅公开中间层.

Another important suggestion coming from Doctrine devs is to not inject an ObjectManager directly inside controllers. This is for encapsulation purposes: it is desirable to hide implementation details of your persistence layer to the Controller and, again, expose only an intermediate layer.

在您的情况下,所有这些都可以通过按合同设计的ConfigService类来完成,该类仅提供您真正需要的方法(即findAll()persist()和其他方便的代理),并且隐藏控制器严格不需要的依赖项,例如EntityManager,输入过滤器等.它还将简化模拟.

In your case, all this could be done by having a ConfigService class, designed by contract, which will only provide the methods you really need (i.e. findAll(), persist() and other handy proxies), and will hide the dependencies that are not strictly needed by the controller, like the EntityManager, input filters and the like. It will also contribute to easier mocking.

这样,如果有一天您想对持久层进行一些更改,则只需要更改实体服务实现其合同的方式即可:考虑添加自定义缓存适配器,或使用Doctrine的ODM而不是ORM,甚至根本不使用教义.

This way, if one day you would want to do some changes in your persistence layer, you would just have to change how your entity service implements its contract: think about adding a custom cache adapter, or using Doctrine's ODM rather than the ORM, or even not using Doctrine at all.

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

Other than that, your unit testing approach looks fine.

TL; DR

  • 您不应将业务逻辑嵌入到Doctrine实体中.
  • 您应该将水化器与输入过滤器一起使用.
  • 您不应将EntityManager注入控制器中.
  • 中间层将有助于实现这些变化,同时保留模型和控制器的去耦.

这篇关于Zend Framework和Doctrine 2-我的单元测试是否足够?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆