КДПВ отсюдова https://telegra.ph/4-sposoba-preodolet-trudnosti-04-18)

Некоторое время назад на одном из проектов по работе я столкнулся с определенными требованиями, которые сразу же разожгли мой живой интерес разобраться с возможностями их обхода, попутно применив свой прошлый опыт написания фреймворков, интерпретаторов и т.п. В результат получилась вполне рабочая технология, позволяющая обходить с моей точки зрения лишние требования в проекте. За конкретикой прошу под кат.

В чем же суть?

В тестах :) А если быть точнее - в системе автоматического расчета процента покрытия ими кодовой базы.

Если позволить себе холиварное и провокационное высказывание, то я выскажу тезис о том, что тесты не нужны. Точнее, от них вреда больше, чем пользы. Разумеется, их можно писать так, чтобы сместить баланс польза/вред в сторону пользы, но по моему опыту многих проектов, это почти никому и никогда не удается. Но при этом общее убеждение таково, что тесты нужны и важны, поэтому они присутствуют почти в каждом проекте. Впрочем, цель и тема этой публикации не в том, чтобы очередной раз обсудить этот вопрос с философской точки зрения, поэтому продолжим.

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

Но среди разработчиков попадаются люди творческие, или как минимум любознательные. Которые могут потратить время и разобраться, как работают системы расчета покрытия. И выяснится, что системы расчета кавераджа обладают следующим поведением:

  • они не делают проверок результатов выполнения кода и даже факта того, что в процессе выполнения не будет выброшено исключение - то есть они не заменяют собой тесты;

  • зато они проверяют, по каким веткам условных операторов шло выполнение кода, и помечают как непокрытые те участки, выполнения которых не происходило в процессе прогона всех тестов.

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

Что же предлагается для избавления от страданий? Вспомним, как работает система расчета кавераджа. Она проверяет, что были вызваны все ветки всех условных операторов, какие есть в вашем языке (if, switch и т.п.) Рассмотрим тривиальный и немного искусственный пример: нам нужна функция, которая при аргументе 1 будет возвращать 10, при аргументе 2 возвращать 20, а при всех остальных аргументах возвращать 30. Как бы вы написали эту функцию? Я сознательно не конкретизирую язык программирования, поскольку в большинстве языков есть конструкции для условных переходов и условного выполнения. Допустим, вы написали что-то типа

if (1 == n) // вроде так советуют писать с ==, чтобы не присвоить ненароком :)
  r = 10;
else if (2 == n)
  r = 20;
else
  r = 30;

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

bool t = n >= 1 && n <= 2;
int a[] = {30, 10, 20};
int r = a[n * t];

то логика работы не изменится, но система проверки покрытия удовлетворится едиственным вызовом этого кода с любым значением n. Или заполните хешмапку нужными ключами и значениями, а потом вызовите на ней метод взятия значения по ключу (с возможным дефолтным значением, если такого ключа нет). Или что-нибудь еще, что вам предоставляет ваш язык программирования. Суть в том, чтобы не писать в коде условные операторы. То есть вообще. Если очень хочется, можно написать свою сервисную функцию - тернарный оператор, один единственный юнит-тест для нее, и использовать потом в своем коде.

Это уже серьезное заклинание, с помощью которого можно легко победить систему кавераджа на 100%, при этом не мучаясь с тестами, фикстурами, миграциями и прочей нудной ерундой. Однако, внимательный читатель уже заподозрил подвох! Семантика вычисления условных выражений в подавляющем большинстве языков ленивая! То есть, если условие ложно, то блок кода, соответствующий этому условию, не будет выполняться. Именно это и позволяет нам писать рекурсивные функции, которые не зацикливаются, и вообще выполнять инструкции или вычислять выражения только когда это необходимо. А во всех наших вышеперечисленных вариантах вычисления всегда выполняются по всем веткам!

Но это ограничение преодолимо. Как я уже говорил, приведенная методика проверена на коде из рабочего проекта и отлично работает. Но здесь подходы могут варьироваться в зависимости от выбранного языка программирования и его возможностей. Например, вам надо выполнить определенный блок кода только при определенном условии. Вы создаете сервисную функцию do_when от двух аргументов - значение условия и лямбда-функции, которая вызывается если первый аргумент истина. А при написании кода оборачиваете нужный вам блок кода в нульарную лямбду - древний народный метод организовать ленивое вычисление. Код внутри лямбды не будет выполняться до ее вызова. Лямбда как объект первого класса прекрасно передается в качестве аргумента в сервисную функцию do_when, внутри которой она либо вызовется, либо нет. Результат - один единственный юнит-тест на функцию do_when обеспечит нам 100% каверадж кода всего проекта, в котором мы будем использовать ее многократно.

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

Вот, собственно, и всё, что я хотел рассказать о войне во Вьетнаме (С). Я постарался сделать это кратко, без подробностей, обозначив только основную идею. Чуть больше деталей я показал в своем ролике на ютубе на эту тему

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

Комментарии (35)


  1. libroten
    19.05.2023 01:09
    +16

    Это просто саботаж


    1. santjagocorkez
      19.05.2023 01:09

      В ситуациях, когда приходится выжимать максимум производительности, такие "хаки" могут стать единственным выходом, поскольку не всегда компилятор сможет схлопнуть хорошо пачку if.

      К примеру, код выше на https://perfbench.com/:

      С условными переходами:

      ID             Calls        Min        Avg        Max      StDev
      map total        100      0.015      0.018      0.022      0.001 

      С магией (только я определение массива вынес в `static const`):

      ID             Calls        Min        Avg        Max      StDev
      map total        100      0.015      0.017      0.019      0.000 

      Казалось бы, немного, Но, тем не менее, разница есть.


      1. IIvana Автор
        19.05.2023 01:09
        +1

        В ролике на ютубе приводил пример. Сравните

        if 215 == n
          n = 137;
        else
          n = 215;
        
        n = (137 + 215) - n; // вылезаем за 1 байт
        
        n ^= 137 ^ 215; // остаемся в рамках одного байта
        

        PS и это не считая лишнего напряжения предиктора переходов по меткам, в панике мечущегося - какой код подгружать


        1. isadora-6th
          19.05.2023 01:09
          +3

          Без замеров оратора

          Вообще, что это у вас за язык такой магический, на котором byte вычисления быстрее чем int? Тем более что 137 или 215 - это литерал int для C/C++.

          Тут я хотел найти статью где sturct RGBA[] быстрей чем sturct RGB[] при итерировании c подсчетом средней яркости из-за выравниваний и необходимости сдвигов. Но не нашел...

          Спасибо за сайт perfbench, очень полезный тул в копилочку.

          Ну и предикторы в процессорах хорошо работают.

          Branchless Programming in C++ - Fedor Pikus - CppCon 2021

          Так что перф... это сильно сложней чем влазим в байт.


          1. IIvana Автор
            19.05.2023 01:09
            +2

            Этот магический язык называется C. На архитектурах с однобайтовыми регистрами, например многих МК. И в оригинале кот писался на ассемблере, так что весь перф колхозился руками, без расчета на умных разработчиков оптимизирующих компиляторов. Впорчем, это оффтоп к теме данной публикации.


        1. nice_nick_matter
          19.05.2023 01:09
          +1

          Но ваш код совсем другой. Оригинальный, с if, для любого n заменит значение переменной на 215. Единственное исключение -- если n == 215, тогда результатом будет 137. А "исправленный" код этим свойством не обладает, в чем нетрудно убедиться (мы говорим про байты, поэтому перебрать 256 значений можно даже руками).

          И это как раз тот случай, когда тесты помогают -- при рефакторинге кода они сразу скажут, что что-то стало не так.


    1. Jianke
      19.05.2023 01:09
      +4

      Это просто саботаж

      Не согласен! Это реально круто! Всегда восхищал такой код:


  1. maxzh83
    19.05.2023 01:09
    +5

    Хочется вам пожелать побольше отлаживать чужого кода, написанного в таком стиле. А как прикол такое вполне имеет право на существование.


  1. Radisto
    19.05.2023 01:09
    +4

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


  1. AlexSteelax
    19.05.2023 01:09
    +12

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

    Но писать тесты надо на то, что надо, а на то что не надо - писать их не надо)


  1. panzerfaust
    19.05.2023 01:09
    +11

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


  1. XTBZ
    19.05.2023 01:09
    +4

    Хаха, тут надо код переработать под новые требования. У нас сотрудник хорошо писал, полное покрытие тестов, никогда не было нареканий, в общем, разберешься....


  1. IlyaFD81
    19.05.2023 01:09
    +2

    Если нормальный техлид увидет это, можно и нарваться за саботаж.

    И потом : как все эти выкрутасы в коде объяснить чужим людям при код. ревью ?


    1. santjagocorkez
      19.05.2023 01:09
      -1

      А говорят, в геймдеве такое сплошь и рядом. Значит, там техлидов не завозят нормальных?


  1. khajiit
    19.05.2023 01:09
    +20

    Введение любых метрик приводит к работе на выполнение метрик, вместо работы.


  1. VladimirFarshatov
    19.05.2023 01:09
    +5

    Ну .. табличные процессоры всегда были самым шустрым решением, да и чем меньше нагружаешь код ветвлениями, тем проще работать конвееру проца и его кешатору. Имеет место быть, почему нет?


    1. IIvana Автор
      19.05.2023 01:09
      +5

      Глас вопиющего в пустыне: приготовьте путь Господу, прямыми сделайте стези Ему (Мф 3:3)


      1. VladimirFarshatov
        19.05.2023 01:09

        На самом деле заставить такого героя покрыть тестами эту фичу, тоже не проблема ни разу на коде ревью: видим статический массив .. требуем фикстуру на каждое значение в тестах. Бинго!


  1. bogolt
    19.05.2023 01:09
    +11

    А если зайти с другой стороны, перед запуском теста кавереджа сгенерировать пару терабайт бессмысленных нопов, и подсунуть их как часть кодовой базы. Покрыть их один тестом, вызывающим его, и все, у нас 99% покрытия без того чтобы бороться с ифами.


    1. amakhrov
      19.05.2023 01:09

      Часто меряют покрытие не только строк кода, то и ветвлений.

      100500 ноопов без ветвлений не помогут. А 100500 нооп-ветвлений еще попробуй-ка протестируй.

      Хотя и тут можно выкрутиться, если автоматом (в цикле) сгенерить тесты всех этих нооп-веток


  1. igrek11
    19.05.2023 01:09
    +3

    Как и многие, я заметил ухудшение читаемости кода. Получается, что Вы экономите на написании тестов, но повышаете время на поддержку кода. Это уже звучит пугающе.

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

    Как видите, в любом случае мы чем-то жертвуем


    1. 314159abc
      19.05.2023 01:09
      +5

      ChatGPT, перелогиньтесь


    1. VladimirFarshatov
      19.05.2023 01:09

      Не заметил ухудшения читаемости кода. В чем по вашему оно проявилось? Компактнее , часто "да". Меньше переключений внимания, наоборот лучше.


  1. XaBoK
    19.05.2023 01:09
    +4

    Ну это известный способ уменьшить ветвление кода с помощью карты (reduce branching with decision map). Улучшает метрики (complexity/maintenability) и производительность, снижает читаемость. Я так в C# писал, но без загонов со словарём из делегатов. Из дополнительных плюшек - такой код хорошо читают системы аудита кода. Как и SCA так и SAST /DAST, так что прям вот все R#, SonarCube, Chekmarx и т.д.


  1. arTk_ev
    19.05.2023 01:09
    +4

    Веселая компания, саботаж и полное непонимание что такое тесты.

    Сталкивался с саботажем, когда "коллеги" обмазывали код пустым try-catch, что починить ошибку. Что-то из этой серии.


  1. aleksandy
    19.05.2023 01:09
    +2

    Но система кавераджа заставит вас прогнать эту функцию для всех трех веток условий.

    То ли лыжи не едут, то ли я не особо умный. Но ведь все эти 3 ситуации в любом случае надо протестировать, чтобы проверить соответствие реализации заданию.

    нужна функция, которая при аргументе 1 будет возвращать 10, при аргументе 2 возвращать 20, а при всех остальных аргументах возвращать 30.

    Далее,

    для юнит-тестов это еще можно тупо накопипастить, создав комбинаторно растущую лапшу кода тестовых кейсов

    Модульный тест на то и модульный, чтобы быть максимально простым, а если в нём получается "комбинаторнорастущая лапша", то это просто означает, что пишется такой код, который не только сложно тестировать, но и читать/поддерживать.

    для интеграционных тестов нам придется долго и нудно ...

    При достаточном количестве качестве модульных тестов, интеграционных может быть совсем чуть-чуть, только для того, чтобы проверить happy-case.


  1. Perlovich
    19.05.2023 01:09

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

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


    1. IIvana Автор
      19.05.2023 01:09
      -1

      Вопрос зачем призван выявить целеполагание, а я не настолько манипулятор, чтобы вставлять подобные фразы ради достижения какой-то цели или эффекта. Скорее, тут более уместен вопрос почему, выявляющий причины. Я просто честный и открытый человек, и считаю недостойным скрывать мое отношение к определенной части данного ресурса. Я здесь достаточно давно, чтобы видеть динамику "нетортовости" и иметь по этому поводу определенное мнение.
      ЗЫ один из нескольких минусов в карму за эту статью я получил с пометкой "Статья/тема не для Хабра". Так вот когда (если) Хабр снова станет торт, подобные темы снова станут онтопом, ресурс снова станет профессиональным а не школьным, тогда я с радостью поменяю свое мнение и отношение.


      1. panzerfaust
        19.05.2023 01:09
        +2

        ресурс снова станет профессиональным а не школьным

        Вы себя тоже тут профессионалом не показываете.

        Профессионально - это собрать своих коллег и ЛПРов и предметно доказать им, что ваша точка зрения ("тесты не нужны", "покрытие ну нужно") верна. Далее в вашей организации дружно отменили бы все "лишние" проверки, тесты и код-ревью заодно. Потом вы бы собрали метрики, которые показывают, что без тестов сложное ПО пишется быстрее и содержит меньше ошибок. Потом с этой фактурой вы пришли бы на Хабр и доказали бы уже всем, что ваша точка зрения верна.

        А ничего этого не происходит, и вы просто выражаете маргинальную точку зрения без пруфов. А "школьный" при этом хабр, да.


        1. IIvana Автор
          19.05.2023 01:09

          Вы себя тоже тут профессионалом не показываете.

          Возможно, да. А возможно, что это вы не видите :) Или вы действительно считаете, что я запушил в мастер код по вышеприведенным принципам?
          Если бы я написал статью в стиле, который вы описали выше, вероятно это бы придало больший вес моему виртуальному образу в глазах вас и ваших единомышленников. И да, проблема не техническая а административная, и решать ее надо соответственно... И самое смешное, что я могу делать так, как вы описали. Но это скучно :) Успешно решить задачу обхода кавераджа технически - гораздо интереснее!
          В плане же взаимоотношений с социумом, меня больше привлекает реакция типа "о, это тот самый чувак, который нагнул систему кавераджа, предложив рабочий метод ее обхода!" и "господа, давайте не будем усердствовать с лимитом процента покрытия для прохождения деплоя, а то на Хабре есть статья как это дело прохачивать". Я понимаю, что есть большой соблазн навесить на меня ярлык маргинала, и многие ему поддаются :) Но я не готов лишать себя маленьких радостей решения задач и совершения открытий ради поддержания образа "серьезного человека".


          1. panzerfaust
            19.05.2023 01:09

            И самое смешное, что я могу делать так, как вы описали. Но это скучно :)
            Успешно решить задачу обхода кавераджа технически - гораздо интереснее!

            "господа, давайте не будем усердствовать с лимитом процента покрытия для прохождения деплоя, а то на Хабре есть статья как это дело прохачивать"

            Пруфы, Билли, нам нужны пруфы. Без пруфов вы никакой не "тот чувак", а именно что маргинал, который хотел пошатать систему, а пошатал полторы строчки кода и родил из этого статью.


            1. IIvana Автор
              19.05.2023 01:09

              В видео показано больше примеров и подробнее. Не полторы строчки кода, а апи-хендлер из реального проекта. Если даже после этого для вас все еще не убедительно, то сорри, математически строгого доказательства не искал, мне хватило моих примеров.


  1. sorgpro
    19.05.2023 01:09
    +1

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

    Итак, имеем псевдокод А:

    if (1 == n) // вроде так советуют писать с ==, чтобы не присвоить ненароком :)
      r = 10;
    else if (2 == n)
      r = 20;
    else
      r = 30;
    

    и псевдокод B:

    bool t = n >= 1 && n <= 2;
    int a[] = {30, 10, 20};
    int r = a[n * t];
    
    • С точки зрения code review псевдокод A выглядит сносно, а вот для псевдокода B потребуется, как минимум, переименование переменной t, чтобы объяснить её дальнейшее участие в алгоритме.

    • Читабельность кода пострадала. В более сложных случаях, хотя даже и в этом, было бы разумно потребовать написать комментарий, объясняющий неочевидность происходящего при беглом просмотре кода, в отличии от псевдокода A. Более очевидным выбором было бы использование, например, словаря ключ-значение, вместо массива, но здесь не так, и поэтому следует указать, что эта конструкция - альтернатива if/switch.

    • Использование "хитрого" алгоритма привносит дополнительные трудности:

      • при возникновении еще одного условия, например, при n == 3 нужно возвращать 25, легко добавить правку в код, чтобы все отлично заработало, но также легко забыть добавить тест для этого значения. При этом инструмент проверки покрытия кода тестами для псевдокода B нам ничем не поможет, в то время как для псевдокода A он обязательно бы отметил этот момент, изменив процент покрытия, поскольку добавленная ветка else if (3 == n) не выполняется.

      • требования меняются, нужно добавить в существующую программу какой-то специфичный случай, и придется менять этот неочевидный алгоритм. Например, если для текущего псевдокода при n == 0 или n == 8 нужно возвращать какое-либо значение, то алгоритм псевдокода B a[n * t] перестанет работать и придется придумывать еще более изощренный и менее читабельный вариант.

    • Язык программирования не конкретизировался, поэтому я и назвал это пседкокодом, а значит у кого-то может возникнуть идея реализовать этот подход на других языках. Но в других языках придется вносить правки, поскольку компилироваться/работать этот код не будет. Это чревато последствиями, которые в случае псевдокода A просто бы не возникли:

      • для компиляции объявления массива, например в C#, интуитивно напрашивается ключевое слово new и вуаля - строка скомпилировалось! Только вот теперь объявление приведет к выделению объекта в куче, а это влечет за собой снижение производительности, а также добавление работы сборщику мусора, что дополнительно снизит производительность.

      • для того, чтобы как в оригинале на C++, код использовал только стек, нужно писать иначе, но не каждый начинающий разработчик догадается/умеет, а ведь именно такие разработчики могут последовать советам из этой статьи. Но! Даже если использовать инициализацию массива на стеке мы потеряем в производительности по сравнению с псевдокодом A.

      • чтобы производительность все же приросла, массив можно объявить статическим, как это сделал автор одного из комментариев выше, в котором он приводит замеры производительности. Но это значит, что мы выделили объект в куче на все время работы программы - мы повысили требование к памяти (размеру кучи), которое при массовом использовании этого приема может составить существенное значение, а в случае псевдокода A такое явление не возникает.

    Так чего же добились, применяя псевдокод B?
    Усложнили работу разработчикам, которые будут поддерживать и развивать программу?
    Обманули инструмент, контролирующий покрытие кода тестами?
    Себя? Тестировщиков? Работодателя? Качество продукта?

    Если позволить себе холиварное и провокационное высказывание, то я выскажу тезис о том, что тесты не нужны.
    Точнее, от них вреда больше, чем пользы.

    Это троллинг, но, как я и указал в начале комментария, статья может принести много пользы, как пример-антипаттерн.


    1. DBalashov
      19.05.2023 01:09

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

      @sorgproнаписал максимально правильно. В моменте (натянув разные ограничения) оно и может быть прокатит (опять же с оговорками), но как часть процесса разработки - полная лажа.


      1. IIvana Автор
        19.05.2023 01:09

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