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


20

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

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

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

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

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


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

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

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


Увы, в реальном мире не все классы придерживаются SRP и др., И это становится проблемой, когда вы тестируете устаревший код.
— Петер Тёрёк

3
Не уверен, как SRP относится здесь. У меня есть несколько методов в моем классе, и мне нужно написать модульные тесты для всех различных требований, которые определяют их поведение. Лучше ли иметь один тестовый класс с 50 тестовыми методами или 5 тестовых классов с 10 тестами, каждый из которых относится к определенному методу в тестируемом классе?
— SonOfPirate

2
@SonOfPirate: Если вам нужно так много тестов, это может означать, что ваши методы делают слишком много. Так что SRP здесь очень важен. Если вы примените SRP к классам, а также к методам, у вас будет множество небольших классов и методов, которые легко протестировать, и вам не понадобится так много методов тестирования. (Но я согласен с Петером Тёроком в том, что когда вы тестируете устаревший код, перед вами выходят передовые практики, и вы просто должны делать все возможное, что вам дают ...)
— c_maker

Лучший пример, который я могу привести, - это метод CheckIn, в котором у нас есть бизнес-правила, определяющие, кому разрешено выполнять действие (авторизацию), находится ли объект в надлежащем состоянии для проверки, например, извлечен ли он и изменен ли были сделаны. У нас будут модульные тесты, которые проверяют соблюдение правил авторизации, а также тесты, чтобы убедиться, что метод ведет себя правильно, если CheckIn вызывается, когда объект не извлечен, не изменен и т. Д. Вот почему у нас много тесты. Вы предполагаете, что это неправильно?
— SonOfPirate

Я не думаю, что есть жесткие и быстрые, правильные или неправильные ответы на эту тему. Если то, что вы делаете, работает на вас, это здорово. Это всего лишь моя точка зрения на общее руководство.
— FishBasketGordo

2

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

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


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, который будет использоваться всеми вложенными классами, а также внутри каждого из них. вложенный класс для тестов, которые он содержит.


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. Помогите читателю понять смысл программы. Это будет намного проще, если тесты для тесно связанных функций будут храниться вместе, потому что увидеть их согласованность - это быстрый путь к пониманию.

Так?

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.