Как стать автором
Обновить
8
0
Толмачёв Дмитрий @FiresShadow

Разработчик ПО

Отправить сообщение
Хотя, если есть возможность пропатчить стандартную библиотеку применительно к только какому-то одному модулю (классу), и есть возможность сделать это внутри самого модуля, то тестируется содержимое одного модуля, и тест модульный.
Если тестировать методом черного ящика и писать тест до реализации, то непонятно, получится он интеграционным или модульным — зависит от того, обращается ли к методам других классов ReverseString. Тип теста можно будет узнать только после реализации ReverseString. Кот Шрёдингера какой-то получается.
Тесты тестируют модули исходного кода. Когда вы патчите стандартную библиотеку, появляется исходный код. Раз несколько модулей исходного кода — значит тест интеграционный.
Ну вы же сами себе отвечаете: не надо править мок, нет такого этапа.
Так в том то и дело, что этапа нету, а править надо, раз поведение мокированного класса изменилось.

И как это обосновывает применимость его в философии TDD?
По Фаулеру, The classical TDD style is to use real objects if possible and a double if it's awkward to use the real thing. Поскольку real objects, то тест интеграционный.
А если я вместо стандартной библиотеки времени использую нестандартную, то уже не модульное?
Совершенно верно.
Если у вас код не зависит от поведения класса мока, то вам не надо править мок.
Речь шла не о том, в каких случаях нужно править мок, а о том, как решить, что вот прямо сейчас пришло время поправить мок. В рамках TDD все этапы чётко расписаны, что и после чего нужно делать. И правка моков не вписывается ни в один этап.

И вы легко приведёте пруфы?
Пруфы содержаться в статье. Ссылка на определение интеграционного теста и ссылка на определение классического и мокисткого TDD.
Вообще, разница между юнит-тестами и интеграционными, на мой взгляд не так велики. Тест, в котором используются какие-то библиотеки языка, которые решили не мокировать (те же объеты даты и времени) можно считать как юнит-тестом, так и интеграционным.

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

В статье приведена ссылка на статью из русскоязычной википедии, в которой дано следующее определение: модульное тестирование, или юнит-тестирование (англ. unit testing) — процесс в программировании, позволяющий проверить на корректность отдельные модули исходного кода программы. (Соответственно в статье я подразумеваю под модульным тестированием именно это.) Также приведена ссылка на определение интеграционного теста: одна из фаз тестирования программного обеспечения, при которой отдельные программные модули объединяются и тестируются в группе.

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

При TDD разработка мне видится так:
1. при необходимости внесения изменений человек описывает парочку интеграционных тестов, если это возможно, которые показывают как всё должно работать в целом.
Нет, в рамках TDD тесты пишутся не на «всё в целом», а на конкретный функционал. При этом в классическом TDD игнорируется тот факт, что этот функционал может использовать другой функционал, а в мокистком TDD эта связь с другим функционалом разрывается. Это важное различие между классическим и мокистким TDD. Классическое TDD проверяет корректность функционала, а мокисткое — корректность поведения класса.
Хотя, в чём-то вы правы :) В проекте в любом случае будет содержаться некоторая доля модульных тестов: для тех классов, внутри которых нет обращений к методам других классов. Формально и фактически такие тесты не будут интеграционными. Они будут потенциально интеграционными. Если в них добавится обращение к методам других классов, то по классическому TDD эти тесты будут преобразованы и станут интеграционными фактически. В статье шло скорее не сравнение интеграционных тестов и модульных, а сравнение мокисткого подхода и классического. Раз это не так очевидно, то добавлю в статью, что даже при классическом подходе какая-то часть тестов будет модульными. Хотя, имхо, это и так очевидно, вроде бы.
Мокисткое правило — «Всегда мокируйте любое поведение объектов». Правило классического TDD — «Всегда используйте настоящие объекты, если это возможно». Если вы изобретёте новое правило, то это будет новым подходом. Не представляю, как можно быть где-то посередине: использовать немного настоящий объект, который является немного мокой. Может, можно как-то извратиться и сделать, но не представляю зачем и как.
при полноценном TDD сначала пишется интеграционный тест, а затем, при необходимости, юнит-тесты на уровень ниже.
А почему не «а затем, при необходимости, интеграционные тесты на уровень ниже»?

Подходы только юнит или только интеграционные — это две крайности.
Довольно спорное философское утверждение. А быть негром или европеоидом — это тоже две крайности? Есть две школы (классическая и мокисты), которые по-разному подходят к TDD. В какой-то фиксированный момент времени вы либо ведёте себя как последователь одной школы, либо как последователь другой. В противном случае вы ведёте себя как человек, не следующий учению ни одной из школ, и вы можете основать свою школу, если сумеете систематизировать свои знания и формально описать новый подход (а не просто сказав, что выбираем тип теста в зависимости от дня недели, не объяснив, почему модульные тесты лучше писать по вторникам, а интеграционные — по пятницам). Возможно эта новая школа будет сочетать лучшие традиции предыдущих двух школ, и они исчезнут, слившись в одну. Но пока что этого не произошло. При всём уважении, желание быть всегда «где-то посередине» — тоже крайность, имхо.
Во-первых, надо разделять классы так, что бы это не было проблемой.
А можете пояснить, как именно нужно разделить классы? Не представляю, как перераспределение кода по классам способно решить эту идеологическую проблему, если изменение моков не вписывается ни в один из этапов TDD-шного цикла.

В случае, если вы тут не будете использовать моки — у вас при изменении поведения упадёт 1000 тестов. Да, заметить это легче, но чинить сложнее.
Этот вопрос подробно рассматривался в статье.

Во-вторых, вы зачем-то смешиваете «classical TDD» и интеграционные тесты. Это совершенно разные вещи.
Тест — это фрагмент кода, TDD — это философия, рабочий процесс. Интеграционные тесты используются в классическом TDD. Я не говорил, что тест = TDD.

Интергационный тест — это более чем про одну feature, и к TDD имеет очень отдалённое отношение.
А можете пояснить, почему по-вашему интеграционные тесты не имеют отношения к TDD?
Представьте такую ситуацию. Гейм-дизайнеры решили, что какой-то перк (или способность) слишком сильная, и для соблюдения баланса решили уменьшить её эффективность в 2 раза. Или путём уменьшения в 2 раза какой-то характеристики перка, или путём добавления каких-то новых условий, которые необходимы, чтобы перк сработал. При этом этот перк должен остаться у тех же персонажей, которые им владели изначально. В этой ситуации проще в коде класса существующего перка константу изменить или условие добавить, чем удалять отовсюду старый перк и добавлять новый. А раз меняется бизнес-логика работы класса, то и тест нужно изменить. При этом в рамках TDD тест нужно изменить в первую очередь.
Ситуации разные бывают. Иногда при изменении функционала нужно существующие тесты изменить, иногда их нужно удалить, иногда нужно новые написать, не трогая существующих, — всё от ситуации зависит. Но если вы меняете логику работы класса, то нужно в любом случае взглянуть и на его тесты, и что-то с этими тестами сделать (добавить\удалить\изменить)
На стадии сопровождения часто требуется изменить действующий функционал. Например, новую колонку добавить в уже существующий отчёт, новый перк добавить уже существующему персонажу, внешнему сервису, с которым уже налажено взаимодействие, какие-то другие передать и так далее. В таких случаях зачастую проще изменить существующий тест и существующий класс, чем стереть тест и класс и написать всё заново. Это не ломание рабочего теста и рабочего класса, а просто приведение их в соответствие с новой бизнес-логикой и новыми требованиями заказчика.
Тут немедленно возникает разумный вопрос: под какие же задачи пролог заточен?
Вот этот комментарий частично отвечает на ваш вопрос. Пример автоматического доказательства теоремы можете посмотреть в этой статье, я тоже на эту тему писал.
Ну да, пролог не для GUI. В программе, написанной на языке логического программирования, решение задачи часто ищется посредством хорошо оптимизированного брутфорса. Изначально это задумывалось как перебор математических фактов с целью доказательства теорем. Можно создать обычную прикладную программу на прологе, и она будет гораздо гибче и понятнее, чем программа из любой другой парадигмы, но будет она очень тормознутой — ибо брутфорс. Можно написать на прологе и быструю программу, но тогда нужно будет не просто описывать известные факты о системе, а как бы направлять ход выполнения программы, по сути возвращаясь к императивному программированию. Логическое программирование тем и ценно, что программист, например, описывает что такое ток, проводочки и диоды, задаёт максимальное количество элементов на микросхеме, и просит программу собрать ему синхрофазотрон. Без шуток, этот очень оптимизированный брутфорс творит чудеса, и Ватсон свидетель чудес его!
предикат lexr/2 последовательно вызывает три предиката: list_text/2, delb/2, lexr1/2.

Эта фраза не совсем корректна, в логической парадигме нет понятия «вызов функции» или вызова чего бы то ни было. Точно также нет присвоения переменных.
Согласен, имена предикатов в данном случае малоинформативные.
Я указываю проблему Prolog это интеграция.

Не занимался интеграцией с прологом, но первое, что приходит в голову — это передача итоговых данных по TCP на localhost. И это не единственный способ.
Сделать на нем какой либо интерфейс взаимодействия с пользователем тот еще цирк.

Есть Visual Prolog. Например, button::defaultAction :- Name = idc_ancestordialog_personname:getText(). В логической парадигме программирования нет присвоения переменных и вызовов функций, поэтому концептуально это можно перевести как «defaultAction истинно, когда Name совпадает с тем, что ввёл пользователь в окошко». Потом в каком-то месте программы утверждается, что defaultAction истинно (напомню, вызовов функций нет), и Prolog начинает искать все Name, которые совпадают с тем, что ввёл пользователь. Для этого у пользователя просят ввести данные. После этого считается, что Name определён. В общем-то да, GUI в логической парадигме немного запутаннее, чем в функциональной или объектно-ориентированной, но тем не менее, вполне осуществим.
Имхо, автор открыл (ну, или поспособствовал открытию) замечательный способ множественного наследования из мира ненормального программирования. Вместо того, чтобы класс реализовывал IPrintable, IExcelable, IDisposable можно написать три универсальных класса Printable, Excelable, Disposable, способных печатать и диспозить что угодно. Всю логику передаём в универсальные классы через делегаты. Поскольку в C#/Java нет множественного наследования, то делаем Printable[Excelable[Disposable[ViewModel]]].
Можно вообще сделать просто один класс Doer[T], делающий вообще всё что угодно. Пусть у класса 3 метода, тогда переменная будет иметь тип Doer[Doer[Doer[StreamReader]]](). Код вызова 1го метода: doer.Do(). Второго: doer.Value.Do(). Третьего: doer.Value.Value.Do(). Логику методов можно передавать через делегаты: new StreamReader.ToDoer(x => x.Dispose()).ToDoer(x => x.Value.ToString()).ToDoer(x => x.Value.Value.GetHashCode()). Чудесненько.

Имхо, в данном случае автор предлагает своеобразный способ, как «переопределить» Dispose в StreamReader, не реализуя наследника от StreamReader. Это незначительно экономит время: вместо объявления класса и метода нужно просто передать тело метода через делегат в ToDisposable(Action[T] delegate). Цена: использование нестандартного подхода, замена переменных типа StreamReader на Disposable[StreamReader], возможность переопределить только один метод (иначе приходим к Printable[Excelable[Disposable[StreamReader]]], а это уже явный перебор). Этот способ имеет смысл, когда нужно переопределить метод в запечатанном классе без публичных конструкторов. Однако автор предлагает использовать его повсеместно при любом освобождении ресурсов. Непонятно, почему бы просто не реализовать наследника StreamReader, раз уж так сильно хочется переопределить Dispose. Хотя не до конца понятно, а зачем его переопределять. Взяли из пула соединение, передали в функцию, функция отработала, вернули соединение в пул.
Допустим, у вас в ViewModel «на одну ответственность меньше», а сколько тогда ответственностей у Disposable[ViewModel]? А если понадобится добавить функцию печати и импорта в Excel, вы напишите Printable<Excelable<Disposable[ViewModel]>>?

Информация

В рейтинге
Не участвует
Откуда
Россия
Дата рождения
Зарегистрирован
Активность