Symfony2单元测试服务

时间:2015-04-24 07:55:14

标签: symfony testing service phpunit

我对symfony还很新,真的很享受。

我正处于创建和设置服务的阶段,服务本身使用2个依赖项:

  • 返回json数据的Data API(这是一个单独的库 我已经实现了一项服务并且自带了单元测试。)
  • The Doctrine Entity Manager。

该服务使用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;
    }
}

******更新******* 看完ilpaijin的回答后。

我已经简化了服务,所以它不再返回日志,我最初有这个,所以我可以检查通过将日志发送到我的控制器中的树枝模板添加了什么,我最终计划运行一个命令,所以我可以通过一个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();
    }
}

1 个答案:

答案 0 :(得分:3)

您基本上需要的是使用所谓的&#34; Test doubles&#34;单元测试服务。

这意味着您应该模拟服务所依赖的依赖关系,这样您就可以在不依赖于deps的情况下单独测试服务,而只能在它们的模拟版本上使用硬编码值或行为进行测试。

基于您的实际实现的真实示例是不可能的,因为您有紧密耦合的代数为$currentStadium = new Stadium();。您应该在构造函数中或通过getter / setter传递这些deps,以便能够在单元测试时模拟它。

一旦完成,一个非常有说服力的例子将是:

// 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 Doc

----- ----- EDIT2

是的他是对的,你不应该。想到的一种可能方式是在受保护/私有方法中提取实体的创建。类似的东西:

// 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模式,但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返回一个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; 
    }
}