Как справиться с модульным / интеграционным тестом, который содержит слишком много строк кода? - PullRequest
0 голосов
/ 12 апреля 2020

В моей нынешней компании у нас есть веб-приложение, которое разрабатывается в PHP более чем 20 различными разработчиками с 2014 года. Недавно я слышал такие жалобы от членов моей команды, что некоторые из модульных тестов слишком громоздки (нечитаемы и не легко отследить код внутри), и вместо добавления новых сценариев / методов в тестовый класс, они хотели бы создать новые тестовые классы и добавить эти новые сценарии / методы в новые. , Давайте предположим, что у нас есть один класс следующим образом:

<?php
    class Foo {   
        public function getOne(): string
        public function getAll(): array
        public function validate(): bool
    }
?>

, и у нас есть существующий класс модульного тестирования следующим образом:

<?php
    class FooTest {   
        public function testGetOne() {
            // scenario 1
            // scenerio 2
            // scenario 3
            // scenario 4
            // scenario 5
            // scenario 6
            ...
        }
        public function testGetAll() {
            // scenario 1
            // scenario 2
            // scenario 3
            ...
        }
        public function testValidate() {
            // scenario 1
            // scenario 2
            ...
        }
    }
?>

Как вы могли видеть выше, мы следуем некоторым рекомендациям для тестовых классов:

  • Имя тестового класса формируется как оригинальное имя класса + Постфикс теста
  • Внутри тестового класса определен один метод (например, testValidate ) для каждого тестируемого метода исходного класса.
  • Тестовый сценарий ios перечислены в каждом тестовом методе

Мы считаем, что он хорош для отслеживаемости. С другой стороны, есть тестовые классы, которые превышают 1000 строк , и мы хотели бы как-то их реорганизовать. Для меня следующие вопросы:

  1. Можно ли создавать несколько классов модульных тестов для одного класса? Если это так, то на основании каких критериев мы должны их разделить? (Например, следует ли нам создавать класс модульного теста для каждой тестируемой функции отдельно FooValidateTest ?)
  2. Как мы должны обрабатывать соглашения об именах с этими тестовыми классами? Опять же, на основании каких правил?
  3. Как мы будем избегать многократного копирования / закрытия вспомогательных и фиктивных методов для каждого класса модульного теста?

Я хотел бы услышать ваши предложения!

Ответы [ 3 ]

1 голос
/ 13 апреля 2020
  1. Да, я думаю, что этот вариант лучше, чем нечитаемый тестовый класс. Вы можете разделить тесты по функциональности, в вашем случае - по методам.
  2. Правила именования тестов должны быть именем, отражающим, какие функции тестируются. «GetOneFooTest» может быть примером имени.
  3. Классы тестирования функциональности («GetOneFootTest» ...) происходят из класса «FooTestCase», где размещается настройка для всех тестов Foo.
0 голосов
/ 19 апреля 2020

1) - Я бы сказал, что если вы будете проводить рефакторинг своего тестового класса, у вас будет более чистый и легкий класс, например, у меня есть класс userTest, который содержит более 4000 строк кода. То, как я структурирую свои тестовые папки, является способом, которым была настроена моя папка проекта. Так, например, если у меня есть App / Repositories / UserRepository, то у меня будет Tests / Repositories / UserTest. Гораздо чище, я бы предпочел не делить свои тесты на разные классы. Потому что другие разработчики никогда не поймут причину.

2) - я бы предложил использовать это соглашение для именования, MethodName_ExpectedBehavior_StateUnderTest, validate_should_throw_an_error_if_X_field_is_missing или вы можете использовать PascalCase. https://dzone.com/articles/7-popular-unit-test-naming

3) - вы всегда можете инкапсулировать ваши фиктивные и вспомогательные методы в отдельный класс. Вы можете определить их статически или создать новый экземпляр класса с помощью метода setup в вашем тестовом классе.

0 голосов
/ 16 апреля 2020

Еще пара идей для вас -

1 / Вы можете выполнить некоторые шаги настройки в новом слое класса, из которого вы расширяете - например, вот один из моих более обобщенных c test-parent классы, которые я использовал для создания общих настроек, а также проверки качества кода для самого кода тестов:

<?php

namespace Tests;

use PHPUnit\Framework\TestCase as PHPUnit_TestCase;
use PHPUnitGoodPractices\Traits as PHPUnitGoodPractices;

/**
 * @SuppressWarnings(PHPMD.NumberOfChildren)
 */
abstract class TestCase extends PHPUnit_TestCase
{
    use TestCaseTrait;      // setup Mockery
    use \Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration;
    use PHPUnitGoodPractices\ExpectationViaCodeOverAnnotationTrait;
    use PHPUnitGoodPractices\ExpectOverSetExceptionTrait;
    use PHPUnitGoodPractices\IdentityOverEqualityTrait;
    #use PHPUnitGoodPractices\ProphecyOverMockObjectTrait;
}

Многие из моих собственных тестов будут расширяться Tests\TestCase.

. Используемые в этом классе, вы также можете использовать черты для перемещения общей функциональности из отдельных тестовых классов. Аннотации Soem DocBlock, такие как @ before , также будут запускать код так же, как метод, подобный protected function setUp(): void {}.

2 / Еще более полезным может быть @ dataProvider - они возвращают массив данных (или yield из метода Generator). Если ваши тесты запускают один и тот же тестовый код для более чем нескольких версий различных данных, то использование метода DataProvider позволяет назвать один тест десятками, даже сотнями раз с параметрами данных для метода теста.

<?php
use PHPUnit\Framework\TestCase;

class DataTest extends TestCase
{
    /**
     * @dataProvider additionProvider
     */
    public function testAdd($a, $b, $expected)
    {
        $this->assertSame($expected, $a + $b);
    }

    public function additionProvider()
    {
        return [
            [0, 0, 0],
            [0, 1, 1],
            [1, 0, 1],
            [1, 1, 3],    // this test would fail
        ];
    }
}
...