Как Оценить Инструменты Тестирования Прошивки



Вступление от автора поста Имея опыт разработки ПО для критических систем более 8 лет, хотел бы познакомить сообщество с некоторыми материалами, связанными с разработкой и верификацией ПО для критических систем (аэрокосмическая промышленность, медицина, транспорт и промышленность).

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

Буду рад, если статья заинтересует наше сообщество.

В данной статье использованы материалы компании Vector Software, Inc. На ваши вопросы отвечу в комментариях или в личном сообщении.



Какой инструмент тестирования вы используете?

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

В Википедии перечислены 38 инструментов оценки тестовой среды только для языков программирования C/C++.

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

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



Вы не можете оценить инструмент тестирования, прочитав его спецификацию.

Все характеристики выглядят довольно похоже.

Ключевые слова те же: «лидер отрасли», «уникальная технология», «автоматизированное тестирование», «передовые методы».

Скриншоты похожи друг на друга: гистограммы, блок-схемы, HTML-отчеты и проценты.

Все это мне скучно.



Что такое тестирование программного обеспечения?

Любой, кто когда-либо тестировал программное обеспечение, знает, что оно состоит из множества компонентов.

Для простоты мы будем использовать три термина:

  • Системное тестирование – тестирование полностью интегрированного программного обеспечения.

  • Интеграционное тестирование – тестирование интегрированных групп программных модулей.

  • Модульное тестирование – тестирование отдельных модулей исходного кода приложения.

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

Обратите внимание, что речь идет о некоторых действиях, а не о «всех возможных».

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

Немногие люди занимаются интеграционным тестированием, и очень немногие занимаются модульным тестированием.

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

На самых требовательных уровнях тестирования объем кода, написанного для тестирования, обычно превышает объем тестируемого кода.

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



Что означает «автоматическое тестирование»?

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

Но что означает «автоматическое тестирование»? Разные люди понимают слово «автоматизация» по-своему.

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

К сожалению, такого инструмента не существует. А если бы он существовал, захотели бы вы его использовать? Думаю об этом.

Что бы это значило, если бы инструмент показал, что ваш код «в порядке»? Означает ли это, что код идеально отформатирован? Может быть.

Означает ли это, что код соответствует вашим стандартам кодирования? Может быть.

Означает ли это, что код правильный? Точно нет! Полностью автоматизированное тестирование недостижимо и в принципе нежелательно.

Автоматизация должна сосредоточиться на алгоритмической и трудоемкой части процесса тестирования.

Это освободит разработчиков программного обеспечения от постоянного создания все более продвинутых и сложных тестов.

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



Анализ инструментов тестирования

Инструменты тестирования в целом предоставляют разнообразные функциональные возможности.

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

  • Анализатор — Этот компонент позволяет инструменту тестирования понять ваш код. Он читает код и создает промежуточное представление кода (обычно в иерархической структуре).

    По сути, компилятор делает то же самое.

    Его выходные данные, или «данные синтаксического анализа», обычно хранятся в промежуточном языковом файле.

  • Генератор кода - Генератор кода использует «данные анализатора» для построения исходного кода тестовой программы.

  • Тестовая программа — Хотя тестовая программа, строго говоря, не является неотъемлемой частью средства тестирования, решения, принимаемые в процессе построения тестовой программы, влияют на остальные характеристики средства тестирования.

    Поэтому архитектура такой программы имеет большое значение при оценке инструмента тестирования.

  • Компилятор — Позволяет инструменту тестирования инициировать компиляцию и связывание компонентов тестовой программы.

  • Целевой компонент — Позволяет запускать тестовые сценарии в различных средах, включая поддержку эмуляторов, симуляторов, встроенных отладчиков и коммерческих операционных систем реального времени.

  • Редактор тестов - Редактор тестов позволяет пользователю использовать как язык сценариев, так и сложный графический интерфейс пользователя для установки предварительных условий и ожидаемых значений (критерии прохождения/неудачи) для тестового сценария.

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

  • Компонент отчетности — Позволяет объединить данные, собранные в ходе испытаний, в единую конструкторскую документацию.

  • Интерпретатор командной строки — Обеспечивает дополнительную автоматизацию при использовании инструмента тестирования за счет активации инструмента с помощью скриптов, утилиты make и т. д.
  • Компонент регрессии - Позволяет повторно запускать тесты, созданные для одной версии программного обеспечения, для новых версий.

  • Интеграционный компонент — Интеграция со сторонними инструментами может стать значимым фактором при решении инвестировать в инструменты тестирования.

    Обычно распространена интеграция с системами управления конфигурациями, инструментами управления требованиями и инструментами статического анализа.

В следующих разделах будет более подробно описано, как следует оценивать вышеуказанные компоненты в рассматриваемом инструменте тестирования.



Типы инструментов тестирования/уровни автоматизации

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

Оцениваемый инструмент можно отнести к одной из следующих групп:

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

    Обычно они предоставляют язык сценариев и/или набор библиотечных функций, которые можно использовать для общих процессов, таких как подтверждение тестов или создание табличных отчетов для тестовой документации.

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

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

  • Автоматизированный — Автоматизированные инструменты связаны с каждой функциональной областью или компонентом, перечисленными в предыдущем разделе.

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



Неявные различия в инструментах тестирования

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

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

В результате в инструменте тестирования могут быть скрытые недостатки.

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

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

Будьте осторожны, если поставщик говорит: «Мы поддерживаем все компиляторы и цели».

Это означает только одно: «Вам придется проделать всю работу, чтобы наш инструмент работал в вашей производственной среде».



Как оценить инструменты тестирования

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

В идеале следует подтвердить полученную информацию на практике, протестировав каждый из предложенных инструментов.

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

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

Кроме того, говоря об условностях, мы должны обращать внимание на терминологию.

Термин «функция» относится как к функции языка программирования C, так и к методу класса языка программирования C++, а термин «модуль» относится как к файлу языка программирования C, так и к классу языка программирования C++.

Наконец, помните, что почти все инструменты тестирования так или иначе поддерживают пункты, упомянутые в «ключевых моментах», ваша задача — оценить уровень автоматизации, простоту использования и степень поддержки.



Анализатор и генератор кода

Сравнительно легко построить анализатор для языка программирования C; Гораздо сложнее построить полноценный анализатор для языка программирования C++.

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

Некоторые поставщики испытательных приборов покупают и перепродают лицензированные технологии анализаторов, в то время как другие имеют свои собственные анализаторы.

Надежность анализатора и генератора кода можно проверить с помощью сложных конструкций кода — типовых образцов кода, которые вы будете использовать в своем проекте.



Тест-драйвер

Тест-драйвер — это основная программа, управляющая тестированием.

Вот простой пример драйвера, который будет тестировать математическую функцию синуса из стандартной библиотеки языка программирования C:

  
   

#include <math.h> #include <stdio.h> int main () { float local; local = sin (90.0); if (local == 1.0) printf ("My Test Passed!\n"); else printf ("My Test Failed!\n"); return 0; }

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

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

Интересно, заметили ли вы, что в этом драйвере есть ошибка? Это связано с тем, что функция синуса фактически использует в качестве входных данных угла радианы, а не градусы.



Использование заглушек для зависимых функций

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

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

Многие инструменты требуют, чтобы вы вручную создавали тестовый код, чтобы заглушка могла выполнять что-то более сложное, чем возврат статического скаляра (return0;).



Данные испытаний

Существует два основных подхода, используемых полуавтоматическими и автоматизированными инструментами при реализации тестового примера.

Первая — это архитектура, управляемая данными, вторая — архитектура модульного тестирования.

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

При выполнении теста инструмент передает данные в информационный поток, например дескриптор файла или физический интерфейс, например UART. Для архитектуры с одним тестом при каждом запуске теста инструмент создает тестовый драйвер и компилирует его в исполняемую программу.

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

Это означает, что оцениваемый прибор может нормально работать в определенных номинальных сценариях, но работать неправильно в более сложных тестах.



Автоматизированное создание тестовых данных

Различные автоматизированные инструменты обеспечивают определенную степень автоматизации создания тестовых сценариев.

Для этого используются разные подходы, некоторые из них показаны в таблице ниже:

  • (МММ) Сценарии тестирования мин-средне-макс.

    — МММ-тесты проверяют функции на граничные значения типов входных данных.

    Код на языках программирования C и C++ часто не защищает себя от входных данных, выходящих за пределы допустимого диапазона.

  • (EC) Классы эквивалентности — EC-тесты заключаются в следующем: данные разбиваются на классы эквивалентности по принципу, что программа ведет себя одинаково с каждым представителем отдельного класса.

    Таким способом проверяются не все возможные данные, а отдельные представители класса.

  • (RV) Случайные величины — Тесты RV определяют комбинации случайных значений для каждого параметра функции.

  • (BP) Тесты на ветвях условных операторов — Тесты BP могут автоматически создавать покрытие высокого уровня на основе ветвей условных операторов.

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

Чтобы быть правильным, вы должны создавать тесты, основываясь на том, что приложение должно делать (требования), а не на том, что оно делает (код).



Интеграция компилятора

Интеграция компилятора имеет два значения.

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

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

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

Некоторые инструменты обнаруживают такие расширения как пустую строку.

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

Например, рассмотрим следующую глобальную внешнюю переменную с атрибутом GCC:

extern int MyGlobal __attribute__ ((aligned (16)));

Если рассматриваемый инструмент не поддерживает атрибут при определении глобального объекта MyGlobal, то код будет вести себя по-разному при тестировании и в рабочей среде, поскольку выравнивание не будет одинаковым.



Поддержка тестирования на целевой платформе

В этом разделе мы будем использовать термин «набор инструментов» для обозначения среды разработки, включая кросс-компилятор, интерфейс отладки (эмулятор), целевую платформу и операционную систему реального времени (RTOS).

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

Также важно узнать уровень автоматизации и надежность интеграции с целевой платформой.

Как упоминалось ранее, если поставщик говорит: «Мы поддерживаем все компиляторы и целевые платформы», это означает только одно: «Вам придется проделать всю работу, чтобы наш инструмент работал в вашей производственной среде».

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

Дополнительной проблемой тестирования на целевой платформе является проблема доступности оборудования.

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

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

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



Редактор тестовых сценариев

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

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

Помните, что мы говорили вначале: наша цель — дать инженерам возможность использовать свое время для создания более качественных и полных тестов.

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

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

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



Покрытие кода

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

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

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

Хотя таблица является хорошим обобщением информации, если вы пытаетесь добиться 100% покрытия кода, лучшим способом будет листинг с комментариями.

В этом листинге показан файл исходного кода с раскраской покрытых, частично покрытых и непокрытых структур.

Это позволяет легко увидеть, какие дополнительные тестовые примеры необходимы для достижения 100% покрытия.

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

Важно понимать, имеет ли ваше программное обеспечение ограничения по памяти или времени выполнения (или и то, и другое).

Это поможет вам сосредоточиться на том, что наиболее важно для вашего программного обеспечения.



Регрессионное тестирование

Выбирая инструмент тестирования, мы должны помнить о двух основных целях.

Первая цель — сэкономить время тестирования.

Если вы дочитали до этого места, то вы с нами согласны.

Вторая цель — обеспечить эффективное использование созданных тестов на протяжении всего жизненного цикла программного обеспечения.

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

Главное — оценить в интересующем вас инструменте, какие отдельные сущности следует сохранить для прохождения тех же тестов в будущем и как контролируется повторный запуск.



Составление отчетов

Большинство инструментов предоставляют функции отчетности аналогичным образом.

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



Интеграция с другими инструментами

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

Крупные компании потратили огромное количество времени и денег на покупку мелких компаний, которые разработали инструмент, который делает «все для всех».

Эти меганаборы инструментов характеризуются тем, что «общая сумма меньше суммы частей».

Компании часто берут 4-5 небольших, но полезных инструментов и объединяют их в один громоздкий и бесполезный инструмент. Помимо уже рассмотренных интеграций с набором инструментов, наиболее полезными интеграциями для инструментов тестирования являются интеграции со статическим анализом, инструментами управления конфигурацией и управления требованиями.

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



Дополнительные желательные характеристики инструмента тестирования

Мы завершили обзор раздела «Анализ инструментов тестирования».

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

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

Эти характеристики могут иметь различную степень применимости к вашему проекту.



Настоящее интеграционное тестирование/многоблочное тестирование

Интеграционное тестирование — это расширение модульного тестирования.

Он используется для тестирования интерфейсов между модулями и требует объединения модулей, выполняющих определенный функциональный процесс.

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

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

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

Тестирование интерфейсов между модулями в целом выявит множество скрытых допущений и ошибок в программном обеспечении.

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



Использование динамических заглушек

Создание динамических заглушек означает, что вы можете динамически включать или отключать собственные функции заглушек.

Это позволяет вам создать тест для одной функции, отключив все остальные функции (даже если они существуют в том же модуле, что и тестируемая функция).

Для очень сложного кода это критически важная функция, значительно упрощающая тестирование.



Тестирование на уровне библиотеки и приложения

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

Если приложение встроено, контролировать входные данные будет еще сложнее.

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

Это позволит вам построить набор тестовых сценариев, основанных только на программном API. Некоторые из более современных инструментов позволяют проводить тестирование таким образом.

Дополнительным преимуществом этого режима тестирования является отсутствие необходимости в исходном коде для тестирования приложения.

Вам просто нужно определение API (обычно файлы заголовков).

Эта методология дает тестировщику автоматизированный и сценарийный способ выполнения тестирования системы.



Гибкое тестирование и разработка через тестирование (TDD)

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

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



Двунаправленная интеграция с инструментами управления требованиями

Если вас беспокоит связывание требований с тестовыми примерами, то желательна интеграция инструмента тестирования с инструментом управления требованиями.

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

Это позволяет получить ощущение завершения тестирования требований.



Квалификация инструмента

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

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

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



Заключение

Помните, что почти каждый инструмент тестирования прошивки так или иначе поддерживает пункты, упомянутые в ключевых пунктах.

Важно понять, насколько она автоматизирована, насколько проста в использовании и оценить полезность поддержки.

Теги: #проверка #тестирование #разработка #переводы #Разработка сайтов #Тестирование ИТ-систем

Вместе с данным постом часто просматривают: