Троллю юнит-тесты

zorin29

Выделяю в отдельный топик интересующий меня вопрос. Несмотря на тему треда, вопрос меня и правда интересует.
Предыстория. Год назад я, наконец, перестал писать под WinGDI на Delphi, и начал писать на C# под ASP.NET. И захотел попробовать себя в TDD. Книжки по программированию я не читаю, я скорее практик, нежели теоретик: интересующие меня ответы нахожу гуглом на просторах ENternet.
Проблема номер один: куча моего кода представляет собой вызовы внешних по отношению ко мне интерфейсов (далее интерфейсами я называю WCF-сервисы, web-сервисы ASP.NET, REST-сервисы и т.д.) Причем:
- тестовой версии интерфейсов нет, есть только production
- как правило, повторить запрос к интерфейсу невозможно. Например, некоторые интерфейсы требуют уникальный ID запроса, некоторые - меняют внутреннее состояние системы и т.д.
Как писать юнит-тесты? Написать mock-объект, дублирующий поведение интересующего меня интерфейса? Но >50% кода - это формирование запроса к интерфейсу и интерпретация ответа, так что кучу кода я так не протестирую.
Написать mock-объект, который маскирует только сетевое обращение, и подсунуть под запрос готовый ответ? Тогда получается, что мне нужно убить немалое время на построение библиотеки запросов и ответов. Причем покрытие будет плохое, т.к. я не всегда хорошо представляю, какие может ответы отдавать интерфейс.
Проблема номер два. Немалая часть логики моей системы хранится в виде объектов БД: хранимых процедур, constraint-ов, триггеров.
Чтобы написать хотя бы ОДИН integration-тест, мне нужно инициализировать БД некоторым набором данных.
Причем своим набором для каждого теста.
Причем после теста БД портится, т.е. для каждого теста надо БД переинициализировать.
Вообще избавиться от БД не вариант, т.к. мне-то как раз интересно протестировать работу БД.
Гуру tdd, посоветуйте мне менее трудозатратные решения.
Внимание! НЕ ПРЕДЛАГАЙТЕ МНЕ забить на TDD! ПОЖАЛУЙСТА! Не спрашивайте, почему я прошу не предлагать это. Если вам не нравится TDD - просто пройдите мимо :)

apl13

Год назад я, наконец, перестал писать под WinGDI на Delphi
Тебе не стыдно еще об этом вспоминать? :ooo:

val63

Причем после теста БД портится, т.е. для каждого теста надо БД переинициализировать.
О, подымай из бекапа в 100500 потоках!

kokoc88

И захотел попробовать себя в TDD.
И прочитал книжку.... Или просто возникло абстрактное желание?

zorin29

нет, не абстрактное. Ты дочитал вообще топик? Я несколько человеко-недель потратил на изучение вопроса и практику. Но какой-то конкретной книжки не читал.

kokoc88

Ты дочитал вообще топик? Я несколько человеко-недель потратил на изучение вопроса и практику. Но какой-то конкретной книжки не читал.
Прости, ты троллишь TDD, а я троллю тебя. Ты должен был воспринять мой пост, как направление для действия. :grin: :grin: :grin: Я бы рекомендовал отказаться от TDD и выбрать просто автоматическое функциональное тестирование.
Но >50% кода - это формирование запроса к интерфейсу и интерпретация ответа, так что кучу кода я так не протестирую.
Почему ты так решил? С помощью заглушки ты протестируешь весь код, который формирует запрос и интерпретирует ответ. Если ты используешь DTO, то сможешь отделить данные от бизнес логики. Тогда ты сможешь отдельно протестировать формирование запроса на основе DTO и бизнес логику. Я таким образом тестирую реализацию протокола XMPP:
 
@Element(ns = @Namespace(uri = "urn:ietf:params:xml:ns:xmpp-sasl"
name = "auth",
namespaces = @Namespace(uri = "urn:ietf:params:xml:ns:xmpp-sasl"
public class Auth {
@Attribute(name = "mechanism")
protected MechanismType mechanism;
.....
}

Причем покрытие будет плохое, т.к. я не всегда хорошо представляю, какие может ответы отдавать интерфейс.
В твоём случае покрытие (по control flow) может вообще не изменяться. Выбери такую стратегию разработки: напиши тесты для некоторых запросов и ответов, после чего добавляй новые каждый раз, когда происходит ошибка. Я так весьма успешно тестировал реализацию POP3 для одного из своих проектов, при чём на Си++.
Причем после теста БД портится, т.е. для каждого теста надо БД переинициализировать
Use transactions, Luke! Например, в Яндексе все DAO тестируются на снапшоте базы, который откатывается транзакцией. В моём текущем стартапе мы используем SQL Server CE для тестов, каждый раз заполняя базу из скриптов: при этом все тесты проходят достаточно быстро. Если возникает ошибка, то она добавляется в скрипт и тестируется.

bleyman

Проблема номер один: куча моего кода представляет собой вызовы внешних по отношению ко мне интерфейсов
Ну, один из подходов к этому делу, который мне лично не очень нравится, но другой альтернативы в подобных ситуациях по-видимому нет, состоит в том, что вместо проверки "эта часть программы при исполнении переводит внешний мир из состояния X в состояние Y", ты делаешь проверку "эта часть программы при исполнении отправляет внешнему миру запросы X1, X2, ..., Xn, получая ответы Y1, Y2, ..., Yn".
Другими словами, ты забиваешь на вторую букву в TDD, и пишешь интеграционные тесты так: отдебажил вроде бы прогу, запускаешь, запоминаешь все запросы-ответы, и пишешь мок, который пробегает по сценарию, проверяя что очередной запрос ровно такой, как нужно, и отдавая соответствующий ответ.
Разумеется если ты существенно меняешь логику взаимодействия то всё ломается, но по крайней мере ты защищён от разнообразных багов в unrelated местах. Ещё может и отловишь всякий мерзенький недетерминизм, который может иногда приводить к багам. Если автоматизировать процесс запоминания сценария, то вообще отлично!
Причем после теста БД портится, т.е. для каждого теста надо БД переинициализировать.
Ну дык и переинициализируй, в чём проблема? "Трудозатратно" — так не твой же труд, у роботов нет профсоюзов =). Если тормозит, можешь с транзакциями поиграться, ну или там может твоя БД умеет какие-нибудь милые быстрые temporary tablespaces etc... Тут я не знаю, не пробовал.
Ну и вообще, имей в виду, что написание легко-тестируемого кода — это Искусство, которое причём порою требует жертв, так что если тебе что-то тяжело тестить, то в первую очередь нужно думать о том, как бы поменять код, чтобы стало легко тестить, а не о том, в какую бы магию этот код обернуть, чтобы его стало легко тестить.

zorin29

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

kokoc88

Транзакции, насколько мне известно, не бывают вложенными, так что в моем случае они не покатят.
Транзакции бывают вложенными, но для тестирования работы с БД это может быть вовсе необязательно. Если использовать DAO или другой способ отделения данных от доступа к ним, то можно будет написать код так, чтобы он не делал COMMIT в пределах одного теста.
Нужен скорее скрипт инициализации бд с данными, чего я, собственно, и не хотел.

То есть ты хотел и шоколадку съесть, и на... ёлку залезть?

kokoc88

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

zorin29

А есть тут c# -еры, которые пользовались какими-нибудь библиотеками для mock-объектов ("заглушек")?
Я смотрел Moq, и в целом остался доволен, но он требует интерфейсов там, где мне интерфейсы не нужны. То есть, любой mock-объект должен реализовывать некий интерфейс. А у меня нету интерфейсов, мне нет смысла их делать. Есть простой статический класс-обертка для запроса к удаленному сервису. Оборачивать его еще и в интерфейс - жаба давит.
Другие framework-и, что я гуглил, страдают тем же, что и Moq: необходимостью интерфейса mock-объекта.
Кроме Isolator, который, судя по описанию - просто библиотека моей мечты. Но платная. Кто-то пробовал ее?

FRider

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

kokoc88

Я смотрел Moq, и в целом остался доволен, но он требует интерфейсов там, где мне интерфейсы не нужны.
Есть очень хорошее правило: если юнит тесты требуют интерфейсов там, где они тебе не нужны, то на самом деле они там нужны. Например, твоя простая статическая обёртка нарушает SRP, имеет плохой показатель lines per class, плохой показатель lines per method, несколько обёрток к разным сервисам будут дублировать код или нарушать ещё какие-нибудь формальные принципы. Это вовсе не означает, что один интерфейс автоматически тебя спасёт. Но решение задачи тестирования в целом даст более качественно спроектированный код.
Moq вполне подходит для тестирования, я его сейчас использую.

6yrop

то на самом деле они там нужны.
отличная аргументация :grin:

kokoc88

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

zorin29

простая статическая обёртка нарушает SRP, имеет плохой показатель lines per class, плохой показатель lines per method, несколько обёрток к разным сервисам будут дублировать код или нарушать ещё какие-нибудь формальные принципы.
Все вышеперечисленное! :)
А еще она сгенерирована автоматически по WSDL, и не реализовывает никакого интерфейса.
Я вижу два пути:
- дописать еще столько же кода для того, чтобы реализовывался интерфейс IWebService. Минус: куча кода.
- забить на обертку целиком, написать WebServiceCaller: implements IWebServiceCaller, и уже его при тестировании подменять на mock-объект. Минус: не тестируется формирование запроса и интерпретация ответа.

6yrop

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

kokoc88

А еще она сгенерирована автоматически по WSDL, и не реализовывает никакого интерфейса.
А чем ты для этого пользовался?
дописать еще столько же кода для того, чтобы реализовывался интерфейс IWebService. Минус: куча кода.
Для выделения интерфейса не нужно писать дополнительного кода.

zorin29

А чем ты для этого пользовался?
Ну MS VS это умеет сама из диалога Add Service Reference. Думаю, она делает это так же, как и утилиты disco и wsdl из .net framework tools.
Для выделения интерфейса не нужно писать дополнительного кода.
А ведь верно! :)

kokoc88

Ну MS VS это умеет сама из диалога Add Service Reference. Думаю, она делает это так же, как и утилиты disco и wsdl из .net framework tools.
Там должен создаваться интерфейс, который ты можешь использовать.

zorin29

И снова ты прав. Чертов кодогенератор не добавляет к имени интерфейса букву I, поэтому я и не видел интерфейса :)

Phoenix

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

Очень может быть, что будет быстрее.
и то, что поведение будет не полностью соответствовать реальному объекту - вполне вписывается в TDD. Узнал новое поведение - переписал mock-объект -> переписал код.
Оставить комментарий
Имя или ник:
Комментарий: