简体   繁体   中英

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. 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:

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 .

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:

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. You want to test that class so use the real class.

You can get rid of all the "setMethod" calls. 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.

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.

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 :)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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