简体   繁体   English

如何让我的PHPUnit测试更简洁,更长久?

[英]How can I make my PHPUnit tests more succinct, less long?

The PHPUnit tests I'm writing for my web application are killing me with their length and their opaqueness. 我正在为我的Web应用程序编写的PHPUnit测试用他们的长度和不透明性来杀死我。 It seems there's an order of magnitude more code in the tests than in the code they're testing. 似乎测试中的代码比他们测试的代码要多一个数量级。

For example, say my web site has a CatController object on which is this method: 例如,假设我的网站有一个CatController对象,就是这个方法:

public function addCat(Default_Model_Cat $cat)
{
    $workflow = $this->catWorkflowFactory->create(array($this->serviceExecutor));
    $workflow->addCat($cat);
}

The unit test I would have to create to test it thoroughly would be something like this: 我必须创建的单元测试才能彻底测试它是这样的:

public function testAddCat()
{
    $cat = $this->getMockBuilder('Default_Model_Cat')
        ->disableOriginalConstructor()
        ->getMock();
    $controller = $this->getMockBuilder('CatController')
        ->disableOriginalConstructor()
        ->setMethods(array('none'))
        ->getMock();
    $workflow = $this->getMockBuilder('Default_Model_Workflow_Cat')
        ->setMethods(array('addCat'))
        ->disableOriginalConstructor()
        ->getMock();
    $workflow->expects($this->once())
        ->method('addCat')
        ->with($cat);
    $controller->serviceExecutor = $this->getMockBuilder('ServiceExecutor')
        ->disableOriginalConstructor()
        ->getMock();
    $controller->catWorkflowFactory = $this->getMockBuilder('Factory')
        ->disableOriginalConstructor()
        ->setMethods(array('create'))
        ->getMock();
    $controller->catWorkflowFactory->expects($this->once())
        ->method('create')
        ->with($controller->serviceExecutor)
        ->will($this->returnValue($workflow));
    $controller->addCat($cat);
}

Is there any syntax I can use to make unit tests shorter and easier to read? 是否有任何语法可以使单元测试更短更容易阅读? For example, rather than saying "this object is a mock on which this method will be called, and when this method is called on it then it will be called once with this argument and will return this value" it would be nice if I could just say something like once(object->method(argument)) => $returnvalue . 例如,而不是说“这个对象是一个模拟将调用此方法的方法,并且当调用此方法时,它将使用此参数调用一次,并将返回此值”如果我可以,那将是很好的只说once(object->method(argument)) => $returnvalue

The more you can design your classes to be usable within unit tests without needing to be mocked, the less mocking code you'll need to write. 你可以设计的类越多,你就可以在单元测试中使用它而不需要模拟,你需要编写的代码就越少。 But for the above example my first reaction is that this method doesn't need a unit test because it isn't really performing any logic and won't change after being written. 但是对于上面的例子,我的第一反应是这个方法不需要单元测试,因为它不是真正执行任何逻辑,并且在写入后不会改变。

That being said, assuming you will need a workflow instance in other methods of this class, extract the code that creates it to a new method. 话虽这么说,假设您需要在此类的其他方法中使用工作流实例,请将创建它的代码提取到新方法中。 This allows you to mock that method for each test and only have the longer mocking in the one test. 这允许您为每个测试模拟该方法,并且在一次测试中只有更长的模拟。

For example, if you also had a removeCat() method it would look like this: 例如,如果您还有一个removeCat()方法,它将如下所示:

public function addCat(Default_Model_Cat $cat) {
    $this->createWorkflow()->addCat($cat);
}

public function removeCat(Default_Model_Cat $cat) {
    $this->createWorkflow()->removeCat($cat);
}

public function createWorkflow() {
    return $this->catWorkflowFactory->create(array($this->serviceExecutor));
}

The above methods are supremely short and really don't need unit tests, but they'll be a little shorter now: 上面的方法非常短,实际上不需要单元测试,但现在它们会更短一些:

public function testAddCat() {
    $cat = $this->getMockBuilder('Default_Model_Cat')
        ->disableOriginalConstructor()
        ->getMock();
    $controller = $this->getMockBuilder('CatController')
        ->disableOriginalConstructor()
        ->setMethods(array('createWorkflow'))
        ->getMock();
    $workflow = $this->getMockBuilder('Default_Model_Workflow_Cat')
        ->setMethods(array('addCat'))
        ->disableOriginalConstructor()
        ->getMock();
    $controller->expects($this->once())
        ->method('createWorkflow')
        ->will($this->returnValue($workflow));
    $workflow->expects($this->once())
        ->method('addCat')
        ->with($cat);
    $controller->addCat($cat);
}

If you have many such methods in the controller, you can create helper methods in your test case to create the mocks. 如果控制器中有许多此类方法,则可以在测试用例中创建辅助方法来创建模拟。 Finally, do you really need to disable the original constructors on your mocks? 最后,你真的需要禁用你的模拟器上的原始构造函数吗? I rarely need to do that myself. 我自己很少需要这样做。

If you have a CatController object you shouldn't be mocking it in the test if at all possible. 如果你有一个CatController对象,你不应该在测试中嘲笑它,如果可能的话。 You want to test that class so use the real class. 你想测试那个类,所以使用真正的类。

You can get rid of all the "setMethod" calls. 你可以摆脱所有"setMethod"调用。 By default all methods will be mocked and that is what you want. 默认情况下, 所有方法都将被模拟,这就是您想要的。

There are other mocking libraries that make mocking less lines of code like Phake and Mockery that some people like but I don't have too much personal experience with it. 还有其他一些Phake库可以PhakePhakeMockery这样的代码行,而有些人喜欢它,但我没有太多的个人经验。

What strikes me as a little odd is that you set the mocks using public properties. 令我觉得有点奇怪的是你使用公共属性设置模拟。 I would have expected those to go into the Controllers constructor. 我原以为那些会进入Controllers构造函数。

Given that thats your method that could be done: 鉴于那就是你可以做到的方法:

public function testAddCat()
{
    $cat = $this->getMockBuilder('Default_Model_Cat')
        ->disableOriginalConstructor()
        ->getMock();

    $workflow = $this->getMockBuilder('Default_Model_Workflow_Cat')
        ->disableOriginalConstructor()
        ->getMock();
    $workflow->expects($this->once())
        ->method('addCat')
        ->with($cat);

    $controller = new CatController(/*if you need params here pass them!*/);
    // You can use this to avoid mocking the object if you want
    // If your tests are more of a usage doc maybe don't
    $controller->serviceExecutor = "My fake Executor";

    $controller->catWorkflowFactory = $this->getMockBuilder('Factory')
        ->disableOriginalConstructor()
        ->getMock();
    $controller->catWorkflowFactory->expects($this->once())
        ->method('create')
        ->with(array("My fake Executor"))
        ->will($this->returnValue($workflow));

    $controller->addCat($cat);
}

Let's take some common stuff into setup 让我们把一些常见的东西放到设置中

Just to get each function a little nicer to read lets take out the default mocks into setup. 只是为了让每个函数都更好看一下,让我们把默认的模拟带入设置。

public function setUp() {

    $this->controller = new CatController(/*if you need params, pass them!*/);
    $this->serviceExecutor = $this->getMockBuilder('ServiceExecutor')
        ->disableOriginalConstructor()
        ->getMock();
    $this->controller->serviceExecutor = $this->serviceExecutor;
    $this->cat = $this->getMockBuilder('Default_Model_Cat')
        ->disableOriginalConstructor()
        ->getMock();
    $this->workflow = $this->getMockBuilder('Default_Model_Workflow_Cat')
        ->disableOriginalConstructor()
        ->getMock();
    $this->controller->catWorkflowFactory = $this->getMockBuilder('Factory')
        ->disableOriginalConstructor()
        ->getMock();
}

and the method: 和方法:

public function testAddCat()
{
    $this->workflow->expects($this->once())
        ->method('addCat')
        ->with($this->cat);

    $this->controller->catWorkflowFactory->expects($this->once())
        ->method('create')
        ->with(array($this->serviceExecutor))
        ->will($this->returnValue($this->workflow));

    $this->controller->addCat($cat);
}

It's still not really pretty but we split it up into more manageable chunks. 它仍然不是漂亮,但我们将它分成更易于管理的块。

Setup creates all the fake objects but they don't do anything (so they don't fail any test and the setup time should be negligence 安装程序会创建所有虚假对象,但它们不会执行任何操作(因此它们不会因任何测试而失败,并且安装时间应该是疏忽

while the tests focuses on describing what should happen. 而测试则侧重于描述应该发生的事情。


In general I'd say "if it is that complicated to use the class maybe it's a good thing the tests show that a lot of stuff needs to be done". 一般来说,我会说“如果使用这个类很复杂,那么测试表明需要做很多事情是件好事”。 If thats a problem maybe change the class. 如果那是一个问题,可能会改变课程。 The production could that uses it will also have a hard time setting everything right. 生产可以使用它也将很难设置一切正确。 But many frameworks/approaches make Controllers 'special' in that regard so you know best :) 但是很多框架/方法使控制器在这方面“特别”,所以你最了解:)

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM