Pull to refresh

Comments 34

Я, обычно, каждый новый тест провоцирую на «падение». Если создаешь в тесте условие, при котором тест должен упасть, а он не падает, значит сам тест нужно тестировать. Где-то один раз из 10-15 это приходится делать.

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


Но, это скорее допустимое исключение, чем норма… стремиться надо к минимализму и понятности… просто в реальном мире не всегда удаётся в первой итерации декомпозиции получить простейшие сущности

тут не только тесты на тесты которые будут сложнее тестов на сорс, а деобфускатор нужен
это образец обфускации С++ на гнольий ибо
  • имя класса пишем с маленькой буквы, чтобы их не отличать
  • злоупотребление_подчёркиванием
  • в дополнение к этому, активно используем в названиях цифры типа so_1_2_5
  • в дополенние к этому, обфускация переноса на новую строку делает визуально плохоразличимыми обьявления конструкторов и функций, также отлично обфусцировано лябмда- выражение и не понятен перегруз this >>= st_test; без контекста
  • Первый для случая, когда deadletter handler реализуется указателем на метод, Второй — для случая лямбда функции. Для этого без припудривания мозгов, в одном тестовом классе пишутся два метода для теста первого и второго, возможные общие моменты выносятся в отдельные методы
  • разумеется для полного использования возможностей шаблонов С++, на них нужно написать не только шаблоны внутри шаблонов, а ещё и тьюринг машину, лисп машину и правила пролог. При этом, назначение тестов в том чтобы они были максимально простыми, очевидными, лишёнными подводных камней и по возможности понятными без контекста

Вот почему нет.
при этом в коде нет таких тривиальных необходимых вещей как разделение на предворительный сеттинг окружения в до-тестовое состояние и уборка после теста.
В вашем комментарии прекрасно все, от начала и до конца.
Хочется, однако, прояснить один момент: вы когда нибудь в глаза видели названия классов, методов и функций из стандартной библиотеки С++? Ну или из Boost-а?
стандартная либа исторически состоит из шедевров типа ptr_ioblablabla_t вместо говорящих за себя названий, но на то она и стандартная чтоб все знали эти названия. В бусте там матрицы, вектора или обьекты в Python'e имеют математическое наименование. У тебя же тест класс, который есть чисто ООП сущность, опять же, тест должен быть максимально понятный на случай чтобы другому человеку было удобно читать без углубления что такое например this >>= st_test;. То есть заместо so_5 MsgTypeTestTemplated
Но тут и много других обфускаций кроме названий.
На практике такой код, тем более со сложными темплейтами, это вырвиглаз, и как правильно написал человек выше требует написания теста к тесту, в частности чтоб потестить темплейты, который будет сложнее самого теста.
А провоцирование падения может вызвать ложноположительное исполнение без падения.
то есть, тьфу, наоборот, ложноотрицательное падение когда падает по другим причинам а те по тем что думаешь ты а визуально сообщение об ошибке будет такое же
Может давайте поговорим не об абстрактных «ложноотрицательных падения», а о более приземленных вещах. Вот, например, берем тест для so_unsubscribe_deadletter_handler из статьи. Пишем его нормальный вариант, запускаем. Получаем успешный результат теста.

Потом берем, и меняем
   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++?
UFO just landed and posted this here
Эти имена явно не образец для подражания для кода, который будут поддерживать.
Интересно, а какие имена вызывают наибольшие нарекания?
Докрутил до комментариев, чтобы посмотреть, написал ли кто о том, что эта стена кода абсолютно нечитаема, и не был разочарован.
UFO just landed and posted this here
Касательно имен и читаемости я бы отметил три аспекта.

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 просто не принимаются во внимание.
UFO just landed and posted this here
Я бы тут отметил, что упоминать в каждом классе «test_case» как-то выглядит излишним. Они же и так в тестах и не будут использоваться нигде вне них, зачем это повторять?
Помнится, изначально казалось, что в тесте потребуется больше классов, поэтому-то и появились суффиксы _mbox_case_t и _test_case_t. Но потом выяснилось, что больше ничего и не нужно, но избавиться от префиксов в голову уже не пришло.
Споры по поводу нотации сродни спорам о превосходстве той или иной национальной кухни: зачастую начинаешь использовать тот вариант, который принят в команде, компании, языке, а спустя много тысяч строк кода привыкаешь. Конечно, бывают случаи идеологической неприязни или особого чувства прекрасного, при котором тот или иной вариант кажется единственно верным, но в таком случае тяжело угодить всем.

Кроме того, в C++ есть прекрасная штука using, которая может сделать так, чтобы интерфейсные классы внешней библиотеки начали соответствовать нужному стилю написания.

А что касается тестов, то конечно хотелось бы, чтобы они были простыми и понятными, а главное рабочими, но зачастую остается хотеть, чтобы они просто БЫЛИ, а из списка быстро написанные, понятные, покрывающие большой процент кода, как в известном приколе-картинке, приходится выбирать 2 из 3.
Справедливости ради нужно сказать, что using не поможет с именами методов, к примеру.
Да и тестов, которые не работают, пользу нет. Так что если тесты есть, то они должны быть рабочими.

Другое дело, что споры о code convention начинаются тогда, когда о сути и смысле статьи сказать нечего. И уж тем более странно видеть споры о code convention для C++, в котором нет официального и общепринятого соглашения об именовании. И в котором давно и успешно сосуществуют snake_case, CamelCase и Camel_With_Underscore_Case.
поймите меня правильно, но как иначе тестировать шаблонный код? Например, для проверки корректности работы какого-нибудь std::vector нужно прогнать тесты для:
  1. T разных размеров
  2. Тривиальных/нетривиальных T
  3. T с доступностью разных конструкторов (copy_constructible, default_constructible, move_constructible)
  4. T с доступностью разных операторов присваивания (move_assignable/copy_assignable)
  5. T с различными политиками исключений (noexcept_copyable, noexcept_constructible, noexcept_move_assignable...)
  6. overaligned T
  7. собственно, различных тестовых сценариев

Итого бойлерплейтом получатся тысячи простых тестов. Или всё-таки лучше несколько сложных?

Есть ещё возможный подход: написать генератор простых тестов вместо собственно простых тестов. Оправданность зависит от того, насколько сложно отлаживать «сложные» тесты и насколько сложно создать собственно генератор, а также для чего генератор будет полезен. При правильное реализации генератор должен генерировать достаточно хорошо читаемый и хорошо отлаживаемый boilerplate.


На C ещё был бы вариант с использованием ffi для написания тестов на языке более высокого уровня, но ни макросы ни шаблоны C++ так не протестируешь (хотя, может, clang предоставляет какое‐нибудь полезное API…).

UFO just landed and posted this here

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

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

Или когда модулей, к которым можно применить генератор много. Для десяти случаев на один тестируемый модуль и копирования с вставкой хватит — я просто говорю, что можно и генератор рассмотреть.

Очень жаль, что автор наброса бесследно исчез не пожелав ответить на конкретные вопросы. Так же жаль, что не удалось установить, программирует ли он вообще на C++ или нет (судя по высказываниям о Boost-е, smer44 от C++ очень далек). Поэтому придется разбирать наброс просто в надежде, что smer44 или кто-то из заплюсовавших его комментарий найдет возможность поговорить о конкретике.

Итак:
тут не только тесты на тесты которые будут сложнее тестов на сорс
Что именно тут нуждается в тестах для тестов?
имя класса пишем с маленькой буквы, чтобы их не отличать
Это 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_)? Придумать объяснение я сам не могу.

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

С другой стороны, в своих разработках мы используем нотацию, которой уже очень много лет, и в которой мы используем для членов префикс «m_». Поэтому, если фрагмент кода был взят из реального теста или из реальной программы, то там вместо суффиксов-подчеркиваний будут префиксы «m_».
UFO just landed and posted this here
Единообразие и привычка. Плюс никто не знает, что будет в будущем. Сейчас нет методов, завтра появятся, меньше работы.
UFO just landed and posted this here
Хороший пример применения шаблонов в небиблиотечном коде, спасибо!
Вам спасибо!
можно добавить просто ещё один аргумент шаблона
Можно было бы. Но это явно бы сделало реализацию теста более сложной. Тем более, что нам нужно ограничиваться тем подмножеством C++11, которое реализовано в MSVC++12.0. Там фокус с static auto functor() не так-то просто провернуть.

А в описанной реализации небольшая копипаста дает простоту и независимость от ограничений старых C++ компиляторов.
UFO just landed and posted this here
Sign up to leave a comment.

Articles