Один или несколько файлов для модульного тестирования одного класса?

20

Изучая передовые практики модульного тестирования, которые помогут собрать рекомендации для моей организации, я столкнулся с вопросом о том, лучше или полезно разделять контрольные приборы (тестовые классы) или хранить все тесты для одного класса в одном файле.

Кстати, я имею в виду «модульные тесты» в чистом смысле, что они представляют собой тесты «белого ящика», нацеленные на один класс, одно утверждение на тест, все проверенные зависимости и т. Д.

Примером сценария является класс (назовите его Document), который имеет два метода: CheckIn и CheckOut. Каждый метод реализует различные правила и т. Д., Которые контролируют их поведение. Следуя правилу «одно утверждение на тест», у меня будет несколько тестов для каждого метода. Я могу поместить все тесты в один DocumentTestsкласс с такими именами, как CheckInShouldThrowExceptionWhenUserIsUnauthorizedи CheckOutShouldThrowExceptionWhenUserIsUnauthorized.

Или я мог бы иметь два отдельных тестовых класса: CheckInShouldи CheckOutShould. В этом случае имена моих тестов будут сокращены, но они будут организованы так, чтобы все тесты для определенного поведения (метода) были вместе.

Я уверен, что есть «за» и «против» для любого подхода, и мне интересно, прошел ли кто-нибудь путь с несколькими файлами, и если да, то почему? Или, если вы выбрали подход с одним файлом, почему вы чувствуете, что он лучше?

SonOfPirate
источник
2
Ваши имена методов тестирования слишком длинные. Упростите их, даже если это означает, что метод теста будет содержать несколько утверждений.
Бернард
12
@Bernard: считается плохой практикой иметь несколько утверждений на метод, однако длинные имена методов тестирования НЕ считаются плохой практикой. Мы обычно документируем, что метод тестирует в самом имени. Например, constructor_nullSomeList (), setUserName_UserNameIsNotValid () и т. Д.
c_maker
4
@Bernard: я использую длинные имена тестов (и только там!) Тоже. Я люблю их, потому что так ясно, что не работает (если ваши имена - хороший выбор;)).
Себастьян Бауэр
Многочисленные утверждения не являются плохой практикой, если все они проверяют один и тот же результат. Например. ты бы не стал testResponseContainsSuccessTrue(), testResponseContainsMyData()а testResponseStatusCodeIsOk(). Вы бы их в один , testResponse()который три утверждающей: assertEquals(200, response.status), assertEquals({"data": "mydata"}, response.data)иassertEquals(true, response.success)
Юха Untinen

Ответы:

18

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

Карл Манастер
источник
3
Хороший пример, почему этот подход был представлен мне в первую очередь. Например, когда я хочу протестировать метод CheckIn, я всегда хочу, чтобы тестируемый объект был настроен одним способом, но когда я тестирую метод CheckOut, мне требуется другая настройка. Хорошая точка зрения.
SonOfPirate
14

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

  1. Нет необходимости дублировать (и поддерживать несколько версий) установочный код
  2. Проще запустить все тесты для класса из IDE, если они объединены в один тестовый класс
  3. Упрощенное устранение неполадок благодаря сопоставлению «один к одному» между тестовыми классами и классами.
кашка
источник
Хорошие моменты. Если тестируемый класс является ужасным препятствием, я бы не исключил нескольких тестовых классов, где некоторые из них содержат вспомогательную логику. Мне просто не нравятся очень жесткие правила. Помимо этого, отличные моменты.
Работа
1
Я бы исключил дубликат кода установки, имея общий базовый класс для тестовых приборов, работающих против одного класса. Не уверен, что я согласен с другими двумя пунктами вообще.
SonOfPirate
Например, в JUnit вы можете протестировать вещи с использованием разных Runners, что приводит к необходимости в разных классах.
Иоахим Нильссон
Поскольку я пишу класс с большим количеством методов со сложной математикой в ​​каждом, я обнаружил, что у меня 85 тестов, и я написал только тесты для 24% методов. Я нахожусь здесь, потому что мне было интересно, может ли быть безумием разделить это огромное количество тестов на отдельные категории, чтобы я мог запускать подмножества.
Mooing Duck
7

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

Кроме того, допустимо иметь более длинные имена методов в модульных тестах, но если это вас беспокоит, вы всегда можете переосмыслить соглашение об именовании модульных тестов, чтобы сократить имена.

FishBasketGordo
источник
Увы, в реальном мире не все классы придерживаются SRP и др., И это становится проблемой, когда вы тестируете устаревший код.
Петер Тёрёк
3
Не уверен, как SRP относится здесь. У меня есть несколько методов в моем классе, и мне нужно написать модульные тесты для всех различных требований, которые определяют их поведение. Лучше ли иметь один тестовый класс с 50 тестовыми методами или 5 тестовых классов с 10 тестами, каждый из которых относится к определенному методу в тестируемом классе?
SonOfPirate
2
@SonOfPirate: Если вам нужно так много тестов, это может означать, что ваши методы делают слишком много. Так что SRP здесь очень важен. Если вы примените SRP к классам, а также к методам, у вас будет множество небольших классов и методов, которые легко протестировать, и вам не понадобится так много методов тестирования. (Но я согласен с Петером Тёроком в том, что когда вы тестируете устаревший код, перед вами выходят передовые практики, и вы просто должны делать все возможное, что вам дают ...)
c_maker
Лучший пример, который я могу привести, - это метод CheckIn, в котором у нас есть бизнес-правила, определяющие, кому разрешено выполнять действие (авторизацию), находится ли объект в надлежащем состоянии для проверки, например, извлечен ли он и изменен ли были сделаны. У нас будут модульные тесты, которые проверяют соблюдение правил авторизации, а также тесты, чтобы убедиться, что метод ведет себя правильно, если CheckIn вызывается, когда объект не извлечен, не изменен и т. Д. Вот почему у нас много тесты. Вы предполагаете, что это неправильно?
SonOfPirate
Я не думаю, что есть жесткие и быстрые, правильные или неправильные ответы на эту тему. Если то, что вы делаете, работает на вас, это здорово. Это всего лишь моя точка зрения на общее руководство.
FishBasketGordo
2

Один из моих аргументов против разделения тестов на несколько классов состоит в том, что другим разработчикам в команде становится труднее (особенно тем, кто не так разбирается в тестах) найти существующие тесты ( интересно, есть ли уже тест для этого Метод? Интересно, где это будет? ), а также где поставить новые тесты ( я собираюсь написать тест для этого метода в этом классе, но не совсем уверен, должен ли я поместить их в новый файл или существующий один? )

В случае, когда разные тесты требуют радикально разных настроек, я видел, как некоторые специалисты по TDD помещали «приспособление» или «настройку» в разные классы / файлы, но не в сами тесты.

rally25rs
источник
1

Хотел бы я вспомнить / найти ссылку, где техника, которую я выбрал, была впервые продемонстрирована. По сути, я создаю отдельный абстрактный класс для каждого тестируемого класса, который содержит вложенные тестовые таблицы (классы) для каждого тестируемого члена. Это обеспечивает изначально желаемое разделение, но сохраняет все тесты в одном файле для каждого местоположения. Кроме того, это генерирует имя класса, которое позволяет легко группировать и сортировать в тестовом средстве.

Вот пример того, как этот подход применяется к моему первоначальному сценарию:

public abstract class DocumentTests : TestBase
{
    [TestClass()]
    public sealed class CheckInShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }

    [TestClass()]
    public sealed class CheckOutShould : DocumentTests
    {
        [TestMethod()]
        public void ThrowExceptionWhenUserIsNotAuthorized()
        {
        }
    }
}

В результате в списке тестов появляются следующие тесты:

DocumentTests+CheckInShould.ThrowExceptionWhenUserIsNotAuthorized
DocumentTests+CheckOutShould.ThrowExceptionWhenUserIsNotAuthorized

По мере добавления новых тестов они легко группируются и сортируются по имени класса, что также сохраняет все тесты для тестируемого класса вместе.

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

SonOfPirate
источник
1

Попробуйте подумать об обратном на минуту.

Почему у вас есть только один тестовый класс на класс? Вы делаете классный тест или модульный тест? Вы вообще зависите от занятий ?

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

Как бы вы подумали об этом псевдокоде:

// check_in_test.file

class CheckInTest extends TestCase {
        /** @test */
        public function unauthorized_users_cannot_check_in() {
                $this->expectException();

                $document = new Document($unauthorizedUser);

                $document->checkIn();
        }
}

Теперь вы не тестируете checkInметод напрямую . Вместо этого вы тестируете поведение (которое, к счастью, регистрируется;)), и если вам нужно провести рефакторинг Documentи разделить его на другие классы или объединить в него другой класс, ваши тестовые файлы по-прежнему являются связными, они по-прежнему имеют смысл, поскольку Вы никогда не меняете логику при рефакторинге, только структуру.

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

Стив Шамайяр
источник
0

В общем, я бы рекомендовал думать о тестах как о тестировании поведения класса, а не метода. Т.е. некоторым вашим тестам может потребоваться вызвать оба метода в классе, чтобы проверить ожидаемое поведение.

Обычно я начинаю с одного класса модульных тестов для каждого производственного класса, но в конечном итоге могу разделить этот класс модульных тестов на несколько классов тестов на основе поведения, которое они тестируют. Другими словами, я бы рекомендовал не разбивать тестовый класс на CheckInShould и CheckOutShould, а скорее разбивать по поведению тестируемого модуля.

Кристиан Хорсдал
источник
0

1. Подумайте, что может пойти не так

На начальном этапе разработки вы хорошо знаете, что делаете, и оба решения, вероятно, будут работать нормально.

Это становится более интересным, когда тест не проходит после изменения намного позже. Есть две возможности:

  1. Вы серьезно сломались CheckIn(или CheckOut). Опять же, в этом случае все в порядке как с одним файлом, так и с двумя файлами.
  2. Вы изменили оба CheckInи CheckOut(и, возможно, их тесты) способом, который имеет смысл для каждого из них, но не для обоих вместе. Вы нарушили согласованность пары. В этом случае разделение тестов на два файла усложнит понимание проблемы.

2. Рассмотрим, для чего используются тесты

Тесты служат двум основным целям:

  1. Автоматически проверяйте, что программа все еще работает правильно. Для этой цели не имеет значения, находятся ли тесты в одном файле или нескольких.
  2. Помогите читателю понять смысл программы. Это будет намного проще, если тесты для тесно связанных функций будут храниться вместе, потому что увидеть их согласованность - это быстрый путь к пониманию.

Так?

Обе эти перспективы предполагают, что совместное проведение тестов может помочь, но не повредит.

Лутц Пречелт
источник