Как стать автором
Обновить

Комментарии 14

Хорошая статья, но для полного понимания читателям, можно было бы сослаться на книгу, где про stub, mock и юнит-тестирование просто и подробно написано The Art of Unit Testing: With Examples in .Net
«Однако рано или поздно вам может надоесть это чудесное занятие по ручной реализации интерфейсов и вы обратите свое внимание на один из Isolation фреймворков, таких как Rhino Mocks, Moq или Microsoft Moles. Там эти термины встретятся обязательно и понимание отличий между этими типами фейков вам очень пригодится.»
Вот как раз в Moq специально сделано так, чтобы не было отличий между стабами и моками.

«Как показывает практика, нормальный ОО дизайн либо уже является достаточно «тестируемым» или же требует лишь минимальных телодвижений, чтобы сделать его таковым.»
Отнюдь. Вот на выходных рефакторил очередной ощутимый кусок кода под unit-testing. С ОО там все было хорошо, а вот для полной изоляции там пришлось добавить еще нехилый объем прослоек.

Обычно (не всегда, но часто) нормальное изолированное тестирование требует, чтобы каждый изолируемый кусок был выделен как интерфейс, что и создает совершенно ненужный код.
А наследование для тестирования не пробовали применять? Наследуемся от тестируемого класса, переопределяя конструкторы и сеттеры, так чтобы они использовали моки для разрешения зависимостей. Исходный код при этом практически не изменяется (разве что поменять private на protected).
«А наследование для тестирования не пробовали применять?»
Это будет работать только в том случае, когда все нужные методы можно переопределить. А это далеко не всегда так.

«Исходный код при этом практически не изменяется (разве что поменять private на protected). „
А это нарушает принцип максимального сокрытия информации.
А насколько имеет смысл тестировать конструкторы, а также сеттеры? То есть имеет ли смысл тестировать состояние? Ведь приходится нарушать инкапсуляцию, либо небезопасно (изменяя видимость), либо вводя геттеры (увеличения объема кода тестируемого класса лишь для тестирования).

Когда только осваивал юнит-тестирование, то тестировал всё, потом понял, что в подавляющем большинстве случаев (подстраховка :) ) достаточно тестировать только поведение: создали объект, настроили, вызвали тестируемый метод, проверили, что он возвращает и, при необходимости, что вызывает то, что нужно. Что происходит внутри нас как бы и не волнует — тестовый клиент такой же клиент как и другие.

И, кстати, такая Assert.That(mock.SavedUserName, Is.EqualTo(mock.SavedUserName)); проверка разве является достаточной для того, чтобы быть уверенным, что вызывался метод SaveLastUserName, а не значение mock.SavedUserName было присвоено напрямую или другим методом?
Очепятку в коде я поправил, смысла тестировать, что mock.SavedUserName равен mock.SavedUserName мало смысла. А вот сравнить это поле со значением UserName вью-модели смысл имеет.

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

Да, и +100 500 по поводу, что все тестировать не нужно. Разумно тестировать только наиболее важные части кода. При этом, кстати, не обязательно, что наиболее важным является поведение; иногда это так, иногда, важным является состояние. Но идея, правильное.

В одном из подкастов Кент Бек выразил очень прикольную мысль о том, что каждый тест должен рассказывать полезную и интересную историю о тестируемом классе, чтобы читатель этого теста мог не только понять, что «ага, эта хрень, кажись, работает», а и то, что именно она делает полезного в данном конкретном случае. Т.е. тест — это еще и отличный источник спецификации.
А вашу опечатку я прочел как будто её нет :) Но дело не в логике теста, а в том, что, например, в LoginViewModel вместо вызова _provider.SaveLastUserName(UserName); происходит присваивание _provider.SavedUserName = UserName; — Тест работать будет, а настоящий провайдер, вероятно, нет.
Сам понял, что ступил — тут же сильная типизация и язык компилируемый — компилятор ошибку выдаст. Слаботипизированные интерпретируемые языки заставляют строже к тестам относиться.
Да, там все ок, поскольку в интерфейсе с которым работает вью-модель нет свойства SavedUserName.
покрывать тестами нужно весь код на 100%. Не на 90, 95 или 99, а на 100%. Не вводите новичков в заблуждение, если вы сами так не делаете. А то это звучит примерно как «вы знаете, наша машина отлично работает. Мы протестировали её и можем гарантировать, что руль поворачивается, а колёса крутятся. Ведь это самые важные моменты, не так ли?!»
Я правильно понимаю, что вы покрываете тестами и подобный код:

class MyClass
{
private object someData;

pulbic MyClass(object someData)
{
if (someData == null)
throw new ArgumentNullException("someData");

this.someData = someData;
}

public override string ToString()
{
return someData.ToString();
}
}


И теперь в тесте:
[Test(ExpectedException(typeof(ArgumentNullException))]
public void MyClass_Constructor_Failure()
{
var myClass = new MyClass(null);
}

[Test]
public void MyClass_Custructor_Success()
{
var myClass = new MyClass(new object());
}

[Test]
public void MyClass_ToString_CallsObjectToString()
{
object o = new object();
var myClass = new MyClass(o);
Assert.AreEquals(myClass.ToString(), o.ToString());
}


Ведь если этого не сделать, то 100% покрытия тестами у вас не будет:) Я, например, первый вариант предпочитаю «проверять» с помощью контрактов и статического анализатора, в результате, если что-то пойдет не так, то у меня код просто не скомпилится (ну, скомпилится, но я ошибку я увижу во время компиляции). Кроме того, натравите на свой собственный код какой-нить анализатор, у вас ведь тоже не 100%;)

Я продолжу свою мысль (не столь категоричную, как вашу): тестов должно быть _достаточно_.

Знаете, что самое интересное: Кент Бек — небезызвестный папа TDD, не является фанатом 100% покрытия тестами (пруф в подкасте от SERadio). Не все тесты приносят что-то полезное, они могут и захламлять код тестов, пряча при этом за грудой ненужных тестов парочку важных, которые найти в подобном хаосе будет не просто.

Так что да, я не хочу идеального качества, потому что идеальное качество требует бесконечного количества усилий для его достижения;) Прагматизм рулит и юнит-тесты в этом вопросе не исключение.
Я бегло глянул ваш код. Скорее всего да, нужно писать именно в таком ключе, проверяя каждый экспешен. И меня ни раз спасало именно это от длительных дебагов и овертаймов. Ведь написать тест нужно ну от силы секунд 40. (Если всё правильно писалось до этого, с предварительной инициализацией, паттернами и т.д. А если нет, то TDD уж очень сложно идёт)
Вы подняли вопрос красоты кода (разделение разных тестов «а-ля сам дурак» и тестов поведения по бизнес кейсам ) в рамках одного тестового класса. Тест нужно искать. Написал и забыл, пока он не упал. Если уж очень надо, то нужно правильно именовать его: имяМетода_входныеПараметры_ожидаемыйРезультат.

анализатор — хорошая вещь, но это дополнение.
Давайте рассмотрим вариант с конструктором, который является довольно простым и популярным (при небольшом количестве внешних зависимостей он работает просто прекрасно).
А что делать, если зависимостей много? Разбивать класс на несколько мелких, использующих каждый мало внешних зависимостей?
Часть из этих зависимостей будет примитивными, типа примитивных типов данных или простых классов данных. С ними, ессно, делать ничего не нужно. А если класс зависит от 8 других тяжелых сущностей, которые нужно мокать, то это признак того, что с дизайном чего-то не то.
Да, нужно разбить класс на более мелкие, объединить все зависимости в одну сущность (если это имеет смысл) и поднять зависимостям уровень абстракции.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации