【问题标题】:unit testing legacy code单元测试遗留代码
【发布时间】:2014-03-23 02:17:27
【问题描述】:

我是单元测试新手,正在尝试对使用表单的旧 zend 应用程序的模型验证进行单元测试。

在其中一个表单中,它创建了第二个类的实例,我正在努力理解如何模拟依赖对象。表格内容如下:

class Default_Form_Timesheet extends G10_Form {

  public function init() {
  parent::init();

  $this->addElement( 'hidden', 'idTimesheet', array( 'filters' => array ('StringTrim' ), 'required' => false, 'label' => false ) );
  $this->addElement('checkbox', 'storyFilter', array('label' => 'Show my stories'));


  $user = new Default_Model_User();
  $this->addElement('select', 'idUser', array('filters' => array('StringTrim'), 'class' => 'idUser', 'required' => true, 'label' => 'User'));
  $this->idUser->addMultiOption("","");
  $this->idUser->addMultiOptions($user->fetchDeveloper());
  ...
  ......

当调用 $user->fetchDeveloper() 时出现我的问题。我怀疑它与模拟对象和依赖注入有关,但任何指导都会受到赞赏。我失败的单元测试如下...

require_once TEST_PATH . '/ControllerTestCase.php';

class TimesheetValidationTest extends ControllerTestCase {

  public $Timesheet;
  public $UserStub;
  protected function setUp()
  {
    $this->Timesheet = new Default_Model_Timesheet();
    parent::setUp();
  }

  /**
  * @dataProvider timesheetProvider
  */
  public function testTimesheetValid( $timesheet ) {

    $UserStub = $this->getMock('Default_Model_User', array('fetchDeveloper'));
    $UserStub->expects( $this->any() )
      ->method('fetchDeveloper')
      ->will( $this->returnValue(array(1 => 'Mickey Mouse')));

    $Timesheet = new Default_Model_Timesheet();
    $this->assertEquals(true, $Timesheet->isValid( $timesheet ) );
  }

我的数据提供者在一个单独的文件中。

它在没有输出的命令行处终止,我有点难过。任何帮助将不胜感激。

【问题讨论】:

  • 你可以模拟init(),或者你需要确保找到外部类
  • 感谢您的反馈。正在找到 User 类,它似乎是导致问题的“fetchDeveloper”调用。

标签: zend-framework phpunit


【解决方案1】:

您不能在表单测试中模拟 Default_Model_User 类。因为您的代码在内部实例化了该类,所以您无法用模拟替换它。

您有几个选项来测试此代码。

您查看fetchDeveloper 正在做什么并控制它返回的内容。通过您可以在某处注入的模拟对象(看起来不太可能)或通过设置一些数据以便您知道数据将是什么。这会使您的测试变得有点脆弱,因为当您使用的数据发生变化时它可能会中断。

另一种选择是重构代码,以便您可以将模拟传递到您的表单中。您可以设置一个构造函数,允许您设置 Default_Model_User 类,然后您就可以使用编写的测试来模拟它。

构造函数是这样的:

class Default_Form_Timesheet extends G10_Form {
    protected $user;

    public function __construct($options = null, Default_Model_User $user = null){
        if(is_null($user)) {
            $user = new Default_Model_User();
        }
        $this->user = $user;
        parent::__construct($options);
    }

Zend Framework 允许将选项传递给表单构造函数,我不确定您是否在代码中的任何地方使用了这些选项,因此这不会破坏您当前的任何功能。然后何时可以再次传递可选的Default_Model_User,以免破坏您当前的功能。您需要在调用parent::__construct 之前设置$this->user 的值,否则Zend 会抛出错误。

现在您的 init 函数必须从以下位置更改:

$user = new Default_Model_User();

$user = $this->user;

在您的测试中,您现在可以传入您的模拟对象,它将被使用。

public function testTimesheetValid( $timesheet ) {

    $UserStub = $this->getMock('Default_Model_User', array('fetchDeveloper'));
    $UserStub->expects( $this->any() )
      ->method('fetchDeveloper')
      ->will( $this->returnValue(array(1 => 'Mickey Mouse')));

    $Timesheet = new Default_Model_Timesheet(null, $UserStub);
    $this->assertEquals(true, $Timesheet->isValid( $timesheet ) );
}

创建模拟不会替换对象,因此在调用 new 时会创建模拟对象。它创建了一个新对象来扩展您现在可以传递的类。 new 是可测试性的死亡。

【讨论】:

  • 虽然我认为更改代码以获得更好的可测试性是有意义的,但我认为这是关于“如何测试一些遗留代码”的错误答案。我会寻找一种方法来测试它而不更改代码。我只是提到这一点。 :)
  • @hek2mgl 我给了两个选项。一个允许不更改代码(使用可用的数据并且不要尝试模拟事物),另一个涉及更改代码。
  • Because your code is instantiating the class internally you are not able to replace it with a mock. .. 但是您可以模拟方法本身。但是,问题中缺少有趣的代码
  • @hek2mgl:你可以模拟哪种方法? Default_Form_Timesheet::init()?这就是您要测试的方法。你不能模拟你试图测试的方法......那么你就不会测试它。你不能模拟fetchDeveloper() 方法,因为你不能模拟它被调用的对象。我不明白你的建议。你能提供你自己的解决方案来说明你在说什么吗?
  • 我告诉过,我需要看代码。你在哪里看到init()是要测试的方法?我没有。为什么不能模拟fetchDeveloper()? (仔细看,居然被嘲讽了)
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-12-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-09
  • 1970-01-01
  • 2010-09-10
相关资源
最近更新 更多