PHPUnit TDD问题

I'm learning TDD and I have some questions. - The idea is to create a Simple Factory guided by tests. The thing is that my test coverage isn't 100% and that is where my questions lies.

Before I show you the code, let me explain what I want.

  • The factory is responsable to instantiate the classes and to test this I have to simulate the instantiation of the class, doing this I'm using a concrete class, it works, but when I see the test coverage, the concrete class appear and it isn't 100% tested. (You will understand). - I want to avoid that, I want to test the factory as abstract as possible.
  • And the other problem is that I want to insert the factory as dependency on the constructor, how to test this?

The coverage:

  • PizzaStore.php - 33,33% - The constructor with the factory dependency isn't tested.
  • PizzaFactory.php - 100%
  • Pizza.php - 100%
  • Pizza/Greek.php - 0.00% - Appeared after testing the factory using the concrete class.

yoda

PizzaTest.php:

<?php namespace Pattern\SimpleFactory;

use PHPUnit_Framework_TestCase;

class PizzaTest extends PHPUnit_Framework_TestCase {

  private $pizza;

  public function setUp()
  {
    $this->pizza = $this->getMockForAbstractClass('Pattern\SimpleFactory\Pizza');
  }

  /**
   * @covers Pattern\SimpleFactory\Pizza::setName
   * @covers Pattern\SimpleFactory\Pizza::getName
   */
  public function testPizzaSetsAndReturnsTheExpectedName()
  {
    $pizzaName = 'Greek Pizza';
    $this->pizza->setName($pizzaName);
    $this->assertEquals($pizzaName, $this->pizza->getName());
  }

  /**
   * @covers Pattern\SimpleFactory\Pizza::setDescription
   * @covers Pattern\SimpleFactory\Pizza::getDescription
   */
  public function testPizzaSetsAndReturnsTheExpectedDescription()
  {
    $pizzaDescription = 'A Pepperoni-style pizza with dough, tomato, and cheese';
    $this->pizza->setDescription($pizzaDescription);
    $this->assertEquals($pizzaDescription, $this->pizza->getDescription());
  }

}

PizzaStoreTest.php:

<?php namespace Pattern\SimpleFactory;

use PHPUnit_Framework_TestCase;

class PizzaStoreTest extends PHPUnit_Framework_TestCase {

  /**
   * @covers Pattern\SimpleFactory\PizzaStore::order
   */
  public function testStoreShouldReturnsTheRequestedPizzaWhenOrdered()
  {
    $factory = new PizzaFactory();
    $store = new PizzaStore($factory);
    $pizza = $store->order('greek');
    $this->assertInstanceOf('Pattern\SimpleFactory\Pizza', $pizza);
  }

}

PizzaFactoryTest.php:

<?php namespace Pattern\SimpleFactory;

use PHPUnit_Framework_TestCase;

class PizzaFactoryTest extends PHPUnit_Framework_TestCase {

  /**
   * @var PizzaFactory
   */
  private $pizzaFactory;

  public function setUp()
  {
    $this->pizzaFactory = new PizzaFactory();
  }

  /**
   * @covers Pattern\SimpleFactory\PizzaFactory::make
   */
  public function testPizzaFactoryMakesAPizza()
  {
    $pizza = $this->pizzaFactory->make('greek');
    $this->assertInstanceOf('Pattern\SimpleFactory\Pizza', $pizza);
  }

  /**
   * @covers Pattern\SimpleFactory\PizzaFactory::make
   */
  public function testPizzaFactoryReturnsNullWhenMakingANonexistentPizza()
  {
    $pizza = $this->pizzaFactory->make(null);
    $this->isNull($pizza);
  }

}

PizzaStore.php:

<?php namespace Pattern\SimpleFactory;

/**
 * @package Pattern\SimpleFactory
 */
class PizzaStore {

  /**
   * @var PizzaFactory
   */
  private $factory;

  /**
   * @param PizzaFactory $factory
   */
  function __construct(PizzaFactory $factory)
  {
    $this->factory = $factory;
  }

  /**
   * @param string $name
   * @return null|Pizza
   */
  public function order($name)
  {
    return $this->factory->make($name);
  }

}

PizzaFactory.php:

<?php namespace Pattern\SimpleFactory;

/**
 * @package Pattern\SimpleFactory
 */
class PizzaFactory {

  /**
   * @var array
   */
  private $pizzas = [
    'greek'     => 'Pattern\SimpleFactory\Pizza\Greek',
    'pepperoni' => 'Pattern\SimpleFactory\Pizza\Pepperoni',
  ];

  /**
   * @param string $name
   * @return null|Pizza
   */
  public function make($name)
  {
    if (isset($this->pizzas[$name]))
    {
      return new $this->pizzas[$name];
    }

    return null;
  }

}

Pizza.php:

<?php namespace Pattern\SimpleFactory;

/**
 * @package Pattern\SimpleFactory
 */
abstract class Pizza {

  /**
   * @var string
   */
  private $name;
  /**
   * @var string
   */
  private $description;

  /**
   * @return string
   */
  public function getName()
  {
    return $this->name;
  }

  /**
   * @param string $name
   */
  public function setName($name)
  {
    $this->name = $name;
  }

  /**
   * @return string
   */
  public function getDescription()
  {
    return $this->description;
  }

  /**
   * @param string $description
   */
  public function setDescription($description)
  {
    $this->description = $description;
  }

}

Pizza\Pepperoni.php:

<?php namespace Pattern\SimpleFactory\Pizza;

use Pattern\SimpleFactory\Pizza;

/**
 * @package Pattern\SimpleFactory\Pizza
 */
class Pepperoni extends Pizza {

  function __construct()
  {
    parent::setName('Pizza Pepperoni');
    parent::setDescription('A Pepperoni-style pizza with dough, tomato, and cheese');
  }

}

Pizza\Greek.php:

<?php namespace Pattern\SimpleFactory\Pizza;

use Pattern\SimpleFactory\Pizza;

/**
 * @package Pattern\SimpleFactory\Pizza
 */
class Greek extends Pizza {

  function __construct()
  {
    parent::setName('Pizza Greek');
    parent::setDescription('A Greek-style pizza with feta cheese, onion, olive and tomato');
  }

}

And that is it, anything wrong, please, say it. Thank you in advance.

The factory is responsible to instantiate the classes and to test this I have to simulate the instantiation of the class, doing this I'm using a concrete class, it works, but when I see the test coverage, the concrete class appear and it isn't 100% tested. (You will understand). - I want to avoid that, I want to test the factory as abstract as possible.

This missing coverage at PizzaStore class is caused because you don't testing case when factory doesn't found class.

Missing test case:

/**
 * @covers Pattern\SimpleFactory\PizzaStore::order
 */
public function testStoreShouldReturnsTheRequestedPizzaWhenOrdered()
{
    $factory = new PizzaFactory();
    $store = new PizzaStore($factory);
    $pizza = $store->order('not exists pizza type');
    $this->assertNull($pizza);
}

And the other problem is that I want to insert the factory as dependency on the constructor, how to test this?

You could use mocked PizzaFactory class and verify is then called. I don't know how this work with PHPUnit mocks, but in ouzo-goodies mock utility this looks like:

public function testFactory()
{
    $factory = Mock::create('\Pattern\SimpleFactory\PizzaFactory');
    Mock::when($factory)->make()->thenReturn(new \Pattern\SimpleFactory\Pizza\Greek());

    $magic = new Magic($factory);

    $order = $magic->order();

    Mock::verify($factory)->make();
    $this->assertInstanceOf('\Pattern\SimpleFactory\Pizza\Greek', $order);
}

This Mock::verify method checks that method PizzaFactory::make was called.

Additionally in my opinion in your case better use interface over inheritance.