0

我最近一直在尝试在我的项目中采用 TDD 方法,但我很难知道如何测试某些代码。我已经阅读了很多关于该主题的内容,但我很难将其付诸实践。既然是这样,我将发布我的方法并询问您将如何尝试对其进行测试。

public function simulate(){
    while (!isComplete()) {
        if ($this->currentOuts == 3) {
            advanceInning();
        } else {
            $batter = getBatter();
            $pitcher = getPitcher();
            $atBat = new AtBat($batter, $pitcher);
            $result = $atBat->simulate();
            handleAtBatResult();
        }
    }
}

假设模拟内的所有函数调用都经过了正确的测试。真的还有什么要测试的吗?也许调用了某些函数?缺乏明显的测试(至少对我而言)是否指向设计问题?

4

2 回答 2

2

开始使用 TDD 时,我最终问了您在此处提出的相同问题。经过一些研究,以及几周的单元测试等工作,我想出了两个术语;“流程测试”和“模块测试”。

模块测试:作为工程师,我们应该努力遵循 DRY(不要重复自己)原则,因此,我们最终会得到抽象的代码片段,这些代码会被推送到应用程序的最低层,以便可以使用它们任何地方。这些代码片段,无论是类的方法还是独立函数,都应该是原子可测试的,这意味着对任何其他模块、函数等的依赖性最低。显然,当您处理包含几个的方法/函数时,这是可以避免的模块,但这就是流程测试发挥作用的地方。

流测试:我们所有的基本模块都处于可测试状态,我们还需要能够在与现实世界需求相称的场景中测试它们。为了正确地进行流程测试,我们需要建立我所说的“已知商品”。这意味着我们构建了反映流测试中模块返回值的数据,因此我们可以将它们与从 API 生成的值进行比较。

为了帮助更好地展示这些想法,这是我为测试我的缓存 api 所做的流测试(添加了一些额外的注释以更好地解释):

<?php

class HobisTest_Api_Flow_CacheTest extends PHPUnit_Framework_TestCase
{
    // Setting some constants so it's easier to construct known goods
    const TEST_EXPIRY       = 30;
    const TEST_KEY_PREFIX   = 'test';
    const TEST_VALUE        = 'brown chicken, brown cow';

    //-----
    // Support methods
    //-----
    protected $object;
    protected $randomNumber;

    // Here we generate a known good key, this allows us to test that the api internal workings generate what we expect
    protected function getKnownGoodKey()
    {
        return self::TEST_KEY_PREFIX . Hobis_Api_Cache_Key::SEPARATOR . $this->getRandomNumber() . Hobis_Api_Cache_Key::SEPARATOR . '1';
    }

    protected function getObject()
    {
        return $this->object;
    }

    protected function getRandomNumber()
    {
        return $this->randomNumber;
    }
    //-----

    //-----
    // Setup and teardown
    //-----

    // You will want to add setup and teardown functions to your test classes
    //  These allow you to reference items for EVERY test within the current class
    //  While ensuring they are not carried over from one test to the other
    //  Basically a clean slate for every test
    public function setUp()
    {
        $this->object       = $this->getMock('Hobis_PhpUnit_DefaultTestObject');
        $this->randomNumber = mt_rand();
    }

    public function tearDown()
    {
        unset(
            $this->object,
            $this->randomNumber
        );
    }
    //-----

    //-----
    // Test methods
    //-----

    // The actual test method
    public function testCache()
    {
        // Configure object 
        //  Setting up so any references to $this->getId() will return 1
        //  If you look in the getKnownGoodKey() it is constructed with 1 as well
        $this->object->expects($this->any())->method('getId')->will($this->returnValue(1));

        // So now I am calling on my API to generate a cache key based on
        //  values used here, and when I constructed my "known good" key
        $key = Hobis_Api_Cache_Key_Package::factory(
            array(
                'dynamicSuffixes'   => array($this->getRandomNumber(), $this->getObject()->getId()),
                'expiry'            => self::TEST_EXPIRY,
                'staticPrefix'      => self::TEST_KEY_PREFIX,
                'value'             => self::TEST_VALUE
            )
        );

        // Calling set via api
        $setStatus = Hobis_Api_Cache_Package::set($key);

        // Check that call was what we expect
        $this->assertTrue($setStatus);

        // Now let's retrieve the cached value so we can test if it's available
        $cachedValue = Hobis_Api_Cache_Package::get($key);

        // Test the attributes against "known good" values
        $this->assertSame($key->getKey(), $this->getKnownGoodKey());
        $this->assertSame($cachedValue, self::TEST_VALUE);
    }
    //-----
}
于 2013-06-13T23:24:15.053 回答
1

如果一个函数很难测试,那就是代码异味。是什么使测试变得困难,是否可以对其进行可行的更改以使其更容易。

就您而言,在我看来,您的职能正在做太多事情。您正在检查模拟是否完成,获得击球手和投手,并模拟击球。当您描述该功能的用途并使用“AND”一词时,请分解功能。

您还缺乏依赖注入,因此您无法传入模拟对象($batterpitcher)。

您还希望避免new在您的函数中使用(除非它是工厂的一部分)您不能替换该对象并且依赖于该类具有的功能。您现在无法控制该对象的作用。

更新

回复:您对advanceInningisComplete被移至的评论someObject。从行为的角度思考问题。不要仅仅因为将函数放入对象中。我将拥有一个Game具有公共方法isComplete的对象。playNextInning您将拥有哪些对象都取决于您的抽象以及您要实现的目标。 您的对象应该代表并负责一件事。 你有一个代表游戏的游戏。每场比赛都有一个局,所以你可能会有一个局对象。您有两个团队,因此您可能会有一个团队对象。您可能希望有一个局工厂来为您创建局,并将其传递给游戏构造函数(然后您可以在进行测试时模拟它)。根据最终得到的逻辑,您甚至可以将半局抽象为对象。这一切都将取决于您试图实现的行为。

你会发现你最终得到了大量非常小的物体,这是一件好事。因为您的设计将更加灵活和可扩展。

于 2013-06-14T13:59:02.803 回答