Как я могу сделать свои тесты PHPUnit более краткими, менее продолжительными? - PullRequest
3 голосов
/ 02 декабря 2011

Тесты PHPUnit, которые я пишу для своего веб-приложения, убивают меня своей длиной и непрозрачностью.Кажется, что в тестах на порядок больше кода, чем в коде, который они тестируют.

Например, скажем, на моем веб-сайте есть объект CatController, к которому относится этот метод:

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

Модульный тест, который я должен был бы создать, чтобы тщательно его протестировать, будет выглядеть примерно так:

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);
}

Можно ли использовать какой-либо синтаксис, чтобы сделать модульные тесты короче и проще для чтения?Например, вместо того, чтобы говорить «этот объект - макет, на котором будет вызываться этот метод, и когда этот метод вызывается для него, он будет вызван один раз с этим аргументом и вернет это значение», было бы неплохо, если бы я могпросто скажи что-то вроде once(object->method(argument)) => $returnvalue.

Ответы [ 2 ]

2 голосов
/ 03 декабря 2011

Если у вас есть объект CatController, вы не должны насмехаться над ним в тесте, если это вообще возможно.Вы хотите проверить этот класс, поэтому используйте реальный класс.

Вы можете избавиться от всех вызовов "setMethod".По умолчанию все методы будут проверены, и это то, что вы хотите.

Существуют и другие библиотеки-насмешки, которые делают меньше насмешливых строк кода, таких как Phake и Mockery, которые нравятся некоторым людям, но у меня не слишком много личного опыта.

Чтомне кажется немного странным, что вы устанавливаете макеты, используя открытые свойства.Я ожидал, что они войдут в конструктор Controllers.

Учитывая, что это ваш метод, который можно сделать:

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);
}

Давайте рассмотрим некоторые общие вещи в настройке

Просто чтобы сделать каждую функцию более приятной для чтения, давайте возьмем макеты по умолчанию для установки.

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();
}

и метод:

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);
}

Это все еще не на самом деле красиво, но мы разделили его на более управляемые куски.

Программа установки создает все поддельные объекты, но ничего не делает (поэтому они не проваливают тест и время установки должно быть небрежным)

, в то время как тесты фокусируются на описании того, что должно произойти.


В общем, я бы сказал: "если это так сложно использовать класс, может быть, это хорошая вещь, тесты показывают, чтомного чего нужно сделать ". Если в этом проблема, возможно, поменять класс. Производителю, который его использует, также будет трудно все исправить. Но многие фреймворки / подходы делают спецификации контроллеровв этом отношении, чтобы вы знали лучше:)

2 голосов
/ 02 декабря 2011

Чем больше вы можете разработать свои классы для использования в модульных тестах без необходимости имитации, тем меньше будет кода для написания кода.Но для приведенного выше примера моя первая реакция заключается в том, что этот метод не нуждается в модульном тесте, потому что он на самом деле не выполняет никакой логики и не изменится после написания.

При этом предполагается, что вы будетеНужен экземпляр рабочего процесса в других методах этого класса, извлеките код, который его создает, в новый метод.Это позволяет вам смоделировать этот метод для каждого теста и использовать только более длительный макет в одном тесте.

Например, если у вас также есть метод 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));
}

Вышеуказанные методы очень короткие и на самом деле не требуют модульных тестов, но теперь они будут немного короче:

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);
}

Если у вас много таких методов в контроллере, вы можетесоздать вспомогательные методы в вашем тестовом примере для создания макетов.Наконец, вам действительно нужно отключить оригинальные конструкторы на ваших макетах?Я редко должен сделать это сам.

...