Русский
Русский
English
Статистика
Реклама

Тесты

SEO npm-пакета почему важно правильно настраивать конфиг и писать тесты

07.09.2020 02:17:11 | Автор: admin

Не так давно я опубликовал статью о своем CLI для React-компонент, который для меня стал первым публичным npm-пакетом. И так как мне хотелось поделиться своими наработками с как можно большим кругом разработчиков я начал изучать разные способы улучшения своей позиции в поисковой выдаче на разных специализированных сайтах. В попытках улучшить свое положение я опирался на поиск в npm, yarn и npms. И если вы сейчас откроете страничку моего пакета в любом из этих трех сайтов, то результаты там будут, к сожалению, достаточно скромными и я попробую объяснить почему и порассуждаю на эту тему.

Popularity, quality, maintainance

Если мы сделаем поиск в npm по любому запросу, то напротив каждого из пакетов будет указан набор из трех характеристик: popularity, quality и maintainance. Они в каком-то роде напоминают значения по пирам и сидам на каком-нибудь торрент-трекере и в достаточной мере влияют на выдачу и на последующий выбор людей.

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

Так как у нас пакет новый то popularity у нас само собой будет на нуле, поэтому будем смотреть на остальные два рейтинга. Quality мы оставим на десерт, а сначала поговорим о maintainance. И тут все достаточно просто, он отображает состояние пакета с точки зрения разработки. Либо его активно разрабатывают и поддерживают, либо на него забили и не занимаются. Тут учитывается то, как часто происходят коммиты, релизы, насколько много issue и как быстро они закрываются. В целом вроде все просто, но если в npms у меня этот показатель заполнен на 100%, потому что я активно дорабатываю пакет и прикручиваю новые фишки почти каждую неделю, то в npm у меня этот рейтинг только 33%. И как бы я не старался, выше он не поднимается. Более того, если взять любой другой популярный генератор кода, то окажется что все эти пакеты имеют данный рейтинг тоже равный 33%, что выглядит немного подозрительным. Даже у самого React этот рейтинг такой же.

Popularity у нас на нуле, maintainance на максимум, а что с quality? А тут все гораздо интереснее. Изначально я писал свою CLI на чистом js и без тестов. Но к тому моменту, когда я решил вытащить её в публичное поле я уже переписал её на Typescript и более или менее причесал, но все еще тестов не было. И я точно не помню сколько у меня был этот показатель качества, но думаю был примерно в районе 20%. Сейчас же я его разогнал до 61% и могу немного рассказать, как этого можно добиться. Причем я пытался применять сразу несколько практик одновременно, поэтому я до конца не уверен все ли они влияют, но даже если нет, то в целом это стоит того чтобы добавить в свой проект, если вы тоже решитесь опубликовать пакет.

У меня получился следующий список настроек:

  1. В проекте должен быть настроен линтер. Я лично использую ESLint, возможно с TSLint пакетные менеджеры тоже нормально работают.

  2. Стоит написать хороший readme.mdи changelog.md и поддерживать их в актуальном состоянии

  3. Стоит добавить файл лицензии

  4. Не лишним будет добавить файл .editorconfig Не факт что оно влияет на рейтинг, но пригодиться если кто-то решит вам помочь с вашим пакетом. Хотя и линтер тоже с этим поможет.

  5. Также я подключил свой проект на github к Travis и создал для него конфиг с помощью файла .travis.yml. Когда я его подключал я преследовал исключительно цель попробовать подняться в рейтинге, однако это оказалось достаточно неплохим инструментом тестирования. Более того, в моем CLI очень важно чтобы все корректно работало как на Linux так и на Windows и для меня оказалось приятной неожиданностью, когда Travis прогнал мои тесты у себя на Linux и я поймал баг, о котором совсем не знал, потому что разрабатываю под виндой.

  6. Не уверен что это влияет на рейтинг, но достаточно важно корректно заполнить файл package.json. Указать в нем все скрипты, главный файл, описание и теги.

Ну и конечно помимо банальных настроек проекта нужно писать тесты. И чтобы рейтинг поднялся высоко, покрытие должно быть очень высоким. Как я уже сказал выше, итоговый рейтинг в npm у меня вышел равным 61%, при этом покрытие тестами у меня всего лишь 49%, ну и есть все эти настройки, указанные выше. На nmps все получше, там у меня 96%. Само собой, в первую очередь, я покрыл критическую функциональность пакета, поскольку с каждой новой фичей тестировать все кейсы становится все сложнее и сложнее, хотя в некоторых случаях я откровенно читерил и покрывал тестами файлы, в которых максимально сложно совершить ошибку, но зато тесты пишутся легко и покрытие растет очень дешево.

Цифры или PR

Ну хорошо, настроили мы проект, пушим в него часто и фиксим оперативно все баги, покрываем тестами, но что-то никто не приходит скачивать наш пакет. Открываем поиск того же npm, вбиваем релевантный запрос для нашего пакета и начинаем листать страницы: 1, 2, 3,... 21. Находим наш пакет на какой-нибудь очень далекой странице и как нам понять что пошло не так?

Начну, пожалуй, с одной забавной истории про одно поле. Когда я всеми силами пытался вытащить свой пакет в первые строчки yarn, я настраивал проект, писал тесты, выбирал теги получше и улучшал readme. Писал посты на reddit и пиарил пакет среди коллег. Из кожи вон лез, но пакет качали очень слабо, где-то скачиваний 20-30 в день было. И хоть даже это меня радовало, хотелось узнать, как на это повлиять и я начал смотреть что есть в пакетах конкурентов. Многие пакеты в поиске по релевантному для меня запросу были вообще не подходящими по сути и делали совершенно не то, что я как бы пытаюсь искать, но тем не менее были выше и я пытался выяснить почему. Первое что достаточно сомнительно выстреливает, когда мы говорим о поиске - это то что мелкие, крайне бесполезные пакеты, в которых 150 строк кода, покрытых тестами, вылезают в топ за счет невероятно высокого покрытия. Часто бывало так что у меня только index файл был длиннее чем пакеты, которые обходили меня в рейтинге, хотя при этом и популярными они тоже не были, потому что такой пакет может написать каждый за парочку часов. И вот я натыкаюсь на очередной пакет, который крайне маленький, бесполезный, но почему-то находящийся выше в yarn. Стало очень любопытно и я начал проглядывать каждый файл репозитория и сравнивать настройки со своими. И тут я вижу что единственное отличие моей репки, от репки конкурента - это поле description в package.json. Ну, я подумал, что вряд ли это мне как-то поможет, но почему бы его не добавить, а то я как-то про него забыл. В общем добавляю поле и на следующий день бац и +200 скачиваний, потом еще больше и еще. Более того, по одному из запросов в yarn я находился на 23 станице до которой вряд ли бы кто-то дошел, пытаясь найти нужный пакет, но, указав это поле, пакет оказался буквально на первой строчке поисковика.

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

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

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

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

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

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

Подробнее..

HowToCode Адаптация системного подхода к разработке для React и TypeScript

07.03.2021 00:21:34 | Автор: admin

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

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

Кардинальным образом ситуация изменилась после того, как я прошел курс HowToCode[ссылка удалена модератором, т.к. нарушает правила]. В курсе описан системный и, как всё гениальное, простой и красивый подход к разработке, который сводит воедино анализ, проектирование, документацию, тестирование и разработку кода. Весь курс построен на использовании функциональной парадигмы и языка Scheme (диалекта Lisp), тем не менее, рекомендации вполне применимы и для других языков, а для JavaScript и TypeScript, к которым я постарался их адаптировать, так и вообще подходят отлично.

Результаты мне очень понравились:

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

  • Во-вторых, заработал подход TDD, к которому я делал несколько подходов, но никак не мог начать ему следовать

  • В-третьих, я практически избавился от отладки: того самого процесса, когда код уже написан, но ещё не работает или работает не так как надо, чем дико раздражает

  • Ну и ещё одно очень приятное наблюдение: я не боюсь что-то забыть или пропустить, потому что в каждый момент времени я работаю над одной конкретной небольшой задачей, а все остальные задачки - записаны

  • Да, и, предвидя вопросы, скажу - по ощущениям время разработки если и замедлилось, то не сильно, даже с учётом написания тестов и документации. А экономия на поддержке кода - просто колоссальная.

Но давайте уже перейдём к сути и посмотрим, как этот подход устроен.

Общая идея

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

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

Этапы проектирования

Подход включает 3 этапа:

  1. Анализ задачи и предметной области

  2. Проектирование структур данных

  3. Проектирование функций

Анализ задачи и предметной области

Анализ задачи необходим для того, чтобы понять, что мы, собственно, будем делать.

Входными данными для этого этапа является постановка задачи. Она может выглядеть как угодно: устная формулировка, пара предложений в тикете Jira или полноценное ТЗ на десятки страниц. На самом деле это не так уж и важно.

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

Процедура анализа задачи

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

Алгоритм

В общем виде алгоритм выглядит так:

Шаг 1. Представить задачу в динамике

Шаг 2. Выделить константы, переменные и события.

Шаг 1. Представить задачу в динамике

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

  • в исходном состоянии

  • в различных промежуточных состояниях: возможно, появляется загрузка, или раскрываются какие-то списки или открываются какие-то окна

  • при окончании работы

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

Пример

Допустим, первоначальная постановка задачи звучала как: "Реализовать для web-портала раскрывающийся список, который бы показывал перечень сотрудников, входящих в учебную группу. Для каждого сотрудника должно быть указано ФИО и дата его рождения."

Звучит неплохо, но что делать, пока ещё непонятно. Берем карандаш, бумагу и, если есть возможность, самого заказчика и начинаем рисовать (Ну а я буду рисовать в редакторе).

Начальное состояни

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

Промежуточные состояния

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

Когда список открывается, мы должны показать список сотрудников, входящих в группу, и изменить состояние индикатора. Скорее всего, выглядеть наше приложение станет как-то так:

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

Шаг 2. Выделить константы, переменные и события

Теперь давайте пойдём дальше: выделим 3 группы параметров, которые лежат в основе описания приложения на языке компьютера: константы, переменные и события.

Константы

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

  • оформление: цвета, шрифты, расстояния, логотипы и иконки, размеры сетки и экрана и т.д.

  • сетевые данные, например, адреса серверов,

  • строковые константы: названия, заголовки и т.п.

Пример выделения констант

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

  • Адрес сервера, с которым работает наше приложение

  • Название приложения, которое отображается в виде заголовка

  • Оформление

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

Переменные

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

  • координаты объектов

  • значения таймера

  • различные переменные, отражающие состояние элементов интерфейса и т.д.

Пример выделения переменных

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

У меня получился следующий список:

  • Состояние загрузки приложения: загружается или данные уже загружены

  • Группа, для описания которой нам, скорее всего, понадобится её идентификатор, название, статус открытия списка группы

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

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

Переменные, как и константы мы записываем отдельной колонкой под схемами:

События

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

Событиями могут быть:

  • Изменения этапов жизненного цикла приложения или его отдельных компонентов: инициализация, обновление, удаление, и т.п.

  • Пользовательский ввод: события мыши или клавиатуры

  • Таймер

  • Получение данных с сервера и т.д.

Ну а в нашем приложении такими событиями, на мой взгляд, будут:

  • загрузка данных приложения и

  • открытие и закрытие списка сотрудников по клику мыши на шапку группы

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

Теперь мы уже существенно продвинулись вперед:

  1. Во-первых, мы уже покрутили задачу в голове и более или менее её поняли

  2. Во-вторых, мы сделали её куда как более конкретной, свели все возможные варианты реализации списков к одному

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

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

Проектирование структуры данных

Теперь мы можем переходить ко второму этапу нашего алгоритма - проектированию структур данных.

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

Назначение

Для чего нам это надо?

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

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

  3. И в-третьих, если структуры данных описать полностью и с примерами, как рекомендует алгоритм, то примеры становятся очень неплохим подспорьем для unit-тестов.

Алгоритм

Структуры мы описываем в следующем порядке:

  1. Фиксируем название структуры

  2. Описываем её тип

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

  4. Придумываем один - два примера заполненной структуры.

На этом этапе уже начинают играть роль используемые технологии. В нашем случае появляются TypeScript и JSDoc, но для других языков и платформ вполне может использоваться и что-то другое. Ну а раз мы говорим о веб-разработке и библиотеке React JS, то надо сразу отметить, что мы дальше будем использоваться понятия свойств (props) и состояний (state), характерных для React. Ничего страшного, если вы с этими понятиями не знакомы, думаю, что всё будет и так понятно.

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

Пример

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

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

Фиксируем название структуры. Название должно отражать суть структуры. В нашем случае, назовём её AppState - состояние приложения:

export interface AppState {}

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

Описание

Название переменной

Источник

Тип данных

Обязательность

Комментарий

Название приложения

title

Константы

Строка

+

Адрес сервера

backendAddress

Константы

Строка

+

Флаг загрузки данных

isLoading

Переменные

Логическое значение

+

true - данные загружаются

false - данные загружены

Данные об отображаемой группе

group

Переменные

Объект типа Group

-

На момент загрузки данные о группе не определены

Метод загрузки данных

loadData

События

Функция

+

Фиксируем это в коде:

export interface AppState {        title: string;        backendAddress: string;    isLoading: boolean;    group?: Group;    loadData: Function;}

Пишем интерпретацию структуры

TypeScript дает отличное представление того, что из себя структура представляет, но всё же, часть информации теряется. Поэтому, давайте сразу зафиксируем, что подразумевается под каждым из полей структуры и каким образом мы можем представить информацию из реального мира в виде такой структуры. Для этого нам пригодится ещё один инструмент из мира JavaScript - JSDoc.

/** * Общее состояние приложения * @prop title - заголовок приложения * @prop backendAddress - адрес сервера * @prop isLoading - флаг загрузки данных (true - загружаются, false - загружены) * @prop group - данные об отображаемой группе. На момент загрузки данных group не определена * @method loadData - метод загрузки данных */export interface AppState {    title: string;    backendAddress: string;    isLoading: boolean;    group?: Group;    loadData: Function;}

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

Для заглушки достаточно правильно указать имя новой структуры, описать, что она будет собой представлять и поставить отметку TODO - как правило IDE автоматически распознают отметки TODO и формируют из них удобные списки задач

/** * Общее состояние приложения * @prop title - заголовок приложения * @prop backendAddress - адрес сервера * @prop isLoading - флаг загрузки данных (true - загружаются, false - загружены) * @prop group - данные об отображаемой группе. На момент загрузки данных group не определена * @method loadData - метод загрузки данных */export interface AppState {    title: string;    backendAddress: string;    isLoading: boolean;    group?: Group;    loadData: Function;}/** * Данные об отображаемой группе*///TODOexport interface Group {}

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

Делать это мне всегда тяжело, потому что кажется, что примеры - это пустая трата времени. И каждый раз, когда я дохожу до написания unit-тестов, я радуюсь тому, что я всё-таки их написал. В примерах вместо методов я указываю заглушки и вместо содержания вложенных структур, в нашем случае group - ссылки на переменные нужного типа, для которых временно тоже делаю заглушки. Получается что-то вроде:

/** * Общее состояние приложения * @prop title - заголовок приложения * @prop backendAddress - адрес сервера * @prop isLoading - флаг загрузки данных (true - загружаются, false - загружены) * @prop group - данные об отображаемой группе. На момент загрузки данных group не определена * @method loadData - метод загрузки данных */export interface AppState {    title: string;    backendAddress: string;    isLoading: boolean;    group?: Group;    loadData: Function;}//Пример 1const appState1: AppState = {    title: "Заголовок 1",    backendAddress: "/view_doc.html",    isLoading: true,    group: undefined,    loadData: () => {}}//Пример 2const appState2: AppState = {    title: "Заголовок 2",    backendAddress: "/view_doc_2.html",    isLoading: false,    group: group1, //Заглушка для вложенной структуры    loadData: () => {}}/** * Данные об отображаемой группе*///TODOexport interface Group {}//TODOconst group1 = {}

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

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

Можно сделать ещё один шаг и описать простой контракт между клиентом и сервером нашего приложения. Для этого достаточно выделить из описанных структур те данные, которыми приложение обмениваемся с сервером, и сформировать, например, описание класса - сервиса:

export default abstract class AbstractService {           /**     * Метод загрузки данных о группе с сервера     * @fires get_group_data - имя действия, вызываемого на сервере     * @returns данные о группе     */    abstract getGroupData(): Promise<Group>;}

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

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

Проектирование функций

Только теперь мы, наконец, переходим к разработке.

Как и для других этапов, для проектирования функций у нас есть свой рецепт - алгоритм:

Алгоритм первоначального проектирования функций

  1. Создаем заглушку. Заглушка - это определение функции, которое:

    • отражает правильное название функции

    • принимает правильное количество и типы параметров

    • возвращает произвольный результат, но корректного типа

  2. Описываем входные, выходные данные и назначение функции

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

  4. Пишем тело функции.

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

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

    2. добавляем отметку (TODO), помечающую нашу функцию как объект списка задач

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

Пример проектирования функции

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

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

Шаг 1 - Создаем заглушку

Заглушка функции - это минимальное работоспособное её описание, в котором есть:

  • правильное название функции,

  • правильное количество и тип параметров и

  • возвращаемое значение, произвольное по содержанию, но правильного типа.

Для простой функции на TypeScript будет вполне достаточно написать что-то вроде:

export const getWorkDuration = (worktimeFrom: string, worktimeTo: string): string => {    return "6ч 18мин";}

где:

  • getWorkDuration - правильное название фукнции, то есть то, которое мы и дальше будем использовать

  • worktimeFrom: string, worktimeTo: string - два строковых параметра. Именно столько параметров и такого типа функция должна принимать

  • : string после закрывающей круглой скобки - тип возвращаемого значения

  • return "6ч 18мин" - возврат произвольного значения, но правильного типа из функции

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

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

Для функциональных компонентов:

const componentName = (props: PropsType) => { return <h1>componentName</h1> }

Для компонентов классов:

class componentName extends React.Component<PropsType, StateType>{    state = {        //произвольные значения для всех обязательных свойств состояния    }    render() {                return <h1>componentName</h1>     }}

где:

  • PropsType - это описание типа передаваемых свойств

  • StateType - описание типа состояния компонента

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

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

interface AppProps {}export default class App extends Component<AppProps, AppState> {    state = {        title: "Отображение списка групп",        backendAddress: "",        isLoading: true,        loadData: this.loadData.bind(this)    }    /**     * Метод загрузки данных с сервера     */    //TODO    loadData() {    }    render() {        return <h1>App</h1>    }}

Надо отметить пару особенностей этой реализации:

  1. В компонент App не передаётся никаких свойств "сверху", поэтому интерфейс AppProps пустой

  2. Тип состояния компонента AppState мы импортируем напрямую из описания типов, которое мы создали, когда проектировали структуры

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

Шаг 2 - Описываем входные и выходные данные

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

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

/** * Рассчитывает количество часов и минут между указанным начальным и конечным временем * Если время начала работы больше времени конца, то считается, что конечное время - это время следующих суток * @param worktimeFrom - время начала работы в формате ЧЧ:ММ (от 00:00 до 23:59) * @param worktimeTo - время конца работы в формате ЧЧ:ММ (от 00:00 до 23:59) * @return количество часов и минут между переданным временем в формате Хч Y?мин, например 6ч 18мин или 5ч, если количество часов полное *///TODOexport const getWorkDuration = (worktimeFrom: string, worktimeTo: string): string => {    return "6ч 18мин";}

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

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

Шаг 3. Тестирование

После того, как назначение и входные и выходные данные определены, можно начать продумывать поведение функции. Лучшее средство для этого - unit-тесты. Есть пара рекомендаций о том, как лучше их писать:

  • unit-тесты должны, как минимум, один раз проходить по каждой ветке внутри функции - то есть проверять все ветвления, если они есть

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

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

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

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

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

Допустим, у нас есть структура данных, описывающая состояние светофора. Эта структура называется "перечисление" или enum. То есть переменная этого типа может находиться в нескольких дискретных значениях:

type TrafficLights = "красный" | "желтый" | "зеленый";

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

function trafficLightsFunction (trafficLights: TrafficLights) {    switch (trafficLights) {        case "красный":            ...        case "желтый":            ...        case "зеленый":            ...    }}

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

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

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

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

Но сначала давайте посмотрим на взаимосвязь между типами данных и тестами для чистых функций:

Тип данных

Описание

Пример данных

Правила тестирования

1

Атомарные данные

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

Строка

Логическое значение

Число

Для строк допустим 1 сценарий тестирования

для логических значений - 2 (true / false)

Для чисел - 1 сценарий, но может потребоваться дополнительный сценарий для значения 0

2

Перечисления

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

Цвета светофора

Пятибальная шкала оценок

Размеры одежды

и т.д.

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

Поэтому, если вам нужно протестировать 100-бальную шкалу оценок, но при этом вы понимаете, что значения группируются в 4 класса:

1 - 25,

26 - 50,

51 - 75,

76 - 100

То вам понадобится только 4 сценария.

3

Интервалы

Числа в определённом диапазоне

Скорость автомобиля (0 - 300]

Температура воздуха

и т.д.

Необходимо протестировать:

значение внутри диапазона

граничные значения

4

Детализация

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

Подразделение расформировано или нет? Если расформировано, то какова дата расформирования

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

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

Должно быть, как минимум, столько тест-кейсов, сколько комбинаций подклассов может быть.

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

подразделение нерасформировано и

подразделение расформировано и указана дата расформирования

А для примера с типами вопросов - как минимум, по одному для каждого типа.

5

Сложный тип (объект)

Данные, состоящие из нескольких независимых частей

Объект "Сотрудник" имеющий поля:

id

ФИО

Дата рождения

Пол

и т.д.

Как минимум 2 сценария. При этом надо изменять значения всех полей

6

Массив

Набор данных, объем которых может изменяться

Список сотрудников

Массив учебных курсов, назначенных сотруднику

Как правило, несколько сценариев:

один, проверяющий работу функции при пустом массиве

один, проверяющий работу функции при массиве с одним элементом

один или несколько, проверяющих работу функции, когда элементов в массиве 2 и больше

Примеры тестов

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

/** * Рассчитывает количество часов и минут между указанным начальным и конечным временем * Если время начала работы больше времени конца, то считается, что конечное время - это время следующих суток * @param worktimeFrom - время начала работы в формате ЧЧ:ММ (от 00:00 до 23:59) * @param worktimeTo - время конца работы в формате ЧЧ:ММ (от 00:00 до 23:59) * @return количество часов и минут между переданным временем в формате Хч Y?мин, например 6ч 18мин или 5ч, если количество часов полное *///TODOexport const getWorkDuration = (worktimeFrom: string, worktimeTo: string): string => {    return "6ч 18мин";}

Итак, функция расчёта рабочего времени. Пусть вас не смущает тип string у параметров, каждый из параметров - это время, представленное в виде строки ЧЧ:ММ, то есть не атомарный тип, а интервал от 00:00 до 23:59. Согласно таблице, для интервалов надо проверить граничные значения и значения внутри диапазона. А так как параметра два, то это справедливо для каждого из них. Для этого нам потребуется 3 тест-кейса:

  1. Первый параметр выходит за граничное значение, а второй нет

  2. Второй параметр выходит за граничное значение, а первый - нет

  3. Оба параметра - в пределах нормальных значений

И ещё в документации есть отметка о том, что рабочее время считается по разному в зависимости от соотношения значений. Вместо одного тест-кейса, где параметры находятся в пределах нормы, напишем 3 кейса, где первый параметр меньше, больше и равен второму, итого 5

Название

worktimeFrom

worktimeTo

Ожидаемое значение

1

Проверка корректности worktimeFrom

Граничное значение, не входящее в диапазон, например

"24:00"

Нормальное значение, например

"18:00"

Исключение

2

Проверка корректности worktimeFrom

Нормальное значение, например

"18:00"

Граничное значение, не входящее в диапазон, например

"24:00"

Исключение

3

Нормальная работа,

worktimeFrom < worktimeTo

Нормальное значение, меньшее чем worktimeTo, например

"00:00"

Нормальное значение, большее чем worktimeFrom, например,

"23:59"

23ч 59мин

4

Нормальная работа,

worktimeFrom > worktimeTo

Нормальное значение, большее чем worktimeTo, например

"18:49"

Нормальное значение, меньшее чем worktimeFrom, например,

"10:49"

16ч

5

Нормальная работа worktimeFrom = worktimeTo

Нормальное значение, например,

"01:32"

Нормальное значение, например,

"01:32"

Тест-кейсы мы к тому же составили так, чтобы одновременно проверялись выводимые значения: с минутами и без. Остается только написать сами тесты. Я их пишу при помощи Jest и Enzyme - стандартной комбинации для React JS. В самом написании тестов особых премудростей нет, поэтому приведу только один пример:

describe('Тестирование расчёта времени между началом и концом работы', () => {        it('Когда начало и конец смены совпадают, функция возвращает 0ч', ()=>{        const result = getWorkDuration("01:32", "01:32");        expect(result).toBe("0ч");    });        //Подобным образом описываем все сценарии    ...});

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

Давайте протестируем компонент App. Заглушка для него выглядела следующим образом:

interface AppProps {}export default class App extends Component<AppProps, AppState> {    state = {        title: "Отображение списка групп",        backendAddress: "",        isLoading: true,        loadData: this.loadData.bind(this)    }    /**     * Метод загрузки данных с сервера     */    //TODO    loadData() {    }    render() {        return <h1>App</h1>    }}

Данные в этот компонент не передаются, а его поведение полностью определяется его состоянием, структуру которого мы определили ранее как:

/** * Общее состояние приложения * @prop title - заголовок приложения * @prop backendAddress - адрес сервера * @prop isLoading - флаг загрузки данных (true - загружаются, false - загружены) * @prop group - данные об отображаемой группе. На момент загрузки данных group не определена * @method loadData - метод загрузки данных */export interface AppState {    title: string;    backendAddress: string;    isLoading: boolean;    group?: Group;    loadData: Function;}

Для начала определимся, к какому типу данных относится эта структура:

  • Во-первых, это сложная структура, состоящая из нескольких полей и членов.

  • Во-вторых, среди полей есть зависимые. Так, наличие поля group зависит от состояния загрузки, пока данные не загружены, данных о группе - нет. Если есть зависимые данные, то можно смело относить эти данные к типу "Детализация".

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

  • для сложных типов (объектов) нужно написать как минимум 2 теста, изменяя при этом значения полей

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

Таким образом, нам следует написать 4 отдельных теста:

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

  • 2 - изменяя статус загруженности и соответствующие данные, чтобы проверить, правильность поведения компонента в этом случае

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

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

Начнём с простого - с вызова метода loadData при загрузке компонента:

import React from 'react';import Enzyme, { mount, shallow } from 'enzyme';import Adapter from 'enzyme-adapter-react-16';Enzyme.configure({ adapter: new Adapter() });import App from './App';describe('App', () => {    test('Когда компонент App смонтирован, вызывается функция loadData', () => {        //Добавляем слушателя к функции loadData        const loadData = jest.spyOn(App.prototype, 'loadData');                //Монтируем компонент        const wrapper = mount(<App></App>);                //Проверяем количество вызовов функции loadData        expect(loadData.mock.calls.length).toBe(1);    });}

Здесь мы подключили enzyme к нашему файлу с тестами, импортировали сам компонент и написали первый тест. Внутри теста мы:

  1. добавили слушателя к функции loadData внутри компонента,

  2. смонтировали компонент в тестовой среде (то есть сымитировали его появление в приложении) и

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

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

test('Когда данные загружаются, отображаются только заголовок и спиннер', () => {        //Монтируем компонент        const wrapper = mount(<App></App>);        //Подменяем состояние компонента на нужное для тестирования        wrapper.setState({            title: "Заголовок 1",            backendAddress: "/view_doc.html",            isLoading: true,            group: undefined,            loadData: () => {}        })        //Проверяем правильность отображения компонента        //Проверяем наличие и содержание заголовка        expect(wrapper.find('h1').length).toBe(1);        expect(wrapper.find('h1').text()).toBe("Заголовок 1");        //Заглушка, отображающаяся в процессе загрузки        expect(wrapper.find(Spinner).length).toBe(1);        //Компонент, отображающий группу отображаться не должен        expect(wrapper.find(Group).length).toBe(0);    });

Этот сценарий мы реализуем по следующей схеме:

  1. Монтируем компонент

  2. Подменяем его состояние на нужное нам. Тут как раз понадобятся примеры, которые мы писали в описании структуры данных. Нужные нам состояния можно не придумывать заново, а копировать подходящие значения оттуда

  3. Проверяем правильность отображения компонента

Пока мы писали тест, стало понятно, что для нашего приложения потребуются ещё 2 компонента, которые мы пока не реализовывали:

  • заглушка для отображения в процессе загрузки - спиннер и

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

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

Реализуем третий сценарий по аналогичной схеме:

test('Когда данные загружены, отображается заголовок и группа. Спиннер не отображается', () => {        const wrapper = mount(<App></App>);        wrapper.setState({            title: "Заголовок 2",            backendAddress: "/view_doc_2.html",            isLoading: false,            group: {                id: "1",                name: "Группа 1",                listOfCollaborators: []            },            loadData: () => {}        })        expect(wrapper.find('h1').length).toBe(1);        expect(wrapper.find('h1').text()).toBe("Заголовок 2");        expect(wrapper.find(Spinner).length).toBe(0);        expect(wrapper.find(Group).length).toBe(1);    });

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

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

Шаги 4 и 5. Реализация функций и формирование списка задач

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

  • во-первых, старайтесь писать более высокоуровневый код,

  • во-вторых, избегайте коварной ошибки, которая называется Knowledge Shift (сдвиг области знаний).

Высокоуровневый код

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

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

/** * Рассчитывает количество часов и минут между указанным начальным и конечным временем * Если время начала работы больше времени конца, то считается, что конечное время - это время следующих суток * @param worktimeFrom - время начала работы в формате ЧЧ:ММ (от 00:00 до 23:59) * @param worktimeTo - время конца работы в формате ЧЧ:ММ (от 00:00 до 23:59) * @return количество часов и минут между переданным временем в формате Хч Y?мин, например 6ч 18мин или 5ч, если количество часов полное *///TODOexport const getWorkDuration = (worktimeFrom: string, worktimeTo: string): string => {    return "6ч 18мин";}

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

  1. Привести параметры к числовым значениям, например, минутам, прошедшим с 00:00

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

  3. Привести получившуюся разницу к формату Xч Y?мин, который и вернуть из функции

И тут мы могли бы пойти двумя путями:

  • сразу начать делить параметры на части по разделителю ":", проверяя входные значения и умножая часы на минуты и т.д. или

  • выделить каждый шаг в отдельную функцию.

В результате мы бы получили такую картину:

/** * Рассчитывает количество часов и минут между указанным начальным и конечным временем * Если время начала работы больше времени конца, то считается, что конечное время - это время следующих суток * @param worktimeFrom - время начала работы в формате ЧЧ:ММ (от 00:00 до 23:59) * @param worktimeTo - время конца работы в формате ЧЧ:ММ (от 00:00 до 23:59) * @return количество часов и минут между переданным временем в формате Хч Y?мин, например 6ч 18мин или 5ч, если количество часов полное */export const getWorkDuration = (worktimeFrom: string, worktimeTo: string): string => {    const worktimeFromInMinutes = getWorktimeToMinutes(worktimeFrom);    const worktimeToInMinutes = getWorktimeToMinutes(worktimeTo);    const minutesDiff = calcDiffBetweenWorktime(worktimeFromInMinutes, worktimeToInMinutes);    return convertDiffToString(minutesDiff);}/** * Вычиcляет количество минут, прошедших с начала суток * @param worktimeFrom - время в формате ЧЧ:ММ (от 00:00 до 23:59) * @returns количество минут, прошедших с 00ч 00минут *///TODOexport const getWorktimeToMinutes = (worktime: string): number => {    return 0;}/** * Вычисляет количество минут между началом и концом рабочего дня с учётом суток * @param worktimeFrom - время начала рабочего дня в виде количества минут, прошедших с начала суток * @param worktimeTo - время конца рабочего дня в виде количества минут, прошедших с начала суток * @returns количество минут между началом и концом рабочего дня с учётом суток *///TODOexport const calcDiffBetweenWorktime = (worktimeFrom: number, worktimeTo: number): number => {    return 0;}/** * Преобразовывает количество минут во время в формате Хч Y?мин * @param minutes - количество минут * @returns время в формате Хч Y?мин, например 6ч 18мин или 5ч *///TODOexport const convertDiffToString = (minutes: number): string => {    return "6ч 18мин";}

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

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

Knowledge Shift

Говоря о проектировании функций, конечно же, нельзя обойти стороной такую ошибку как Knowledge Shift.

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

В этом случае, мы рискуем столкнуться с ситуацией, когда функция начинает выполнять какие-то операции над входными структурами, а затем переключается на работу с вложенными, как бы сдвигая фокус с родителя на дочерний объект. Это и есть Knowledge Domain Shift или просто Knowledge Shift.

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

Чтобы этого избежать, рекомендуется передавать обработку внутренних сложных структур в отдельные функции, как мы сделали это при реализации нашего реакт-компонента App:

export default class App extends Component<AppProps, AppState> {    state = {        title: "Отображение списка групп",        backendAddress: "",        isLoading: true,        group: undefined,        loadData: this.loadData.bind(this)    }    /**     * Метод загрузки данных с сервера     */    //TODO    loadData() {}    componentDidMount() {        //Вызываем метод loadData при загрузке приложения        this.loadData();    }    render() {        const {isLoading, group, title} = this.state;        return (            <div className="container">                <h1>{title}</h1>                {                    isLoading ?                    <Spinner/>                    //Структуру типа Group передаем на обработку отдельному компоненту                    : <Group group={group}></Group>                }            </div>        );    }}

Мы добавили в компонент реализацию метода жизненного цикла componentDidMount, который вызовет наш метод загрузки данных при отображении компонента. Но основной функционал у нас содержится в методе render. В нем мы выводим заголовок и, в зависимости от статуса загрузки, рисуем либо заглушку - спиннер, либо компонент Group.

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

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

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

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

Подробнее..

Эффективная среда для подготовки к сертификационному экзамену

20.07.2020 08:14:49 | Автор: admin


Во время "самоизоляции" подумалось получить пару сертификатов. Посмотрел на одну из сертификаций AWS. Материала для подготовки очень много видео, спецификации, how-to. Очень времязатратно. Но ведь самое эффективное при сдаче экзаменов, основанных на тестах просто решать экзаменационные или похожие на них вопросы.


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


Что не так?


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


  1. Может содержать ошибки в формулировке
  2. Может содержать ошибки в ответах (если они есть)
  3. Может содержать "самодельные" некорректные вопросы
  4. Может содержать устаревшие вопросы, которые на экзамене уже не встречаются
  5. Неудобен для работы, нужно дополнительно еще делать заметки в блокноте о вопросах

Небольшой бизнес-анализ предметной области


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


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


Получаем тэги и фильтрацию списка вопросов по ним


Вдобавок к вышеперечисленным стандартным "Легкий", "Сложный", "Мудрёный" добавим пользовательские тэги, чтобы пользователь мог отфильтровать, например, только по "Сложный" и "Lambda"


Еще примеры тэгов: "Устаревший", "Некорректный".


Что имеем в итоге?


Я прохожу все вопросы один раз, помечая тэгами. После этого забываю о "Легких". В моем тесте 360 вопросов, это значит больше 200 вычеркиваются. Они больше не будут отнимать внимание и время. Для вопросов на языке не родном для пользователя это ощутимая экономия.


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


Эффективно, по-моему.


Добавляем возможность делать заметки и вести дискуссии по каждому вопросу с другими пользователями, делаем не перегруженный дизайн на Vue.js и в итоге получаем рабочую бетта-версию:


https://certence.club


Источник вопросов


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


Инструкция по самостоятельной загрузке вопросов

Необходимо установить в браузер веб расширение и пройти по всем страницам examtopics.com с вопросами, которые вы хотите добавить. Расширение само определит сертификацию, вопросы и они сразу же появятся на certence.com (F5)


Расширение представляет собой сотню строк простого JavaScript кода, вполне читаемого на предмет малварности.


Загрузить расширение в Chrome Webstore у меня почему-то каждый раз оборачивается какими-то нечеловеческими муками, поэтому для Хрома нужно скачать архив, раззиповать в пустую папку, затем Chrome -> Дополнительные инструменты -> Расширения -> Загрузить распакованное расширение. Указать папку.


Для Firefox ссылка. Должно установится само. Тот же zip, просто с другим расширением.


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


Дискуссии пока в режиме read-only с того же сайта-донора, но помогают сильно.


В настройках есть выбор режима просмотра. Все данные пользователя хранятся на клиенте в локальном кэше броузера (авторизация еще не реализована).


Пока только десктопная версия.


Как сделать хороший UI/UX для мобильного экрана мне пока неочевидно.


Хотелось бы получить отзывы и предложения.

Подробнее..

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

04.02.2021 10:09:11 | Автор: admin
Когда-то мы договорились внутри компании, что будем запускать фичи в приложении под A/B-тестами. Но всё равно были вещи из серии да это же очевидно, что так нужно сделать. Вот история одного из самых долгих и крупных да это же очевидно, помешавшего в итоге пользователям.

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



Возможность появилась в нашем флагманском приложении в 2019 году. Реализована она была через партнёра Smart Engine, решение Smart IDReader. Вот их публикация про применение этого SDK. Партнёра выбирали после долгих тестов: этот обеспечивает очень хорошее качество распознавания и готов юридически на все необходимые процедуры по передаче и защите данных. Они поставляли такие решения чуть ли не таможне.

Сначала подключили фичу только на авиабилеты и начали смотреть, что получается. Важно то, что это была одна из последних фич, когда не было возможности проводить A/B именно в приложении, мы использовали последовательные тесты на iOS/Android и A/B-тесты на вебе. Пока мы узнали только то, что конверсия не падает, а сканирование паспорта использует 10,1% iOS-юзеров и 8,2% Android-юзеров. Всё выглядело хорошо, и мы начали раскатывать фичу дальше.


Раскатка дальше


Тут важно сказать, что чем больше сканирований, тем дешевле единичное распознавание по условиям партнёра. Соответственно, мы раскатали на весь пул пользователей, получили сладкие цены и через некоторое время сделали ещё один контрольный замер после релиза. Тоже всё было хорошо. Единственное, что несколько удивляло, это то, что доля внесения правок после сканирования на Android была 83,7%, на iOS 28,9%. Видимо, речь про не самые хорошие камеры или плохое взаимодействие софта и камеры. Но люди пользовались фичей, и вроде всё было отлично.

Через 2 года нужно было продлевать пакет, и мы решили посмотреть, а что же там в A/B-тестах, благо такая возможность в приложении на тот момент уже давно была. Идея была в том, чтобы понять, насколько увеличивает конверсию такой быстрый и удобный ввод документов и сколько это вообще создаёт добавленной стоимости продукту.

Запустили ухудшающие A/B-тесты (скрывающие от пользователя часть функционала).

Получили результаты.

Удивились.

Выключили фичу.

Результаты тестов


Без сканирования: +8,7% продаж на новых железнодорожных билетах. Это лучший прирост конверсии за 3 года: нужно было просто выпилить фичу, в которую мы все верили.

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

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

Выводы


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

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

Итоговый результат парадоксален: чем больше времени человек проводит в приложении, тем выше вероятность покупки. Сканирование, которое экономит ему около 30-40-50 секунд, ухудшает показатели конверсии. Возможных гипотез две:
  1. Возможно, пользователи, которые приложили больше усилий, больше дорожат этими усилиями и меньше уходят, поэтому ручной ввод работает на цель.
  2. Либо сканированием пользовались те люди, которые меньше были настроены на покупку. Барьер входа ниже, они пробуют и уходят тогда мы создали условия искусственного отбора именно таких.


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

А мораль такая тестируйте даже очевидное. Ещё раз спасибо, Капитан Очевидность, но нам это стоило довольно много времени разработки и, соответственно, денег.
Подробнее..

Как мы делаем страховое приложение для людей

14.12.2020 00:07:52 | Автор: admin

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

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

Но сначала пару слов о нашем первом приложении. Мы делали его в качестве небольшого эксперимента, чтобы посмотреть, а нужно ли оно вообще и будут ли клиенты им пользоваться. У нас был 1 (один) разработчик, который в марте этого года собрал весь свой энтузиазм и начал с создания обертки под web view. В апреле мы выкатили приложение в Google Play, а в июне в App Store.

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

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

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

Поэтому мы пошли делать новое приложение.

В общем, что имеем. Порядка 70-80% наших пользователей заходят на сайт Манго Страхования с мобильных устройств. Заставлять их и дальше читать огромные простыни текста с мобильной версии сайта было не очень человеколюбивым поступком. Поэтому (не сразу) решено было сделать приложения на React Native для каждой платформы, с ее сценариями использования, чтобы все было максимально привычно и вписывалось в стандартные контролы ОС и удобные свайпы.

Мы в 5 раз увеличили нашу команду (до 5 человек, да) и у нас появились:

  • React-разработчик

  • фронтендер

  • UX-дизайнер

  • редактор текстов

  • тестировщик

Разрабатывать эту версию мы начали в сентябре. Мы могли лишь предполагать, что приложение по умолчанию установлено у тех, кто уже является нашим клиентом, но это не отменяло главного вопроса А нужно ли клиентам приложение вообще?. Вот здесь возник нюанс параллельно с запуском разработки мы запустили серию JTBD-интервью, чтобы выяснить какие сценарии самые востребованные для наших клиентов. Интервью проводились с AB-сегментом нашей клиентуры, и мы выяснили, что для большинства острой необходимости в приложении не было (они заходили в него пару раз), а тех, для кого оно было полезно, оно бесило. Так что в целом потребность в приложении была не то чтобы однозначная, но была надежда, что, если мы сделаем современное приложение, то для активной части пользователей оно станет полезным.

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

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

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

Со стороны выглядит довольно очевидным, но такие тесты часто недооценивают. Мы как создатели приложения имеем настолько замыленный глаз, что можем всей командой пропустить какой-то небольшой затык, который поставит точку на выполнении пользовательского сценария. Поэтому привлекать к тестам не-клиентов полезнее, чем кажется. От старта разработки до первого релиза прошло чуть больше 2 месяцев (1 сентября 8 ноября), учитывая, что в середине этого процесса ещё и провели полный редизайн, результат вышел отличный.

Дизайн

Как видите по скриншотам, ориентировались мы на современные финтех-приложения. Банкам необходимо конкурировать друг с другом в плане дизайна и удобства, поэтому посматривать в их сторону и перенимать лучшие практики вполне себе полезно. Еще есть Lemonade Insurance, наш старший брат из США, тоже с хорошим дизайном.

Новый дизайн стоит на трёх китах:

  1. Одностраничность. В случае с приложением одноэкранность. Каждый смысловой блок информации не должен занимать более одного экрана. Делать в красивом приложении здоровенную простыню, которая долго листается, это почти то же самое, что снова наказать человека webview.

  2. Лаконичность. Убираем все вырвиглазное, вот эти странные заливки пустого места ярким цветом (а чего он пустой стоит) или капсом. Многие люди всё ещё воспринимают капс так, будто на них орут. Или пытаются что-то впарить. Или что-то впаривают и в процессе орут.

  3. Кроссплатформенные приложения на React Native. Все свайпы, все скроллы, всё должно быть так, как на той или иной платформе задумано и привычно.

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

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

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

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

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

Пока у нас 4 000 скачиваний приложения и 2 000 активных пользователей.

Чего у нас не будет

У IT-компаний и финтеха в приложениях или в описаниях работы самих сервисов очень часто проскакивают довольно хайповые названия технологий. Например, ИИ встроили куда-то туда, куда вроде как и не надо. Или у кого-то теперь есть data science-подразделение, которое теперь может работать с большими данными от всех 500 клиентов.

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

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

А ещё не будет полного диджитала и вот этого перевода всего и вся на ботов вместо людей. И вот почему.

Люди, эмоции и помощь

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

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

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

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

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

Ещё в плане быстрой помощи приложение удобно, потому что это наглядно. Представьте, застраховал человек квартиру, и через какое-то время его соседи сверху залили, причем всем, что было. Здесь тоже важно иметь возможность зафиксировать всё случившееся прямо сейчас, наделав кучу фоток на смартфон. Плюс в будущем можно будет через приложение сразу выйти на нашего сотрудника поддержки и по видеосвязи показать ему прямо сейчас, СМОТРИ, КАК ХЛЕЩЕТ!.

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

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

Но это не в ближайших релизах.

Планы на будущее

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

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

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

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

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

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

Спасибо, что дочитали.

Подробнее..

Так как же не страдать от функциональных тестов?

22.04.2021 18:21:21 | Автор: admin

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

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

Я думаю, все знают принципы хороших тестов:

  • Тест должен быть атомарным, т.е. проверять единицу логики (например, один HTTP-метод или один метод класса)

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

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

Проблема возникает тогда, когда вы начинаете пытаться реализовывать подобные тесты и сталкиваетесь с реальностью: pipeline всего из 3000 функциональных тестов проходит около двух часов!


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

Любой запуск функциональных тестов для API можно разделить на следующие этапы:

  1. Применить миграции (Структура БД)

  2. Применить фикстуры (Тестовые данные в БД)

  3. Выполнить HTTP-запрос

  4. Выполнить необходимые проверки

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

Для обеспечения требования повторяемости (а также, чтобы тесты в принципе были близки к реальности) в ходе тестирования используется настоящая СУБД той же версии, которая используется на промышленной среде. Для каналов передачи данных, по возможности, тоже не делаются заглушки: поднимаются и другие зависимости вроде Redis/RabbitMQ и отдельное HTTP приложение, содержащее моки для имитации вызовов сторонних сервисов.

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

В итоге получается примерно следующий интерфейс:

Пример описания запроса
{  "method": "patch",  "uri": "/v2/project/17558/admin/items/physical_good/sku/not_existing_sku",  "headers": {    "Authorization": "Basic MTc1NTg6MTIzNDVxd2VydA=="  },  "data": {    "name": {      "en-US": "Updated name",      "ru-RU": "Обновленное название"    }  }}
Пример описания запроса
{  "status": 404,  "data": {    "errorCode": 4001,    "errorMessage": "[0401-4001]: Can not find item with urlSku = not_existing_sku and project_id = 17558",    "statusCode": 404,    "transactionId": "x-x-x-x-transactionId-mock-x-x-x"  }}
Пример описания самого теста
<?php declare(strict_types=1);namespace Tests\Functional\Controller\Version2\PhysicalGood\AdminPhysicalGoodPatchController;use Tests\Functional\Controller\ControllerTestCase;class AdminPhysicalGoodPatchControllerTest extends ControllerTestCase{    public function dataTestMethod(): array    {              return [                // Negative cases                'Patch -- item doesn\'t exist' => [                        '001_patch_not_exist'                ],            ];    }}

Структура директории с тестами:

TestFolder Fixtures    store       item.yml Request    001_patch_not_exist.json Response    001_patch_not_exist.json   Tables    001_patch_not_exist        store            item.yml AdminPhysicalGoodPatchControllerTest.php

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

...

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

1. Применять миграции единожды

Миграция это скрипт, который позволяет перевести текущую структуру БД из одного консистентного состояния в другое.

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

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

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

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

2. Кэшировать миграции

Чем дольше живет приложение, тем больше миграций он несет вместе с собой. Усугубляться все может сценарием, в котором несколько приложений работают с одной БД (соответственно и миграции общие).

Для одной из наших самых старых схем в БД существует около 667 миграций на текущий момент. А таких схем не один десяток. Надо ли говорить, что каждый раз применять все миграции может оказаться достаточно расточительным?

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

Пример скрипта для сборки миграций
#!/usr/bin/env bashif [[ ! -f "dump-cache.sql" ]]; then    echo 'Generating dump'    # Загрузка миграций из удаленного репозитория    migrations_dir="./migrations" sh ./scripts/helpers/fetch_migrations.sh    # Применение миграций к БД    migrations_dir="./migrations" host="percona" sh ./scripts/helpers/migrate.sh    # Генерируется дамп только для интересующих нас схем (store, delivery)    mysqldump --host=percona --user=root --password=root \      --databases store delivery \      --single-transaction \      --no-data --routines > dump.sql    cp dump.sql dump-cache.sqlelse    echo 'Extracting dump from cache'    cp dump-cache.sql dump.sqlfi

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

Пример CI-job (gitlab)
build migrations:  stage: build  image: php72:1.4  services:    - name: percona:5.7  cache:    key:      files:        - scripts/helpers/fetch_migrations.sh    paths:      - dump-cache.sql  script:    - bash ./scripts/ci/prepare_ci_db.sh  artifacts:    name: "$CI_PROJECT_NAME-$CI_COMMIT_REF_NAME"    paths:      - dump.sql    when: on_success    expire_in: 30min

3. Использовать транзакции БД при применении фикстур

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

  1. Применить фикстуры

  2. В цикле для каждого теста:

    1. Начать транзакцию

    2. Выполнить тест

    3. Проверить результат

    4. Откатить транзакцию

При локальном запуске 19 тестов (каждый из которых заполняет 27 таблиц) по 10 раз были получены результаты (в среднем): 10 секунд при использовании данного подхода и 18 секунд без него.

Что необходимо учесть:

  • У вас должно использоваться одно соединение внутри приложения, а также для инициации транзакции внутри теста. Соответственно, необходимо достать инстанс соединения из DI-контейнера.

  • При откате транзакции счетчики AUTO INCREAMENT не будут сбрасываться, как это происходит при применении TRUNCATE. Могут возникнуть проблемы в тестах, в которых в ответе возвращается идентификатор созданной сущности.

Пример кода
public static function setUpBeforeClass(): void{        parent::setUpBeforeClass();        foreach (self::$onSetUpCommandArray as $command) {            self::getClient()->$command(self::getFixtures());        }}.../** * @dataProvider dataTestMethod */public function testMethod(string $caseName): void{        /** @var Connection $connection */        $connection = self::$app->getContainer()->get('doctrine.dbal.prodConnection');        $connection->beginTransaction();                $this->traitTestMethod($caseName);        $this->assertTables(\glob($this->getCurrentDirectory() . '/Tables/' . $caseName . '/**/*.yml'));                $connection->rollBack();}

4. Разделить тесты по типу операции

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

Что необходимо учесть:

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

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

Пример кода
public function tearDown(): void{        parent::tearDown();        // После первого выполненного теста массив команд DB-клиента будет обнулен        // Поэтому в последующие разы фикстуры не будут применяться        self::$onSetUpCommandArray = [];}public static function tearDownAfterClass(): void{        parent::tearDownAfterClass();        self::$onSetUpCommandArray = [            Client::COMMAND_TRUNCATE,            Client::COMMAND_INSERT        ];}

5. Распараллелить выполнение тестов

Тесты это операция, которая явно напрашивается на распараллеливание. И да, в действительности распараллеливание потенциально дает всегда наибольший прирост к производительности. Однако, здесь есть ряд проблем.

Распараллелить выполнение тестов можно как в рамках целого pipelineа, так и в рамках конкретной джобы.

При распараллеливании в рамках pipelineа необходимо просто создать отдельные джобы для каждого набора тестов (Используя testsuite у phpunit). У нас тесты разделены по версии контроллера.

Пример кода
<testsuite name="functional-v2">        <directory>./../../tests/Functional/Controller/Version2</directory></testsuite>
functional-v2:  extends: .template_test  services:    - name: percona:5.7  script:    - sh ./scripts/ci/migrations_dump_load.sh    - ./vendor/phpunit/phpunit/phpunit --testsuite functional-v2 --configuration config/test/phpunit.ci.v2.xml --verbose

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

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

Если подвести итог:

  • Сделать несколько отдельных джоб на уровне CI самый простой способ ускорить прохождение тестов

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

  • Стоит учесть, что из-за накладных расходов (порождение нового процесса, поднятие нового контейнера для тестов) выполнение тестов параллельно может быть медленнее. Если у вас недостаточно свободных раннеров в CI, то такое деление может не иметь смысла.

...

6. Не пересоздавать экземпляр приложения

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

Решением может быть использовать всегда один и тот же инстанс приложения (сохранять его в статическое свойство класса тестов). В этом случае, опять же, тесты перестанут быть изолированными друг от друга, т.к. множество служб из DI-контейнера могут сохранять локальный стейт (например, какое-то кэширование для быстродействия, открытые соединения с БД и т.п.).

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

Пример кода
interface StateResetInterface{    public function resetState();}
$container = self::$app->getContainer();foreach ($container->getKnownEntryNames() as $dependency) {        $service = $container->get($dependency);        if ($service instanceof StateResetInterface) {                $service->resetState();        }}

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

После всех оптимизаций время прохождения в CI для функциональных тестов уменьшилось до 12-15 минут. Я, конечно, сомневаюсь, что описанные выше приемы в их изначальном виде окажутся полезны, но надеюсь, что они вдохновили и натолкнули на собственные идеи!

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

Подробнее..

Каков вопрос таков и ответ, или Как правильно составлять педагогические тесты

29.12.2020 10:06:55 | Автор: admin
Наверняка вы сами хоть раз в жизни составляли тесты и уж точно неоднократно проходили разнообразное тестирование от серьезного до совсем шуточного из серии Какой ты овощ во время карантина?.

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

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

Источник

Вместо предисловия


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

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

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

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

Методику, о которой пойдет речь, разработали ведущие отечественные специалисты в области тестологии: В. С. Аванесов, М. Б. Челышкова, А.Н. Майоров и другие.

В качестве примеров приводятся тестовые задания из открытого банка заданий ЕГЭ, размещенного на официальном сайте ФГБНУ Федеральный институт педагогических измерений, из книги М. Б. Челышковой Теория и практика конструирования педагогических тестов, из книги В. С. Аванесова Композиция тестовых заданий, а также задания, составленные автором статьи и преподавателями разных образовательных организаций.

Сначала немного теории


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

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

Преподаватели используют тесты, чтобы:

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


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

Итак, выполняя закрытые тесты, обычно надо:

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

Открытые тесты требуют:

  • дополнить имеющийся ответ;
  • полностью сконструировать ответ самостоятельно.

Какой материал следует включить в тест


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

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

Инструкция: Выберите один правильный ответ.
Количество ребер

  1. у мужчин больше, чем у женщин
  2. у женщин больше, чем у мужчин
  3. индивидуально у каждого человека
  4. зависит от жизненных обстоятельств

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

Основные правила подбора материала для теста следующие:

  1. Материал, заложенный в тест, должен соответствовать содержанию темы тестирования и программы учебного курса.
  2. В тест включается только то содержание темы, которое является признанным, объективно истинным и поддается рациональной аргументации. Спорные точки зрения в тестовые задания включать не рекомендуется. Суть тестовых заданий заключается как раз в том, что они требуют четкого, заранее известного преподавателям ответа, признанного ими в процессе разработки заданий объективно истинным.
  3. Уровень детализации содержания теста зависит от целей тестирования. Если вы планируете использовать тесты для сравнения уровня подготовки тестируемых в какой-то области (например, для конкурсного отбора соискателей вакансии или кандидатов на обучение), то уровень детализации материала должен быть низким. В заданиях такого теста достаточно отобразить только наиболее значимые элементы содержания.
  4. Если же тесты предназначены для выяснения, насколько успешно испытуемый освоил учебный материал (курс, раздел, тему) и освоил ли он его вообще, то уровень детализации области содержания должен быть довольно подробным. Это позволит сделать вывод о знаниях каждого испытуемого, при необходимости аттестовать его, а также проанализировать, какой материал лучше или хуже освоили испытуемые.
  5. Вместе с тем в тест необходимо включать те элементы содержания, которые можно отнести к наиболее важным, без которых знания по заявленной теме становятся несущественными. Нет смысла перегружать тест второстепенными деталями, не имеющими большого значения.
  6. Если с помощью тестовых заданий планируется оценивать знания по всему учебному курсу, следует равномерно включить в итоговый тест задания по всем изучаемым темам курса, убедившись в том, что они охватывают все самые важные аспекты предметной области и в правильной пропорции.

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


Сколько времени нужно выделить для тестирования


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

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

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

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

Всегда нужно иметь в виду, что испытуемые устают. А значит, тестирование не должно занимать слишком много времени, что напрямую связано с объемом самого теста. Практика показывает, что объем осознанно воспринимаемой информации начинает существенно снижаться примерно через 40-45 минут с начала тестирования (Ким В. С. Тестирование учебных достижений. Уссурийск, 2007, стр. 29-35).

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

  1. Время, затраченное преподавателем на выполнение составленного им теста, умножить на 3.
  2. Суммировать время, необходимое для выполнения каждого задания:

    задание закрытой формы с выбором одного правильного ответа примерно 10 секунд;
    задание более сложных форм в среднем от 30 секунд до 1 минуты.

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

Составление тестовых заданий закрытой формы


А теперь давайте рассмотрим конкретные примеры и поймем, как составлять свои тесты. В фокусе нашего внимания тестовые задания закрытой формы.

В таких тестовых заданиях выделяют следующие элементы:

  • инструкцию (содержит общие требования к выполнению задания рис. 1);
  • основную часть (задание, постановка проблемы рис. 2);
  • варианты ответа (верный ответ(ы) и ответы-обманки рис. 3).


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

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

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

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

Если вы проводите тестирование по старинке, в письменном виде, то вам потребуется ответник, трафарет для проверки ответника и эталон выполнения тестового задания.

Как оформить тестовые задания


Тестовое задание должно быть оформлено таким образом, чтобы облегчить тестируемому работу над ним.

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

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


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

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

  1. Текст задания выделяется полужирным шрифтом (рис. 2). Иногда текст задания пишут прописными буквами, но это too much выглядит слишком громоздко и сильно снижает скорость чтения.
  2. Задания нумеруются арабскими цифрами.
  3. Во избежание путаницы варианты ответов рекомендуется индексировать буквами кириллического или латинского алфавитов (рис. 3).
  4. Варианты ответа пишутся с маленькой буквы (если не представляют собой имена собственные), поскольку являются продолжением тестового задания, сформулированного в виде утверждения. Если задание сформулировано в вопросительной форме, варианты ответа пишутся так же.
  5. Варианты ответа располагают в столбик, причем желательно, чтобы все они были немного сдвинуты вправо относительно текста задания.
  6. Между вариантами ответа и по завершении задания знаки препинания, как правило, не ставятся. Это нарушает правила русской пунктуации, если рассматривать задание как текст, но тестологи мотивируют отсутствие знаков препинания тем, что и без них все части тестового задания графически выделены. Обилие запятых или точек с запятой между вариантами ответов способны помешать восприятию текста задания.
  7. Не рекомендуется перегружать задания вспомогательными словами: Вопрос, Варианты ответов и пр.
  8. В печатном варианте теста каждое задание должно быть расположено целиком на одной странице.
  9. И, конечно же, стиль оформления заданий должен быть единым по всему тесту.



Как написать инструкцию к тестам


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

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

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

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

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

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

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

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

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

Как правильно сформулировать основную часть задания


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

1. Рекомендуется, чтобы задания в тесте были формулированы в форме утверждений, которые после ответов испытуемых естественным образом превратятся в истинные или ложные высказывания (рис. 1).


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

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

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

Пример 1. Задание в вопросительной форме:

Инструкция: Выберите один правильный ответ.
С каким из названных событий связано окончание периода разрядки международной напряженности в 1970-е гг.?

  1. начало Корейской войны
  2. разрыв отношений с Югославией
  3. конфликт с Китаем
  4. ввод советских войск в Афганистан

То же задание в виде логического утверждения:

Окончание периода разрядки международной напряженности в 1970-е гг. связано c

  1. началом Корейской войны
  2. разрывом отношений с Югославией
  3. конфликтом с Китаем
  4. вводом советских войск в Афганистан

Пример 2. Задание в вопросительной форме:

Инструкция: Выберите несколько правильных ответов.
Какие из перечисленных примеров относят к ароморфозам?

  1. возникновение теплокровности у позвоночных
  2. развитие трехкамерного сердца у земноводных
  3. формирование торпедообразного тела у акул
  4. развитие зародыша внутри матки
  5. появление рогов у копытных
  6. формирование крыльев у летучих мышей

То же задание в виде логического утверждения:

К ароморфозам относят

  1. возникновение теплокровности у позвоночных
  2. развитие трехкамерного сердца у земноводных
  3. формирование торпедообразного тела у акул
  4. развитие зародыша внутри матки
  5. появление рогов у копытных
  6. формирование крыльев у летучих мышей

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

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

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

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

2. Текст задания должен содержать достаточную, недвусмысленную и релевантную информацию для ответа.

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

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

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

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

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

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

9. Стоит исключить из формулировки задания частицу не. А если НЕ получается, ее надо выделить прописными буквами (как в этом предложении).

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

11. Рисунки, графики, схемы, используемые в задании, должны соответствовать тексту (рис. 2). И, конечно же, присутствие графических объектов в задании должно быть оправдано. Не стоит добавлять картинку просто для красоты.


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


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

Инструкция: Выберите один правильный ответ.
Невская битва произошла в году.

  1. 1198
  2. 1240
  3. 1242
  4. 1245

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


Как подобрать дистракторы


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

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

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

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

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

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

Инструкция: Выберите один правильный ответ.
Во вращении предплечья наружу участвует

  1. двуглавая мышца плеча
  2. двуглавая мышца бедра
  3. икроножная мышца
  4. прямая мышца живота
  5. собственно жевательная мышца

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

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

Если в предложенном задании мы заменим неработающие дистракторы на правдоподобные, теоретически вероятные (все перечисленные мышцы тем или иным образом двигают предплечье), справиться с ним так просто будет нельзя:

Во вращении предплечья наружу участвует

  1. плечевая мышца
  2. двуглавая мышца плеча
  3. трехглавая мышца плеча
  4. локтевая мышца
  5. круглый пронатор

3. Дистракторы должны быть однородными, подобранными по общему (единому) основанию.

Пример 1: Задание с однородными дистракторами

Инструкция: Выберите один правильный ответ.
Телиоспоры возникают из

  1. урединиоспор
  2. эциоспор
  3. базидиоспор
  4. пикниоспор

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

А в следующем задании дистракторы подобраны неудачно.

Пример 2: Задание с неоднородными дистракторами

Инструкция: Выберите несколько правильных ответов.
Телиоспоры возникают

  1. из урединиоспор
  2. из эциоспор
  3. поздней осенью на том же мицелии, на котором летом формировались урединиоспоры
  4. весной в результате слияния дикариона и последующего мейоза

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

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

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

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

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

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

5. Все повторяющиеся в вариантах ответа слова (как в примере ниже) следует перенести в формулировку заданий.

Инструкция: Выберите один правильный ответ.
Заштрихованная территория на карте России показывает районы

  1. газовых месторождений
  2. нефтяных месторождений
  3. месторождений каменного угля
  4. месторождений калийной соли

Вот что получится после небольшой правки этого примера:

Инструкция: Выберите один правильный ответ.
Заштрихованная территория на карте России показывает районы месторождений

  1. газовых
  2. нефтяных
  3. каменного угля
  4. калийной соли

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

7. Из числа неправильных исключаются ответы, вытекающие один из другого.

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

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

Инструкция: Выберите один правильный ответ.
PDF-файл можно преобразовать в документ Word, если на вкладке Файл выбрать команду

  1. Открыть
  2. Сохранить как
  3. Экспорт
  4. PDF-файл нельзя преобразовать в документ Word

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

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

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

Если цель задания выяснить, знает ли испытуемый, можно ли вообще преобразовать PDF-файл в документ Word или нет, лучше сформулировать вопрос по-другому, без нарушения логики:

Преобразовать PDF-файл в документ Word

  1. можно
  2. нельзя

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

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

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

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

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

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

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

15. Правильный ответ не должен быть длиннее других.

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

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

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

18. Число дистракторов и правильных ответов в разных заданиях может быть разным. Оно не должно быть одинаковым во всех заданиях теста.

Источник

По какому принципу придумывают тестовые задания


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

1. Принцип противоречия применяется при создании заданий с двумя вариантами ответов. Здесь один ответ отрицает другой. Например:

Инструкция: Выберите один правильный ответ.
Поощрения в трудовую книжку

  1. записываются
  2. не записываются

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

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

  1. возрастает
  2. убывает

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

Инструкция: Выберите один правильный ответ.
При движении тягового органа конвейера реле скорости

  1. включено
  2. выключено
  3. заблокировано

3. Тесты, основанные на принципе однородности, содержат ответы, относящиеся к одному роду, виду, или отображают основные стороны, грани явления. Например:

Инструкция: Выберите один правильный ответ.
Основателем андрагогики как науки является

  1. Ноулз
  2. Джарвис
  3. Холтон
  4. Ушинский

Инструкция: выберите несколько правильных ответов.
Буква о пишется в словах

  1. пл_вец
  2. пок_рить вершину
  3. распол_гать
  4. осн_щенный
  5. ум_лять значение

4. Принцип кумуляции означает, что второй ответ вбирает в себя (аккумулирует) содержание первого, третий содержание второго и т.д.

Пример:

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

  1. траекторию
  2. траекторию и закон движения
  3. траекторию, закон движения, начало отсчета
  4. траекторию, закон движения, начало отсчета, скорость

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

5. Принцип сочетания. Используется сочетание слов (знаков) по два или по три, реже четыре, в каждом ответе.

Пример 1: сочетание более или менее однородных и правдоподобных пар ответов

Инструкция: Выберите один правильный ответ.
Ф. Шопен писал

  1. оперы и симфонии
  2. балеты и оратории
  3. мазурки и ноктюрны

Пример 2: сочетание одного слова (понятия) с несколькими другими

Инструкция: Выберите один правильный ответ.
НЕ имеют ядра клетки крови

  1. эритроциты и лейкоциты
  2. эритроциты и тромбоциты
  3. эритроциты и лимфоциты
  4. эритроциты и базофилы

Пример 3: сочетание ответов по правилу цепочки, когда последнее слово в первом ответе становится первым во втором ответе, последнее во втором первым в третьем и т.д.)

Инструкция: Выберите один правильный ответ.
Служебными частями речи являются

  1. предлоги, союзы, частицы
  2. частицы, союзы, местоимения
  3. местоимения, частицы, предлоги

6. При использовании принципа градуирования ответы располагаются по возрастанию (увеличению) или по убыванию (уменьшению). Этот принцип применим для заданий с тремя и большим числом ответов. Например:

Инструкция: Выберите один правильный ответ.
Растворимость газов при повышении температуры

  1. увеличивается
  2. остается без изменений
  3. уменьшается

Согласно ч. 3 ст. 5.63 КоАП РФ, нарушение должностным лицом порядка или сроков рассмотрения жалобы влечет наложение административного штрафа в размере рублей.

  1. от трех тысяч до пяти тысяч
  2. от пяти тысяч до десяти тысяч
  3. от десяти тысяч до пятнадцати тысяч
  4. от двадцати тысяч до тридцати тысяч

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

Инструкция, как мы уже говорили, обычно пишется так: Выберите наиболее правильный ответ.

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

Инструкция: Выберите наиболее правильный ответ.
Зигота это

  1. одна яйцеклетка
  2. оплодотворенная яйцеклетка
  3. диплоидная клетка, образующаяся в результате оплодотворения
  4. одноклеточная стадия развития многоклеточного организма млекопитающих

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

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

Составили тест? Не поленитесь и пройдите по этому чек-листу.

Проверка качества тестовых заданий


  • Соответствует ли задание целям тестирования?
  • Все высказывания логичны?
  • Все формулировки однозначны и лаконичны?
  • Технологичен ли тест? (Задания становятся технологичными, если их содержание точно и быстро понимается испытуемыми, и если форма заданий способствует процессу компьютеризации тестирования)
  • Правильно ли выбрана форма задания?
  • Содержание корректно?
  • Правила оценки ответов разработаны заранее и соответствуют форме теста?
  • Правильно ли расположены элементы задания?
  • Инструкции одинаковы для всех тестируемых?
  • Инструкция соответствует форме и содержанию задания?

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

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

Сложно ли работать QA

19.02.2021 00:11:18 | Автор: admin

Сразу напрашивается вопрос, а кто спрашивает?


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

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

Если уж ударяться в краиности, то дальше воображаем человека лет 50-55, работал где-то, даже программировал , но, например, на чистом C. Новое не изучал, но вот подумал, что надо что-то менять, попробовать. Честно в резюме описываешь опыт , весь сугубо техническии. Но откликов нет, все же понимают, что придется обучать. Опыт привлекает, но возраст отпугивает , и не понимают, сможет ли такои человек обучаться новому и быстро. А тут уж даже если и устроится человек, то простым его путь не вижу.

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

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

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

Не знание какого-то инструмента.

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

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

Одним из сложных моментов я бы ещё назвала неоднозначные формулировки в описании ТЗ (техническое задание, по которому тестируем).

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

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

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

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

Подробнее..

Перевод Код без тестов легаси

26.02.2021 12:14:08 | Автор: admin

Если вы работаете в IT, то о легаси вы слышите часто обычно с множеством негативных коннотаций. Понятно, что это не хороший код, но какой? Может старый, может не поддерживаемый или не обновляемый, а может просто чужой? Есть ли полноценное определение легаси, на которое можно ссылаться? А когда разберемся что нам делать с легаси? Попробуем разобраться. Спойлер: выводы неочевидны.

Автор Николас Карло, веб-разработчик в Busbud (Монреаль, Канада). Специализируется на легаси. В свободное время организует митап Software Crafters и помогает с конференциями SoCraTes Canada и The Legacy of SoCraTes.

Данная статья была скомпилирована (и отредактирована) их двух статей Николаса: What is Legacy Code? Is it code without tests? и The key points of Working Effectively with Legacy Code. Показалось логичным рассказать о том, что такое легаси, а потом как с ним работать.

Что такое легаси?

Возможно, если вы задавались этим вопросом, то встречали определение от Майкла Физерса. Майкл выпустил книгу Working Effectively with Legacy Code в 2004 году, но она до сих пор актуальна. Комикс это отлично иллюстрирует.

В своей книге Майкл пишет своё определение:

Для меня легаси это просто код без тестов.

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

Это хорошее определение: чаще всего тесты отсутствуют, так что это хорошее начало. Но это ещё не всё есть нюансы.

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

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

Перейдём к моему определению легаси.

Легаси это ценный код, который вы боитесь менять.

Например, мы ищем первопричину ошибки или выясняете, куда вставить свою функцию. Мы хотим поменять код, но это трудно, потому что непонятно как не нарушить существующее поведение. Готово у нас легаси!

Но есть нюансы.

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

Хорошие тесты помогают легко менять незнакомый код. А плохие тесты не помогают. Отсюда и определение Физерса.

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

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

А теперь один из важнейших нюансов.

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

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

В итоге мы получаем, что легаси это:

  • код без тестов;

  • который мы пытаемся понять, чтобы отрефакторить;

  • но боимся.

Что же делать?

Как же эффективно работать с легаси?

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

Добавить тесты, а затем внести изменения

Логично, что если добавить тесты, они помогут его прощупать и он перестанет быть устаревшим. Поэтому первое, что нужно сделать написать тесты. Только тогда мы будем в безопасности, чтобы рефакторить код.

Но чтобы запустить тесты, мы должны поменять код. Возникает парадокс легаси. Мы обречены? Нет. Поменяем как можно меньше кода для тестов:

  • Определим точки изменения швы.

  • Разорвём зависимости.

  • Напишем тесты.

  • Внесём изменения.

  • Отрефакторим.

Первые два пункта самые сложные, а как только доберёмся до тестов, мы знаем, что делать.

Найти швы для разрыва зависимостей

Обычно когда мы добавляем тесты к легаси возникает проблема зависимостей: код, который мы хотим протестировать, не может работать, потому что ему нужно что-то сложное для тестирования. Иногда это соединение с базой данных, иногда вызов на сторонний сервер, а иногда параметр, который сложно создать. А чаще всё и сразу.

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

Шов место, где можно изменить поведение программы, не меняя код.

Швы бывают разные. Если это объектно-ориентированный ЯП, то обычно это объект, например, в JavaScript.

export class DatabaseConnector {// A lot of codeconnect() {// Perform some calls to connect to the DB.}}

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

class FakeDatabaseConnector extends DatabaseConnector {connect() {// Override the problematic calls to the DBconsole.log("Connect to the DB")}}

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

Напишем unit-тесты

Дискуссии о лучших практиках тестирования обычно перерастают в холивары. Применять принцип пирамиды тестов, и писать максимум unit-тестов? Или использовать Кубок тестирования и писать в основном интеграционные?

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

Чтобы избежать путаницы, Майкл даёт четкое определение того, что такое НЕ unit-тест:

  • он не работает быстро (< 100ms / test);

  • он взаимодействует с инфраструктурой, например, базой данных, сетью, файловой системой, переменными;

Напишите максимум тестов, которые обладают этими 2 качествами, при этом неважно, как вы их назовёте.

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

Тесты для определения характеристик

Это тесты, которые формализуют фактическое поведение части кода.

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

Это мощная техника, потому что:

  • В большинстве систем то, что код делает важнее того, что он должен делать.

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

Этот метод также называют Approval Testing (тестированием одобрения), Snapshot Testing или Golden Master.

Но обычно на всё это очень мало времени.

Когда совсем нет времени на рефакторинг

Несколько советов, если предыдущие не подходят.

Большие куски кода обладают гравитацией и привлекают ещё больше кода. Теория разбитых окон в действии: небольшой беспорядок влечёт за собой беспорядок серьёзнее. Если класс уже содержит 2000 строк, то какая разница, что вы добавите еще 3 if оператора и будете поддерживать класс длиной в 2010 строк?

Это всего лишь 3 if: тяжело себя убедить, что нужно потратить на них 2 дня, хотя и должны. Что делать, если действительно нет времени писать тесты для этого класса? Используйте техники Sprout (прорастание), Wrap (обёртывание) и скретч-рефакторинг.

Sprout

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

Рассмотрим на примере:

class TransactionGate {//  a lot of codepostEntries(entries) {for (let entry of entries) {entry.postDate()}//  a lot of codetransactionBundle.getListManager().add(entries)}//  a lot of code}

Допустим, нам нужно убрать дубли файла entries, но postEntries() трудно проверить нет на это времени. Мы можем прорастить код где-то ещё, например, в новом методе uniqueEntries(). Этот новый метод легко протестировать, потому что он изолирован. Затем вставим вызов этого метода в существующий, не проверенный код.

class TransactionGate {//  a lot of codeuniqueEntries(entries) {// Some clever logic to dedupe entries, fully tested!}postEntries(entries) {const uniqueEntries = this.uniqueEntries(entries)for (let entry of uniqueEntries) {entry.postDate()}//  a lot of codetransactionBundle.getListManager().add(uniqueEntries)}//  a lot of code}

Минимальные изменения, минимальный риск. Можете вырастить один метод, целый класс или что-то ещё, что изолирует новый код.

Wrap

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

  • Переименуем старый метод, который хотим обернуть.

  • Создадим новый с тем же именем и подписью, что и старый.

  • Вызовем старый метод из нового.

  • Поместим новую логику до/после вызова другого метода.

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

class TransactionGate {//  a lot of codepostEntries(entries) {for (let entry of entries) {entry.postDate()}//  a lot of codetransactionBundle.getListManager().add(entries)}//  a lot of code}

Ещё один способ решить эту проблему это обернуть её, поэтому мы переходим к postEntries(), списку записей, из которых мы удалили дубли.

class TransactionGate {//  a lot of codepostEntries(entries) {// Some clever logic to retrieve unique entriesthis.postEntriesThatAreUnique(uniqueEntries)}postEntriesThatAreUnique(entries) {for (let entry of entries) {entry.postDate()}//  a lot of codetransactionBundle.getListManager().add(entries)}//  a lot of code}

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

class TransactionGate {//  a lot of code+ postEntries(entries) {+  // Some clever logic to retrieve unique entries+  this.postEntriesThatAreUnique(uniqueEntries)+ }+ postEntriesThatAreUnique(entries) {- postEntries(entries) {for (let entry of entries) {entry.postDate()}//  a lot of codetransactionBundle.getListManager().add(entries)}//  a lot of code}

Эти методы не идеальны, и у них есть недостатки. Но это полезные инструменты при работе с легаси. А при необходимости можно даже немного нарушить правила.

Скретч-рефакторинг

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

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

Выводы

Легаси будет везде, где бы вы ни работали, в каждой кодовой базе. Можно сопротивляться и чувствовать себя плохо, когда вы застряли в нём. А можно рассматривать это как возможность. Работа со старым кодом это очень ценный навык, его надо изучать теоретически (почитайте книгу Working Effectively with Legacy Code) и практиковать в ежедневных задачах.


Похожие и интересные статьи:

Больше новостей про разработку в Додо Пицце я пишув канале Dodo Pizza Mobile. Также подписывайтесь начат Dodo Engineering, если хотите обсудить эту и другие наши статьи и подходы, а также на каналDodo Engineering, где мы постим всё, что с нами интересного происходит.

А если хочешь присоединиться к нам в Dodo Engineering, то будем рады сейчас у нас открыты вакансииiOS-разработчиков(а ещё для Android, frontend, SRE и других).

Подробнее..

Обработка оценок за тесты в Google Forms

13.02.2021 06:17:09 | Автор: admin

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

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


Ответы можно просматривать в интерфейсе самой Формы во вкладке Responses. Кроме того, ответы можно выгрузить в таблицу Google Sheet. Таблица выглядит следующим образом:

Интерес представляет столбец Score. В нем в виде дроби представлена информация о набранных баллах и максимальном количестве баллов за тест. Сложность состоит в том, что физически в ячейке записано только число набранных баллов, а строка "/ 2" является частью Custom Number Format. Чрезвычайно удобная функция IMPORTRANGE(), позволяющая вставить заданный диапазон на другой лист или даже в другую таблицу, успешно копирует этот формат для каждой ячейки. А вот функция QUERY() - нет. Информация о максимальном количестве баллов за тест в некоторых случаях теряется.

Итак, пусть у нас есть три Формы: Test 1, Test 2 и Test 3. Первым вопросом в тестах идет "Full Name". По этому полю мы будем идентифицировать учащихся. Для трех тестов есть три таблицы с ответами: Test 1 (Responses), Test 2 (Responses), Test 3 (Responses). Первые три столбца в каждой таблице одинаковые: Timestamp, Score, Full Name. Далее идут ответы на вопросы теста, которые нам не понадобятся.

Создадим новый документ Googe Sheet. Назовем его, например, Grade Book. Нам понадобится по одному листу на каждый тест (T1, T2, T3), лист Source, чтобы собрать всё вместе, и лист Grade Book для сводной таблицы.

Листы в документе "Grade Book"Листы в документе "Grade Book"

Листы T1, T2, T3...

Импортируем на листы T1, T2, T3 первые три столбца из таблиц ответов. Для этого в ячейку A1 на каждом листе вставим формулу:

=IMPORTRANGE("https://docs.google.com/spreadsheets/d/1dee7GYwj1NgZfDNZJgLMVOcWRmPnvSAvg3KJ0ahqkmI","Form Responses 1!A2:C")

Где "https://..." - это URL таблицы "Test X (Responses)", который можно скопировать из адресной строки браузера, "Form Responses 1" - название Листа с результатами теста, "A2:C" - диапазон ячеек, который мы хотим скопировать (заголовок игнорируем).

Теперь нужно разделить значения из колонки "Score" на два значения: количество набранных баллов и максимальное количество баллов в тесте. Для этого в ячейку D1 поместим формулу:

=ArrayFormula(split(B1:B, "/"))

Теперь в колонке D хранится количество баллов за тест, а в колонке E - максимальное количество баллов за тест.

Лист "T1"Лист "T1"

Лист Source

Лист Source будет чем-то вроде таблицы базы данных, в которую мы соберем ответы на все тесты добавив ещё один столбец - идентификатор теста. Затем уже можно будет пересчитать баллы в оценки и немого причесать поле Full Name.

В ячейку A1 на Листе "Source" вставим формулу:

=QUERY({QUERY('T1'!A1:E, "SELECT 'T1', A, B, C, D, E WHERE A IS NOT NULL LABEL 'T1' ''");QUERY('T2'!A1:E, "SELECT 'T2', A, B, C, D, E WHERE A IS NOT NULL LABEL 'T2' ''");QUERY('T3'!A1:E, "SELECT 'T3', A, B, C, D, E WHERE A IS NOT NULL LABEL 'T3' ''")}, "SELECT * ")

Где T1, T2, T3 - названия Листов, куда мы импортировали данные из таблиц ответов, SELECT 'T1'... - это необходимо, чтобы добавить в каждую строку идентификатор теста.

В ячейку G1 на листе "Source" добавим формулу для пересчета баллов в оценки по пятибалльной шкале:

=ArrayFormula(E1:E/F1:F*5)

А в ячейку H1 добавим формулу, чтобы вырезать из Full Name только первое слово. По опыту студенты пишут свою фамилию каждый раз одинаково, а дальше пишут то имя, то имя и отчество, то инициалы. Чтобы связать несколько ответов одного студента вместе в моём случае оказалось достаточно вырезать фамилию.

=ArrayFormula(INDEX(SPLIT(D1:D, " "), 0, 1))

Лист "Source" будет выглядеть следующим образом:

Лист Grade Book

В ячейку A1 на листе "Grade Book" вставим следующую формулу:

=QUERY(Source!A1:H,"SELECT H, MAX(G) WHERE C IS NOT NULL GROUP BY H PIVOT A")

Где Source - лист, с которого брать данные, MAX(G) - максимальная оценка из всех попыток каждого студента сдать тест, PIVOT(A) задает столбец для колонок сводной таблицы, в нашем случае - идентификатор теста.

Вот и готова таблица с оценками:

Лист "Grade Book"Лист "Grade Book"

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

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

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru