Комментарии 34
А тесты на тесты писать нужно?
думаю, что для сложной логики допустимо тестирование с поэтапным/послойным снижением сложности…
То есть, более простые тесты для более сложных, которые, в свою очередь, все же проще, чем тестируемый исходный код.
Но, это скорее допустимое исключение, чем норма… стремиться надо к минимализму и понятности… просто в реальном мире не всегда удаётся в первой итерации декомпозиции получить простейшие сущности
это образец обфускации С++ на гнольий ибо
- имя класса пишем с маленькой буквы, чтобы их не отличать
- злоупотребление_подчёркиванием
- в дополнение к этому, активно используем в названиях цифры типа so_1_2_5
- в дополенние к этому, обфускация переноса на новую строку делает визуально плохоразличимыми обьявления конструкторов и функций, также отлично обфусцировано лябмда- выражение и не понятен перегруз this >>= st_test; без контекста
- Первый для случая, когда deadletter handler реализуется указателем на метод, Второй — для случая лямбда функции. Для этого без припудривания мозгов, в одном тестовом классе пишутся два метода для теста первого и второго, возможные общие моменты выносятся в отдельные методы
- разумеется для полного использования возможностей шаблонов С++, на них нужно написать не только шаблоны внутри шаблонов, а ещё и тьюринг машину, лисп машину и правила пролог. При этом, назначение тестов в том чтобы они были максимально простыми, очевидными, лишёнными подводных камней и по возможности понятными без контекста
Вот почему нет.
при этом в коде нет таких тривиальных необходимых вещей как разделение на предворительный сеттинг окружения в до-тестовое состояние и уборка после теста.
Хочется, однако, прояснить один момент: вы когда нибудь в глаза видели названия классов, методов и функций из стандартной библиотеки С++? Ну или из Boost-а?
Но тут и много других обфускаций кроме названий.
На практике такой код, тем более со сложными темплейтами, это вырвиглаз, и как правильно написал человек выше требует написания теста к тесту, в частности чтоб потестить темплейты, который будет сложнее самого теста.
А провоцирование падения может вызвать ложноположительное исполнение без падения.
Потом берем, и меняем
virtual void
do_next_step() override
{
so_drop_deadletter_handler< Msg_Type >( m_mbox_holder.mbox() );
so_5::send< Msg_Type >( *this );
}
на virtual void
do_next_step() override
{
// so_drop_deadletter_handler< Msg_Type >( m_mbox_holder.mbox() );
so_5::send< Msg_Type >( *this );
}
Запускаем еще раз. И мы можем оказаться в двух ситуациях.1. Тест «упал». Значит есть вероятность, что тест был написан правильно.
2. Тест «не упал». Значит в тесте 100% есть ошибка.
Соответственно, если в результате провоцирования неправильной работы теста (путем комментирования отдельных фрагментов, добавления дополнительных операций, изменения параметров операций и т.д.) возникают ситуации №2, то, как по мне, это гораздо лучше, чем полный отказ от попыток проверки корректности работы тестов.
А вы как предлагаете проверять корректность тестов?
А можно еще вопрос: а вы на C++ давно программируете? И вообще: программируете ли вы на C++?
1. Удачность и понятность конкретных имен классов/методов. Например, pfn_test_case_t для меня, как для прошедшего через венгерскую нотацию, вполне нормальное и читабельное имя. Но вот для тех, кто помоложе и про венгурку не слышал, буквосочетание pfn может быть совершенно нечитаемым. Поэтому и возник вопрос, но он повис в воздухе без ответа. Что наводит на мысли о том, что конструктива автор комментария явно не предполагал изначально.
2. Следование общепринятым стандартам кодирования. Например, если бы кто-то в Java или в C# коде начал использовать только snake_case, то это должно было бы вызвать нарекания у других Java/C# разработчиков, т.к. для этих языков уже есть давно устоявшиеся соглашения об именовании. Но в C++ же такого нет, в C++ вполне благополучно живут и развиваются проекты, которые используют snake_case, а так же проекты, которые используют CamelCase. И даже проекты, которые используют Camel_With_Underscores_Case. Поэтому в C++ коде претензии к стилю оформления — это чистой воды вкусовщина. На которую вообще не следовало бы обращать внимания. Но поди ж ты :(
3. У меня лично возврат к snake_case был вполне себе обоснованным и, где-то, выстраданным решением после многих лет использования CamelCase. Так что использование snake_case — это вполне себе обдуманное и взвешенное решение. Я могу понять, что кому-то такой стиль именования не нравится. Но если snake_case мне объективно помогает, то чисто вкусовые претензии по поводу отсутствия CamelCase просто не принимаются во внимание.
Я бы тут отметил, что упоминать в каждом классе «test_case» как-то выглядит излишним. Они же и так в тестах и не будут использоваться нигде вне них, зачем это повторять?Помнится, изначально казалось, что в тесте потребуется больше классов, поэтому-то и появились суффиксы _mbox_case_t и _test_case_t. Но потом выяснилось, что больше ничего и не нужно, но избавиться от префиксов в голову уже не пришло.
Кроме того, в C++ есть прекрасная штука using, которая может сделать так, чтобы интерфейсные классы внешней библиотеки начали соответствовать нужному стилю написания.
А что касается тестов, то конечно хотелось бы, чтобы они были простыми и понятными, а главное рабочими, но зачастую остается хотеть, чтобы они просто БЫЛИ, а из списка быстро написанные, понятные, покрывающие большой процент кода, как в известном приколе-картинке, приходится выбирать 2 из 3.
Да и тестов, которые не работают, пользу нет. Так что если тесты есть, то они должны быть рабочими.
Другое дело, что споры о code convention начинаются тогда, когда о сути и смысле статьи сказать нечего. И уж тем более странно видеть споры о code convention для C++, в котором нет официального и общепринятого соглашения об именовании. И в котором давно и успешно сосуществуют snake_case, CamelCase и Camel_With_Underscore_Case.
- T разных размеров
- Тривиальных/нетривиальных T
- T с доступностью разных конструкторов (copy_constructible, default_constructible, move_constructible)
- T с доступностью разных операторов присваивания (move_assignable/copy_assignable)
- T с различными политиками исключений (noexcept_copyable, noexcept_constructible, noexcept_move_assignable...)
- overaligned T
- собственно, различных тестовых сценариев
Итого бойлерплейтом получатся тысячи простых тестов. Или всё-таки лучше несколько сложных?
Есть ещё возможный подход: написать генератор простых тестов вместо собственно простых тестов. Оправданность зависит от того, насколько сложно отлаживать «сложные» тесты и насколько сложно создать собственно генератор, а также для чего генератор будет полезен. При правильное реализации генератор должен генерировать достаточно хорошо читаемый и хорошо отлаживаемый boilerplate.
На C ещё был бы вариант с использованием ffi для написания тестов на языке более высокого уровня, но ни макросы ни шаблоны C++ так не протестируешь (хотя, может, clang предоставляет какое‐нибудь полезное API…).
Итак:
тут не только тесты на тесты которые будут сложнее тестов на сорсЧто именно тут нуждается в тестах для тестов?
имя класса пишем с маленькой буквы, чтобы их не отличатьЭто C++. Не Java, ни JavaScript, ни Rust, ни Go. Здесь нет общепринятого code guidelines. Кто-то использует CamelCase, кто-то snake_case, кто-то Camel_With_Underscores_Case. В мире C++ это нормально. Ненормально, когда в рамках одного исходника наблюдается «разброд и шатание», т.е. часть классов в CamelCase, часть в snake_case, часть еще как-то. Здесь этого нет. Кроме того, у имен типов есть суффикс _t. Как раз специально, чтобы их можно было отличать.
злоупотребление_подчёркиваниемСм. предыдущий пункт.
в дополнение к этому, активно используем в названиях цифры типа so_1_2_5Цифры используются только в виде so_5::. Это название пространства имен верхнего уровня, такое же, как std:: или boost::. И цифра 5 в этом названии играет важную роль. Почему so_5, а не so5 или sobjectizer_five? Потому что это дело вкуса.
в дополенние к этому, обфускация переноса на новую строку делает визуально плохоразличимыми обьявления конструкторов и функцийМы уже давно используем нотацию, в которой тип возвращаемого значения описывается на предыдущей строке. Так гораздо проще видеть границы функций/методов в ситуациях вроде вот таких:
virtual const typename Some_Long_Template_Name::some_long_nested_type_name
first_method(...) ...
{}
template<typename T>
typename std::enable_if<...some_long_condition...>::type
second_method(...) ...
{}
Соответственно, для консистентности мы используем такой стиль везде. Поэтому так и выглядят те фрагменты, которые выдернуты из реального кода, а не написаны специально для статьи.
и не понятен перегруз this >>= st_test; без контекстаА все остальное без контекста понятно? Например, почему делается наследование от so_5::agent_t? Какую роль играют методы so_define_agent? Что именно делает so_5::send? Для чего и почему используется mhood_t<Msg_Type>? Примеры выдернуты из реального кода. И чтобы объяснить в деталях каждый кусок реального кода, придется написать статью такого же объема, если не больше. Поэтому, когда демонстрируется реальный код, а не написанный специально для статьи, всегда будет что-то, что непонятно неподготовленному читателю. Тем более, что this >>= st_test — это мелкая фоновая деталь, которая вообще не имеет отношения к основной теме статьи.
Для этого без припудривания мозгов, в одном тестовом классе пишутся два метода для теста первого и второго, возможные общие моменты выносятся в отдельные методыТут, боюсь, у комментатора вообще непонимание предмета. Суть проблемы в том, что в некий класс so_5::agent_t был добавлен набор новых методов. Один из этих методов, а именно so_subscribe_deadletter_handler, и нужно было протестировать. Этот метод должен вызываться наследниками so_5::agent_t. И so_subscribe_deadletter_handler может получать либо указатель на метод наследника, либо лямбду. Т.е. в тесте нужно создать именно наследника от so_5::agent_t и чтобы этот наследник вызвал нужный вариант so_subscribe_deadletter_handler. Сделать это можно как показано в статье. Код класса-наследника в этом случае получается очень простым. Либо, если я правильно понял, как предлагает smer44, что-то вроде:
enum class test_variant_t { pfn, lambda };
template<typename Mbox_Case, typename Msg_Type>
class test_agent_t : public so_5::agent_t {
const test_variant_t variant_;
public:
test_agent_t(context_t ctx, test_variant_t variant)
: so_5::agent_t(std::move(ctx)), variant_(variant) {}
...
virtual void so_define_agent() override {
...
if(test_variant_t::pfn == variant_)
so_subscribe_deadletter_handler(..., &test_agent::on_deadletter);
else
so_subscribe_deadletter_handler(..., [this](mhood_t<Msg_Type>) {...}
...
}
...
};
Только вот этот вариант не будет проще. И ошибиться в нем как раз легче.разумеется для полного использования возможностей шаблонов С++, на них нужно написать не только шаблоны внутри шаблонов, а ещё и тьюринг машину, лисп машину и правила пролог.Чистой воды наброс. В комментариях не нуждается.
При этом, назначение тестов в том чтобы они были максимально простыми, очевидными, лишёнными подводных камней и по возможности понятными без контекстаТесты к любой нетривиальной библиотеке в принципе не могут быть понятным без контекста. В принципе. Что до «очевидности», «лишенности подводных» камней, то хотелось бы увидеть конкретные претензии: что не очевидно, где подводные камни?
при этом в коде нет таких тривиальных необходимых вещей как разделение на предворительный сеттинг окружения в до-тестовое состояние и уборка после теста.Еще один момент, который наводит на мысль о том, что автор не работает с C++. В статье вообще не было показано как тесты запускаются, как создается тестовое окружение, как определяется успешность или не успешность тестов. Т.е. того самого окружения, о котором говорит smer44 вообще в статье нет. Поскольку это окружение не имеет никакого отношения к предмету статьи. Статья рассказывает о том, как с помощью наследования и шаблонов сделать набор классов, которые затем будут использоваться в каком-то тестовом окружении. И все.
Было бы очень интересно услышать пояснения или комментарии от самого smer44 или кого-то из тех, кто разделяет его точку зрения.
Почему публичные поля заканчиваются на подчеркивание, но не все? (например obj.config_)? Придумать объяснение я сам не могу.
С другой стороны, в своих разработках мы используем нотацию, которой уже очень много лет, и в которой мы используем для членов префикс «m_». Поэтому, если фрагмент кода был взят из реального теста или из реальной программы, то там вместо суффиксов-подчеркиваний будут префиксы «m_».
Хороший пример применения шаблонов в небиблиотечном коде, спасибо!Вам спасибо!
можно добавить просто ещё один аргумент шаблонаМожно было бы. Но это явно бы сделало реализацию теста более сложной. Тем более, что нам нужно ограничиваться тем подмножеством C++11, которое реализовано в MSVC++12.0. Там фокус с static auto functor() не так-то просто провернуть.
А в описанной реализации небольшая копипаста дает простоту и независимость от ограничений старых C++ компиляторов.
Задействовать для простых тестов наследование, полиморфизм и шаблоны? Почему бы и нет…