dongpin2969 2013-10-04 13:16
浏览 46
已采纳

PHPUnit,在我的抽象类及其子类测试中避免重复的正确方法

I'm trying to follow/learn TDD. I'm using PHPUnit. At the moment I'm writing small, very simple classes/projects simply for taking it all in.

At the moment I'm writing a play card setup.

So to start it, I wrote this test, as I would avoid repetition in my card suits, I decided that the test should be for an abstract class which the suits could extend from.

The pattern of the classes, hopefully, are obvious when you get to the end of the post.

BaseSuitTest.php

use Cards\French\Suits\BaseSuit as Card;

class BaseSuitTest extends PHPUnit_Framework_TestCase 
{

    protected $Card;

    public function setUp()
    {
        $this->Card = $this->getMockForAbstractClass('Cards\French\Suits\BaseSuit', [10]);
    }


    /**
    *   @expectedException InvalidArgumentException
    */
    public function testConstructThrowsExceptionIfFirstArgLessThan1()
    {
        $Card = $this->getMockForAbstractClass('Cards\French\Suits\BaseSuit', [0]);
    }


    /**
    *   @expectedException InvalidArgumentException
    */
    public function testConstructThrowsExceptionIfFirstArgHigherThan13()
    {
        $Card = $this->getMockForAbstractClass('Cards\French\Suits\BaseSuit', [14]);
    }


    public function testGetSuitReturnsClassName() 
    {
        $suit = $this->Card->getSuit();
        $this->assertSame( get_class($this->Card), $suit);
    }


    public function testGetValueReturnsValue()
    {
        $value = $this->Card->getValue();
        $this->assertSame(10, $value);
    }
}

Afterwards I wrote the following file, making all tests pass. As this point I'm also thinking if I should implement a CardInterface to the BaseSuit (and should I write a test to check for the implementation or is this not part of the tests). I decided not to.

BaseSuit.php

namespace Cards\French\Suits;

abstract class BaseSuit 
{

    const MIN_VALUE = 1;
    const MAX_VALUE = 13;


    protected $value;


    public function __construct($value)
    {
        if( $this->isWithinValueRange($value) === false)
            throw new \InvalidArgumentException('The value must be higer than 0 and less than 14 (1-13) Given ' . $value);
        $this->value = $value;
    }


    protected function isWithinValueRange($value)
    {
        if($value < self::MIN_VALUE OR $value > self::MAX_VALUE)
            return false;
        return true;
    }


    public function getSuit()
    {
        return get_class($this);
    }


    public function getValue()
    {
         return $this->value;
    }
}

Finally I started to make tests for the actual classes/suits that would extend from the base class.

The test is very very similar to those in the BaseSuit, which is already annoying me inside my head.

HeartTest.php

use Cards\French\Suits\Heart as Heart;

class HeartTest extends PHPUnit_Framework_TestCase 
{

    protected $Heart;


    public function setUp()
    {
        $this->Heart = new Heart(10);
    }


    /**
    *   @expectedException InvalidArgumentException
    */
    public function testConstructThrowsExceptionIfFirstArgLessThan1()
    {
        $Heart = new Heart(0);
    }


    /**
    *   @expectedException InvalidArgumentException
    */
    public function testConstructThrowsExceptionIfFirstArgHigherThan13()
    {
        $Heart = new Heart(14);
    }


    public function testGetSuitReturnsClassName() 
    {
        $suit = $this->Heart->getSuit();
        $this->assertSame( get_class($this->Heart), $suit);
    }


    public function testGetValueReturnsValue()
    {
        $value = $this->Heart->getValue();
        $this->assertSame(10, $value);
    }
}

I once again write my code so the tests pass.

Heart.php

namespace Cards\French\Suits;

class Heart extends BaseSuit {}

All tests pass but all is not cool. I would like to avoid the repetition in the tests for suits that extend from the BaseSuit (like Heart).

My intial thought was to rename the methods in the BaseSuit to protected And then extending from this test. Here is an example of one of the BaseSuit tests renamed.

A refactored method in BaseSuit.php

/**
*   @expectedException InvalidArgumentException
*/
protected function constructThrowsExceptionIfFirstArgLessThan1($class)
{
    // .. i have not written this code, it just a figment of my imagination.
}

This is where I split. I'm unsure on how to continue.

Moving the tests in the BaseSuitTest to protected methods would mean I would either loose the current public test methods. This could be fixed my making new tests method which calls the protected methods.

Code examples, refactoring suggetions, pattern suggestion and whatever constructive pops inside your head while reading this would be much to my appreciation.

I have been recommended to use a provider for the tests that run on the same method. Smart, I will do this is in the future, but I'm not able to see how this could fix my situation. Simply when its the abstract BaseSuit, I want to use getMockForAbstract in the test and when it is the suits, well i just I should be more like the HeartTest. I need help understanding how to implement this the right way.

  • 写回答

1条回答 默认 最新

  • dongshenchi5364 2013-10-05 18:10
    关注

    I think you're missing the primary goal of TDD. Instead of asking how to refactor your tests to avoid code and effort duplication, you should be using the tests to inform the design of the non-test code. Why is Heart its own class? What behavior does it alter or add to BaseSuit that will differ from Spade, Diamond, and Club?

    You've already made the card's ordinal value into a data attribute; you should do the same with the suit. Instead of each card being a suit with a value it should have a suit and a value. This allows you to have a single, concrete Card class with no need for base or abstract versions. Create a Suits class that defines the different suit values using constants and any methods needed for ordering (though those probably belong to a Rules class).

    If you still find that you need to model the suits with more complexity, I would push for sticking to a single generic Suit class with the name, symbol, order, etc. as data attributes.

    本回答被题主选为最佳回答 , 对您是否有帮助呢?
    评论

报告相同问题?

悬赏问题

  • ¥17 pro*C预编译“闪回查询”报错SCN不能识别
  • ¥15 微信会员卡接入微信支付商户号收款
  • ¥15 如何获取烟草零售终端数据
  • ¥15 数学建模招标中位数问题
  • ¥15 phython路径名过长报错 不知道什么问题
  • ¥15 深度学习中模型转换该怎么实现
  • ¥15 HLs设计手写数字识别程序编译通不过
  • ¥15 Stata外部命令安装问题求帮助!
  • ¥15 从键盘随机输入A-H中的一串字符串,用七段数码管方法进行绘制。提交代码及运行截图。
  • ¥15 TYPCE母转母,插入认方向