6/25/2009 2:34:06 PM

Зашел тут как-то спор на работе по поводу юнит-тестов. Спорили-спорили. Интересно, но все остались при своих. Полез смотреть в интернет, оказывается есть два “лагеря”, которые никак не могут примириться.

Проблема простая: тестировать или не тестировать приватные методы.

Сторонники одного лагеря настаивают на том, что юнит-тестами должны быть покрыты только публичные функции и классы. Объяснение этому такое:

  1. Тестировать нужно интерфейс класса или подсистемы, который не зависит от внутренней реализации.
    Любое изменение “внутренностей” этого класса или подсистемы не должно сказываться на функционировании интерфейса. Таким образом, покрыв юнит-тестами интерфейс мы всегда имеем возможность убедиться, что подсистема/класс функционирует именно так, как нам нужно. А что и как у неё делается внутри, с этой точки зрения не важно.
  2. Тестирование приватных функций отнимает много времени, усложняет поддержку и нерационально.
    В ходе любого проекта происходит масса рефакторинга. По моим оценкам, на рефакторинг уходит до 20% времени при “нормальной” организации работы и неплохом качестве архитектуры и кода. Как только в проекте появляются юнит-тесты, так сразу возникает необходимость поддержки не только кода продукта, но и юнит-тестов в актуальном состоянии. При этом реализация классов/подсистем меняется гораздо сильнее и чаще, чем интерфейсы. Всяческие там оптимизации и т.д. Поэтому поддержка юнит-тестов на уровне интерфейса – оправдана, так как даёт уверенность при использовании подсистемы/класса. А покрытие ими реализации – не оправдано, так как, во-первых, если “падает” какой-то внутренний функционал, то “падает” и один из “интерфейсных” вариантов использования, а во-вторых существенно увеличивает объем работы.
  3. Подход полностью соответствует TDD.
    Создавая публичный метод мы, де-факто, создаём интерфейс, его и тестируем. С этой точки зрения нам, опять же, совершенно не важно, что там делает этот метод внутри и как именно.

Второй лагерь настаивает на обратном:

  1. Полезно тестировать каждый отдельно взятый функциональный аспект приложения.
    В термине “юнит-тестирование” под “юнитом” понимается настолько малый и обособленный функциональный фрагмент, насколько это возможно. Фрагмент этот не обязательно должен быть публичным. Разработчики имеют право быть уверены в том, что каждый маленький, но важный кусок (а неважный никто и писать бы не стал – поленился бы) выполняет свою задачу. Независимо от зоны его видимости, которая, кстати, может меняться в процессе рефакторинга.
  2. Приватная функция может быть использована в нескольких местах.
    То же касается и приватного класса. Для того, чтобы использовать эту функцию или класс, неплохо было бы быть уверенным, что этот кусок кода хорошо делает свою работу.
  3. Бόльшая определённость в покрытии юнит-тестами.
    Для каждой отдельной конечной приватной функции достаточно легко написать тест, грамотно покрывающий варианты её использования. Другое дело, когда тестируются только публичные функции, вызывающие другие (приватные) функции. Написать тесты для такой сложной функции может оказаться сложнее. Кроме того, никогда точно не знаешь, какой объем вариантов использования покрыт.

В общем, нет согласия в рядах.

А к какому лагерю склоняетесь вы?

Comments (11) -

6/25/2009 3:01:21 PM

Vadim

Я склоняюсь к тому, что тестировать нужно только сложные и часто меняющиеся методы. Остальное - зря потраченное время.

Vadim

6/25/2009 3:29:19 PM

Artem

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

Artem Russia

6/25/2009 5:01:13 PM

Sergii

Алексей, доброго времени суток.
Тестирование должно быть не ради тестирования. Даже 90% покрытие тестами почти всегда не достижимо. Поэтому я склоняюсь, что должны быть протестированы как минимум все основные классы. Бывает, к сожалению, что тесты полностью не покрываю даже интерфейс.     Сложно отнести себя к какому-либо лагерю )))

Sergii Russia

6/25/2009 5:33:56 PM

Вадим

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

Вадим Russia

6/26/2009 11:29:18 AM

Vaspo

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

Если особой логики в методе нет (см. ClearControls()), то и желания тестировать его не будет, его "тело" и так будет покрыто тестами, если вызывающий его метод (см. Refresh()) протестирован.

        public void Refresh()
        {
            ClearControls();
            BuildXXX();
            Invalidate();
        }

        private void ClearControls()
        {
            stackPanel.Children.Clear();
        }

Приватные методы - зло. Использую их не более, чем #region в .NET, чтобы просто как-то выделить код.
Каждому методу/классу своя ответственность и своё место.

Vaspo Russia

6/26/2009 11:58:12 AM

Alexey Raga

Лично моё мнение: мне не важно, приватный метод или публичный. Если у меня есть кусок функциональности, который я хочу тестировать - я хочу иметь возможность его тестировать Smile
Тут я согласен с Артёмом и Сергеем.

С Вадимом не согласен категорически. Считаю, что рефакторить код по причине тестирования - не правильно. Код решает определенную бизнес-задачу. У него есть какая-то архитектура. И это совершенно странно, плодить лишние, да еще и публичные, сущности только для того, чтобы их тестировать. Не для тестирования оно пишется.

С Vaspo тоже не согласен. Комментарий звучит как какая-то идеологическая догма Smile Всё проще: если функционал имеет смысл только в рамках работы данного класса (интерфейса), то не вижу никаких причин куда-то его выносить, делать какие-то отдельные публичные классы и т.д.
И в случае, если ClearControls должен будет "почистить" не только stackPanel, а еще пару коллекций, плюс к этому сбросить какое-то внутреннее состояние (например, мы хранили связи идентификаторов с контролами где-то в коллекции), да еще и в лог записать "коллекция почищена"... То это какая-никакая, а логика Smile
Я не говорю, что такую логику уже надо бросаться тестировать, я привожу пример приватного метода, содержащего эту логику.
По поводу же условий - очередная догма. Условия - это нормальная вещь, легко читаемая и легко поддерживаемая (не говорю о злоупотреблениях). И выносить любое простое условие в какой-то паттерн - это только увеличивать сложность.
Примитивный пример:

private void FireEvent(MyArgs args)
{
   if (this._initialized && this.EventsEnabled)
   {
     var handler = this.MyEvent;
     if (handler != null)
     {
        handler(this, args);
     }
   }
}

Именно что - каждому методу и классу - _своя_ ответственность ;)

P.S. А вот регионы не люблю.

Alexey Raga Australia

6/26/2009 12:42:42 PM

Artem

> Приватные методы - зло. Использую их не более, чем #region в .NET...

#region - тоже интересный холивар. Я например их категорически не люблю, и для облегчения навигации по коду использую Bookmarks. А вы? Smile

Artem Russia

6/26/2009 1:56:17 PM

Alexey Raga

Регионы используются для того, чтобы сгруппировать функционал как-то.
Самое глупое, что я видел - это регионы с названиями Variables, Properties, Private Methods, Public Methods, Event handlers и т.д.
Такого разделения кода я не понимаю, это и с темой топика кореллирует (с двумя "л"?) ;)
Другое дело - разделение на какие-то более-менее функциональные части. Но тут уже возникает вопрос: если в классе есть что делить, то почему это находится в одном классе, а не вынесено в соответствии с замечательным принципом "один выстрел - один труп"?
Даже если оно _должно_ быть там логически, то у нас существуют partial classes. Вынеси эту часть в отдельный файл, всем будет легче!

Тем не менее, регионы я использую. Оооочень редко, но случается.
Случай, который я смог вспомнить: контракты на методы.
Ну, например,
Check.Require(myObject != null);
Check.Require(myObject.MyCollection != null);
Check.Require(myObject.Id != 0);
Check.Require(myObject.myCollection.All(x=>x.ParentId == myObject.Id));

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

Alexey Raga Australia

6/26/2009 3:55:58 PM

zkot

Только публичные.
Моя точка зрения:
- Для меня "тест" == "сценарий использования" моего компонента.
- Я тестирую код для того чтобы он удовлетворял требованиям пользователям моего компонента, а не меня - как программиста компонента.
- Ко всему прочему тест это ещё и документация для программиста поддерживающего проект и я не хочу захломлять его тестами приватныых методов. Особенно в таком виде как это делает студия.
- Никто (и даже я в своих тестах) не вправе использовать мой компонент кроме как через публичные методы. Если кто-то (и даже я) пытается использовать приватные методы снаружи (не важно в тестах или нет) - это хак. А хаки до добра не доводят - Догма Smile)
- Но самое главное: Тестирование приватного метода означает, что покрыты не все ветки публичного метода (иначе зачем тестировать приватный метод?), а значит тест может врать, что хуже чем если бы теста вообще не было, т.к. внушает мнимую уверенность в коде.

Про покрытие 90% кода я понимаю (и принимаю, и так делаю Smile) только если Вы говорите в ключе:
Те методы, для которых есть тесты покрываются на 100%, но для некоторых очевидных методов ну или для которых лениво писать тесты Smile тестов совсем нет.
Но если имеется в виду:
Я протестировал в методе 9 веток из 10, то лучше бы таких тестов совсем не было Smile Опять таки потому что внужают мнимую уверенность в коде.

zkot Denmark

6/26/2009 4:02:32 PM

zkot

Считаю что регионы хороши чтобы оборачивать методы из реализации интерфесов класса:
class A : IA, IB, IC
{
... public ... <-- не регион просто располагаю публику в начале класса, а приватное в конце Smile

#region IA Implementation
   ...
#endregion
#region IB Implementation
   ...
#endregion
#region IC Implementation
   ...
#endregion

... private ...
}

zkot Denmark

6/27/2009 9:55:08 AM

Alexey Raga

Zkot, то, что ты говоришь - это правильно. Я это воспринимаю как black box - тесты. Но unit тесты, на мой взгляд, должны иметь возможность тестировать минимый unit of work. Которым может быть и приватный метод в частности.
Однако, точку зрения я понял, разводить идеологических споров тут не будем ;)

Про регионы: я в этих случаях просто использую partial clases, каждая имплементация - в отдельном файле. Можно сделать его nested в проекте, если хочется Smile
Таким образом получаем:
A.cs
A.IA.cs
A.IB.cs
A.IC.cs
и далее по тексту. Если в этом есть смысл, конечно. Ибо выносить в отдельный файл реализацию, скажем, IDisposable - глупо. Но и оборачивать его в region, думаю, смысла нет Smile

Alexey Raga Australia

Comments are closed

Powered by BlogEngine.NET 2.5.0.6

About the author

Alexey Raga Alexey Raga
.NET software developer.

E-mail me Send mail

Twitter


Recent posts

Archive

Disclaimer

The opinions expressed herein are my own personal opinions and do not represent my employer's view in anyway.

© Copyright 2012

Sign in