我对 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(),
));
}
}
由于字符限制,我无法粘贴到我的单元测试中,但到目前为止,这里是我的单元测试的链接:
对于 Controller : https://github.com/hputus/config-app/blob/master/module/Application/test/ApplicationTest/Controller/ConfigControllerTest.php
一些问题:
- 我在这里做错了什么吗?
- 在实体测试中,我对许多不同的字段重复相同的测试 - 有没有办法将这种情况最小化?例如,有一组标准的测试可以在整数列上运行吗?
- 在 Controller 中,我试图“模拟”学说的实体管理器,以便更改不会真正保存到数据库中 - 我这样做是否正确?
- Controller 中还有什么我应该测试的吗?
提前致谢!
最佳答案
虽然您的代码看起来足够可靠,但它存在一些设计疏忽。
首先,Doctrine 建议将实体视为简单、愚蠢的值对象,并声明它们持有的数据始终被假定为有效。
这意味着任何业务逻辑,如水合作用、过滤和验证,都应该从实体外部移到一个单独的层。
说到水合作用,与其自己实现 fromArray
和 toArray
方法,不如使用提供的 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/