Является ли статика универсальным «злом» для модульного тестирования, и если да, то почему Resharper рекомендует его? [закрыто]


85

Я обнаружил, что существует только 3 способа статических зависимостей модульного тестирования (макет / заглушка) в C # .NET:

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

Имеет ли это статические методы и такое «зло» (в смысле модульного тестирования)? И если да, то почему Resharper хочет, чтобы я сделал все, что может быть статичным, статичным? (Предполагая, что резарпер тоже не «зло».)

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


3
TheLQ: Вы можете. Я полагаю, что он говорит о невозможности тестирования статических методов, потому что большую часть времени это касается статических переменных. Таким образом меняется состояние после и между тестами.

26
Лично я думаю, что вы слишком далеко ушли от определения «единицы». «Единица» должна быть «самой маленькой единицей, которая имеет смысл тестировать в изоляции». Это может быть метод, это может быть больше, чем это. Если статический метод не имеет состояния и хорошо протестирован, то наличие второго вызова модульного теста не является проблемой (IMO).
млк

10
«Лично я думаю, что вы слишком далеко зашли от определения« единицы ».» Нет, просто у него стандартное использование, а вы сами определяете.

7
"Почему Resharper хочет, чтобы я сделал все, что может быть статичным, статичным?" Решарпер не хочет , чтобы ты что-то делал. Это просто заставляет вас осознать, что модификация возможна и, возможно, желательна из POV анализа кода. Решарпер не является заменой вашему собственному суждению!
Адам Нейлор

4
@ acidzombie24. Обычные методы также могут изменять статическое состояние, поэтому они будут такими же «плохими», как и статические методы. Тот факт, что они также могут изменять состояние с более коротким жизненным циклом, делает их еще более опасными. (Я не поддерживаю статические методы, но вопрос об изменении состояния - удар и по обычным методам, тем более)
mike30

Ответы:


105

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

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


44
Статический метод является модульно тестируемым, но как насчет методов, которые вызывают статический метод? Если вызывающие абоненты находятся в другом классе, у них есть зависимость, которую необходимо отделить для выполнения модульного теста.
Ваккано

22
@Vaccano: Но если вы пишете статические методы, которые не поддерживают состояние и не имеют побочных эффектов, они все равно более или менее функционально эквивалентны заглушке.
Роберт Харви

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

21
@Vaccano: у статического метода, который имеет выходные данные или случайные исключения, есть побочные эффекты.
Тихон Джелвис

10
@TikhonJelvis Роберт был говорить о выходах; и «случайные» исключения не должны быть побочным эффектом, они по сути являются формой вывода. Дело в том, что всякий раз, когда вы тестируете метод, который вызывает статический метод, вы оборачиваете этот метод и все перестановки его потенциального вывода и не можете тестировать свой метод изолированно.
Николь

26

Вы, кажется, путаете статические данные и статические методы . Решарпер, если я правильно помню, рекомендует делать privateметоды внутри класса статичными, если они могут быть сделаны так - я считаю, что это дает небольшое преимущество в производительности. Он не рекомендует делать «все, что может быть» статичным!

В статических методах нет ничего плохого, и их легко тестировать (если они не изменяют статические данные). Например, подумайте о библиотеке Maths, которая является хорошим кандидатом для статического класса со статическими методами. Если у вас есть (надуманный) метод, подобный этому:

public static long Square(int x)
{
    return x * x;
}

тогда это в высшей степени проверяемое и не имеет побочных эффектов. Вы просто проверяете, что когда вы проходите, скажем, 20, вы получаете 400. Нет проблем.


4
Что происходит, когда у вас есть другой класс, который вызывает этот статический метод? Кажется простым в этом случае, но это зависимость, которая не может быть изолирована, кроме как с помощью одного из трех инструментов, перечисленных выше. Если вы не изолируете его, то вы не «модульное тестирование», а «интеграционное тестирование» (потому что вы проверяете, насколько хорошо ваши различные блоки «интегрируются» вместе.
Vaccano

3
Ничего не произошло. С чего бы это? .NET Framework полон статических методов. Вы говорите, что не можете использовать их ни в каких методах, которые вы хотите использовать для модульного тестирования?
Дэн Дип

3
Что ж, если ваш код находится на производственном уровне / качестве .NET Framework, тогда продолжайте. Но весь смысл модульного тестирования - это тестирование модуля в изоляции. Если вы также вызываете методы в других модулях (будь то статические или иные), то вы сейчас тестируете свой модуль и его зависимости. Я не говорю, что это не полезный тест, но не «модульный тест» по большинству определений. (Потому что вы сейчас тестируете тестируемый юнит и юнит, который имеет статический метод).
Ваккано

10
Предположительно, вы (или другие) уже проверили статический метод и показали, что он работает (по крайней мере, как ожидалось), прежде чем писать слишком много кода вокруг него. Если что-то сломается в следующей части, которую вы тестируете, то вам стоит посмотреть в первую очередь, а не в том, что вы уже тестировали.
Чао

6
@Vaccano Так как же тогда Microsoft тестирует .NET Framework, а? Многие из классов в Framework ссылаются на статические методы в других классах (например, System.Math), не говоря уже об обилии статических фабричных методов и т. Д. Кроме того, вы никогда не сможете использовать какие-либо методы расширения и т. Д. Дело в том, что простые функции, подобные этой, фундаментальное значение для современных языков. Вы можете проверить их изолированно (так как они обычно будут детерминированными), а затем использовать их в своих классах без беспокойства. Это не проблема!
Дэн Дип

18

Если реальный вопрос здесь - «Как мне проверить этот код?»:

public class MyClass
{
   public void MethodToTest()
   {
       //... do something
       MyStaticClass.StaticMethod();
       //...more
   }
}

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

public class MyClass
{
   private readonly IExecutor _externalExecutor;
   public MyClass(_IExecutor executor)
   {
       _exeternalExecutor = executor;
   }

   public void MethodToTest()
   {
       //... do something
       _exetrnalExecutor.DoWork();
       //...more
   }
}

public class MyStaticClassExecutor : IExecutor
{
    public void DoWork()
    {
        MyStaticClass.StaticMethod();
    }
}

9
К счастью, у нас также есть вопросы, касающиеся читабельности кода и KISS для SO тоже :)
gbjbaanb

не будет ли делегату легче и делать то же самое?
JK.

@jk может быть, но тогда трудно использовать контейнер IoC.
Солнечный

15

Статика не обязательно является злой, но она может ограничить ваши возможности, когда дело доходит до юнит-тестирования с помощью fakes / mocks / stubs.

Есть два основных подхода к насмешкам.

Первый (традиционный - реализованный RhinoMocks, Moq, NMock2; ручные макеты и заглушки также находятся в этом лагере) основан на тестовых швах и внедрении зависимостей. Предположим, вы тестируете модульный код и у него есть зависимости. В коде, разработанном таким образом, часто происходит то, что статика создает свои собственные зависимости, инвертируя инверсию зависимостей . Вскоре вы обнаружите, что не можете внедрить макетированные интерфейсы в тестируемый код, разработанный таким образом.

Второй (макет чего-либо - реализованный TypeMock, JustMock и Moles) использует API профилирования .NET . Он может перехватить любую из ваших инструкций CIL и заменить часть вашего кода на фальшивую. Это позволяет TypeMock и другим продуктам в этом лагере макетировать что угодно: статику, закрытые классы, частные методы - вещи, не предназначенные для тестирования.

Между двумя школами мысли продолжаются дебаты. Кто-то говорит: следуйте принципам SOLID и проектируйте для обеспечения тестируемости (это часто включает в себя упрощение статики). Другой говорит, купи TypeMock и не волнуйся.


14

Проверьте это: «Статические методы - смерть для тестируемости» . Краткое резюме аргумента:

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


32
Я не сохраняю глобальное состояние и не вызываю побочные эффекты в своих статических методах, поэтому этот аргумент кажется мне неуместным. В статье, на которую вы ссылаетесь, используется аргумент скользкого наклона, который не имеет оснований, если статические методы ограничены простым процедурным кодом, действующим «общим» способом (например, математическими функциями).
Роберт Харви

4
@ Роберт Харви - как вы тестируете метод, который использует статический метод из другого класса (то есть он зависит от статического метода). Если вы просто позволяете ему это называть, значит, вы не «модульное тестирование», а «интеграционное тестирование»
Vaccano

10
Не просто читайте статью в блоге, читайте множество комментариев, которые с ней не согласны. Блог - это просто мнение, а не факт.
Дэн Дип

8
@Vaccano: статический метод без побочных эффектов или внешнего состояния функционально эквивалентен значению. Зная это, он ничем не отличается от модульного тестирования метода, который создает целое число. Это одна из ключевых идей, которую дает нам функциональное программирование.
Стивен Эверс

3
Запрещая работу встроенных систем или приложения, чьи ошибки могут привести к международным инцидентам, IMO, когда «тестируемость» влияет на архитектуру, вы ее исказили до того, как приняли методологию test-each-last-corner-first-first-first. Также я устал от версии XP, которая доминирует в каждом разговоре. XP это не авторитет, это индустрия. Вот оригинальное, разумное определение модульного тестирования: python.net/crew/tbryan/UnitTestTalk/slide2.html
Эрик Реппен,

5

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

Но у него не будет ключа, определяющего свойства теста; терпеть неудачу, когда что-то не так, проходить, когда они правы.

Это относится ко всем статическим вызовам, вызовам конструктора и любой ссылке на методы или поля, не унаследованные от базового класса или интерфейса . Если имя класса появляется в коде, это зависимость, видимая компилятору, и вы не можете проверить ее без него. Любой меньший кусок просто не является допустимым тестируемым модулем . Любая попытка трактовать его так, как если бы он был, будет иметь результаты, не более значимые, чем написание небольшой утилиты для выдачи XML, используемого вашей тестовой средой, чтобы сказать «тест пройден».

Учитывая это, есть три варианта:

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

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

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


3
Ничто не говорит о том, что модульный тест - это тест одного класса. Это тест одной единицы. Определяющими свойствами модульных тестов является то, что они быстрые, воспроизводимые и независимые. Ссылка Math.Piв методе не делает его интеграционным тестом по какому-либо разумному определению.
Сара

то есть вариант 1. Я согласен, что это лучший подход, но может быть полезно знать, что другие люди используют термины по-разному (разумно или нет).
сору

4

Там нет двух способов об этом. Предложения ReSharper и некоторые полезные функции C # не будут использоваться так часто, если вы пишете отдельные атомарные модульные тесты для всего своего кода.

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

ДО:

    public static DBConnection ConnectToDB( string dbName, string connectionInfo ) {
    }

ПОСЛЕ:

    public static Func<string, string, DBConnection> ConnectToDB (dbName, connectionInfo ) {
    };

Два совместимы по вызову. Абоненты не должны меняться. Тело функции остается прежним.

Затем в своем коде Unit-Test вы можете заглушить этот вызов следующим образом (при условии, что он находится в классе с именем Database):

        Database.ConnectToDB = (dbName, connectionInfo) => { return null|whatever; }

Будьте осторожны, чтобы заменить его исходным значением после того, как вы закончите. Вы можете сделать это с помощью try / finally или, в вашем модульном тесте, который вызывается после каждого теста, написать такой код:

    [TestCleanup]
    public void Cleanup()
    {
        typeof(Database).TypeInitializer.Invoke(null, null);
    }

который повторно вызовет статический инициализатор вашего класса.

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

  1. Если статический метод был методом расширения, вы должны сначала изменить его на метод без расширения. Решарпер может сделать это для вас автоматически.
  2. Если какой-либо из типов данных статических методов представляет собой сборку встроенного взаимодействия, например, для Office, необходимо обернуть метод, обернуть тип или изменить его на тип «объект».
  3. Вы больше не можете использовать инструмент рефакторинга Resharper для изменения подписи.

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

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

Так почему же в C # есть статические методы? Почему он допускает не виртуальные методы экземпляра?

Если вы используете одну из этих «функций», то вы просто не сможете создавать изолированные методы.

Так когда вы их используете?

Используйте их для любого кода, который, как вы ожидаете, никто не захочет заглушить. Некоторые примеры: метод Format () класса String, метод WriteLine () класса Console, метод Cosh () класса Math.

И еще одна вещь. Большинству людей это не нужно, но если вы можете узнать о производительности косвенного вызова, это еще одна причина избегать методов экземпляра. Есть случаи, когда это удар по производительности. Вот почему существуют не виртуальные методы.


3
  1. Я считаю, что это отчасти потому, что статические методы «быстрее» вызывать, чем методы экземпляра. (В кавычках, потому что это пахнет микрооптимизацией) см. Http://dotnetperls.com/static-method
  2. Он говорит вам, что ему не нужно состояние, поэтому его можно вызывать откуда угодно, снимая накладные расходы, если это единственное, что кому-то нужно.
  3. Если я хочу поиздеваться над этим, то я думаю, что обычно это декларация интерфейса.
  4. Если он объявлен в интерфейсе, тогда R # не предложит вам сделать его статичным.
  5. Если он объявлен виртуальным, то R # также не предложит вам сделать его статическим.
  6. Удержание состояния (полей) статически - это то, что всегда нужно тщательно продумывать . Статическое состояние и нити смешиваются, как литий и вода.

R # не единственный инструмент, который сделает это предложение. FxCop / MS Code Analysis также сделает то же самое.

Я бы вообще сказал, что если метод статический, то в целом он должен быть тестируемым, как есть. Это вызывает некоторое внимание к дизайну и, возможно, больше обсуждения, чем у меня сейчас, так терпеливо ожидая отрицательных голосов и комментариев ...;)


Можете ли вы объявить статический метод / объект на интерфейсе? (Я так не думаю). Эфирный путь, я имею в виду, когда ваш статический метод вызывается тестируемым методом. Если вызывающий абонент находится в другом устройстве, тогда статический метод должен быть изолирован. Это очень трудно сделать с помощью одного из трех инструментов, перечисленных выше.
Ваккано

1
Нет, вы не можете объявить статическое на интерфейсе. Это было бы бессмысленно.
МВД

3

Я вижу, что после долгого времени никто еще не установил действительно простой факт. Если resharper говорит мне, что я могу сделать метод статичным, это значит для меня огромную вещь, я слышу, как его голос говорит мне: «Эй, вы, эти кусочки логики не являются ОТВЕТСТВЕННОСТЬЮ текущего класса, чтобы справиться, поэтому он должен остаться вне в каком-то вспомогательном классе или что-то ".


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

2
Я вижу вашу точку зрения только в том случае, если домен очень прост и не подходит для будущей модификации. Иными словами, то, что вы называете «бессмысленной сложностью», для меня это хороший и понятный человеку дизайн. Наличие вспомогательного класса с простой и понятной причиной существования является в некотором роде «мантрой» принципов SoC и Single Responsibility. Кроме того, учитывая, что этот новый класс становится зависимостью для основного класса, он должен раскрывать некоторые открытые члены, и, как естественное следствие, он становится тестируемым изолированно и его легко издавать, когда он действует как зависимость.
g1ga

1
Не имеет отношения к вопросу.
Игби Ларгман

2

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

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


0

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

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

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


Я имею в виду, когда у вас есть тестируемый метод (не статический), который вызывает статический метод в другом классе. Эта зависимость может быть изолирована только одним из трех инструментов, перечисленных выше. Если вы не изолируете его, то вы проводите интеграционное тестирование, а не юнит-тестирование.
Ваккано

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