Symfony2 单元测试服务
Symfony2 Unit Testing a service
我对 symfony 还是很陌生,但真的很喜欢它。
我正处于成功创建和设置服务的阶段,该服务本身使用 2 个依赖项:
- A Data API 即 returns json 数据(这是一个单独的库,它
我已经作为一项服务实施并带有自己的单元测试)。
- Doctrine 实体管理器。
该服务使用 api 提取所需的数据,然后遍历数据并检查数据是否已经存在,如果存在则更新现有实体并持久化它,否则创建一个新实体分配数据并保留它。
我现在需要为此编写一个单元测试,我没有仅使用 symfony2 教程中的 PHPUnit,这些教程正在测试来自控制器的响应。
我该如何为这项服务编写单元测试?
特别是模拟我通常从 api 中提取的数据。
然后检查条目是否需要更新或创建?
一个代码示例将非常有用,因此我可以将其用作模板来为我创建的其他类似服务创建测试。
这是我要测试的服务:
<?php
namespace FantasyPro\DataBundle\DataManager;
use Doctrine\ORM\EntityManager;
use FantasyDataAPI\Client;
use FantasyPro\DataBundle\Entity\Stadium;
class StadiumParser {
/**
* @var EntityManager $em
*/
private $em;
/**
* @var Client $client
*/
private $client;
public function __construct( EntityManager $em, Client $client) {
$this->em = $em;
$this->client = $client;
}
/**
* @return array
*/
public Function parseData(){
//var_dump($this);
$stadiumData = $this->client->Stadiums();
//var_dump($stadiumData);
//get the Repo
$repo = $this->em->getRepository('DataBundle:Stadium');
$log = array();
foreach ($stadiumData as $stadium) {
// Get the current stadium in the list from the database
$criteria = array( 'stadiumID' => $stadium['StadiumID'] );
$currentStadium = $repo->FindOneBy( $criteria );
if ( ! $currentStadium) {
$currentStadium = new Stadium(); //no stadium with the StadiumID exists so create a new stadium
$logData = [
'action' => 'Added Stadium',
'itemID' => $stadium['StadiumID'],
'itemName' => $stadium['Name']
];
$log[] = $logData;
} else {
$logData = [
'action' => 'Updated Stadium',
'itemID' => $stadium['StadiumID'],
'itemName' => $stadium['Name']
];
$log[] = $logData;
}
$currentStadium->setStadiumID( $stadium['StadiumID'] );
$currentStadium->setName( $stadium['Name'] );
$currentStadium->setCity( $stadium['City'] );
$currentStadium->setState( $stadium['State'] );
$currentStadium->setCountry( $stadium['Country'] );
$currentStadium->setCapacity( $stadium['Capacity'] );
$currentStadium->setPlayingSurface( $stadium['PlayingSurface'] );
$this->em->persist( $currentStadium );
}
$this->em->flush();
return $log;
}
}
****** 更新 *******
看了一派真的回答后
我已经简化了服务,所以它不再是 returns 日志,我最初有这个,所以我可以通过将日志发送到我的控制器中的树枝模板来检查添加了什么,我最终计划将此 运行ning 作为命令,这样我就可以 运行 通过 cron 作业来执行它,这样就不需要日志位了。
我现在正在我的构造中设置实体,因为我不知道如何将实体作为注入的依赖项传递。
现在使用 createNewStadium()
方法获取一个新实体。
更新后的服务:
namespace FantasyPro\DataBundle\DataManager;
use Doctrine\ORM\EntityManager;
use FantasyDataAPI\Client;
use FantasyPro\DataBundle\Entity\Stadium;
class StadiumParser {
/**
* @var EntityManager $em
*/
private $em;
/**
* @var Client $client
*/
private $client;
/**
* @var Stadium Stadium
*/
private $stadium;
public function __construct( EntityManager $em, Client $client) {
$this->em = $em;
$this->client = $client;
}
/**
* Gets a list of stadiums using $this->client->Stadiums.
* loops through returned stadiums and persists them
* when loop has finished flush them to the db
*/
public Function parseData(){
$data = $this->client->Stadiums();
//get the Repo
$repo = $this->em->getRepository('DataBundle:Stadium');
foreach ($data as $item) {
// Get the current stadium in the list
$criteria = array( 'stadiumID' => $item['StadiumID'] );
$currentStadium = $repo->FindOneBy( $criteria );
if ( ! $currentStadium) {
$currentStadium = $this->createNewStadium; //no stadium with the StadiumID use the new stadium entity
}
$currentStadium->setStadiumID( $item['StadiumID'] );
$currentStadium->setName( $item['Name'] );
$currentStadium->setCity( $item['City'] );
$currentStadium->setState( $item['State'] );
$currentStadium->setCountry( $item['Country'] );
$currentStadium->setCapacity( $item['Capacity'] );
$currentStadium->setPlayingSurface( $item['PlayingSurface'] );
$this->em->persist( $currentStadium );
}
$this->em->flush();
}
// Adding this new method gives you the ability to mock this dependency when testing
private function createNewStadium()
{
return new Stadium();
}
}
您基本上需要的是使用所谓的“Test doubles”对服务进行单元测试。
这意味着您应该模拟您的服务所具有的依赖关系,这样您就可以仅测试孤立的服务而无需真正依赖依赖项,而只能依赖于它们的模拟版本,具有硬编码的值或行为。
基于您的实际实现的真实示例是不可能的,因为您有紧密耦合的部门 $currentStadium = new Stadium();
。您应该在构造函数中或通过 getter/setter 传递像这样的 dep,以便能够在单元测试时模拟它。
一旦完成,一个非常具有指示性的示例将是:
// class StadiumParser revisited and simplified
class StadiumParser
{
private $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function parseData()
{
$stadiumData = $this->client->Stadiums();
// do something with the repo
$log = array();
foreach ($stadiumData as $stadium) {
$logData = [
'action' => 'Added Stadium',
'itemID' => $stadium['StadiumID'],
'itemName' => $stadium['Name']
];
$log[] = $logData;
}
// do something else with Doctrine
return $log;
}
}
和测试
// StadiumParser Unit Test
class StadiumParserTest extends PHPUnit_Framework_TestCase
{
public function testItParseDataAndReturnTheLog()
{
$client = $this->getMock('FantasyDataAPI\Client');
// since you class is returning a log array, we mock it here
$expectedLog = array(
array(
'action' => 'Added Stadium',
'itemID' => $stadium['StadiumID'],
'itemName' => $stadium['Name']
)
);
// this is the mocked or test double part.
// We only need this method return something without really calling it
// So we mock it and we hardcode the expected return value
$stadiumData = array(
array(
"StadiumID" => 1,
"Name" => "aStadiumName"
)
);
$client->expects($this->once())
->method('Stadiums')
->will($this->returnValue($stadiumData));
$stadiumParser = new StadiumParser($client);
$this->assertEquals($expectedLog, $stadiumParser->parseData());
}
}
我自愿省略了 EntityManager 部分,因为我想你应该看看与 how to unit test code interacting with the database
相关的 Symfony 文档
-----EDIT2-----
是的,他是对的,你不应该。想到的一种可能方法是在 protected/private 方法中提取实体的创建。类似于:
// class StadiumParser
public Function parseData()
{
...
foreach ($stadiumData as $stadium) {
...
if ( ! $currentStadium) {
$currentStadium = $this->createNewStadium();
...
}
// Adding this new method gives you the ability to mock this dependency when testing
private function createNewStadium()
{
return new Stadium();
}
-----EDIT3-----
我想向您推荐另一种方法。如果 Stadium
实体在不同服务或同一服务的不同部分中需要,这可能是一个更好的选择。我提议的 Builder pattern but a Factory 也可以是这里的一个选项。浏览一下它们的差异。
正如您所看到的,这从方法中提取了一些代码,更好地分配 类 之间的责任,让您和您的队友更清晰、更容易阅读。而且您已经知道如何在测试时模拟它。
class StadiumParser
{
private $stadiumBuilder;
...
public function __construct( StadiumBuilder $builder, ...) {
$this->stadiumBuilder = $stadiumBuilder;
...
}
public Function parseData()
{
...
foreach ($stadiumData as $stadium) {
...
$currentStadium = $repo->FindOneBy( $criteria );
if ( ! $currentStadium) {
$currentStadium = $this->stadiumBuilder->build($currentStadium, $stadium);
}
$this->em->persist($currentStadium);
...
在某个地方你有这个新的 Builder return 一个 Stadium
实例。这样,您的 StadiumParser
服务就不再与实体耦合,但 StadiumBuilder
就是了。逻辑是这样的:
// StadiumBuilder class
namespace ???
use FantasyPro\DataBundle\Entity\Stadium;
class StadiumBuilder
{
// depending on the needs but this should also has a different name
// like buildBasic or buildFull or buildBlaBlaBla or buildTest
public function build($currentStadium = null, $stadium)
{
if (!$currentStadium) {
$currentStadium = new Stadium();
}
$currentStadium->setStadiumID( $stadium['StadiumID'] );
$currentStadium->setName( $stadium['Name'] );
$currentStadium->setCity( $stadium['City'] );
$currentStadium->setState( $stadium['State'] );
$currentStadium->setCountry( $stadium['Country'] );
$currentStadium->setCapacity( $stadium['Capacity'] );
$currentStadium->setPlayingSurface( $stadium['PlayingSurface'] );
return $currentStadium;
}
}
我对 symfony 还是很陌生,但真的很喜欢它。
我正处于成功创建和设置服务的阶段,该服务本身使用 2 个依赖项:
- A Data API 即 returns json 数据(这是一个单独的库,它 我已经作为一项服务实施并带有自己的单元测试)。
- Doctrine 实体管理器。
该服务使用 api 提取所需的数据,然后遍历数据并检查数据是否已经存在,如果存在则更新现有实体并持久化它,否则创建一个新实体分配数据并保留它。
我现在需要为此编写一个单元测试,我没有仅使用 symfony2 教程中的 PHPUnit,这些教程正在测试来自控制器的响应。
我该如何为这项服务编写单元测试? 特别是模拟我通常从 api 中提取的数据。 然后检查条目是否需要更新或创建?
一个代码示例将非常有用,因此我可以将其用作模板来为我创建的其他类似服务创建测试。
这是我要测试的服务:
<?php
namespace FantasyPro\DataBundle\DataManager;
use Doctrine\ORM\EntityManager;
use FantasyDataAPI\Client;
use FantasyPro\DataBundle\Entity\Stadium;
class StadiumParser {
/**
* @var EntityManager $em
*/
private $em;
/**
* @var Client $client
*/
private $client;
public function __construct( EntityManager $em, Client $client) {
$this->em = $em;
$this->client = $client;
}
/**
* @return array
*/
public Function parseData(){
//var_dump($this);
$stadiumData = $this->client->Stadiums();
//var_dump($stadiumData);
//get the Repo
$repo = $this->em->getRepository('DataBundle:Stadium');
$log = array();
foreach ($stadiumData as $stadium) {
// Get the current stadium in the list from the database
$criteria = array( 'stadiumID' => $stadium['StadiumID'] );
$currentStadium = $repo->FindOneBy( $criteria );
if ( ! $currentStadium) {
$currentStadium = new Stadium(); //no stadium with the StadiumID exists so create a new stadium
$logData = [
'action' => 'Added Stadium',
'itemID' => $stadium['StadiumID'],
'itemName' => $stadium['Name']
];
$log[] = $logData;
} else {
$logData = [
'action' => 'Updated Stadium',
'itemID' => $stadium['StadiumID'],
'itemName' => $stadium['Name']
];
$log[] = $logData;
}
$currentStadium->setStadiumID( $stadium['StadiumID'] );
$currentStadium->setName( $stadium['Name'] );
$currentStadium->setCity( $stadium['City'] );
$currentStadium->setState( $stadium['State'] );
$currentStadium->setCountry( $stadium['Country'] );
$currentStadium->setCapacity( $stadium['Capacity'] );
$currentStadium->setPlayingSurface( $stadium['PlayingSurface'] );
$this->em->persist( $currentStadium );
}
$this->em->flush();
return $log;
}
}
****** 更新 ******* 看了一派真的回答后
我已经简化了服务,所以它不再是 returns 日志,我最初有这个,所以我可以通过将日志发送到我的控制器中的树枝模板来检查添加了什么,我最终计划将此 运行ning 作为命令,这样我就可以 运行 通过 cron 作业来执行它,这样就不需要日志位了。
我现在正在我的构造中设置实体,因为我不知道如何将实体作为注入的依赖项传递。
现在使用 createNewStadium()
方法获取一个新实体。
更新后的服务:
namespace FantasyPro\DataBundle\DataManager;
use Doctrine\ORM\EntityManager;
use FantasyDataAPI\Client;
use FantasyPro\DataBundle\Entity\Stadium;
class StadiumParser {
/**
* @var EntityManager $em
*/
private $em;
/**
* @var Client $client
*/
private $client;
/**
* @var Stadium Stadium
*/
private $stadium;
public function __construct( EntityManager $em, Client $client) {
$this->em = $em;
$this->client = $client;
}
/**
* Gets a list of stadiums using $this->client->Stadiums.
* loops through returned stadiums and persists them
* when loop has finished flush them to the db
*/
public Function parseData(){
$data = $this->client->Stadiums();
//get the Repo
$repo = $this->em->getRepository('DataBundle:Stadium');
foreach ($data as $item) {
// Get the current stadium in the list
$criteria = array( 'stadiumID' => $item['StadiumID'] );
$currentStadium = $repo->FindOneBy( $criteria );
if ( ! $currentStadium) {
$currentStadium = $this->createNewStadium; //no stadium with the StadiumID use the new stadium entity
}
$currentStadium->setStadiumID( $item['StadiumID'] );
$currentStadium->setName( $item['Name'] );
$currentStadium->setCity( $item['City'] );
$currentStadium->setState( $item['State'] );
$currentStadium->setCountry( $item['Country'] );
$currentStadium->setCapacity( $item['Capacity'] );
$currentStadium->setPlayingSurface( $item['PlayingSurface'] );
$this->em->persist( $currentStadium );
}
$this->em->flush();
}
// Adding this new method gives you the ability to mock this dependency when testing
private function createNewStadium()
{
return new Stadium();
}
}
您基本上需要的是使用所谓的“Test doubles”对服务进行单元测试。
这意味着您应该模拟您的服务所具有的依赖关系,这样您就可以仅测试孤立的服务而无需真正依赖依赖项,而只能依赖于它们的模拟版本,具有硬编码的值或行为。
基于您的实际实现的真实示例是不可能的,因为您有紧密耦合的部门 $currentStadium = new Stadium();
。您应该在构造函数中或通过 getter/setter 传递像这样的 dep,以便能够在单元测试时模拟它。
一旦完成,一个非常具有指示性的示例将是:
// class StadiumParser revisited and simplified
class StadiumParser
{
private $client;
public function __construct(Client $client)
{
$this->client = $client;
}
public function parseData()
{
$stadiumData = $this->client->Stadiums();
// do something with the repo
$log = array();
foreach ($stadiumData as $stadium) {
$logData = [
'action' => 'Added Stadium',
'itemID' => $stadium['StadiumID'],
'itemName' => $stadium['Name']
];
$log[] = $logData;
}
// do something else with Doctrine
return $log;
}
}
和测试
// StadiumParser Unit Test
class StadiumParserTest extends PHPUnit_Framework_TestCase
{
public function testItParseDataAndReturnTheLog()
{
$client = $this->getMock('FantasyDataAPI\Client');
// since you class is returning a log array, we mock it here
$expectedLog = array(
array(
'action' => 'Added Stadium',
'itemID' => $stadium['StadiumID'],
'itemName' => $stadium['Name']
)
);
// this is the mocked or test double part.
// We only need this method return something without really calling it
// So we mock it and we hardcode the expected return value
$stadiumData = array(
array(
"StadiumID" => 1,
"Name" => "aStadiumName"
)
);
$client->expects($this->once())
->method('Stadiums')
->will($this->returnValue($stadiumData));
$stadiumParser = new StadiumParser($client);
$this->assertEquals($expectedLog, $stadiumParser->parseData());
}
}
我自愿省略了 EntityManager 部分,因为我想你应该看看与 how to unit test code interacting with the database
相关的 Symfony 文档-----EDIT2-----
是的,他是对的,你不应该。想到的一种可能方法是在 protected/private 方法中提取实体的创建。类似于:
// class StadiumParser
public Function parseData()
{
...
foreach ($stadiumData as $stadium) {
...
if ( ! $currentStadium) {
$currentStadium = $this->createNewStadium();
...
}
// Adding this new method gives you the ability to mock this dependency when testing
private function createNewStadium()
{
return new Stadium();
}
-----EDIT3-----
我想向您推荐另一种方法。如果 Stadium
实体在不同服务或同一服务的不同部分中需要,这可能是一个更好的选择。我提议的 Builder pattern but a Factory 也可以是这里的一个选项。浏览一下它们的差异。
正如您所看到的,这从方法中提取了一些代码,更好地分配 类 之间的责任,让您和您的队友更清晰、更容易阅读。而且您已经知道如何在测试时模拟它。
class StadiumParser
{
private $stadiumBuilder;
...
public function __construct( StadiumBuilder $builder, ...) {
$this->stadiumBuilder = $stadiumBuilder;
...
}
public Function parseData()
{
...
foreach ($stadiumData as $stadium) {
...
$currentStadium = $repo->FindOneBy( $criteria );
if ( ! $currentStadium) {
$currentStadium = $this->stadiumBuilder->build($currentStadium, $stadium);
}
$this->em->persist($currentStadium);
...
在某个地方你有这个新的 Builder return 一个 Stadium
实例。这样,您的 StadiumParser
服务就不再与实体耦合,但 StadiumBuilder
就是了。逻辑是这样的:
// StadiumBuilder class
namespace ???
use FantasyPro\DataBundle\Entity\Stadium;
class StadiumBuilder
{
// depending on the needs but this should also has a different name
// like buildBasic or buildFull or buildBlaBlaBla or buildTest
public function build($currentStadium = null, $stadium)
{
if (!$currentStadium) {
$currentStadium = new Stadium();
}
$currentStadium->setStadiumID( $stadium['StadiumID'] );
$currentStadium->setName( $stadium['Name'] );
$currentStadium->setCity( $stadium['City'] );
$currentStadium->setState( $stadium['State'] );
$currentStadium->setCountry( $stadium['Country'] );
$currentStadium->setCapacity( $stadium['Capacity'] );
$currentStadium->setPlayingSurface( $stadium['PlayingSurface'] );
return $currentStadium;
}
}