我最近一直试图在我的项目中采用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();
}
}
}
假设正在测试simulate中的所有函数调用。还有什么可以测试吗?也许某些功能被称为?缺乏明显的测试(至少对我而言)是否指向设计问题?
答案 0 :(得分: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);
}
//-----
}
答案 1 :(得分:1)
如果某项功能很难测试,那就是代码味道。什么使测试变得困难,并且可以通过改变来使其变得更容易。
在你的情况下,在我看来,你的功能是做太多事情。您正在检查模拟是否完成,获得击球手和投手,以及模拟击球。当您描述函数的功能并使用“AND”时,请分解功能。
您也缺少依赖注入,因此您无法传递模拟对象($batter
和pitcher
)。
您还希望避免在函数中使用new
(except if it is part of a factory)您无法替换该对象并依赖于该类具有的功能。您现在无法控制该对象的作用。
的更新强> 的
RE:您对advanceInning
和isComplete
移至someObject
的评论。从行为的角度思考问题。不要只是因为将函数放入对象中。我会有一个Game
对象,它将isComplete
和playNextInning
作为公共方法。你将拥有什么对象都取决于你的抽象和你想要实现的目标。 Your objects should represent and be responsible for one thing.你有一个代表游戏的游戏。每个游戏都有一个局,所以你可能会有一个局。你有两个团队,所以你可能会有一个团队对象。你可能想要一个为你创建局的局,你传入游戏构造函数(然后你可以在测试时模拟它)。根据你最终得到的逻辑,你甚至可以将半局抽象成对象。这一切都将取决于你想要实现的行为。
你会发现你最终得到了很多非常小的物品,这是件好事。因为您的设计将灵活且更具可扩展性。