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

Тестирование веб-сервисов

AEM Test Automation Create Pages via HTTP Requests

28.04.2021 20:06:12 | Автор: admin

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

AEM - content management system от Adobe (как выразилась коллега - WordPress на стероидах). Что это значит? Мы не создаем непосредственные веб страницы, а работаем над сложной админкой (AEM-author), которая в будущем позволит контенщикам (Editors) создавать эти страницы, используя набор определенных компонентов. Эти страницы будут видны (после publish действия) конечным юзерам (AEM-publish). Так что наша работа, собственно, и заключается в создании этих компонент.

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

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

  1. Screenshots tests Поскольку AEM content management system => Значит наши стили, да и вообще весь фронт очень важен, ведь это то с чем сталкивается конечный пользователь (возможно расскажу об этом в другой статье).

  2. Web-component tests самые обычные UI тесты с использованием Cypress в качестве основы. Только проверялись не страницы, а компоненты.

  3. Web Performance tests мониторинг производительности наших Web-страниц (с помощью Sitespeed)

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

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

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

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

И так какой же выход?

А что если мы будем создавать страницы на лету?

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

Long story short

К сожалению, АЕМ не предоставляет никакой информации о своем API. А если такая информация где-то и есть, я не смог ее найти. Да и тестирование АЕМ-а минимально описанная головная боль (Тестируете АЕМ? Делитесь в комментариях как).

Так что последующие выводы целиком и полностью reverse engineering построенный на запросах отправленных фронтом (AEM-author) на бек.

В моем случае, и я уверен в подавляющем большинстве случаев, - создание страниц в АЕМ-е сведется к следующим действиям:

  1. Создание страницы с помощью темплейта (page template)

  2. Добавление компонент на страницу

  3. Конфигурация компонент

  4. Паблиш страницы

    И, конечно, мы будем:

  5. Удалять страницы


Приступим...

Нам понадобиться dev-tools на вкладке Network.

1. Создание страницы с помощью темплейта

  1. Открываем AEM-author

  2. Sites

  3. Переходим в нужную нам папку

  4. Жмякаем Create

  5. Выбираем нужный нам template

  6. Заполняем интересующие нас поля

  7. Запускаем запись Network запросов в dev-tools

  8. Нажимаем Create

Первый же запрос `${aem-author-URL}/libs/wcm/core/content/sites/createpagewizard/_jcr_content` будет содержать всю необходимую нам информацию.

Тут мы сталкиваемся с первой сложностью. Это POST запрос с FormData. Т.е. запрос не содержит привычный многим тестировщикам body в виде JSON/XML объекта. Вместо этого данные отправляются как Content-Type: application/x-www-form-urlencoded.

Во многих случаях любой JS объект можно легко перевести в данный формат (по-сути, это будет простая url encoded строка key=value записанная через &). Правда, тут нужно быть осторожными, поскольку данный формат не подразумевает, что ключ (key) является уникальным. Т.е. ваша Form Data может содержать tags=Tag1&tags=Tag2 (несколько тегов у страницы, в моем случае).

[Request Example] URL encoded FormData[Request Example] URL encoded FormData[Request Example] Parsed FormData[Request Example] Parsed FormData

Самое важное на что стоит обратить внимание на этих скриншотах:

  • parentPath папка/страницв в AEM-e, в которой будет создана наша страница

  • template путь к теплейту, который будет использован для создания страницы

Ниже идут поля, применимые к моему проекту

  • ./jcr:title имя/тайтл моей страницы (обязательное поле на UI)

  • ./cq:tags тег, который я добавил странице (опциональное поле)

  • ./articleDate, ./articleTimeToRead и :cq_csrf_token

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

Теперь, на счет авторизации. Как видно выше, запросы содержат token. Я использовал cypress.io для написания автотестов, так что для авторизации API запросов просто указывал auth объект с username и password, на ряду с методом, хедером и body. (For more info check Cypress: Request - arguments and http-authentication).

Реализация отправки POST запроса с FormDataРеализация отправки POST запроса с FormData

key takeaways: все запросы на создание новых страниц идут на `${aem-author-URL}/libs/wcm/core/content/sites/createpagewizard/_jcr_content`, parentPath и template присутствуют для всех вариантов создания страниц с помощью темплейта.


2. Добавление компонент на страницу

  1. Находим нужную нам страницу в AEM author

  2. Жмякаем Edit

  3. Выбираем нужную позицию (место) для компоненты

  4. Жмякаем +

  5. Запускаем запись Network запросов в dev-tools

  6. Находим и выбираем нужный нам компонент

Нужный нам запрос `${aem-author-URL}/content/${page-path}/jcr:content/par/${some-url-part}/par/`.

[Request Example] Add Component to the page[Request Example] Add Component to the page

Из важного тут:

  • ./@CopyFrom темплейт (default) конфигурации компоненты (button в моем случае)

  • ./sling:resourceType название и путь к компоненте, которую я добавлял

  • parentResourceType тут я не уверен, судя по всему место, куда добавить компоненту


3. Конфигурация компонент

  1. На нужной нам странице выбираем необходимый компонент

  2. Жмякаем гаечный ключ

  3. Модифицируем конфигурацию данной компоненты

  4. Запускаем запись Network запросов в dev-tools

  5. Жмякаем кнопку Done

Наш запрос первый в списке `${aem-author-URL}/content/${page-path}/_jcr_content/par/${component-name}`.

[Request Example] Configure Component[Request Example] Configure Component

Из важного тут:

  • ./sling:resourceType название и путь к компоненте, которую я добавлял

  • :cq_csrf_token токен, значит нужно использовать auth


4. Паблиш страницы

  1. Находим нужную нам страницу

  2. Запускаем запись Network запросов в dev-tools

  3. Жмякаем Quick Publish -> Publish

4.1.

В данном случае нам нужны первые 2 запроса.

4.1.1. Получение связанных ассетов

Запрос reference.json `${aem-author-URL}/libs/wcm/core/content/reference.json?${url-params}` получаем информацию о ассетах (assets), cвязаных с нашей страницей.

[Request Example] Check Assets related to the published page[Request Example] Check Assets related to the published page

Из важного тут используются query string params. В path указан путь к нашей странице.

В ответе нам придет массив ассетов. Нам понадобятся path`s тех, чей published статус false.

[Responce Example] Check Assets[Responce Example] Check Assets

4.1.2. Публикация страницы и ассетов

Запрос replicate `${aem-author-URL}/bin/replicate` запрос на публикацию нашей страницы и связанных с ней сущностей.

[Request Example] Publish Page and Related Assets[Request Example] Publish Page and Related Assets

Как вы видите, основное на что стоит обратить внимание:

  • cmd: Activate команда для паблиша страницы

  • path пути к нашей странице и к 2м ассетам

4.2.

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

Для этого нам понадобиться еще один запрос GET `${aem-author-URL}/etc/replication/agents.author/publish_publish/jcr:content.queue.json`. Он вернет нам массив страниц ожидающихпаблишинга. Так что придётся сделать, как минимум еще один запрос, проверить есть ли необходимая нам страница в массивеbody.queue.Опять же искать по path. Если страница все еще присутствует в очереди, придётся повторить проверку один или даже несколько раз (я выставил timeout в 1 секунду, но думаю можно и меньше).


5. Удаление страницы

  1. Находим нужную нам страницу

  2. Выбираем ее

  3. Запускаем запись Network запросов в dev-tools

  4. Жмякаем Delete-> Delete

Наш запрос `${aem-author-URL}/bin/wcmcommand`.

[Request Example] Delete Page[Request Example] Delete Page

Из важного тут:

  • cmd deletePage

  • path путь к странице

  • force: false но я бы рекомендовал ставить true (дабы при удалении не происходило дополнительных проверок)

  • checkChildren: true можно опустить


Итак, подведем итог

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

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

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

  • AEM заменяет пробелы на `-`. То есть если вы создали страницу с Тайтлом `Bla 1 2 3 4` и не указали специфический путь к ней, тогда АЕМ сделает эту страницу доступной по пути /bla-1-2-3-4

  • Пути страниц всегда будут в lowerCase (см пред. пример)

  • При использовании `_` в тайтле начиная с (приблизительно с 18и символов) АЕМ удалит все последующие `_`. Те если вы создали страницу с тайтлом BlaBla123456789123456_blabla, то доступ к ней будет не по /blabla123456789123456_blabla, а по /blabla123456789123456blabla

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

Подробнее..

Перевод Как проводить сквозное(end-to-end) тестирование вашего приложения используя Cypress.io

04.05.2021 08:16:25 | Автор: admin
Изображение от https://unsplash.com/@kellysikkemaИзображение от https://unsplash.com/@kellysikkema

В этой статье вы узнаете:

  • Что такое Cypress и когда его стоит использовать

  • Основы тестирования с использованием Cypress

  • Расширенные команды Cypress

  • Взаимодействие с элементами пользовательского интерфейса

  • Лучшие практики с использованием Cypress


Введение

Чтобы протестировать свои приложения, вам потребуется сделать следующие шаги:

  • Запустить приложение

  • Подождать пока сервер запустится

  • Провести ручное тестирование приложения(нажать на кнопки, ввести случайные текст в поля ввода или отправить форму)

  • Проверить, что результат вашего теста корректен(изменения заголовка, части текста и т.д.)

  • Повторить эти шаги ещё раз после простых изменений кода

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

Именно здесь в игру вступает Cypress. При использовании Cypress единственное, что вам нужно сделать, это:

  • Написать код вашего теста(нажатие на кнопку, ввод текста в поля ввода и т.п.)

  • Запустить сервер

  • Запустить или перезапустить тест

Только и всего!Библиотека Cypress выполняет все тесты за вас.И самое приятное, что она не только сообщает вам все ли ваши тесты успешны или нет, но также сообщает вам, какой тест не удался.

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

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


Начало

Установка и настройка Cypress

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

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

Наконец, чтобы установить библиотеку Cypress:

Установка CypressУстановка Cypress

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

Теперь, когда Cypress установлен, попробуйте запустить его с помощью следующей команды:

Открытие CypressОткрытие Cypress

Она открывает запускалку тестов(Test Runner):

Интерфейс Test RunnerИнтерфейс Test Runner

А теперь давайте перейдём к написанию тестов.


Основы Cypress

Создание файла

Cypress требует, чтобы все наши тесты находились вкаталоге cypress/integration.Сначала перейдите в этот каталог:

Переход к cypress/integrationПереход к cypress/integration

Теперь создайте файл JavaScript с именемbasicTest.js:

Создание JavaScript файлаСоздание JavaScript файла

Если вы не отключили сервер Cypress, ваши новые файлы появятся в Test Runner в реальном времени:

Обновление структуры файлов в реальном времениОбновление структуры файлов в реальном времени

Теперь давайте напишем наш первый тест.

Простые тесты с утверждением и ожиданием значения

В вашем файле /cypress/integration/basicTest.js напишите следующий код:

Код к файлу basicTest.jsКод к файлу basicTest.js
  • Строка 1:Функция describe сообщает Cypress название набора наших тестов.

  • Строка 2: Функцияit, обозначает название теста.

  • Строка 3: Создаём утверждение.Здесь мы подтверждаем, что 2 + 2 равно 4. Если тест вернётfalse, то он будет немедленно остановлен.

Чтобы запустить вашу программу, щёлкните поbasicTest.js в вашем сервере Cypress.

Щелчок по basicTest.js в Test RunnerЩелчок по basicTest.js в Test Runner

Результат запуска:

Результат запуска тестаРезультат запуска теста

Отлично!Значит, наше утверждение было успешным.

Что, если мы сделаем заведомо ложное утверждение?Теперь в/cypress/integration/basicTest.js добавьте следующий код впределах функцииdescribe:

Код для добавление в basicTest.jsКод для добавление в basicTest.js
  • Строка 2: Если сумма 4 и 5 равна 10, тест будет пройден. В противном случае, незамедлительно остановлен.

Снова запустите код.Результат будет:

Результат нашего второго тестаРезультат нашего второго теста

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

Давайте больше поиграем с утверждениями.Добавьте в basicTest.jsследующий код:

Код для добавления в basicTest.jsКод для добавления в basicTest.js
  • Строка 2: Если сумма 5 и 5неравна 100, то тест должен пройти.

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

Результат теста: успешно!Результат теста: успешно!

Отлично!Наш тест прошел. Функцияexpect выполняетBDD (behavior-driven) утверждения.В следующем разделе мы выполним утверждения, основанные на тестировании(test-driven assertions).

Сейчас/cypress/integration/basicTest.jsдолжен выглядеть так:

Написание утверждений основанных на тестировании(test-driven assertions) с явным использованием assert

Мы даже можем писать утверждения на основе TDD с использованиемassert.

В вашем файлеbasicTest.js напишите следующий код:

  • Строка 2: Создаём объект со свойствамиnameи age.

  • Строка 6: ФункцияisObjectподтверждает,что переменнаяpersonявляется объектом.Если результатtrue, то будет напечатаноvalue is object.В противном случае будет показано, что этот тест не прошел.

  • Строка 10: Убеждаемся, что переменнаяnameсодержитстроковое значение.

  • Строка 14: Убеждаемся, что переменнаяname неявляется целым числом.

Запустите код.Результатом будет:

Результат запуска нашего тестаРезультат запуска нашего теста

Отлично!Наш код работает.В следующем разделе мы научимся работать с сайтами через Cypress.

Сейчас нашbasicTest.jsдолжен выглядеть так:

Запуск веб-сайтов

Здесь мы попробуем запуститьDemoblaze, сайт, созданный для проведения тестов.

В своей папке/cypress/integration/ создайте файл с именемbasicCommandsTest.js.В этом файле напишите следующий код:

Код для basicCommandsTest.jsКод для basicCommandsTest.js
  • Строка 3: Используем методvisit, чтобы сообщить Cypress о переходе на веб-сайт Demoblaze.

Сохраните свой код и нажмите наbasicCommandsTest.js в меню Test Runner:

Клик по basicCommandsTest.js вTest RunnerКлик по basicCommandsTest.js вTest Runner

Результат запуска:

Отлично!Наш код работает.В следующем разделе мы более глубоко погрузимся в тестирование с помощью Cypress.

В итогеbasicCommandsTest.jsдолжен выглядеть так:


Cypress: Расширенные команды

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

Как идентифицировать элементы

Cypress используетселекторы JQueryдля идентификации компонентов на веб-странице.

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

Получение элемента через id элементаПолучение элемента через id элемента

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

Получение элемента через имя классаПолучение элемента через имя класса

Давайте теперь поработаем с взаимодействием с пользовательским интерфейсом нашего сайта.

Нажатие кнопки

В этом разделе мы будем использоватьстраницу The-Internetдля запуска наших тестов.На этом веб-сайте мы будем использоватьразделдобавления/удаленияэлементов.

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

Страница для тестированияСтраница для тестирования

Используя DevTools, заметьте, что уbuttonесть свойствоonclick, имеющее значениеaddElement().

Скриншот из DeveloperToolsСкриншот из DeveloperTools

Соответствующий селектордля этой кнопки будет выглядеть так:

Идентификация элементаИдентификация элемента

В папке/cypress/integration создайте файл с именемrunningClickCommand.js.В этом файле напишите следующий код:

  • Строка 2: Переходим на веб-страницу.

  • Строка 6: Связываем в одну цепочку получение элемента button и нажатие на эту кнопку.

Запустите код.Результат:

Вывод результатаВывод результата

Отлично, наш код работает!Обратите внимание, что как только страница загрузилась, в нашем тесте автоматически происходит нажатие на кнопкуAdd Element.

Давайте теперь поработаем с вводом текста в текстовое поле.

Ввод текста

В этом разделе мы будем использоватьстраницуThe-Internets login.Нам нужен способ сначала идентифицировать элементы.

Скриншот сайта для тестированияСкриншот сайта для тестированияСкриншот из DeveloperToolsСкриншот из DeveloperToolsСкриншот из DeveloperToolsСкриншот из DeveloperTools

Поле username имеетid равноеusername, а полеpassword имеетid равноеpassword.Кроме того, кнопка Login имеет свойствоtype равноеsubmit.Таким образом, для определения полейusername иpassword, нам понадобитсяселектор JQuery id:

Идентификация элемента через его idИдентификация элемента через его id

Более того, чтобы получить кнопкуbutton, нам понадобитсяселектор атрибутов, например:

В своей папке/cypress/integration создайте файл с именемrunningTypeCommand.js.В этом файле напишите следующий код:

  • Строка 3: Переходим на страницу входа в систему.

  • Строка 6: Переходим в полеusername и добавляем методtype вцепочку вызовов, чтобы напечатать в этом текстовом поле значениеtomsmith.

  • Строка 7: Переходим в полеpassword и вводимSuperSecretPassword.

  • Строка 10: Нажимаем на кнопку Отправить.

Запустите код.Результатом будет:

Вывод результата запуска кодаВывод результата запуска кода

И мы закончили!В следующем разделе мы узнаем о работе с чекбоксами.

Переключение чекбоксов

В этом разделе мы будем использоватьраздел чекбоксов на странице The-Internet.

Давайте сначала посмотрим на DevTools:

Developer ToolsDeveloper Tools

Оба этих чекбокса имеют свойствоtypeсо значениемcheckbox.Кроме того, они также являются дочерними элементамидля элементаform сid равнымcheckboxes.В этом случае мы бы использовалиселектор JQuery родитель-потомок:

Идентификация наших чекбоксовИдентификация наших чекбоксов

В каталоге/cypress/integration/ создайте файл с именемrunningCheckCommand.js и напишите следующий код:

  • Строка 4: Находим обе группы чекбоксов, а затем используем методcheck, чтобы отметить их выбранными.

  • Строка 7: Просим Cypress приостановить процесс тестирования на одну секунду.

  • Строка 8: Получаем список отмеченных чекбоксов.Затем используем методuncheck, чтобы снять выбор.

Запустите код.Результат:

Результат запуска тестаРезультат запуска теста

Отлично!Наш код работает.Давайте теперь поработаем над неявными утверждениями с помощью Cypress.

Неявные утверждения

Ранее мы выполняли утверждения для переменных и объектов.Однако в реальном мире мы хотели бы выполнять утверждения для текста, расположенного в нашем элементе HTML, или проверять, есть ли у нашего элементаul дочерние элементыli или нет.

Для выполнения таких утверждений мы будем использовать ключевое словоshould.В этом разделе мы будем делать неявные утверждения настраницедобавления элемента The-Internets Add Element

Скриншот тестируемой страницыСкриншот тестируемой страницыDeveloper ToolsDeveloper Tools

Наша кнопкаDelete имеет классadded-manually.Мы хотим выполнить следующее утверждение для этого элементаbutton:

Наше утверждениеНаше утверждение

Для этого мы должны использовать следующий синтаксис:

Получение элемента и за тем утверждениеПолучение элемента и за тем утверждение

Альтернативно, можем выполнить такое утверждение:

Наше утверждениеНаше утверждение

Мы можем использовать эту строку кода:

Получение элемента и затем утверждениеПолучение элемента и затем утверждение

Перейдите в/cypress/integration/runningClickCommand.jsи добавьте следующий код:

Код для runningClickCommand.jsКод для runningClickCommand.js
  • Строка 1: Получаем элементы с классомadded-manually.Затем проверяем, что их количество(have.length)равно единице.

  • Строка 3: Получаем кнопкуAdd Element, а затем проверяем, что текст на кнопке(have.text)действительно будетAdd Element.

Запустите код.Результат в конце теста должен быть следующим:

Результат запускаРезультат запуска

Отлично!Наш код работает.Теперь перейдем к изучению командыeach.

В итогеcypress/integration/runningClickCommand.js должен выглядеть так:

Команда each

Взглянитееще раз наThe-Internets Add Elements page:

Скриншот тестового сайтаСкриншот тестового сайта

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

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

Откройте Developer Tools:

Все наши кнопкиDelete имеют свойствоclass равноеadded-manually.В этом случае мы будем использоватьселектор классови соединять его с командойeach, например:

Получение элемента и использование each затемПолучение элемента и использование each затем

Перейдите в/cypress/integration/runningClickCommand.jsи добавьте следующий фрагмент кода:

Код для runningClickCommand.jsКод для runningClickCommand.js
  • Строка 2: Получаем все элементы, у которых есть класс.added-manually.Каждый элемент в серии будет представлен параметром$el.

  • Строка 3: Обёртываем этот элемент, чтобы мы могли выполнять с ним команды Cypress.Здесь мы отправляем команду щёлкнуть по этим элементам.

Результат выполнения кода должен быть следующим:

Результат выполнения кодаРезультат выполнения кода

Наш код работает!Поскольку наш тест был быстрым, давайте попробуем добавить на страницу больше элементов.

Найдите следующий фрагмент кода:

Измените это так:

  • Строка 2: Запускаем цикл, чтобы сообщить Cypress, что нужно нажать кнопкуAdd Element 20 раз.

Запустите код еще раз.Результат должен быть таким:

Результат выполнения кодаРезультат выполнения кода

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

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

В итогеcypress/integration/runningClickCommand.jsдолжен выглядеть так:


Лучшие практики

Держите тесты изолированными

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

Не самая лучшая структураНе самая лучшая структура

В вашем Test Runner это будет выглядеть так:

Отображение тестовой структуры в Test RunnerОтображение тестовой структуры в Test Runner

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

Хорошая структура проектаХорошая структура проекта

Следовательно, это выглядело бы так:

По возможности используйте собственные команды

Взгляните на этот фрагмент кода:

Пример кодаПример кода

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

В вашемcypress/support/commands.jsнапишите этот код:

  • Строка 1: Создаём собственную команду, которая будет иметь два параметраidentifier иdata.

  • Строка 2: Получаем элемент с соответствующим идентификатором, а затем вводим в него данные.

Примечание. Считается хорошей практикой записывать свои собственные команды в файл/cypress/support/commands.js.

Теперь вернитесь в свой тестовый файл и замените его вот так:

Пример кодаПример кода

Как видите, наш код выглядит значительно короче.

Избегайте атомарных тестов

Взгляните на этот фрагмент кода:

Здесь мы повторно выполняем тесты на HTML элементе сid равнымfirst.

Cypress не одобряет такого поведения.Это неэффективно, и есть способ лучше переписать этот код, например:

Мы можем использовать методand для связывания дополнительных командshould с нашим элементом.

Не запускайте сервер в Cypress

Команда execприсутствует для запуска команд в терминале.Но запускать сервер с помощью этой команды крайне не рекомендуется.

Если вы хотите протестировать свое приложение наlocalhost, сначала запустите сервер,а затемзапустите свой тест Cypress.

Команды терминалаКоманды терминала

Репозиторий GitHub и дополнительные ресурсы

Код GitHub

Дальнейшее чтение


Заключение

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

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

Подробнее..

Живые инеживые коллекции вJavaScript

06.05.2021 10:07:53 | Автор: admin

Найти несколько DOM-элементов иполучить кним доступ изJavaScript можно разными способами:querySelectorAll,getElementsByTagName,childrenитак далее. Витоге вкаждом случае будет возвращенаколлекция сущность, которая похожа намассив объектов, нопри этом имнеявляется, насамом деле это набор DOM-элементов. Стоит учесть, что фактически разные методы возвращают разные коллекции:

  • HTMLCollectionколлекция непосредственно HTML-элементов.

  • NodeListколлекция узлов, более абстрактное понятие. Например, вDOM-дереве есть нетолько узлы-элементы, нотакже текстовые узлы, узлы-комментарии идругие, поэтомуNodeListможет содержать другие типы узлов.

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

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

Разница между живыми инеживыми коллекциями

Допустим, вразметке есть список книг:

<ul class="books">    <li class="book book--one"></li>    <li class="book book--two"></li>    <li class="book book--three"></li></ul>

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

const booksList = document.querySelector('.books');const liveBooks = booksList.children;// Выведем все дочерние элементы списка .booksconsole.log(liveBooks);
const notLiveBooks = document.querySelectorAll('.book');// Выведем коллекцию, содержащую все элементы с классом bookconsole.log(notLiveBooks);

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

const booksList = document.querySelector('.books');const liveBooks = booksList.children;// Удалим первую книгуliveBooks[0].remove();// Получим 2console.log(liveBooks.length);// Получим элемент book--two, который теперь стал первым в коллекцииconsole.log(liveBooks[0]);
const notLiveBooks = document.querySelectorAll('.book');// Удалим первую книгуnotLiveBooks[0].remove();// Получим 3console.log(notLiveBooks.length);// Получим ссылку на удалённый элемент book--oneconsole.log(notLiveBooks[0]);

Впервом случае информация околичестве элементов внутри коллекции автоматически обновилась после удаления одного элемента изDOM эта коллекция живая. Вовтором случае впеременнойnotLiveBooksхранится первоначальное состояние коллекции, которое было актуально намомент вызова методаquerySelectorAll. Эта коллекция неживая, она ничего незнает обизменении DOM. При этом доступна ссылка наудалённый элементbook--one, которого фактически больше нет вDOM.

Другие способы получить коллекцию

КромеchildrenиquerySelectorAllесть другие способы поиска DOM-элементов:

  • getElementsByTagName(tag)находит все элементы сзаданным тегом,

  • getElementsByClassName(className)находит все элементы сзаданным классом,

  • getElementsByName(name)находит все элементы сзаданным атрибутомname.

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

Как использовать

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

Структура инекоторые свойства коллекции имеют много общего смассивом. Например, унеё тоже есть свойствоlength, иэлементы коллекции можно перебирать вциклеfor...of, потому что это перечисляемая сущность. Но, как упоминалось ранее, коллекции невовсём похожи наобычные массивы. Сколлекциями неработают такие методы массивов, какpush,spliceидругие. Для ихиспользования нужно преобразовать коллекцию вмассив например, спомощью методаArray.from:

const booksList = document.querySelector('.books');const books = booksList.children;// Выведет обычный массив с элементами из коллекции booksconsole.log(Array.from(books));

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


В HTML Academy есть курсы для опытных разработчиков по анимациям для фронтендеров, вёрстке email-рассылок и Vue.js. Приходите, чтобы улучшить навыки и узнать много нового о веб-разработке а там со всеми этими знаниями и до сеньора недалеко.

Подробнее..

Как я решил протестировать нагрузочную способность web сервера

20.04.2021 10:21:38 | Автор: admin

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

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

И так дано - web сервер. Написан на .net core. Сервер используется в корпоративной разработке.

Посмотреть, как работает можно, например здесь бесплатный сервис хранение ссылок http://linkin.link. Про него я писал тут http://personeltest.ru/aways/habr.com/ru/users/developer7/posts

Вступление

Собственно, как тестировать web сервер? Если посмотреть на проблему в лоб то веб сервер должен отдавать все страницы, которые были запрошены клиентами. И желательно отдавать быстро.

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

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

Задача поставлена. Тестировать решил так. На одной машине под windows 10 запускаю web сервер. На web сервере запущен сайт. На сайте размещено куча js, сss, mp4 файлов и, собственно, html страничка. Для простоты я просто взял страницу из готового сайта.

Чем досить сервер? Тут 2 пути скачать что-то готовое или написать свой велосипед. Я решил остановится на втором варианте. И этот выбор я сделал по нескольким причинам.

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

Httpdos

Сказано сделано.

Программа работает так - при старте читается файл urls.txt где занесены url которые надо скачивать. Далее нажимаем старт. Создаются список Task по количеству url. Каждый Task открывает socket, отправляет http запрос, получает данные, закрывает socket. Далее процедура повторяется.

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

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

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

Всё для эксперимента готово. Запускаем web сервер. Запускаем httpdos так назвал программу, чтобы дальше можно было использовать это короткое название. И вижу следующую картину.

Тут я уточню некоторые технические данные. Количество потоков делающие запросы получилось 1200. Эта цифра количество url прочитанных из файла urls.txt, плюс я решил умножить все запросы в 20 раз. Все цифры взял из головы на момент написания программы. В любой момент можно поставить любые другие по желанию. Преимущество велосипеда.

Так же в последствии я добавил сбоку textarea куда вывожу все exception, и ещё решил добавить вывод - количество запросов в секунду.

Картинка меня порадовала. Во первых - всё работает ). Во вторых работает без ошибок. Количество обработанных запросов получилось где-то 4000-6000 в секунду.

Откуда такая цифра? По моим размышлениям она зависит от многих обстоятельств. Самая очевидное это какого размера сами запросы. Как я писал выше я просто скачиваю все данные с определённой web страницы, которая была взята из стороннего web проекта. И там много mp4 файлов, размер которых под 3 мегабайта. Если уменьшить размер запросов, например скачивать только css наверняка количество обработанных запросов увеличится. Мне даже стало интересно, и я начал играть с исходным кодом как со стороны web сервера, так и со стороны httpdos. Там есть куча различных таймеров, буферов и прочего. Я смотрел, как то или иное изменение, окажет влияние на скорость.

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

Так же существенное влияние на скорость оказывало, то что web сервер и httpdos был запущен в режиме отладки в Visual Studio.

Поработав пару минут ни одной ошибки. Посмотрел загрузку процессора диспетчер задач показал примерно 28% на web сервер и 20% на httpdos. Процессор стоит i7-8700k. Не разогнанный. Это 6 ядерный 12 поточный камень. В процессе работы куллер охлаждения не было слышно проц холодный. Специально температуру не смотрел.

Решил параллельно с httpdos сделать загрузки js файла через браузер. Файл закачивается мгновенно. Т.е. httpdos не оказывает существенного влияния на web сервер.

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

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

И я начал расследование.

Такого поведения просто не должно быть!

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

То, что произошло дальше заставило меня напрячься.

В процессе экспериментов я и так и сяк изгалялся над httpdos. И один сценарий привёл к неожиданным результатам.

Если запустить не менее 3 программ. Думаю, что такое количество связанно с количеством одновременных запросов в секунду. Начать dos атаку. Дождаться появления ошибок. Потом резко закрыть программы. То слушающий socket web сервера просто умирает! Вот так нету никакой ошибки на стороне сервера. Все потоки работают. А socket ничего не принимает. Это уже ни в какие ворота.

Эксперименты показали, что socket оживал примерно через 4 минуты, но, если dos атаку проводить долго socket умирал навсегда, по крайней мере я минут 15 ждал оживления, а дальше уже и не интересно было. Такого поведения просто не должно быть!

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

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

Первый шаг

Первое что я сделал - решил попробовать получить более подробную информацию из exсeption на стороне httpdos. Полазив по интернету, нашёл что мне нужен SocketException, а в нём посмотреть свойство ErrorCode. Сделано. Получил код ошибки 10061 - WSAECONNREFUSED. Тут пояснение.

В соединении отказано.

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

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

Запускаем консоль. Вводим netstat -an и видим. Вот он родненький. Слушает 80 порт. Ну по крайней мере система так думает.

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

Пару слов о socket.

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

Socket listenSocket = new Socket(AddressFamily.InterNetwork,                                 SocketType.Stream, ProtocolType.Tcp){NoDelay = true,Blocking = false,ReceiveBufferSize = TLSpipe.TLSPlaintext_max_recive,SendBufferSize = TLSpipe.TLS_CHUNK};//~~~listenSocket.Bind(new IPEndPoint(point.ip, port.port));Socket socket = await listenSocket.AcceptAsync();

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

Предполагается что используй вот так (куда ещё проще?) и не иначе и всё у тебя должно работать. Но как показывает практика есть нюансы.

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

Soсket socket = await listenSocket.AcceptAsync();

Затем подвесил socket. Попытались присоединится клиентом. Программа висела в вышеприведённой строчки кода. Тут мы увидели, что проблема не в коде после socket, а где то внутри socket.

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

Под подозрения попали следующие настройки.

  1. ReceiveTimeout

  2. SendTimeout

  3. Ttl

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

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

Что касается тайм аутов. В коде сервера реализованы различные таймауты, но на более высоком уровне. В приёмнике есть таймауты на приём шапки http, определения длины body, расчёт времени на приём body исходя из длины body и сценария наихудшего канала связи, например 1024 кб/c.

Примерно такие же правила и на отправку.

И если таймауты выходят socket клиента закрывается и удаляется. Для socket вызывается shutdown и close. Всё как и предписывает microsoft.

Поставил таймауты для resive/transmite 2000ms. Не помогло. Идём далее.

Ttl

Что это за параметр? Wiki говорит:

Получает или задает значение, задающее время существования (TTL) IP-пакетов

Т.е. при прохождении очередного шлюза параметр уменьшается на 1. При достижении 0 пакет удаляется. И вроде это не наш случай. Потому как наша система вся на localhost. Но! При гугление я нашёл следующую информацию.

Там было сказано, что windows от этого параметра рассчитывает двойное время нахождения socket в режиме TIME_WAIT. Про этот режим более подробно ниже.

Поставил ttl в минимально возможное значение. Не помогло.

Утечка sockets?

Далее я подумал сервер открывает каждую секунду около 6000 тысяч sockets и закрывает их. И всё это крутится на кучи асинхронных Task. Вдруг количество открытых сокетов и закрытых не совпадает? И есть, некая утечка sockets?

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

Весь дополнительный код состоял из 3 блоков:

Interlocked.Increment(ref Program.cnt);Interlocked.Decrement(ref Program.cnt);Task.Run(async () => { while (true) {await Task.Delay(1000);Console.WriteLine(Program.cnt); });}

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

Запустив программы, я получил следующее:

Видео можно разделить на три части. Запуск. Рабочее состояние длится около 3 минут. В этом состоянии ни одной ошибки. И первое появление ошибки. Собственно, вторую часть я вырезал как не интересную.

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

Как работает веб сервер? Идеальный сервер приняв клиента мгновенно формирует ответ, отправляет его клиенту и мгновенно закрывает socket (хотя в реальности socket не закрывается а ожидает следующих запросов, ну условимся что так работает идеальный сервер). Поэтому количество открытых socket в идеальном сервере каждую секунду было бы 0. Но тут мы видим, что каждую секунду у нас примерно 400 обрабатывающихся клиентов. Что ж, для неидеального сервера вполне норма. Вообще количество одновременных клиентов в нашем сервере задаётся глобальной настройкой. В данном случае 10000 что значительно выше 400.

Так же мы видим, что периодически подпрыгивающее значение до 1000-2000. Связанно это может быть с чем угодно. При желании можно и это выяснить. Может сборщик мусора, может что ещё. Но, собственно, ничего криминального в этом нет.

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

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

Динамический диапазон портов

Далее расследование завело меня в область настроек windows. Должны же быть какие-то настройки для работы tcp/ip стека? Гугление мне подкинуло множество, но особо я хотел бы остановится на наиболее подходящих к нашему случаю.

Собственно настройки tcp/ip стека для windows меня интересовали с самого начала. Я задавал себе вопросы - а какие вообще порты выделяются на клиенте? Да и количество портов как бы ограничено. Всего 65535 значений. Такое число обусловлено исторически переменной uint16 в протоколе TCP.

Если выделять тысячи портов в секунду то можно и исчерпать их всех.

Я даже вначале проверил следующий кейс. Изначально в httpdos использовался стандартный для .net способ открытия socket клиента.

new TcpClient().Connect("127.0.0.1", 80);

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

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

Но какой же диапазон портов мне выдаёт Windows? Тут я узнал ответ. Честно ещё в десятке других мест. Правда в большинстве информация была устаревшая - мол диапазон 1025 до 5000. Но я из практики знал, что диапазон выделяется где-то от 50000.

В дальнейшем я и убедился в своей правоте. Как оказалось Microsoft изменила этот диапазон, и он составляет 49152-65535. И того где-то 15k портов. Явно меньше, чем 65535

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

Я применил следующую команду:

netsh int ipv4 set dynamicport tcp start=10000 num=55535

Команду запускаем под администратором. Проверить значение можно командой:

netsh int ipv4 show dynamicport tcp

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

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

Добавляем код - 1 строка:

Console.WriteLine(((IPEndPoint)socket.RemoteEndPoint).Port);

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

Далее я начал закрывать программы и при закрытии второй программы серверный socket завис. Порты перестали выделятся. Во второй программе httpdos посыпались ошибки открытия socket.

Выводы проблема не в портах. По крайней мере порты явно не заканчиваются.

TIME WAIT

А что у нас есть вообще по протоколу TCP? Идём сюда и внимательно читаем.

Под подозрение попадает одно состояние TIME WAIT. Это одно из стояний, в котором может находится socket, т.е. пара ip+port. После его закрытия.

Получается, что мы закрыли socket но он ещё будет недоступен. Поиск показал, что это время находится в районе 4 минут.

Если бы мы играли в игру холодно-горячо, то я бы заорал горячо! Горячо! Похоже это именно наш случай.

Но зачем такое странное поведение. Опять же из справки получил пояснение. Т.к. протокол tcp/ip гуляет по сети пакетами, а сами пакеты могут пойти по разным маршрутам. И вообще застрять на некоторое время на каком-нибудь шлюзе, может получится ситуация, когда мы открыли socket, поработали с удалённым сервером. Закрыли socket. Потом этот socket открывает другая программа а ей начинают валится пакеты от предыдущей сессии. Как раз время таймаута и выбрано что бы отставшие все пакеты в сети удалились как мусорные.

OK, а можно ли изменить этот параметр? Оказывается в Windows есть целая ветка реестра, отвечающая за параметры tcp протокола.

HKEY_LOCAL_MACHINE \SYSTEM \CurrentControlSet \Services: \Tcpip \Parameters

Нас интересуют пока 2 параметра:

  1. TcpFinWait2Delay

  2. TcpTimedWaitDelay

Цель установить минимальное значение. Минимальное значение 30. Что означает что через 30 секунд порт опять будет доступен. Устанавливаем. Ну а далее наша стандартная проверка.

Запускаем сервер. Запускаем клиенты. Досим. Дожидаемся отказа socket. Потом запускаем консоль и вводим команду:

netstat -an

Такое ощущение что весь выделенный диапазон портов находится в состоянии TIME WAIT. Это пока соответствует ожиданиям. Ждём 30с. Повторяем команду. Листинг уменьшился в разы. Все порты с WAIT TIME пропали.

Проверяем серверный socket на оживление. Глух ((( А такие надежды были на эту настройку.

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

Wireshark

Решил посмотреть, что нам покажет Wireshark. Кто не знает это мощнейший анализатор всевозможных протоколов, в том числе и tcp/ip. До недавнего времени в программе не была реализована функция прослушки localhost и приходилось производить некоторые танцы с бубном. Ставить виртуальную сетевую карту. Трафик пускать через неё. А Wireshark уже мог подключатся к прослушке этой карты.

Недавно подвезли возможность прослушки localhost что очень облегчило работу.

Далее всё стандартно. Запускаем сервер, клиентов. Убиваем слушающий socket.

Запускаем Wireshark ставим фильтр на 80 порт. Открываем браузер пытаемся закачать файл javascript.

Вот какое непотребство мы увидели в сниффере:

Анализируем увиденное.

Видно, наш запрос. Браузер с порта 36036 пытается достучатся к порту 80. Выставляет флаг SYN. Это стандартно. Но вот с порта 80 нам возвращается флаг RST оборвать соединения, сбросить буфер. Всё. И так по кругу.

Вывод. Wireshark нам особо не помощник. Разве только мы увидели, что слушающий socket не мёртв совсем, а отвечает. Он работает по какой-то своей внутренней логике, а не просто умер.

Журнал windows

Решил посмотреть может что в журнале событий windows есть что-то интересное. Для этого захожу в журнал.

Панель управления-> Администрирование-> Управление компьютером-> Служебные программы-> Просмотр событий-> Журналы Windows

Либо запустите eventvwr.msc

Там, разумеется, есть миллионы записей. И чтобы не копаться в этих миллионах, я делаю следующее. Убиваю socket. Захожу в журнал и чищу подразделы. Захожу в браузер пытаюсь скачать файл. Захожу в журнал и смотрю что там только что появилось.

К сожалению, никаких событий, кусающихся моей проблемы, не появилось. Я даже нашёл такой раздел Microsoft/Windows/TCPIP. Но кроме одинокой записи работает, там ничего не было.

Финал?

Поиск в интернете всей доступной информации по проблеме периодически выдавал мне страницы подобной этим:

И ещё во многих других местах.

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

Есть ещё 2 параметра TCP стека с которыми можно поэкспериментировать.

  1. TcpNumConnections - максимальное количество одновременных подключений в системе.

  2. TcpMaxDataRetransmissions - количество повторных посылок при неудаче.

Поиск по этим параметрам для Windows 10 ничего не дал. Только для Windows 2003-2008 Server. Может плохо искал (наверняка). Но я всё же решил их проверить.

Установил следующие значения:

TcpNumConnections REG_DWORD: 00fffffe (hex)

TcpMaxDataRetransmissions REG_DWORD: 00000005 (hex)

Перезагрузился. Повторил в который раз все процедуры. И.

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

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

Но.

На следующий день, я решил закрепить полученную информацию. Запускаю эксперимент socket мёртв. Мы вернулись к тому, с чего начинали!

Эти параметры оказались бесполезны в решении нашей проблемы. Но я их всё-таки оставил потому как по смыслу они полезные.

Продолжаем.

Финал.

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

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

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

Как оказалось не неординарный.

Картина сложилась.

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

Ради интереса провёл эксперимент. Убил socket. Ввёл команду netstat -an. И не увидел ни одного socket клиента на нашем socket сервера. Хотя в приёмном socket сервера висело куча мёртвых подключений.

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

Выводы.

Ну что ж. Я, конечно, ожидал что геройски одержу победу над некоей эпичной ошибкой. А всё оказалось очень банально. Собственно, как всегда.

Все эксперименты у меня заняли где-то полтора дня. Эту статью я писал намного больше.

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

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

Почему у меня заняло так много времени на поиск банального Delay? Я думал над этим и пришёл к выводу, что все эксперименты уводили в сторону от ответа. Сложившееся картина показывала нам что умирал именно socket. Переставал принимать подключения. Спустя время оживал. Все сниферы показывали, что подключений не происходит.

Опять же было неправильное представление, что серверный socket работает в отдельном потоке. И в любом случае должен принимать клиентов. Поэтому я и скал проблему в настройках socket и tcp стека. Главная проблема я думал поток висит на socket. А в нашем аварийном случае он висел на exeption в Delay.

Подробнее..

Работа с частичными моками в PHPUnit 10

26.04.2021 16:20:03 | Автор: admin

В этом году должен выйти PHPUnit 10 (релиз планировался на 2 апреля 2021 года, но был отложен). Если посмотреть на список изменений, то бросается в глаза большое количество удалений устаревшего кода. Одним из таких изменений является удаление метода MockBuilder::setMethods(), который активно использовался при работе с частичными моками. Этот метод не рекомендуется использовать с версии 8.0, но тем не менее он описан в документации без каких-либо альтернатив и упоминания о его нежелательности. Если почитать исходники PHPUnit, issues и пул-реквесты на GitHub, то станет понятно, почему так и какие есть альтернативы.

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

Что такое частичные моки?

У программного кода, который мы пишем, чаще всего есть какие-то зависимости.

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

Про название "мок"

У этого термина в русском языке есть несколько обозначений: мок, mock-объект, подставной объект, имитация. Я буду пользоваться калькой английского слова mock (мок).

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

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

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

Вот код базового класса, реализующий паттерн команда:

abstract class AbstractCommand{    /**     * @throws \PhpUnitMockDemo\CommandException     * @return void     */    abstract protected function execute(): void;    public function run(): bool    {        $success = true;        try {            $this->execute();        } catch (\Exception $e) {            $success = false;            $this->logException($e);        }        return $success;    }    protected function logException(\Exception $e)    {        // Logging    }} 

Реальное поведение команды задаётся в методе execute классов-наследников, а метод run() добавляет общее для всех команд поведение (в данном случае делает код exception safe и логирует ошибки).

Если мы хотим написать тест для метода run, мы можем воспользоваться частичными моками, функционал которых предоставляет класс PHPUnit\Framework\MockObject\MockBuilder, доступ к которому предоставляется через вспомогательные методы класса TestCase (в примере это getMockBuilder и createPartialMock):

use PHPUnit\Framework\TestCase;class AbstractCommandTest extends TestCase{    public function testRunOnSuccess()    {        // Arrange        $command = $this->getMockBuilder(AbstractCommand::class)            ->setMethods(['execute', 'logException'])            ->getMock();        $command->expects($this->once())->method('execute');        $command->expects($this->never())->method('logException');        // Act        $result = $command->run();        // Assert        $this->assertTrue($result, "True result is expected in the success case");    }    public function testRunOnFailure()    {        // Arrange        $runException = new CommandException();        // It's an analogue of $this->getMockBuilder(...)->setMethods([...])->getMock()        $command = $this->createPartialMock(AbstractCommand::class, ['execute', 'logException']);        $command->expects($this->once())            ->method('execute')            ->will($this->throwException($runException));        $command->expects($this->once())            ->method('logException')            ->with($runException);        // Act        $result = $command->run();        // Assert        $this->assertFalse($result, "False result is expected in the failure case");    }} 

Исходный код, результаты прогона тестов

В методе testRunOnSuccess с помощью MockBuilder::setMethods() мы задаём список методов оригинального класса, которые мы заменяем (вызовы которых хотим проверить или результаты которых нужно зафиксировать). Все остальные методы сохраняют свою реализацию из оригинального класса AbstractCommand (и их логику можно тестировать). В testRunOnFailure через метод createPartialMock мы делаем то же самое, но явно.

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

  • подготовка или освобождение каких-то ресурсов (например, соединения с базой данных);

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

  • отправка какой-то отладочной информации или статистики.

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

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

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

Пример:

   public function testRedisHandle()    {        if (!class_exists('Redis')) {            $this->markTestSkipped('The redis ext is required to run this test');        }        $redis = $this->createPartialMock('Redis', ['rPush']);        // Redis uses rPush        $redis->expects($this->once())            ->method('rPush')            ->with('key', 'test');        $record = $this->getRecord(Logger::WARNING, 'test', ['data' => new \stdClass, 'foo' => 34]);        $handler = new RedisHandler($redis, 'key');        $handler->setFormatter(new LineFormatter("%message%"));        $handler->handle($record);    } 

Источник: тест RedisHandlerTest из monolog 2.2.0

Какие проблемы возникают при использовании setMethods?

Двойственное поведение может приводить к проблемам.

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

Небольшая демонстрация. Давайте добавим в код нашего класса команды измерение времени, которое потребовалось для её выполнения:

--- a/src/AbstractCommand.php+++ b/src/AbstractCommand.php@@ -13,6 +13,7 @@ abstract class AbstractCommand     public function run(): bool     {+        $this->timerStart();         $success = true;         try {             $this->execute();@@ -21,6 +22,7 @@ abstract class AbstractCommand             $this->logException($e);         }+        $this->timerStop();         return $success;     }@@ -28,4 +30,14 @@ abstract class AbstractCommand     {         // Logging     }++    protected function timerStart()+    {+        // Timer implementation+    }++    protected function timerStop()+    {+        // Timer implementation+    } } 

Исходный код

В код тестов добавим в мок новые методы, но не будем проверять вызовы через expectations:

--- a/tests/AbstractCommandTest.php+++ b/tests/AbstractCommandTest.php@@ -11,7 +11,7 @@ class AbstractCommandTest extends TestCase     {         // Arrange         $command = $this->getMockBuilder(AbstractCommand::class)-            ->setMethods(['execute', 'logException'])+            ->setMethods(['execute', 'logException', 'timerStart', 'timerStopt']) // timerStopt is a typo             ->getMock();         $command->expects($this->once())->method('execute');         $command->expects($this->never())->method('logException');

Исходный код, результаты прогона тестов

Если прогнать этот тест в PHPUnit версий 8.5 или 9.5, то он успешно пройдёт без каких-то предупреждений:

PHPUnit 9.5.0 by Sebastian Bergmann and contributors..                                                                   1 / 1 (100%)Time: 00:00.233, Memory: 6.00 MBOK (1 test, 2 assertions) 

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

Ещё сложнее отслеживать подобные проблемы при использовании MockBuilder::setMethodsExcept, который переопределяет все методы класса, кроме заданных.

Как эта проблема решена в PHPUnit 10?

Начало решению этой проблемы молчаливого переопределения несуществующих методов было положено в 2019 году в пул-реквесте #3687, который вошёл в релиз PHPUnit 8.

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

В том же PHPUnit 8 setMethods был помечен устаревшим и появилось предупреждение при передаче несуществующих методов в TestCase::createPartialMock().

Если взять предыдущий пример с некорректным названием метода и использовать createPartialMock вместо вызовов getMockBuilder(...)->setMethods(...), то тест пройдёт, но появится предупреждение о будущем изменении этого поведения:

createPartialMock() called with method(s) timerStopt that do not existin PhpUnitMockDemo\AbstractCommand. This will not be allowed in future versions of PHPUnit.

К сожалению, это изменение никак не было отражено в документации там по по-прежнему была описана только работа setMethods(), а всё остальное было скрыто в недрах кода и GitHub.

В PHPUnit 10 проблема setMethods() решена радикально: setMethods и setMethodsExcept окончательно удалены. Это означает, что если вы используете их в своих тестах и хотите перейти на новую версию PHPUnit, то вам нужно убрать все использования этих методов и заменить их на onlyMethods и addMethods.

Как мигрировать частичные моки из старых тестов на PHPUnit 10?

В этой части я дам несколько советов о том, как это можно сделать.

Сразу скажу, что для использования этих советов не обязательно ждать выхода PHPUnit 10 и переходить на него. Всё это можно делать в процессе работы с тестами, которые запускаются в PHPUnit 8 или 9.

Везде, где возможно, замените вызовы MockBuilder::setMethods() на onlyMethods()

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

Используйте MockBuilder::addMethods() для классов с магией

Если метод, который вы хотите переопределить в моке, работает через магический метод __call, то используйте MockBuilder::addMethods().

Если раньше для классов с магией вы использовали TestCase::createPartialMock() и это работало, то в PHPUnit 10 это сломается. Теперь createPartialMock умеет заменять только существующие методы мокаемого класса, и нужно заменить использование createPartialMock на getMockBuilder()->addMethods().

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

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

Приведу пример из библиотеки PhpAmqpLib.

Допустим, вам нужен мок для класса \PhpAmqpLib\Channel\AMQPChannel.

В версии 2.4 там был метод __destruct, который отправлял внешний запрос (и поэтому его стоит замокать).

В версии 2.5 этот метод был удалён и мокать его уже не нужно.

Если в composer.json зависимость прописана подобным образом: "php-amqplib/php-amqplib": "~2.4", то обе версии буду подходить (но моки для них нужны разные) и нужно будет смотреть, какая из них используется.

Решать это можно несколькими способами:

  • максимально фиксировать версию библиотеки (например, в приведённом примере можно использовать ~2.4.0 и тогда разница будет только в patch-версиях);

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

  • использовать для классов из внешних библиотек полные моки, а не частичные (но это не всегда возможно).

Заключение

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

См. также

Подробнее..

Аспекты хороших юнит-тестов

02.05.2021 12:15:03 | Автор: admin

Эта статья является конспектом книги Принципы юнит-тестирования.

Давайте для начала перечислим свойства хороших юнит-тестов.

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

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

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

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

Четыре аспекта хороших юнит-тестов

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

Эти четыре атрибута фундаментальны. Они могут использоваться для анализалюбых автоматизированных тестов, будь то юнит-, интеграционные или сквозные(end-to-end) тесты.

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

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

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

  • объем кода, выполняемого тестом;

  • сложность этого кода;

  • важность этого кода с точки зрения бизнес-логики.

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

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

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

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

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

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

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

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

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

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

Что приводит к ложному срабатыванию?

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

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

Рис. 1 Тест слева связан с наблюдаемым поведением SUT, а не с деталями реализации. Такой тест более устойчив к рефакторингу, чем тест справаРис. 1 Тест слева связан с наблюдаемым поведением SUT, а не с деталями реализации. Такой тест более устойчив к рефакторингу, чем тест справа

Связь между первыми двумя атрибутами

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

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

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

Рис. 2 - Отношение между защитой от багов и устойчивостью к рефакторингуРис. 2 - Отношение между защитой от багов и устойчивостью к рефакторингу

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

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

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

  • насколько хорошо тест выявляет присутствие ошибок (отсутствие ложноотрицательных срабатываний, сфера защиты от багов);

  • насколько хорошо тест выявляет отсутствие ошибок (отсутствие ложных срабатываний, сфера устойчивости к рефакторингу).

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

Рис. 3 Формула точности тестаРис. 3 Формула точности теста

Третий и четвертый аспекты: быстрая обратнаясвязь и простота поддержки

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

Простота поддержки оценивает затраты на сопровождение кода. Метрика состоит из двух компонентов:

  • Насколько сложно тест понять. Этот компонент связан с размером теста. Чемменьше кода в тесте, тем проще он читается и проще изменяется при необходимости.

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

В поисках идеального теста

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

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

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

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

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

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

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

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

Рис. 4 - Тривиальный тест, покрывающий простой фрагмент кодаРис. 4 - Тривиальный тест, покрывающий простой фрагмент кода

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

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

Рис. 5 Места, которые занимают тесты по отношению друг к другуРис. 5 Места, которые занимают тесты по отношению друг к другу

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

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

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

Рис. 6 Компромиссы между атрибутами хорошего тестаРис. 6 Компромиссы между атрибутами хорошего теста

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

Компромисс между первыми тремя атрибутами хорошего юнит-теста напоминает теорему CAP. Эта теорема утверждает, что распределенное хранилище данных не можетпредоставить более двух из трех гарантий одновременно: согласованность (consistency) данных, доступность (availability), устойчивость к разделению (partition tolerance).

Сходство является двойным:

1. В CAP вы тоже можете выбрать максимум два атрибута из трех;

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

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

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

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

Рис. 7 - Пирамида тестирования предписывает определенное соотношение юнит-,интеграционных и сквозных тестовРис. 7 - Пирамида тестирования предписывает определенное соотношение юнит-,интеграционных и сквозных тестовРис. 8 - Разные типы тестов в пирамиде принимают разные решения относительно быстрой обратной связи и защиты от баговРис. 8 - Разные типы тестов в пирамиде принимают разные решения относительно быстрой обратной связи и защиты от багов

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

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

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

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

Выбор между тестированием по принципу черного ящика и белого ящика

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

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

Рис. 9 - Достоинства и недостатки тестирования по принципу черного ящика и белого ящикаРис. 9 - Достоинства и недостатки тестирования по принципу черного ящика и белого ящика

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

Ссылки на все части

Подробнее..

Для чего нужно интеграционное тестирование?

06.05.2021 08:19:11 | Автор: admin

Эта статья является конспектом книги Принципы юнит-тестирования. Материал статьи посвящен интеграционным тестам.

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

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

Что такое интеграционный тест?

Юнит-тест удовлетворяет следующим трем требованиям:

  • проверяет правильность работы одной единицы поведения;

  • делает это быстро;

  • и в изоляции от других тестов.

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

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

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

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

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

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

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

Все внепроцессные зависимости делятся на две категории.

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

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

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

Рис. 1 Взаимодействия с зависимостямиРис. 1 Взаимодействия с зависимостями

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

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

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

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

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

Рис. 2 БД, доступная для внешних приложенийРис. 2 БД, доступная для внешних приложений

Основные приемы интеграционного тестирования

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

  • явное определение границ доменной модели (модели предметной области);

  • сокращение количества слоев в приложении;

  • устранение циклических зависимостей.

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

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

Рис. 3 Типичное корпоративное приложение с несколькими слоямиРис. 3 Типичное корпоративное приложение с несколькими слоями

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

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

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

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

public class CheckOutService{  public void CheckOut(int orderId)  {    var service = new ReportGenerationService();    service.GenerateReport(orderId, this);    /* остальной код */  }}public class ReportGenerationService{  public void GenerateReport(    int orderId,    CheckOutService checkOutService)  {  /* вызывает checkOutService при завершении генерирования */  }}

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

Что же делать с циклическими зависимостями? Лучше всего совсем избавитьсяот них. Отрефакторить класс ReportGenerationService, чтобы он не зависел от CheckOutService и сделать так, чтобыReportGenerationService возвращал результат работы в виде простого значениявместо вызова CheckOutService:

public class CheckOutService{  public void CheckOut(int orderId)  {    var service = new ReportGenerationService();    Report report = service.GenerateReport(orderId);    /* прочая работа */  }}public class ReportGenerationService{  public Report GenerateReport(int orderId)  {  /* ... */  }}

Использование нескольких секций действий в тестах

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

  • подготовка подготовка данных для регистрации пользователя;

  • действие вызов UserController.RegisterUser();

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

  • действие вызов UserController.DeleteUser();

  • проверка запрос к базе данных для проверки успешного удаления.

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

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

Выводы

Интеграционные тесты проверяют, как ваша система работает в интеграциис внепроцессными зависимостями.

Интеграционные тесты покрывают контроллеры; юнит-тесты покрывают алгоритмы и доменную модель.

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

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

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

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

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

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

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

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

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

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

Ссылки на все части

Подробнее..

Используем DevTools в headless Chrome

20.04.2021 12:16:22 | Автор: admin


Если вы когда-нибудь использовали Puppeteer, то наверняка сталкивались с неудобной отладкой скриптов на удалённых нодах headless Chrome. Часто так не хватает консоли, а лучше полноценной панели инструментов для изучения запросов и логов хотя постойте. Puppeteer сам по себе построен поверх Chrome DevTools Protocol, значит, наверняка есть куча решений для проброса данных в локальные DevTools? А вот и нет. Есть только два более-менее рабочих инструмента: отладчик для browserless.io и pptrconsole. Второй по функционалу и стабильности уже далеко впереди, поэтому поговорим про него.

Функционал


Сайт pptrconsole это по сути демка частично опенсорсного проекта ViewFinder, так что можно без проблем развернуть свою ноду и не зависеть от чужого облака. Цель ViewFinder развернуть полноценный доступ к удалённому headless-браузеру прямо во вкладке обычного браузера (здесь и дальше речь в силу очевидных причин пойдёт только про Chrome) внутри песочницы, сохранив доступ к максимальному количеству браузерных событий и взаимодействий, и, конечно, к DevTools. Чтобы максимально обезопасить конечный браузер от уязвимостей и прочих криптомайнеров, сервер не использует API браузера, вместо этого он просто стримит картинку (отображение и полученные данные живут в виртуальной среде). При этом разработчик делает упор на низкую задержку, так что картинка из самого браузера передаётся в минимальном приемлемом качестве, а панель DevTools отображает только запрошенные данные, что позволяет сократить нагрузку от одной вкладки до считаных килобит.

Fun fact: так как страница с панелькой выглядит и ведёт себя в точности как локальные DevTools (ещё бы, ведь обе исполняют код Chromium), можно из интереса сравнить версии



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

  • Поддержка браузерных диалогов многие детекторы ориентируются на возможность и скорость прокликивания всплывающих окон
  • Работа с расширениями
  • Корректный юзер-агент
  • Не определяется использование DevTools Protocol
  • Корректный захват мыши и скролл на любых устройствах
  • Нормально работающие копирование и вставка
  • Заполнение форм (текстовые инпуты, чекбоксы, загрузка файлов)
  • История (отсутствует в привычном виде, но доступна для навигации по кнопкам и из консоли)
  • Можно работать в нескольких вкладках, и в том числе создавать вкладки инкогнито
  • Динамическое изменение качества картинки в зависимости от пропускной способности сети
  • Счетчик трафика на сервере и для вкладки
  • Доступность с любого девайса на всех популярных ОС


Как и сам Puppeteer, приложение построено на Node.js, причём весь браузер ViewFinder легко встраивается в другое приложение. Например, можно завернуть его в Electron и использовать как нативное приложение, или прикрепить DevTools к основной странице.

В интерфейсе можно писать, запускать и отлаживать скрипты Puppeteer:



Установка


Ставить будем на чистый VPS:

  sudo apt update && sudo apt -y upgrade  sudo apt install -y curl git wget certbot  curl -sL https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh -o install_nvm.sh  bash ./install_nvm  source $HOME/.profile  source $HOME/.nvm/nvm.sh  nvm install --lts  npm i -g serve nodemon pm2 npm npx


Склонируем и запустим ViewFinder:

  git clone https://github.com/c9fe/ViewFinder  cd ViewFinder  npm i  npm start


Скрипт start.sh поддерживает следующие аргументы:

  ./start.sh <chrome_port> <app_port> <cookie_name> <username> token2


Также доступен докер-образ (установка для Ubuntu ниже):

  sudo apt-get update  sudo apt-get install \    apt-transport-https \    ca-certificates \    curl \    gnupg \    lsb-release  curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg  echo \  "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu \  $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null  sudo apt-get update  sudo apt-get install docker-ce docker-ce-cli containerd.io


Скачаем и запустим образ:

  docker pull dosyago/browsergapce:2.6  curl -o chrome.json https://raw.githubusercontent.com/c9fe/ViewFinder/master/chrome.json  sudo su -c "echo 'kernel.unprivileged_userns_clone=1' > /etc/sysctl.d/00-local-userns.conf"  sudo su -c "echo 'net.ipv4.ip_forward=1' > /etc/sysctl.d/01-network-ipv4.conf"  sudo sysctl -p  sudo docker run -d -p 8002:8002 --security-opt seccomp=$(pwd)/chrome.json dosyago/browsergapce:2.6


Или можно собрать образ самостоятельно:

  git clone https://github.com/c9fe/ViewFinder  cd BrowserGap  git fetch --all  git branch nexe-build  ./buld_docker.sh  ./run_docker.sh


Заключение


Как и у любого продукта, у ViewFinder есть свои слабые стороны: плохонькие юзабилити и дизайн не слишком мешают, а вот желание разработчика загнать полноценное пользование сервисом в SaaS модель напрягают. К счастью, он принципиально держит фронтенд и вообще большую часть проекта открытыми, да и для частного использования это сейчас практически безальтернативный инструмент. Судя по его комментариям, из-за большого спроса на качество картинки, в следующих релизах появится его регулировка, чтобы пользователи могли сами выбирать между экономией трафика и визуальной составляющей. Кроме того, вместо передачи отдельных фреймов в webp будет реализован h264-стрим (через конвертацию в ffmpeg). В любом случае проект интересный и закрывает целую нишу, так что своих пользователей он уже нашёл.



На правах рекламы


Эпично! Мощные серверы на базе новейших процессоров AMD EPYC для размещения проектов любой сложности, от корпоративных сетей и игровых проектов до лендингов и VPN.

Подробнее..

Что такое База Данных (БД)

08.05.2021 22:04:46 | Автор: admin

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

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

Содержание

Что такое база данных

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

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

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

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

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

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

Чем больше объемы производства, тем больше нужно места. Если в начале пути склад не нужен, всё поместится дома, то потом это будет оправданно.

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

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

Как она выглядит

Да примерно как excel-табличка! Есть колонки с заголовками, и информация внутри:

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

Что за пространство? Ну вот представьте, что вы храните все данные в excel. Можно запихать всю-всю-всю информацию в одну огро-о-о-о-мную таблицу, но это неудобно. Обычно табличек несколько: тут информация по клиентам, там по заказам, а тут по адресам. Эти таблицы удобно хранить в одном месте, поэтому кладем их в отдельную папочку:

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

Пример базы Oracle Пример базы Oracle

Цель та же выделить отдельное место, чтобы у вас не была одна большая свалка:

  • заходишь в папку в винде видишь файлики только из этой папки

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

Хранение данных в виде табличек это не единственно возможный вариант. Вот вам для примера запись из таблицы в системе Users. Там используется MongoDB база данных, она не реляционная. Поэтому вместо таблички словно в excel каждая запись хранится в виде объекта, вот так:

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

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

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

Как получить информацию из базы

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

  • select выбери мне такие-то колонки...

  • from из такой-то таблицы базы...

  • where такую-то информацию...

Например, я хочу получить информацию по клиенту Назина Ольга. Составляю в уме ТЗ:

Дай мне информацию по клиенту, у которого ФИО = Назина Ольга

Переделываю в SQL:

select * from clients where name = 'Назина Ольга';

В дословном переводе:

select -- выбери мне* -- все колонки (можно выбирать конкретные, а можно сразу все)from clients -- из таблицы clientswhere name = 'Назина Ольга'; -- где поле name имеет значение 'Назина Ольга'

См также:

Комментарии в Oracle/PLSQL мой перевод остается работающим запросом, потому что я убрала лишнее в комментарии

Если бы у меня была не база данных, а простые excel-файлики, то же действие было бы:

  1. Открыть файл с нужными данными (clients)

  2. Поставить фильтр на колонку ФИО Назина Ольга.

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

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

А в базе данных вы внутри запроса SQL указываете, какие колонки из каких таблиц вам нужны. И результат запроса их отрисовывает. Скажем, мы хотим увидеть заказ, который сделал клиент, ФИО клиента, и его номер телефона. И всё это в разных таблицах! А мы написали запрос и увидели то, что нам надо:

id_order

order (таблица order)

fio (таблица client)

phone (таблица contacts)

1

Пицца Маргарита

Иванова Мария

+7 (926) 555-33-44

2

Комбо набор 1

Петров Павел

+7 (926) 555-22-33

И пусть в таблице клиентов у нас будет 30 колонок, а в таблице заказов 50, в результате выборки мы видим ровно 4 запрошенные. Удобно, ничего лишнего!

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

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

Как связать данные между собой

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

  • В таблице client лежат данные по клиентам: ФИО, пол, дата рождения и т.д.

last_name

first_name

birthdate

VIP

Иванов

Иван

01.02.1977

true

Петрова

Мария

02.04.1989

false

Сидоров

Павел

03.02.1991

false

Иванов

Вася

04.04.1987

false

Ромашкина

Алина

16.11.2000

true

  • В таблице orders лежат данные по заказам. Что заказали (пиццу, суши, роллы), когда, насколько довольны доставкой?

order

addr

date

time

Пицца Маргарита

ул Ленина, д5

05.05.2020

06:00

Роллы Филадельфия и Канада

Студеный пр-д, д 10

15.08.2020

10:15

Пицца 35 см, роллы комбо 1

Заревый, д10

08.09.2020

07:13

Пицца с сосиками по краям

Турчанинов, 6

08.09.2020

08:00

Комбо набор 3, обед 4

Яблочная ул, 20

08.09.2020

08:30

Но как понять, где чей был заказ? Сколько раз заказывал Вася, а сколько Алина?

Тут есть несколько вариантов:

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

Но есть минусы:

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

  • Поиск будет работать медленнее. Чем меньше информации в таблице, тем быстрее поиск. Когда у нас много строк, количество колонок становится существенным.

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

Чтобы избежать дублей, таблицы принято разделять:

  • Клиенты отдельно

  • Заказы отдельно

  • Новые объекты отдельно

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

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

Можно ссылаться на имя. А что, миленько, в таблице заказов будем сразу имя видеть! Но минуточку... А если у нас два клиента Ивана? Или три Маши? Десять Саш... Ну вы поняли =) И как тогда разобраться, где какой клиент? Не подходит!

Можно вешать foreign key на несколько колонок. Например, на фамилию + имя, или фамилию + имя + отчество. Но ведь и ФИО бывают неуникальные! Что тогда? Можно добавить в связку дату рождения. Тогда шанс ошибиться будет минимален, хотя и такие ребята существуют. И чем больше клиентов у вас будет, тем больше шанс встретить дубликат.

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

Здесь ключ id_orderЗдесь ключ id_order

Вот на него и нужно ссылаться! Обычно таким ключом является ID, идентификатор записи. Его можно сделать автоинкрементальным это значит, что он генерируется сам по алгоритму прошлое значение + 1.

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

Есть таблица постояльцев:

ID

name

year

1

Барсик

2

2

Пупсик

1

Тут привозят еще одного Барсика. Добавляем его в таблицу:

Имя Барсик, 5 лет! (мы не указываем ID)

Система добавляет:

ID

name

year

1

Барсик

2

2

Пупсик

1

3

Барсик

5

ID сгенерился автоматически. Последнее значение было 2, значит, новый Барсик получил номер 3. Обратите внимание Барсиков уже два, но их легко различить, ведь у них разные идентификаторы!

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

id_room

square

id_cat (ссылка на id в таблице котиков)

1

5

11

2

10

2

3

10

Мы видим, что в первой комнате живет котик с id = 1, а во второй с id = 2. В третьей комнате пока никто не живет. Так, благодаря связке таблиц, мы всегда можем понять, что именно за котофей там проживает.

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

И в таблице заказов! id_order пусть генерится сам и всегда будет уникален. А еще в таблицу заказов мы добавим колонку id_client и повесим на нее foreign key, ссылку на id_client в таблице клиентов.

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

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

Как ускорить запрос

Давайте представим, что у нас есть табличка excel. Если она небольшая (пара строк, пара колонок), то найти нужную ячейку не составит труда:

  1. Открыли файлик открывается моментально (если нет проблем с жестким диском)

  2. Нажали Ctrl + F, ввели запрос тут же нашли результат.

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

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

Что делать, чтобы запросы были быстрее? Тут есть несколько путей:

  1. На этапе создания таблицы добавить индексы

  2. На этапе написания запроса к большой таблице использовать хинты

  3. Пересобрать статистику базы

Есть и другие, но мы остановимся на этих трех.

Индексы в таблице

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

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

Совсем другое дело, если книги отсортированы по авторам. А внутри автора по названию. Тогда найти нужную книгу будет легко!

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

Хинты

Хинт это код, вставляемый в SQL-запрос, который позволяет изменить план выполнения запроса. Выглядит это примерно так:

SELECT /*+ NO_UNNEST( @SEL$4 ) */*FROM v;

Хинт идет внутри блока /* */.

А что еще за план выполнения запроса? Смотрите, когда вы выполняете любой запрос, что делает система:

  1. Строит план выполнения запроса (как ей кажется, оптимальный)

  2. Выполняет его

Посмотреть план можно через ключевые слова EXPLAIN PLANOracle):

EXPLAIN PLAN FOR -- построй мне план для...SELECT last_name FROM employees; -- вот такого запроса!

А если вы работаете через sql developer (графический интерфейс для обращения к базе Oracle), то можно просто выделить запрос и нажать F10:

Что мы видим на этой картинке? Запрос говорит: достань мне всю информацию из таблицы клиентов. Так как нет никаких условий where, то мы просто проходим фулл-сканом Table Access (full) по этом таблице. План показывает стоимость (cost) этого запроса 857 ms.

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

Оп, цена запроса уже 5 ms. Это, на минуточку, в 170 раз быстрее!

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

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

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

Допустим, поступает жалоба от заказчика клиент открывает карточку в вебе, а она открывается минуту. Что-то где-то тормозит! Но что и где? Начинаем разбираться. Причины бывают разные:

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

  2. Тормозит на уровне приложения тогда надо копаться внутри кода функции открыть карточку, что она там делает, получив ответ от Базы (и снова есть вариант подыхают диски, на которых установлено ПО).

  3. Тормозит на уровне сети сервер приложения и сервер БД обычно размещают на разных машинах. Значит, есть общение между ними по интернету. А интернет может тупить.

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

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

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

См также

17 Optimizer Hints в Oracle хинты в базе Oracle

Статистика

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

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

Найди мне всех клиентов, созданных в этом году,

У которых оператор связи в телефоне Мегафон

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

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

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

Время же он рассчитывает, ориентируясь на статистику:

  • Сколько данных находится в таблице?

  • Есть ли индекс по колонке, по которой я буду искать?

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

См также:

Ручной и автоматический сбор статистики оптимизатора в базе данных Oracle

Преимущества базы данных

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

  1. Базы поддерживают требования ACID (по крайней мере транзакционные БД)

  2. Это единый синтаксис SQL, который используется повсеместно

Требования ACID

ACID это аббревиатура из требований, которые обеспечивают сохранность ваших данных:

  • Atomicity Атомарность

  • Consistency Согласованность

  • Isolation Изолированность

  • Durability Надёжность

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

См также:

Требования ACID на простом языке подробнее об этих требованиях

Единый синтаксис SQL

Я спросила знакомого разработчика:

Ну и что, что единый синтаксис? В чем его плюшка то?

Ответ прекрасен, так что делюсь с вами:

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

Как разработчик пишет код? Написал, проверил на коленке. Если не работает думает, почему. Если непонятно, идет гуглить похожие ошибки. А что проще нагуглить? Ошибку распространенной БД, или сделанный на коленке костыль для работы с файлами? Вот то-то и оно...

Что знать для собеседования

Для начала я хочу уточнить, что я сама тестировщик. И мои статьи в первую очередь для тестировщиков ))

Так вот, тестировщика на собеседовании не будут спрашивать про базы данных. Разработчика ещё могут спросить, а вас то зачем? Вполне достаточно понимания, что это вообще такое. И про ключи могут спросить что такое primary или foreign key, зачем они вообще нужны.

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

В вакансии написано: уметь составлять простые SQL запросы. А простые это какие в народном понимании?

(inner, outer) join, select, insert, update, create, последнее время популярны индексы, group by, having, distinct.

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

Статьи и книги по теме

База данных

Википедия

Какие бывают базы данных

Базы данных. Виды и типы баз данных. Структура реляционных баз данных. Проектирование баз данных. Сетевые и иерархические базы данных.

SQL

Книги:

Изучаем SQL. Линн Бейли Обожаю эту линейку книг, серию Head First O`Reilly. И всем рекомендую)) Просто и доступно даже о сложном пишут.

Статьи:

Как изучить основы SQL за 2 дня

Полезные запросы

Тренажеры:

http://www.sql-ex.ru/ Бесплатный тренажер для практики

Ресурсы и инструменты для практики с базами данных | SQL

Задачка по SQL. Найти объединенные данные

Резюме

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

Если посмотреть на информацию о таблице в БД, мы можем увидеть ее ключи и индексы. Что это такое:

1.PK primary key, первичный ключ. Гарантирует уникальность данных, часто используется для колонки с ID. Если ключ наложен на одну колонку каждое значение в ячейках этой колонки уникальное. Если на несколько комбинации строк по колонкам уникальны.

2.FK foreign key, внешний ключ. Нужен для связки двух таблиц в разных соотношениях (1:1, 1:N, N:N). Этот ключ указываем в "дочерней" таблице, то есть в той, которая ссылается на родительскую (в таблице с данными по лицевому счету отсылка на client_id из таблицы клиентов).

3.Индекс. Нужен для ускорения выборки из таблицы.

Транзакционные базы данных выполняют требования ACID:

  • Atomicity Атомарность

  • Consistency Согласованность

  • Isolation Изолированность

  • Durability Надежность

См также:

Что такое транзакция

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

Поэтому логика приложения отдельно, база отдельно. Так и получается клиент-серверная архитектура =)

См также:

Клиент-серверная архитектура в картинках

Чтобы достать данные из базы, надо написать запрос к ней на языке SQL (Structured Query Language). Разработчики пишут SQL-запросы внутри кода приложения. А тестировщики используют SQL для:

  • Поиска по базе правильно ли данные сохранились? В нужные таблицы легли? Это select-запросы.

  • Подготовки тестовых данных а что, если это значение будет пустое? А что, если у меня будет 2 лицевых счета на одной карточке? Можно готовить данные через графический интерфейс, но намного быстрее отправить несколько запросов в базу. Когда есть к ней доступ и вы знаете SQL =)

План-минимум для изучения: select, join, insert, update, create, delete, group by, having, distinct.

PS больше полезных статей ищитев моем блоге по метке полезное. А полезные видео намоем youtube-канале

Подробнее..

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

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 минут. Я, конечно, сомневаюсь, что описанные выше приемы в их изначальном виде окажутся полезны, но надеюсь, что они вдохновили и натолкнули на собственные идеи!

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

Подробнее..

Перевод Нагрузочное тестирование на Gatling Полное руководство. Часть 1

16.04.2021 20:09:46 | Автор: admin

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

Краткий обзор руководства

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

Первый раз слышите о Gatling? Тогда для начала уделите внимание моей вводной статье о Gatling. Но если в двух словах:

  • Gatling - это инструмент для нагрузочного тестирования с открытым исходным кодом, полностью написанный на Scala.

  • Простой и выразительный DSL, который предлагает Gatling, упрощает написание скриптов нагрузочного тестирования.

  • Он не содержит графического интерфейса (например, как JMeter), хотя поставляется с графическим интерфейсом для облегчения записи скриптов.

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

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

Что такое тестирование производительности?

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

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

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

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

  • Нагрузочное тестирование (Load Testing) - тестирование системы на заранее определенном объеме пользователей и трафике (пропускной способности);

  • Стресс-тестирование (Stress Testing) - тестирование системы под постоянно увеличивающейся нагрузкой, чтобы найти точку останова (breakpoint).

  • Тестирование стабильности (Soak Testing) - тестирование со стабильным уровнем трафика в системе на более длительном периоде времени для выявления узких мест.

Gatling подходит для всех этих подвидов тестирования производительности.

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

  • Время отклика транзакции (Transaction Response Times) - сколько времени требуется серверу, чтобы ответить на запрос.

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

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

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

Исходный код

Вы можете найти весь исходный код из этого руководства в моем репозитории на Github.


1. Установка Gatling

Прежде чем начать что-либо делать, убедитесь, что у вас установлен JDK8 (или более новый). Если вам нужна помощь с этим, ознакомьтесь с этим руководством по установке JDK.

Самый простой способ установить Gatling - загрузить версию Gatling с открытым исходным кодом с сайта Gatling.io. Кликните Download Now, и начнется загрузка ZIP-архива:

Download GatlingDownload Gatling

Разархивируйте архив в любое место на вашем компьютере. Откройте полученную папку и перейдите в каталог bin. Оттуда запустите:

  • gatling.bat - если вы используете Windows

  • gatling.sh - если вы работаете на Mac или Unix

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

Choose Gatling Simulation to runChoose Gatling Simulation to run

Введите 0, чтобы выбрать computerdatabase.BasicSimulation. Вам будет предложено ввести описание запуска (run description), но это необязательно, и его можно оставить пустым.

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


2. Gatling Recorder

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

Как только вы овладеете Gatling (к концу этого руководства!), вы сможете писать скрипты с нуля в своей IDE или даже просто в текстовом редакторе.

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

2.1 Создание HAR-файла

Самый удобный способ использования Gatling Recorder, как мне кажется, предполагает генерацию HAR-файла (Http-архива) вашего пользовательского пути в Google Chrome.

Создание этих файлов и их импорт в Gatling Recorder позволяет обойти проблемы с записью на HTTPS.

Чтобы создать HAR-файл, выполните следующие действия:

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

  2. Откройте Chrome Developer Tools и перейдите на вкладку Network.

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

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

  5. Кликните правой кнопкой мыши в любом месте вкладки Network и выберите Save all as HAR with content. Сохраните этот файл где-нибудь на вашем компьютере.

  1. Теперь перейдите в свою папку bin Gatling (в которой вы впервые запустили Gatling в предыдущем разделе) и запустите файл recorder.sh в Mac/Unix или recorder.bat в Windows. Загрузится Gatling Recorder.

  2. Измените Recorder Mode в правом верхнем углу на HAR Converter.

  3. В разделе HAR File перейдите к местоположению HAR-файла, созданного на шаге 5.

  4. Назовите свой скрипт, изменив Class Name на, например, MyComputerTest.

  5. Все остальное оставьте как было по умолчанию и кликните Start!

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

Gatling Recorder screenshotGatling Recorder screenshot
  1. Чтобы запустить скрипт, вернитесь в папку bin Gatling и снова запустите gatling.sh или gatling.bat. После того, как Gatling загрузится, вы сможете выбрать только что созданный скрипт.

Если вы хотите посмотреть на только что созданный скрипт, вы можете найти его в папке user-files/simulations в вашем каталоге Gatling. Откройте скрипт MyComputerTest, который вы только что записали, в текстовом редакторе. Он должен выглядеть как-то так:

import scala.concurrent.duration._import io.gatling.core.Predef._import io.gatling.http.Predef._import io.gatling.jdbc.Predef._class MyComputerTest extends Simulation {val httpProtocol = http.baseUrl("http://computer-database.gatling.io").inferHtmlResources().userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Safari/537.36")val headers_0 = Map("Accept" -> "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9","Accept-Encoding" -> "gzip, deflate","Accept-Language" -> "en-GB,en-US;q=0.9,en;q=0.8","Upgrade-Insecure-Requests" -> "1")val scn = scenario("MyComputerTest").exec(http("request_0").get("/computers").headers(headers_0)).pause(9).exec(http("request_1").get("/computers?f=amstrad").headers(headers_0)).pause(4).exec(http("request_2").get("/assets/stylesheets/bootstrap.min.css").resources(http("request_3").get("/assets/stylesheets/main.css")))setUp(scn.inject(atOnceUsers(1))).protocols(httpProtocol)}

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


3. Настройка проекта Gatling

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

3.1 Выберите IDE для создания скриптов нагрузочного тестирования на Gatling

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

У вас есть несколько вариантов:

  • Другой вариант, который (недавно) стал доступен, - это Visual Studio Code или сокращенно VS Code. Эта IDE последние несколько лет развивалась с головокружительной скоростью и стала популярной среди разработчиков множества различных технологических стеков. Ознакомьтесь с другой моей статьей Создаем Gatling скрипты с помощью VS Code для получения указаний по настройке.

  • Мой личный выбор, который я и буду показывать в этом руководстве, - это IntelliJ IDEA. Бесплатная версия достаточно хороша и поставляется со встроенной поддержкой Scala. Она идеально подходит для разработки скриптов Gatling.

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

3.2 Выберите систему сборки для Gatling

Конечно, вы можете использовать Gatling без системы сборки и просто запускать его из первичных zip-файлов (как мы делали в первом разделе). Но есть вероятность, что вскоре вы все-таки захотите использовать систему сборки в своем проекте нагрузочного тестирования Gatling. Это упростит обслуживание в системе контроля версий. Опять же, у вас есть несколько вариантов на выбор:

Я расскажу о настройке нового проекта в IntelliJ Idea с Maven в оставшейся части этого раздела.

3.3 Создание проекта Gatling из архетипа Maven

Откройте терминал или командную строку и введите:

mvn archetype:generate

В конце, вы увидите этот запрос:

Choose a number or apply filter (format: [groupId:]artifactId, case sensitive contains):

Введите gatling.

Затем вы должны увидеть:

1: remote -> io.gatling.highcharts:gatling-highcharts-maven-archetype (gatling-highcharts-maven-archetype)

Просто введите 1 для выбора архетипа Gatling. На следующем экране выберите последнюю версию:

Choose Gatling VersionChoose Gatling Version

Я ввел 35, чтобы выбрать версию 3.3.1 для этого туториала.

Для groupId введите com.gatlingTest.

Для artifactId введите myGatlingTest.

Для version просто нажмите ENTER, чтобы принять 1.0-SNAPSHOT.

Для package нажмите ENTER еще раз, чтобы принять в качестве имени пакета com.gatlingTest.

В конце введите Y, чтобы подтвердить все настройки, и Maven создаст для вас новый проект.

Следующее, что нужно сделать, - это импортировать этот проект в вашу IDE. Я импортирую проект в IntelliJ.

На стартовой странице IntelliJ выберите Import Project.

Import IntelliJ Gatling projectImport IntelliJ Gatling project

Перейдите в папку проекта, которую вы только что создали, и выберите файл pom.xml. Кликните open, и Intellij начнет импортировать проект в IDE за вас.

После того, как импорт проекта будет завершен, откройте панель Project Directory слева и раскройте папку src>test>scala. Дважды кликните по классу Engine.scala. Вы можете увидеть сообщение No Scala SDK in module вверху экрана. Если это так, нажмите Setup Scala SDK:

Import Scala SDK in IntelliJImport Scala SDK in IntelliJ

Проверьте, какие версии Scala у вас есть:

Scala versions in IntelliJScala versions in IntelliJ

Если у вас нет версии, указанной здесь, кликните Create, выберите версию 2.12 и кликните кнопку download:

ПРИМЕЧАНИЕ: Я НАСТОЯТЕЛЬНО рекомендую использовать версию Scala 2.12 с IntelliJ - 2.13, похоже, не очень хорошо работает с Gatling

Download Scala in IntelliDownload Scala in Intelli

В качестве альтернативы, если у вас возникли проблемы с загрузкой бинарников Scala через IntelliJ, вы можете вместо этого загрузить бинарники Scala непосредственно со Scala-lang. Кликните Download the Scala binaries, как показано на этом скриншоте:

Download Scala binaries from Scala-langDownload Scala binaries from Scala-lang

Сохраните бинарники где-нибудь на жестком диске и распакуйте ZIP-архив. Вернувшись в IntelliJ, снова кликните Setup Scala SDK, и на этот раз кликните Configure. Нажмите кнопку Add в левом нижнем углу:

Add the Scala binaries to IntelliJAdd the Scala binaries to IntelliJ

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

Select the Lib folder to import Scala BinariesSelect the Lib folder to import Scala Binaries

Теперь в диалоге Add Scala Support вы сможете выбрать библиотеку Scala, которую вы загрузили:

Add Scala SupportAdd Scala Support

На этом моменте, вам также может потребоваться пометить папку scala как source root в IntelliJ. Для этого кликните правой кнопкой мыши по папке scala и выберите Mark Directory As -> Test Sources Root:

Mark Test Sources as Root in IntelliJMark Test Sources as Root in IntelliJ

На всякий случай также отметьте всю папку src как source root:

Mark Sources RootMark Sources Root

Наконец, кликните правой кнопкой мыши объект Engine и выберите Run:

Run the Engine Object in IntelliJRun the Engine Object in IntelliJ

Вы должны увидеть сообщение типа There is no simulation script. Please check that your scripts are in user-files/simulations. Так и должно быть, далее мы приступим к настройке наших скриптов нагрузочных тестов Gatling.

3.4 Добавление базового скрипта Gatling

Чтобы протестировать нашу новую среду разработки, давайте добавим базовый скрипт Gatling. Этот скрипт запустит тест на базе данных компьютеров Gatling.

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

Кликните правой кнопкой мыши папку scala и выберите New > Scala Package - назовите пакет computerdatabase. Кликните эту папку правой кнопкой мыши и выберите New > Scala Class - назовите этот класс BasicSimulation. Скопируйте весь приведенный ниже код в новый класс:

package computerdatabaseimport io.gatling.core.Predef._import io.gatling.http.Predef._import scala.concurrent.duration._class BasicSimulation extends Simulation {  val httpProtocol = http    .baseUrl("http://computer-database.gatling.io") // Здесь находится корень для всех относительных URL    .acceptHeader(      "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"    ) // Вот общие заголовки    .acceptEncodingHeader("gzip, deflate")    .acceptLanguageHeader("en-US,en;q=0.5")    .userAgentHeader(      "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0"    )  val scn =    scenario("Scenario Name") // Сценарий представляет собой цепь запросов и пауз       .exec(        http("request_1")          .get("/")      )      .pause(7) // Обратите внимание, что Gatling записал паузы в реальном времени  setUp(scn.inject(atOnceUsers(1)).protocols(httpProtocol))}

Теперь давайте запустим скрипт. Кликните правой кнопкой мыши объект Engine и выберите Run Engine. Gatling загрузится, и вы должны увидеть сообщение computerdatabase.BasicSimulation is the only simulation, executing it. Нажмите Enter, и скрипт выполнится.

Мы также можем запустить наш Gatling тест напрямую через Maven из командной строки. Для этого откройте терминал в каталоге вашего проекта и введите mvn gatling:test. Эта команда выполнит Gatling тест с помощью плагина Maven для Gatling.

Наша среда разработки Gatling готова к работе! Теперь нам нужно приложение, которое мы будем тестировать. Мы могли бы использовать базу данных компьютеров Gatling, но вместо этого я разработал API-приложение специально для этого туториала - базу данных игр!


В преддверии старта курса "Нагрузочное тестирование" приглашаем всех желающих записаться на бесплатный демо-урок в рамках которого рассмотрим интерфейс LoadRunner Virtual User Generator, запишем скрипт тестирования web-сайта, проведём его отладку и параметризацию. В результате вы научитесь создавать скрипты нагрузочного тестирования web-сайтов.

ЗАПИСАТЬСЯ НА ДЕМО-УРОК

Подробнее..

Что такое VCS (система контроля версий)

17.04.2021 00:07:00 | Автор: admin

Система контроля версий (от англ. Version Control System, VCS) это место хранения кода. Как dropbox, только для разработчиков!

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

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

Итого содержание:

Что это такое и зачем она нужна

Допустим, что мы делаем калькулятор на Java (язык программирования). У нас есть несколько разработчиков Вася, Петя и Иван. Через неделю нужно показывать результат заказчику, так что распределяем работу:

  • Вася делает сложение;

  • Петя вычитание;

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

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

Итак, все забрали себе файлы из общей папки. Пока их немного:

  • Main.java общая логика

  • GUI.java графический интерфейс программы

С ними каждый и будет работать!

Вася закончил работу первым, проверил на своей машине все работает, отлично! Удовлетворенно вздохнув, он выкладывает свой код в общую папку. Вася сделал отдельный класс на сложение (Sum.java), добавил кнопку в графический интерфейс (внес изменения в GUI.java) и прописал работу кнопки в Main.java.

Петя химичил-химичил, ускорял работу, оптимизировал... Но вот и он удовлетворенно вздохнул готово! Перепроверил ещё раз работает! Он копирует файлы со своей машины в общую директорию. Он тоже сделал отдельный класс для новой функции (вычитание Minus.java), внес изменения в Main.java и добавил кнопку в GUI.java.

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

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

Катя, что случилось??

Вы же сказали, что всё сделали! А в графическом интерфейсе есть только вычитание. Сложения нет!

Вася удивился:

Как это нет? Я же добавлял!

Стали разбираться. Оказалось, что Петин файл затер изменения Васи в файлах, которые меняли оба: Main.java и GUI.java. Ведь ребята одновременно взяли исходные файлы к себе на компьютеры у обоих была версия БЕЗ новых функций.

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

Поэтому, когда он положил документы в хранилище, Васины правки были стерты. Остался только новый файл Sum.java, ведь его Петя не трогал.

Хорошо хоть логика распределена! Если бы всё лежало в одном классе, было бы намного сложнее совместить правки Васи и Пети. А так достаточно было немного подправить файлы Main.java и GUI.java, вернув туда обработку кнопки. Ребята быстро справились с этим, а потом убедились, что в общем папке теперь лежит правильная версия кода.

Собрали митинг (жаргон собрание, чтобы обсудить что-то):

Как нам не допустить таких косяков в дальнейшем?

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

Да, давайте попробуем!

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

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

Когда он пришел с утра, в офисе был переполох. Вася бегал по офису и причитал:

Мои изменения пропали!!! А я их не сохранил!

Увидев Ваню, он подскочил к нему и затряс за грудки:

Зачем ты стер мой код??

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

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

Код теперь не работает! Ты вообще проверял приложение, закончив синхронизацию?

Нет, я только свою часть посмотрел...

Вася покачал головой:

Но ведь при сохранении на общий диск можно допустить ошибку! По самым разным причинам:

  • Разработчик начинающий, чаще допускает ошибки.

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

  • Посчитал, что этот код не нужен что он устарел или что твоя новая логика делает то же самое, а на самом деле не совсем.

И тогда приложение вообще перестанет работать. Как у нас сейчас.

Ваня задумался:

Хм... Да, пожалуй, ты прав. Нужно тестировать итоговый вариант!

Петя добавил:

И сохранять версии. Может, перенесем наш код в Dropbox, чтобы не терять изменения?

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

Через пару дней ребята снова собрали митинг:

Ну как вам в дропбоксе?

Уже лучше. По крайней мере, не потеряем правки!

Петя расстроенно пожимает плечами:

Да, только мы с Васей одновременно вносили изменения в Main.java, создалась конфликтующая версия. И пришлось вручную их объединять... А класс то уже подрос! И глазками сравнивать 100 строк очень невесело... Всегда есть шанс допустить ошибку.

Ну, можно же подойти к тому, кто создал конфликт и уточнить у него, что он менял.

Хорошая идея, давайте попробуем!

Попробовали. Через несколько дней снова митинг:

Как дела?

Да всё зашибись, работаем!

А почему код из дропбокса не работает?

Как не работает??? Мы вчера с Васей синхронизировались!

А ты попробуй его запустить.

Посмотрели все вместе и правда не работает. Какая-то ошибка в Main.java. Стали разбираться:

Так, тут не хватает обработки исключения.

Ой, подождите, я же её добавлял!

Но ты мне не говорил о ней, когда мы объединяли правки.

Да? Наверное, забыл...

Может, еще что забыл? Ну уж давай лучше проверим глазами...

Посидели, выверили конфликтные версии. Потратили час времени всей команды из-за пустяка. Обидно!

Слушайте, может, это можно как-то попроще делать, а? Чтобы человека не спрашивать что ты менял?

Можно использовать программу сравнения файлов. Я вроде слышал о таких. AraxisMerge, например!

Ой, точно! В IDEA же можно сравнивать твой код с клипбордом (сохраненным в Ctrl + C значении). Давайте использовать его!

Точно!

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

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

Да? И что за программы?

Системы контроля версий называются. Вот SVN, например. Давайте попробуем его?

А давайте!

Попробовали. Работает! Еще и часть правок сама синхронизирует, даже если Вася с Петей снова не поделили один файл. Как она это делает? Давайте разбираться!

Как VCS работает

Подготовительная работа

Это те действия, которые нужно сделать один раз.

1. Создать репозиторий

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

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

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

Всё! Теперь у нас есть общее хранилище данных! С ним дальше и будем работать.

2. Скачать проект из репозитория

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

Поэтому Петя, Вася и Иван удаляют то, что было у них было на локальных компьютерах. И забирают данные из репозитория, клонируя его. В Mercurial (один из вариантов VCS) эта команда так и называется clone. В других системах она зовется иначе, но смысл всё тот же клонировать (копировать) то, что лежит в репозитории, к себе на компьютер!

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

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

Ежедневная работа

А это те действия, которые вы будете использовать часто.

1. Обновить проект, забрать последнюю версию из репозитория

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

Так, Вася обновил проект утром и увидел, что Ваня изменил файлы Main.java и GUI.java. Отлично, теперь у Васи актуальная версия на машине. Можно приступать к работе!

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

Тут может возникнуть вопрос в чем отличие от clone? Можно же просто клонировать проект каждый раз, да и всё! Зачем отдельная команда?

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

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

А еще обновление это быстрее. Обновиться могли 5 файликов из 1000, зачем выкачивать всё?

2. Внести изменения в репозиторий

Вася работает над улучшением сложения. Он придумал, как ускорить его работу. А заодно, раз уж взялся за рефакторинг (жаргон улучшение системы, от англ. refactor), обновил и основной класс Main.java.

Перед началом работы он обновил проект на локальном (своём) компьютере, забрав из репозитория актуальные версии. А теперь готов сохранить в репозиторий свои изменения. Это делается одной или двумя командами зависит от той VCS, которую вы используете в работе.

1 команда commit

Пример системы SVN.

Сделав изменения, Вася коммитит их. Вводит команду commit и все изменения улетают на сервер. Всё просто и удобно.

2 команды commit + push

Примеры системы Mercurial, Git.

Сделав изменения, Вася коммитит их. Вводит команду commit изменения сохранены как коммит. Но на сервер они НЕ уходят!

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

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

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

Итого

Когда разработчик сохраняет код в общем хранилище, он говорит:

Закоммитил.

Или:

Запушил.

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

3. Разрешить конфликты (merge)

Вася добавил вычисление процентов, а Петя деление. Перед работой они обновили свои локальные сборки, получив с сервера версию 3 файлов Main.java и Gui.java.

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

Вася закончил первым. Проверив свой код, он отправил изменения на сервер. Он:

  • Добавил новый файл Percent.java

  • Обновил Main.java (версию 3)

  • Обновил Gui.java (версию 3)

При отправке на сервер были созданы версии:

  • Percent.java версия 1

  • Main.java версия 4

  • Gui.java версия 4

Петя закончил чуть позже. Он:

  • Добавил новый файл Division.java

  • Обновил Main.java (версию 3, ведь они с Васей скачивали файлы одновременно)

  • Обновил Gui.java (версию 3)

Готово, можно коммитить! При отправке на сервер были созданы версии:

  • Division.java версия 1

  • Main.java версия 4

  • Gui.java версия 4

Но стойте, Петя обновляет файлы, которые были изменены с момента обновления кода на локальной машине! Конфликт!

Часть конфликтов система может решить сама, ей достаточно лишь сказать merge. И в данном случае этого будет достаточно, ведь ребята писали совершенно разный код, а в Main.java и Gui.java добавляли новые строчки, не трогая старые. Они никак не пересекаются по своим правкам. Поэтому система сливает изменения добавляет в версию 4 Петины строчки.

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

  • Оставить Васин код, затерев Петины правки если Петя посмотрит Васны изменения и поймет, что те лучше

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

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

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

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

Особая боль глобальный рефакторинг, когда затрагивается МНОГО файлов. Обновление версии библиотеки, переезд с ant на gradle, или просто выкашивание легаси кода. Нельзя коммитить его по кусочкам, иначе у всей команды развалится сборка.

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

А что делать? Обновляет проект и решает конфликты. Иногда в работе над большой задачей разработчик каждый день обновляется и мерджит изменения, а иногда только через несколько дней.

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

4. Создать бранч (ветку)

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

Что делать будем? Не коммитить до показа?

У меня уже готовы новые изменения. Давайте закоммичу, я точно ничего не сломал.

Катя хватается за голову:

Ой, давайте без этого, а? Мне потом опять краснеть перед заказчиками!

Тут вмешивается Иван:

А давайте бранчеваться!

Все оглянулись на него:

Что делать?

Иван стал рисовать на доске:

Бранч это отдельная ветка в коде. Вот смотрите, мы сейчас работаем в trunk-е, основной ветке.

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

Потом Вася закоммитил изменения по улучшению классов появилась версия 1 кода.

Потом он добавил проценты появилась версия кода 2.

При этом в самой VCS сохранены все версии, и мы всегда можем:

  • Посмотреть изменения в версии 1

  • Сравнить файлы из версии 1 и версии 2 система наглядно покажет, где они совпадают, а где отличаются

  • Откатиться на прошлую версию, если версия 2 была ошибкой.

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

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

Теперь, если я захочу закоммитить изменения, они по-прежнему пойдут в основную ветку. Бранч при этом трогать НЕ будут (изменения идут в ту ветку, в которой я сейчас нахожусь. В этом примере мы создали branch, но работать продолжаем с trunk, основной веткой)

Так что мы можем смело коммитить новый код в trunk. А для показа использовать branch, который будет оставаться стабильным даже тогда, когда в основной ветке всё падает из-за кучи ошибок.

С бранчами мы всегда будем иметь работающий код!

Подожди, подожди! А зачем эти сложности? Мы ведь всегда может просто откатиться на нужную версию! Например, на версию 2. И никаких бранчей делать не надо!

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

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

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

  • Обновиться на версию 3

  • Исправить баг локально (на своей машине, а не в репозитории)

  • Никуда это не коммитить = потерять эти исправления

  • Собрать сборку локально и отдать заказчику

  • Не забыть скопипастить эти исправления в актуальную версию кода 33 и закоммитить (сохранить)

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

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

Смерджили так называют слияние веток. Это когда мы внесли изменения в branch и хотим продублировать их в основной ветке кода (trunk). Мы ведь объединяем разные версии кода, там наверняка есть конфликты, а разрешение конфликтов это merge, отсюда и название!

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

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

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

А иногда и ещё сложнее!

А как посмотреть, в какой ветке ты находишься?

О, для этого есть специальная команда. Например, в Mercurial это hg sum: она показывает информацию о том, где ты находишься. Вот пример ее вызова:

D:\vcs_project\test>hg sumparent: 3:66a91205d385 tipTry to fix bug with devicebranch: default

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

Потом мы видим сообщение, с которым был сделан коммит. В данном случае разработчик написал Try to fix bug with device.

И, наконец, параметр branch! Если там значение default мы находимся в основной ветке. То есть мы сейчас в trunk-е. Если бы были не в нём, тут было бы название бранча. При создании бранча разработчик даёт ему имя. Оно и отображается в этом пункте.

Круто! Давайте тогда делать ветку!

*****

Git создал интерактивную игрушку, чтобы посмотреть на то, как происходит ветвление https://learngitbranching.js.org

*****

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

Итого

Система контроля версий (от англ. Version Control System, VCS) это dropbox для кода.

Популярные VCS и отличия между ними

Наиболее популярные это:

  • SVN простая, но там очень сложно мерджиться

  • Mercurial (он же HG), Git намного больше возможностей (эти системы похожи по функционалу)

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

Mercurial и Git распределенная система контроля версий. Внесение изменений двухступенчатое сначала коммит, потом push. Это удобно, если вы работаете без интернета, или делаете мелкие коммиты, но не хотите ломать основной код пока не доделаете большую задачу. Тут есть и автоматическое слияние разных бранчей. Больше возможностей дают системы.

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

Но есть и графический интерфейс. Устанавливаете отдельную программу и выполняете действия мышкой. Обычно это делается через черепашку программа называется Tortoise<VCS>. TortoiseSVN, TortoiseHG, TortoiseGit... Часть команд можно сделать через среду разработки IDEA, Eclipse, etc.

Но любой графический интерфейс как работает? Вы потыкали мышкой, а система Tortoise составила консольную команду из вашего тык-тык, её и применила.

См также:

Что такое API подробнее о том, что скрывается за интерфейсом.

Вот некоторые базовые команды и форма их записи в разных VCS:

Действие

SVN

GIT

HG

Клонировать репозиторий

svn checkout <откуда> <куда>

git clone <откуда> <куда>

hg clone<откуда> <куда>

Обновить локальную сборку из репозитория

svn update

git pull

hg pull -u

Проверить текущую версию (где я есть?)

svn log --revision HEAD

git show -s

hg sum

Закоммитить изменения

svn commit -m "MESSAGE"

git commit-a-m "MESSAGE"


git push

hg commit -m "MESSAGE"


hg push

Переключиться на branch

svn checkout <откуда> <куда>

git checkout BRANCH

hg update BRANCH

Тут хочу напомнить, что я тестировщик, а не разработчик. Поэтому про тонкости различия коммитов писать не буду, да и статья для новичков, оно им и не надо =)

Пример выкачиваем проект из Git

Выкачивать мы будем систему с открытым исходным кодом Folks. Так что вы можете повторить этот пример сами!

Для начала установите Git. Когда он установлен, можно выкачивать репозиторий на свой компьютер. Я покажу 3 способа (есть и другие, но я покажу именно эти):

  1. Через консоль

  2. Через IDEA

  3. Через TortoiseGit

Исходный код мы будем в директорию D:\git.

1. Через консоль

1. Запустить консоль git:

2. Написать команду:

git clone Откуда Куда
git clone https://bitbucket.org/testbasecode/folks/src/master/ D:\\git\\folks_console

В консоли нужно писать простой слеш или экранировать обратный. Иначе консоль его проигнорирует!

Также НЕ НАДО использовать в названии папки куда клонируем русские символы или пробелы. Иначе потом огребете проблем на сборке проекта.

2. Через IDEA

1. Запустить IDEA

2. Check out from Version Control Git

3. Заполнить поля:

  • URL https://bitbucket.org/testbasecode/folks/src/master/ (откуда выкачиваем исходный код)

  • Назначение D:\git\folks_idea (куда сохраняем на нашем компьютере)

4. Нажать Clone всё! Дальше IDEA всё сделает сама!

А под конец предложит открыть проект, подтверждаем!

Если открывается пустой серый экран, найдите закладку Project (у меня она слева сверху) и щелкните по ней, чтобы раскрыть проект:

И вуаля и код скачали, и сразу в удобном и бесплатном редакторе открыли! То, что надо. Для новичка так вообще милое дело.

3. Через TortoiseGit

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

1. Скачать TortoiseGit

2. Установить его Теперь, если вы будете щелкать правой кнопкой мыши в папочках, у вас появятся новые пункты меню: Git Clone, Git Create repository here, TortoiseGit

3. Перейти в папку, где у нас будет храниться проект. Допустим, это будет D:\git.

4. Нажать правой кнопкой мыши Git Clone

Заполнить поля:

  • URL https://bitbucket.org/testbasecode/folks/src/master/ (откуда выкачиваем исходный код)

  • Directory D:\git\folks_tortoise_git (куда сохраняем на нашем компьютере)

5. Нажать Ок

Вот и всё! Система что-то там повыкачивает и покажет результат папочку с кодом!

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

Итого

Пусть вас не пугают страшные слова типа SVN, Mercurail, Git, VCS это всё примерно одно и то же. Место для хранения кода, со всеми его версиями. Дропбокс разработчика! И даже круче =) Ведь в дропбоксе любое параллельное изменение порождает конфликтную версию.

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

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

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

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

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

Это нестрашно =) Посмотрите выше пример буквально 1 команда позволяет нам получить этот самый код.

А потом уже, если разрешат, вы сможете даже вносить свои изменения в основной код или код автотестов. Но если уж вы с этим справитесь, то с коммитом и подавно!

PS: больше полезных статей ищитев моем блоге по метке полезное. А полезные видео намоем youtube-канале.

PPS: автор картинок этой статьи Аня Черноморцева, автор стиля Виктория Лапис =)

Подробнее..

Подсказки по написанию тестов в приложениях на Go

23.04.2021 14:18:12 | Автор: admin

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

Используем интерфейсы при разработке

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

package yourpackage import (    "context"     "github.com/go-redis/redis/v8") func CheckLen(ctx context.Context, client *redis.Client, key string) bool {    val, err := client.Get(ctx, key).Result()    if err != nil {    return false    }    return len(val) < 10  }

Тесты для неё могут выглядеть примерно так:

package yourpackage import (    "context"    "testing"     "github.com/go-redis/redis/v8") func TestCheckLen(t *testing.T) {    ctx := context.Background()    rdb := redis.NewClient(&redis.Options{Addr: "localhost:6379"})    err := rdb.Set(ctx, "some_key", "value", 0).Err()    if err != nil {    t.Fatalf("redis return error: %s", err)    }     got := CheckLen(ctx, rdb, "some_key")    if !got {    t.Errorf("CheckLen return %v; want true", got)    }}

Но как проверить ситуацию, когда Redis возвращает ошибку? Или что делать, если мы не хотим добавлять Redis в наш CI? То есть как нам замокать вызов Redis? И ответ на эти вопросы используйте интерфейсы!

Перепишем наш код с использованием интерфейсов:

package yourpackage import (    "context"     "github.com/go-redis/redis/v8") type Storage interface {    Set(ctx context.Context, key string, v interface{}) error    Get(ctx context.Context, key string) (string, error)} type RedisStorage struct {    Redis *redis.Client} func (rs *RedisStorage) Set(ctx context.Context, key string, v interface{}) error {    return rs.Redis.Set(ctx, key, v, 0).Err()} func (rs *RedisStorage) Get(ctx context.Context, key string) (string, error) {    return rs.Redis.Get(ctx, key).Result()} func CheckLen(ctx context.Context, storage Storage, key string) bool {    val, err := storage.Get(ctx, key)    if err != nil {    return false    }    return len(val) < 10}

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

package yourpackage import (    "context"    "testing") type testRedis struct{} func (t *testRedis) Get(ctx context.Context, key string) (string, error) {    return "value", nil}func (t *testRedis) Set(ctx context.Context, key string, v interface{}) error {    return nil} func TestCheckLen(t *testing.T) {   ctx := context.Background()    storage := &testRedis{}     got := CheckLen(ctx, storage, "some_key")    if !got {    t.Errorf("CheckLen return %v; want true", got)    }}

Используем генераторы моков

Понятное дело, что для каждого случая писать свой мок немного избыточно. Можно попробовать написать универсальный мок. А можно попробовать его сгенерировать на основе интерфейса. Существует множество генераторов моков. Нам нравится https://github.com/vektra/mockery.

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

mockery --recursive=true --inpackage --name=Storage

И дальше используем его в тестах следующим образом:

package yourpackageimport (    "context"    "testing"     mock "github.com/stretchr/testify/mock") func TestCheckLen(t *testing.T) {    ctx := context.Background()     storage := new(MockStorage)    storage.On("Get", mock.Anything, "some_key").Return("value", nil)     got := CheckLen(ctx, storage, "some_key")    if !got {    t.Errorf("CheckLen return %v; want true", got)    }

Перехватываем логирование

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

package yourpackage import (    log "github.com/sirupsen/logrus") func Minus(a, b int) int {    log.Infof("Minus(%v, %v)", a, b)    return a - b} func Plus(a, b int) int {    log.Infof("Plus(%v, %v)", a, b)    return a + b} func Mul(a, b int) int {    log.Infof("Mul(%v, %v)", a, b)    return a + b // тут ошибка}

И тесты к этому коду:

package yourpackage import "testing" func TestPlus(t *testing.T) {    a, b, expected := 3, 2, 5    got := Plus(a, b)    if got != expected {    t.Errorf("Plus(%v, %v) return %v; want %v", a, b, got, expected)    }} func TestMinus(t *testing.T) {    a, b, expected := 3, 2, 1    got := Minus(a, b)    if got != expected {    t.Errorf("Minus(%v, %v) return %v; want %v", a, b, got, expected)    }} func TestMul(t *testing.T) {    a, b, expected := 3, 2, 6    got := Mul(a, b)    if got != expected {    t.Errorf("Mul(%v, %v) return %v; want %v", a, b, got, expected)    }}

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

time="2021-03-22T22:09:54+03:00" level=info msg="Plus(3, 2)"time="2021-03-22T22:09:54+03:00" level=info msg="Minus(3, 2)"time="2021-03-22T22:09:54+03:00" level=info msg="Mul(3, 2)"--- FAIL: TestMul (0.00s)yourpackage_test.go:55: Mul(3, 2) return 5; want 6FAILFAILgotest2/yourpackage 0.002sFAIL

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

package yourpackage import (    "io"    "testing"     "github.com/sirupsen/logrus") type logCapturer struct {    *testing.T    origOut io.Writer} func (tl logCapturer) Write(p []byte) (n int, err error) {    tl.Logf((string)(p))    return len(p), nil} func (tl logCapturer) Release() {    logrus.SetOutput(tl.origOut)} func CaptureLog(t *testing.T) *logCapturer {    lc := logCapturer{T: t, origOut: logrus.StandardLogger().Out}    if !testing.Verbose() {    logrus.SetOutput(lc)    }    return &lc} func TestPlus(t *testing.T) {    defer CaptureLog(t).Release()    a, b, expected := 3, 2, 5    got := Plus(a, b)    if got != expected {    t.Errorf("Plus(%v, %v) return %v; want %v", a, b, got, expected)    }} func TestMinus(t *testing.T) {    defer CaptureLog(t).Release()    a, b, expected := 3, 2, 5    got := Minus(a, b)    if got != expected {    t.Errorf("Minus(%v, %v) return %v; want %v", a, b, got, expected)    }} func TestMul(t *testing.T) {    defer CaptureLog(t).Release()    a, b, expected := 3, 2, 5    got := Mul(a, b)    if got != expected {    t.Errorf("Mul(%v, %v) return %v; want %v", a, b, got, expected)    }}

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

--- FAIL: TestMul (0.00s)yourpackage_test.go:16: time="2021-03-22T22:10:52+03:00" level=info msg="Mul(3, 2)"yourpackage_test.go:55: Mul(3, 2) return 5; want 6FAILFAILgotest2/yourpackage 0.002sFAIL

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

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

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

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

$ go tool cover -helpUsage of 'go tool cover':Given a coverage profile produced by 'go test':    go test -coverprofile=c.out...Display coverage percentages to stdout for each function:    go tool cover -func=c.out

Пробуем:

$ go test -coverprofile=c.out ./...ok  gotestcover/minus   0.001s  coverage: 100.0% of statements?   gotestcover/mul [no test files]ok  gotestcover/plus    0.001s  coverage: 100.0% of statements

Уже из этого вывода видно, что у нас два пакета покрыты на 100 % и для одного пакета нет тестовых файлов. Получим отчёт о покрытии:

$ go tool cover -func=c.outgotestcover/minus/minus.go:4:   Minus       100.0%gotestcover/plus/plus.go:4: Plus        100.0%total:                      (statements)100.0%

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

go test -coverpkg=./... -coverprofile=c.out ./

И теперь отчёт выдаёт ожидаемый процент покрытия тестами:

$ go tool cover -func=c.outgotestcover/minus/minus.go:4:   Minus       100.0%gotestcover/mul/mul.go:4:   Mul         0.0%gotestcover/plus/plus.go:4: Plus        100.0%total:                      (statements)66.7%

Считаем покрытие при тестировании приложения как черного ящика

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

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

func TestRunMain(t *testing.T) {    main()}

Запускаем его, потом интеграционные тесты, и завершаем наш тест. Звучит просто, но есть несколько нюансов. Зачастую надо сделать так, чтобы этот тест не запускался со всеми остальными тестами. Он особый, и для него должна быть отдельная логика запуска. Ещё функция main не должна приводить к выходу с ненулевым кодом возврата. И надо реализовать способ выхода из main по сигналу, не завершая при этом сам тест. То есть в целом надо реализовать для нашего web-сервиса graceful shutdown, что несложно сделать, и это в целом полезно. Давайте на примере реализуем небольшой web-сервис, протестируем его с помощью curl, и посчитаем покрытие тестами.

Сервис наш будет выглядеть следующим образом (взято с https://gobyexample.com/http-servers):

package main import (    "context"    "fmt"    "net/http"    "os"    "os/signal"    "time") func hello(w http.ResponseWriter, req *http.Request) {    fmt.Fprintf(w, "hello\n")} func headers(w http.ResponseWriter, req *http.Request) {    for name, headers := range req.Header {    for _, h := range headers {    fmt.Fprintf(w, "%v: %v\n", name, h)    }    }} func main() {    http.HandleFunc("/hello", hello)    http.HandleFunc("/headers", headers)     // Приложим некоторые усилия, чтобы приложение завершилось с нулевым кодом выхода    // Это важно для тестов, и в целом приятно    server := &http.Server{Addr: ":8090", Handler: nil}    // Запускаем приложение в отдельной горутине    go func() {    server.ListenAndServe()    }()     // А в текущей ждём сигнала об остановке приложения    quit := make(chan os.Signal, 1)    signal.Notify(quit, os.Interrupt)    <-quit    ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)    defer cancel()    server.Shutdown(ctx)}

И тест к нему:

// +build testrunmain package main import "testing" func TestRunMain(t *testing.T) {    main()}

Комментарий +build testrunmain говорит о том, что тест будет запускаться только в случае, если передан соответствующий tag. Запускаем наш тест:

$ go test -v -tags testrunmain -coverpkg=./... -coverprofile=c.out  ./...=== RUN   TestRunMain

Тестируем с помощью curl:

$ curl 127.0.0.1:8090/hellohello

И завершаем наше тестирование, нажав Ctrl+C:

$ go test -v -tags testrunmain -coverpkg=./... -coverprofile=c.out  ./...=== RUN   TestRunMain^C--- PASS: TestRunMain (100.92s)PASScoverage: 80.0% of statements in ./...ok  gobintest   100.926s    coverage: 80.0% of statements in ./

Можем посмотреть покрытие тестами и увидеть, что обработчик headers остался не протестированным:

$ go tool cover -func=c.outgobintest/main.go:12:   hello       100.0%gobintest/main.go:16:   headers     0.0%gobintest/main.go:24:   main        100.0%total:              (statements)80.0%

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

Хотите узнать больше о тестировании в Go? Вот ещё несколько интересных статей на хабре: один, два, три.

Подробнее..

Перевод Нестабильные(Flaky) тесты одна из основных проблем автоматизированного тестирования

26.04.2021 10:24:01 | Автор: admin

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

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

Данная статья призвана рассказать как бороться с каждой из причин.

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

  • Сами тесты;

  • Фреймворк для запуска тестов;

  • Сервисы и библиотеки, от которых зависит тестируемая система и тестовый фреймворк;

  • Операционная система и устройство с которым взаимодействует фреймворк автотестирования.

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

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

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

Сами тесты

Сами тесты могут вызвать нестабильность. Типичные причины:

  • Неправильная инциализация или очистка;

  • Неправильно подобранные тестовые данные;

  • Неправильное предположение о состоянии системы. Примером может служить системное время;

  • Зависимость от асинхроных действий;

  • Зависимость от порядка запуска тестов.

Фреймворк для запуска тестов

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

  • Неспособность выделить достаточно ресурсов для тестируемой системы, что приводит к ее сбою;

  • Неправильное планирование тестов, поэтому они "противоречат" и приводят к сбою друг друга;

  • Недостаточно системных ресурсов для выполнения требований тестирования.

Сервисы и библиотеки, от которых зависит тестируемая система и тестовый фреймворк

Приложение (или тестируемая система) может быть источником нестабильности

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

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

Типичные причины:

  • Состояние гонки;

  • Непроинициализированные переменные;

  • Медленный ответ или отсутствие ответа при запросе от теста;

  • Утечки памяти;

  • Избыточная подписка на ресурсы;

  • Изменения в приложении и в тестах происходят с разной скоростью.

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

Герметичная среда менее подвержена нестабильности.

Операционная система и устройство с которым взаимодействует фреймворк автотестирования

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

  • Сбои или нестабильность сети;

  • Дисковые ошибки;

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

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

В следующих статьях мы рассмотрим способы решения этих проблем.

Ссылки на источники

Подробнее..

Начинающему QA полезные функции снифферов на примере Charles Proxy

28.04.2021 16:12:49 | Автор: admin

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

В этой статье я расскажу об основных функциях снифферов, которые могут быть полезны QA. Попробую не вдаваться в теорию, а сфокусироваться на практике. Наиболее популярными представителями анализаторов трафика сейчас являются WhireShark, Fiddler и Charles Proxy. Об удобстве интерфейсов и функционале каждого из них можно рассуждать долго, учитывая все плюсы и минусы. Но здесь я отдал предпочтение Charles, поскольку сам им активно пользуюсь. Буду рассказывать на его примере.

Что собой представляет Charles Proxy

Charles Web Debugging Proxy - это инструмент мониторинга HTTP и HTTPS трафика. Он выступает в роли прокси-сервера (промежуточного звена) между тестируемым приложением и сервером на бэкенде, позволяя не только видеть, но также перехватывать и редактировать запросы.

Главное преимущество Charles Proxy и снифферов в целом - возможность просмотра трафика, в том числе с мобильных устройств, что значительно облегчает работу тестировщика клиент-серверных мобильных приложений.

Первичная настройка

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

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

Как правило, соединение настраивается по Wi-Fi. В настройках Wi-Fi мобильного устройства в качестве proxy-сервера надо указать IP-адрес компьютера и стандартный порт инструмента 8888 (пароль остается пустым).

IP-адрес компьютера можно узнать через командную строку (ipconfig) или в самом Charles Proxy (Help -> Local IP Address).

Этот же адрес есть в инструкции по подключению, доступной в Help -> SSL Proxying -> Install Charles Root Certificate on mobile device remote browser.

После сохранения настроек Charles Proxy сможет читать HTTP-трафик мобильного устройства. Но чтобы смотреть расшифрованный трафик HTTPS, нужны дополнительные манипуляции - требуется установить SSL-сертификат Charles Proxy в браузере на мобильном устройстве.

Скачать сертификат можно по адресу: chls.pro/ssl (адрес, по которому скачивается сертификат, также можно найти в инструкции Help -> SSL Proxying -> Install Charles Root Certificate on mobile device remote browser). Далее в iOS его необходимо сделать доверенным (в Настройки -> Основные -> Профили).

В Android установленные сертификаты верифицируются в Settings -> Trusted Credentials на вкладке User.

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

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

В этом случае дополнительные сертификаты нужно установить на сам ПК. Для скачивания и установки нужна ссылка из Help -> SSL Proxying -> Install Charles Root Certificate.

Сертификат устанавливается в доверенные корневые центры.

Два слова об интерфейсе

Интерфейс Charles Proxy прост. Слева - список перехваченных запросов, справа - детали.

В списке запросов есть две основные вкладки - Structure и Sequence.

В первом случае запросы рассортированы по хостам-папкам. Наведя на любой из них, можно получить всю информацию о количестве запросов к этому корневому хосту, доле удачных, таймингах, размерах и т.п. Фактически, здесь представлена вся та же информация, которую можно получить из панели разработчика в браузере. Выбрав конкретный URL, можно увидеть код ответа, версии протоколов, контент и т.п. Тело запроса, заголовки, cookie (если есть) можно посмотреть в разных форматах - даже в HEX.

С помощью контекстного меню запроса можно настраивать блокировки, повторять и изменять запросы. На этом мы еще остановимся.

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

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

Фильтрация

В Charles Proxy очень много вариантов фильтрации запросов.

Начнем с вкладки Structure. Самое примитивное - скопировать хост и вставить в поле Filter. Так мы увидим только запросы с этого хоста. Примерно того же результата можно добиться, если в контекстном меню хоста выбрать Focus. Остальные запросы будут собраны в Other Hosts. Если при этом перейти на закладку Sequence и отметить настройку Focused, то в списке окажется информация только о тех запросах, которые были выбраны на вкладке Structure.

На вкладке Sequence есть аналогичный фильтр.

Charles Proxy умеет работать с регулярными выражениями. Для этого на вкладке Sequence выбираем Settings и отмечаем пункт Filter uses regex. И вписываем в поле поиска элементарную регулярку.

Например, вот так

^\w{4}\.

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

Там же можно включить Autoscroll списка запросов или указать максимальное количество строк.

В Charles Proxy можно фильтровать не только отображение, но и запись запросов. Для этого надо зайти в Proxy -> Record settings и задать условия на одной из вкладок - Include или Exclude - так мы включаем или выключаем запись запросов данного хоста.

Похожего результата можно добиться, используя блок-листы. Включить хост в блок лист можно из контекстного меню (команда Block list) или через добавление в Tools -> Block list, где следует отметить Enable Block list.

Блокируемый запрос можно прервать двумя способами (подходящий надо выбрать в настройках):

  • сбросить соединение;

  • вернуть ошибку 403.

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

Можно провести эксперимент. Включим наш тестовый стенд в Block list, выбрав простой сброс соединения. С помощью контекстного меню запроса повторим его (команда Repeat) и получим такой ответ:

Статус запроса - Failed, а в описании ошибки указано, что Connection dropped.

Просмотр SSL-трафика

Если ранее мы успешно установили SSL-сертификат, для просмотра зашифрованного трафика остается только включить SSL proxying для нужного хоста в самом Charles Proxy. Это можно сделать через контекстное меню конкретного хоста.

Чтобы не включать каждый хост, можно зайти в Proxy -> SSL Proxying settings и на первой вкладке SSL Proxying включить Enable SSL Proxying.

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

Брейкпоинты

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

Установить Breakpoint можно из контекстного меню запроса. После этого все аналогичные запросы будут перехвачены. Их можно будет просматривать и редактировать.

Чтобы проверить, как это работает можно использовать повтор запроса (Repeat из того же контекстного меню). Запрос перехватывается, его можно редактировать.

В принципе, изменить можно все - от header до авторизационного токена. Когда редактирование будет закончено, можно выбрать Execute и в Charles Proxy появится повторный запрос, который и отправится на сервер, а потом вернется с ответом. В этот момент можно будет посмотреть и отредактировать ответ, который получит приложение - появится поле Edit response.

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

Map remote

Еще одна популярная функция Charles Proxy - подмена ответа сервера. Так мы можем ответ одного хоста подменить на ответ другого. Настраивается это через Tools -> Map Remote.

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

Например, мы можем подменить контура. Я буду посылать запрос на dev-контур, но ответ хочу получить с тестового стенда. Для этого создаем новый пункт в списке Map Remote Settings. Map From - куда изначально был запрос; Map to - откуда берем ответ.

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

Map Local

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

Rewrite

Функция Rewrite может быть полезна, если вам нужно переписать данные, которые отправляются в Charles Proxy. В отличие от простого редактирования Rewrite позволяет задать правила изменения и работать в автоматическом режиме. Можно изменять и добавлять заголовки, искать и заменять текст в теле запроса или ответа. Можно даже менять статус ответа.

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

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

Помимо изменения запросов и ответов мы можем запретить кэширование или cookie (функции No caching и Block cookies). Эти опции повторяют аналогичные инструменты панели разработчика в браузере. В обоих случаях настраивается список хостов, для которых действует настройка. Если же список пуст, то кэширование и cookie отключаются на всех перехваченных хостах.

Throttling

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

Настраивается функция через Proxy ->Throttling settings.

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

Repeat Advanced

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

Конечно, список функций Charles Proxy этим не ограничивается. Есть еще много полезного - от перенаправления доменного имени на другой IP-адрес, до автоматического сохранения полученных ответов.

Отмечу, что Charles Proxy платный. Можно использовать триальную версию. Но раз в 5-7 минут поверх него будет отображаться всплывающее окно с версией, а раз в 30 минут он будет выключаться, при этом сессии не сохраняются. Решайте сами, помешает ли это вашей работе.

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

Автор статьи: Артем Холевко, Максилект.

Текст подготовлен по материалам внутреннего семинара Максилект.

P.S. Мы публикуем наши статьи на нескольких площадках Рунета. Подписывайтесь на наши страницы в VK, FB, Instagram или Telegram-канал, чтобы узнавать обо всех наших публикациях и других новостях компании Maxilect.

Подробнее..

Unit-тесты в СУБД как мы делаем это в Спортмастере, часть третья

29.04.2021 16:13:12 | Автор: admin

Привет, привет!

Пару лет назад было решено поделиться историей про автоматизированное тестирование СУБД и наш опыт применения в Спортмастере. С результатами можно ознакомиться здесь и здесь.

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

Спойлер

Краткое описание предыдущих частей

Есть система лояльности Спортмастера:

  • огромная, сложная и высоконагруженная система 24/7

  • важный функционал для бизнеса

  • активно развивается

  • есть сложные вычислительные алгоритмы

  • много потребителей

  • система преимущественно содержит серверную логику на Oracle

Хотелось повысить качество выпускаемого продукта и сократить время на тестирование, поэтому внедрили систему автоматизированного тестирования на PL/SQL:

  • ядро системы это open source библиотека utPLSQL v 2.3 от Стивена Фейерштейна

  • вокруг utPLSQL развёрнуты самописные модули, которые облегчают работу с автотестами:

    • модуль запуска автотестов

    • модуль генерации тестовых данных

    • модуль управления метаданными

    • модуль отчётности

    • и т.д.

  • сформирован каталог автотестов с определением ключевых настроек

  • автоматизированы запуск автотестов, отчётность и накат изменений

А что же сейчас?

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

Но это всё демагогия, поэтому перейдём к качественным показателям:

  1. Количество автотестов: 2400

  2. Время работы полного запуска: 40 секунд

  3. Показатель Code Coverage: 55%

Скорость работы

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

Автоматизация тестирования на уровне СУБД позволяет избежать большого числа проблем, связанных со внешней средой, но для быстрой работы тестов всё равно необходимо немного пошаманить.

Изначально, наши автотесты работали порядка 30 минут. Небольшое погружение в сторону параллельного запуска позволило сократить общее время работы до 5 минут, но и на этом мы не остановились. Для достижения текущего эталонного результата в 40 секунд было сделано:

  • Разбиение всех автотестов на логические функциональные блоки

  • Обеспечение изолированности тестовых данных под каждый функциональный блок автотестов

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

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

  • Запуск каждого функционального блока в отдельном потоке

  • Формирование сводной отчётности по полному запуску автотестов

Code Coverage

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

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

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

Это оказалось совсем несложным:

  • перед запуском автотестов вызвать: dbms_plsql_code_coverage.start_coverage

  • выключить функционал по окончанию всех работ: dbms_plsql_code_coverage.stop_coverage

  • написать запрос, который считает покрытие

А мы, получив приятную цифру в 50% покрытие, пошли заниматься более интересными вещами.

Автоматическая генерация кода

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

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

Автоматически сгенерённый код должен содержать:

  1. Инициализацию всех входных и выходных переменных простых типов

  2. Инициализацию всех входных и выходных коллекций

  3. Процедуры для сравнения двух коллекций одного типа

  4. Вызов тестируемого метода

  5. Базовые проверки выходных значений

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

utPLSQL v3

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

Так почему же у нас в проекте используется более старая версия фреймворка? К сожалению, utPLSQL v 3 не поддерживает несколько важных для нас фич. Что-то разработчикам кажется концептуально неправильным, до чего-то не доходят руки. Все проблемы решаемы, благо что код библиотеки так и остался открытым и может быть самостоятельно изменён. Соответствующая задача в техническом долге заведена, но она не является наиболее приоритетной в нашей дорожной карте развития автотестов.

Заключение

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

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

Всем добра!

Подробнее..

Что такое JSON

02.05.2021 16:16:26 | Автор: admin

Если вы тестируете API, то должны знать про два основных формата передачи данных:

  • XML используется в SOAP(всегда)и REST-запросах(реже);

  • JSON используется в REST-запросах.

Сегодня я расскажу вам про JSON.

JSON (англ. JavaScript Object Notation) текстовый формат обмена данными, основанный на JavaScript. Но при этом формат независим от JS и может использоваться в любом языке программирования.

JSON используется в REST API. По крайней мере, тестировщик скорее всего столкнется с ним именно там.

См также:

Что такое API общее знакомство с API

Что такое XML второй популярный формат

Введение в SOAP и REST: что это и с чем едят видео про разницу между SOAP и REST

В SOAP API возможен только формат XML, а вот REST API поддерживает как XML, так и JSON. Разработчики предпочитают JSON он легче читается человеком и меньше весит. Так что давайте разберемся, как он выглядит, как его читать, и как ломать!

Содержание

Как устроен JSON

В качестве значений в JSON могут быть использованы:

  • JSON-объект

  • Массив

  • Число (целое или вещественное)

  • Литералы true (логическое значение истина), false (логическое значение ложь) и null

  • Строка

Я думаю, с простыми значениями вопросов не возникнет, поэтому разберем массивы и объекты. Ведь если говорить про REST API, то обычно вы будете отправлять / получать именно json-объекты.

JSON-объект

Как устроен

Возьмем пример из документации подсказок Дадаты по ФИО:

{  "query": "Виктор Иван",  "count": 7}

И разберемся, что означает эта запись.

Объект заключен в фигурные скобки {}

JSON-объект это неупорядоченное множество пар ключ:значение.

Ключ это название параметра, который мы передаем серверу. Он служит маркером для принимающей запрос системы: смотри, здесь у меня значение такого-то параметра!. А иначе как система поймет, где что? Ей нужна подсказка!

Вот, например, Виктор Иван это что? Ищем описание параметра query в документации ага, да это же запрос для подсказок!

Это как если бы мы вбили строку Виктор Иван в GUI (графическом интерфейсе пользователя):

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

Открываем вкладку Network, вбиваем Виктор Иван и находим запрос, который при этом уходит на сервер. Ого, да это тот самый пример, что мы разбираем!

Клиент передает серверу запрос в JSON-формате. Внутри два параметра, две пары ключ-значение:

  • query строка, по которой ищем (то, что пользователь вбил в GUI);

  • count количество подсказок в ответе (в Дадате этот параметр зашит в форму, всегда возвращается 7 подсказок. Но если дергать подсказки напрямую, значение можно менять!)

Пары ключ-значение разделены запятыми:

Строки берем в кавычки, числа нет:

Конечно, внутри может быть не только строка или число. Это может быть и другой объект! Или массив... Или объект в массиве, массив в объекте... Любое количество уровней вложенности =))

Объект, массив, число, булево значение (true / false) если у нас НЕ строка, кавычки не нужны. Но в любом случае это будет значение какого-то ключа:

НЕТ

ДА

{

"a": 1,

{ x:1, y:2 }

}

{

"a": 1,

"inner_object": { "x":1, "y":2 }

}

{

"a": 1,

[2, 3, 4]

}

{

"a": 1,

"inner_array": [2, 3, 4]

}

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

Так правильно

Так тоже правильно

{

"query": "Виктор Иван",

"count": 7

}

{ "query":"Виктор Иван", "count":7}

Ключ ВСЕГДА строка, поэтому можно не брать его в кавычки.

Так правильно

Так тоже правильно

{

"query": "Виктор Иван",

"count": 7

}

{

query: "Виктор Иван",

count: 7

}

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

НЕТ

ДА

{

my query: "Виктор Иван"

}

{

"my query": "Виктор Иван"

}

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

См также:

CamelCase, snake_case и другие регистры подробнее о разных регистрах

Писать ключи можно в любом порядке. Ведь JSON-объект это неупорядоченное множество пар ключ:значение.

Так правильно

Так тоже правильно

{

query: "Виктор Иван",

count: 7

}

{

count: 7,

query: "Виктор Иван"

}

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

Ключ или свойство?

Вот у нас есть JSON-объект:

{  "query": "Виктор Иван",  "count": 7}

Что такое query? Если я хочу к нему обратиться, как мне это сказать? Есть 2 варианта, и оба правильные:

Обратиться к свойству объекта;

Получить значение по ключу.

То есть query можно назвать как ключом, так и свойством. А как правильно то?

Правильно и так, и так! Просто есть разные определения объекта:

Объект

В JS объект это именно объект. У которого есть набор свойств и методов:

  • Свойства описывают, ЧТО мы создаем.

  • Методы что объект умеет ДЕЛАТЬ.

То есть если мы хотим создать машину, есть два пути:

  1. Перечислить 10 разных переменных модель, номер, цвет, пробег...

  2. Создать один объект, где будут все эти свойства.

Аналогично с кошечкой, собачкой, другом из записной книжки...

Объектно-ориентированное программирование (ООП) предлагает мыслить не набором переменных, а объектом. Хотя бы потому, что это логичнее. Переменных в коде будет много, как понять, какие из них взаимосвязаны?

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

Например, создадим кошечку:

var cat = {name: Pussy,year: 1,sleep: function() {// sleeping code}}

В объекте cat есть:

  • Свойства name, year (что это за кошечка)

  • Функции sleep (что она умеет делать, описание поведения)

По коду сразу видно, что у кошечки есть имя и возраст, она умеет спать. Если разработчик решит добавить новые свойства или методы, он дополнит этот объект, и снова всё в одном месте.

Если потом нужно будет получить информацию по кошечке, разработчик сделает REST-метод getByID, searchKitty, или какой-то другой. А в нем будет возвращать свойства объекта.

То есть метод вернет

{name: Pussy,year: 1,}

И при использовании имени вполне уместно говорить обратиться к свойству объекта. Это ведь объект (кошечка), и его свойства!

Набор пар ключ:значение

Второе определение объекта неупорядоченное множество пар ключ:значение, заключенное в фигурные скобки {}.

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

  • client_fio (в коде это свойство fio объекта client)

  • kitty_name (в коде это свойство name объекта cat)

  • car_model (в коде это свойство model объекта car)

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

Но в любом случае, и ключ, и свойство будет правильно. Не пугайтесь, если в одной книге / статье / видео увидели одно, в другой другое... Это просто разные трактовки \_()_/

Итого

Json-объект это неупорядоченное множество пар ключ:значение, заключённое в фигурные скобки { }. Ключ описывается строкой, между ним и значением стоит символ :. Пары ключ-значение отделяются друг от друга запятыми.

Значения ключа могут быть любыми:

  • число

  • строка

  • массив

  • другой объект

  • ...

И только строку мы берем в кавычки!

JSON-массив

Как устроен

Давайте снова начнем с примера. Это массив:

["MALE","FEMALE"]

Массив заключен в квадратные скобки []

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

Значения разделены запятыми:

Значения внутри

Внутри массива может быть все, что угодно:

Цифры

[1, 5, 10, 33]

Строки

["MALE","FEMALE"]

Смесь

[1, "Андрюшка", 10, 33]

Объекты

Да, а почему бы и нет:

[1, {a:1, b:2}, "такой вот массивчик"]

Или даже что-то более сложное. Вот пример ответа подсказок из Дадаты:

[        {            "value": "Иванов Виктор",            "unrestricted_value": "Иванов Виктор",            "data": {                "surname": "Иванов",                "name": "Виктор",                "patronymic": null,                "gender": "MALE"            }        },        {            "value": "Иванченко Виктор",            "unrestricted_value": "Иванченко Виктор",            "data": {                "surname": "Иванченко",                "name": "Виктор",                "patronymic": null,                "gender": "MALE"            }        },        {            "value": "Виктор Иванович",            "unrestricted_value": "Виктор Иванович",            "data": {                "surname": null,                "name": "Виктор",                "patronymic": "Иванович",                "gender": "MALE"            }        }]

Система возвращает массив подсказок. Сколько запросили в параметре count, столько и получили. Каждая подсказка объект, внутри которого еще один объект. И это далеко не сама сложная структура! Уровней вложенности может быть сколько угодно массив в массиве, который внутри объекта, который внутри массива, который внутри объекта...

Ну и, конечно, можно и наоборот, передать массив в объекте. Вот пример запроса в подсказки:

{"query": "Виктор Иван","count": 7,"parts": ["NAME", "SURNAME"]}

Это объект (так как в фигурных скобках и внутри набор пар ключ:значение). А значение ключа "parts" это массив элементов!

Итого

Массив это просто набор значений, разделенных запятыми. Находится внутри квадратных скобок [].

А вот внутри него может быть все, что угодно:

  • числа

  • строки

  • другие массивы

  • объекты

  • смесь из всего вышеназванного

JSON vs XML

В SOAP можно применять только XML, там без вариантов.

В REST можно применять как XML, так и JSON. Разработчики отдают предпочтение json-формату, потому что он проще воспринимается и меньше весит. В XML есть лишняя обвязка, название полей повторяется дважды (открывающий и закрывающий тег).

Сравните один и тот же запрос на обновление данных в карточке пользователя:

XML

<req><surname>Иванов</surname><name>Иван</name><patronymic>Иванович</patronymic><birthdate>01.01.1990</birthdate><birthplace>Москва</birthplace><phone>8 926 766 48 48</phone></req>

JSON

{"surname": "Иванов","name": "Иван","patronymic": "Иванович","birthdate": "01.01.1990","birthplace": "Москва","phone": "8 926 766 48 48"}

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

См также:

Инфографика REST vs SOAP

Well Formed JSON

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

Чтобы проверить JSON на синтаксис, можно использовать любой JSON Validator (так и гуглите). Я рекомендую сайт w3schools. Там есть сам валидатор + описание типичных ошибок с примерами.

Но учтите, что парсеры внутри кода работают не по википедии или w3schools, а по RFC, стандарту. Так что если хотите изучить каким должен быть JSON, то правильнее открывать RFC и искать там JSON Grammar. Однако простому тестировщику хватит набора типовых правил с w3schools, их и разберем.

Правила well formed JSON:

  1. Данные написаны в виде пар ключ:значение

  2. Данные разделены запятыми

  3. Объект находится внутри фигурных скобок {}

  4. Массив внутри квадратных []

1. Данные написаны в виде пар ключ:значение

Например, так:

"name":"Ольга"

В JSON название ключа нужно брать в кавычки, в JavaScript не обязательно он и так знает, что это строка. Если мы тестируем API, то там будет именно JSON, так что кавычки обычно нужны.

Но учтите, что это правило касается JSON-объекта. Потому что json может быть и числом, и строкой. То есть:

123

Или

"Ольга"

Это тоже корректный json, хоть и не в виде пар ключ:значение.

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

2. Данные разделены запятыми

Пары ключ:значение в объекте разделяются запятыми. После последней пары запятая не нужна!

Типичная ошибка: поставили запятую в конце объекта:

{  "query": "Виктор Иван",  "count": 7,}

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

В итоге было так:

{  "count": 7,  "query": "Виктор Иван"}

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

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

  1. У меня уже есть работающий запрос в Postman-е. Но в нем минимум полей.

  2. Я его клонирую

  3. Копирую из документации нужное мне поле. Оно в примере не последнее, так что идёт с запятой на конце.

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

  5. Отправляю запрос ой, ошибка! Из копипасты то запятую не убрала!

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

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

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

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

{  "count": 7;  "query": "Виктор Иван"}

Или добавьте лишнюю запятую в конце запроса эта ошибка будет встречаться чаще!

{  "count": 7,  "query": "Виктор Иван",}

Или пропустите запятую там, где она нужна:

{"count": 7"query": "Виктор Иван"}

Аналогично с массивом. Данные внутри разделяются через запятую. Хотите попробовать сломать? Замените запятую на точку с запятой! Тогда система будет считать, что у вас не 5 значений, а 1 большое:

[1, 2, 3, 4, 5] <!-- корректный массив на 5 элементов -->[1; 2; 3; 4; 5] <!-- некорректный массив, так как такого разделителя быть не должно. Это может быть простой строкой, но тогда нужны кавычки -->!

3. Объект находится внутри фигурных скобок {}

Это объект:

{a: 1, b: 2}

Чтобы сломать это условие, уберите одну фигурную скобку:

{a: 1, b: 2
a: 1, b: 2}

Или попробуйте передать объект как массив:

[ a: 1, b: 2 ]

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

4. Массив внутри квадратных []

Это массив:

[1, 2]

Чтобы сломать это условие, уберите одну квадратную скобку:

[1, 2
1, 2]

Или попробуйте передать массив как объект, в фигурных скобках:

{ 1, 2 }

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

Итого

JSON (JavaScript Object Notation) текстовый формат обмена данными, основанный на JavaScript. Легко читается человеком и машиной. Часто используется в REST API (чаще, чем XML).

  • JSON-объект неупорядоченное множество пар ключ:значение, заключённое в фигурные скобки { }.

  • Массив упорядоченный набор значений, разделенных запятыми. Находится внутри квадратных скобок [].

  • Число (целое или вещественное).

  • Литералы true (логическое значение истина), false (логическое значение ложь) и null.

  • Строка

При тестировании REST API чаще всего мы будем работать именно с объектами, что в запросе, что в ответе. Массивы тоже будут, но обычно внутри объектов.

Правила well formed JSON:

  1. Данные в объекте написаны в виде пар ключ:значение

  2. Данные в объекте или массиве разделены запятыми

  3. Объект находится внутри фигурных скобок {}

  4. Массив внутри квадратных []

См также:

Introducing JSON

RFC (стандарт)

Что такое XML

PS больше полезных статей ищитев моем блоге по метке полезное. А полезные видео намоем youtube-канале

Подробнее..

Перевод Нестабильные тесты одна из основных проблем автоматизированного тестирования(Часть 2)

05.05.2021 08:04:43 | Автор: admin

Это продолжение серии статей о нестабильных тестах.

В первой статье(оригинал/перевод на хабре) говорилось о 4 компонентах, в которых могут возникать нестабильные тесты.

В этой статье дадим советы как избежать нестабильных тестов в каждом из 4 компонентов.

Компоненты

Итак 4 компонента в которых могут возникать нестабильные тесты:

  • Сами тесты;

  • Фреймворк для запуска тестов;

  • Сервисы и библиотеки, от которых зависит тестируемая система и тестовый фреймворк;

  • Операционная система и устройство с которым взаимодействует фреймворк автотестирования.

Это отображено на рисунке 1.

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

Сами тесты

Сами тесты могут быть нестабильными.

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

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

Причины нестабильных тестов

Варианты локализации проблемы

Варианты решения

Неправильная инициализация или очистка.

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

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

Неправильно подобранные тестовые данные.

Перезапустите тесты самостоятельно.

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

Неправильное предположение о состоянии системы. Примером может служить системное время.

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

Удалите или изолируйте зависимости вашего приложение от аспектов среды, которые вы не контролируете.

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

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

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

Зависимость от порядка запуска тестов (Вариант решения схож с второй причиной).

Перезапустите тесты самостоятельно.

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

Фреймворк для запуска тестов

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

Таблица 2 Причины, варианта локализации проблемы, и варианты решения нестабильности в фреймворке для запуска тестов

Причины нестабильных тестов

Варианты локализации проблемы

Варианты решения

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

Проверьте логи, чтобы удостовериться появилось ли приложение.

Выделите достаточно ресурсов.

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

Запустите тесты в другом порядке.

Сделайте тесты независимыми друг от друга.

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

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

Устрани утечки памяти или другие утечки ресурсов. Выделите достаточно ресурсов для прогона тестов.

Сервисы и библиотеки, от которых зависит тестируемая система и тестовый фреймворк

Приложение (или тестируемая система) может быть источником нестабильности.

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

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

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

Причины нестабильных тестов

Варианты локализации проблемы

Варианты решения

Состояние гонки.

Логируйте доступ к общим ресурсам.

Добавьте в тесты элементы синхронизации, чтобы они ждали определенных состояний приложения. НЕ ДОБАВЛЯЙТЕ явные ожидания, это может привести к нестабильности тестов в будущем.

Непроинициализированные переменные.

Ищите предупреждения компилятора о неинициализированных переменных.

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

Медленный ответ или отсутствие ответа при запросе от теста.

Логируйте время когда делаются запросы и ответы.

Проверьте и устраните все причины задержек.

Утечки памяти.

Посмотрите на потребление памяти во время прогона тестов. В обнаружении проблемы поможет инструмент Valgrind.

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

Избыточная подписка на ресурсы.

Проверьте логи, чтобы узнать не закончились ли ресурсы.

Выделите достаточно ресурсов для запуска тестов.

Изменения в приложении и в тестах происходят с разной скоростью.

Изучите историю изменений.

Введите правило при изменении кода, писать на это тесты.

Операционная система и устройство с которым взаимодействует фреймворк автотестирования

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

Таблица 4 Причины, варианта локализации проблемы, и варианты решения нестабильности в ОС и устройстве с которым взаимодействует фреймворк автотестирования

Причины нестабильных тестов

Варианты локализации проблемы

Варианты решения

Сбои или нестабильность сети.

Проверьте наличие ошибок в системных логах.

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

Дисковые ошибки.

Проверьте наличие ошибок в системных логах.

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

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

Изучите активность системного процесса.

Сократите активность процессов не связанных с прогоном тестов.

Заключение

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

Ссылки на источники

Подробнее..

Перевод Меньше сложного тестирования, больше умного тестирования

28.04.2021 16:12:49 | Автор: admin

В связи с набором учащихся на курс "QA Engineer" подготовили перевод материала.

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


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

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

Работать, как одна команда

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

Независимость тестирования не означает независимость тестировщиков.

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

Тренинги по тестированию, сообщество тестировщиков

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

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

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

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

Умное тестирование

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

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

ФРИДРИКО ТОЛЕДО

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

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

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

Умное тестирование, качественные продукты.

  • Надежный план тестирования Наличие плана должно иметь решающее значение для успеха.

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

  • Улучшение качества кода Никакие коммиты не остаются не протестированными.

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

  • Performance / Load Testing & Security Testing Чтобы избежать любого сбоя или любого риска для безопасности.

  • Метрики и Dashboards Отслеживание ключевых показателей качества (например, дефектов производства, анализа рисков, дополнений к требованиям и т.д.). Постоянное совершенствование процесса обеспечения качества.

  • Мониторинг возможного риска на производстве Тестовый мониторинг на производстве.

Заключение

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

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

Полезная литература

Извлеченные уроки в программном тестировании (Lessons Learned in Software Testing) Cem Kaner, James Bach, and Bret Pettichord.

Трансформация Тестирования Непрерывное Тестирование на Предприятии для Agile и DevOps (Enterprise Continuous Testing Transforming Testing for Agile and DevOps) Wolfgang Platz, Cynthia Dunlop.

Гибкое мышление (Lean Thinking) Drs. Womack and Jones


Узнать подробнее о курсе "QA Engineer"

Смотреть вебинар Методологии разработки

Подробнее..

Как выбрать уровень статистической значимости для AB-теста и как интерпретировать результат

27.04.2021 12:23:09 | Автор: admin

Также опубликовано в отдельном блоге автора.

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

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

Регистрация по номеру телефона

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

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

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

Планирование эксперимента

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

Довольно часто в компаниях есть "обычный" уровень значимости, который выбирают просто потому, что так принято, - допустим, 95%. Однако не подойдёт ли в данном случае уровень значимости 90% или даже 80%? Ведь не хотелось бы отказаться от полезного улучшения, только потому что мы, мол, не абсолютно уверены в улучшении - на все сто ведь никогда нельзя быть уверенным.

Что результаты значат на самом деле

Допустим, мы выбрали 90%-тную значимость, опасаясь, что при большей тест может провалиться, а меньшую вроде бы никто вокруг нас не применяет. Запустили тест, он работал ровно пять полных недель (полных - чтобы сгладить возможное отличие поведения пользователей в будни и на выходных), каждый вариант увидело примерно 10000 человек, тест завершился успешно, калькулятор сообщил что-то вроде "p-value is 0.07, You can be 90% confident that this result is a consequence of the changes you made". Что же на самом деле это означает?

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

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

P-value AB-теста как раз и показывает, насколько редкое событие мы наблюдаем, если бы предложение вводить номер телефона на самом деле (на длительном периоде времени) ничего не улучшало, а возможно даже и ухудшало.

Представьте себе, что в такой печальной ситуации мы провели бы не один, а сотню одинаковых AB-тестов: каждый по те же пять недель, каждый по 10000 посетителей на вариант. В большинстве таких ста тестов телефонный вариант принесёт меньше регистраций, чем email-вариант или столько же, как и email-вариант. Однако в части тестов телефонный вариант может принести немного больше регистраций.

Наблюдаемое p-value 0.07 как раз и означает, что если телефонный вариант на самом деле ничуть не лучше email'ового, то оказаться впереди email'ового столь сильно или ещё сильнее, чем мы наблюдаем, он смог бы в семи тестах из ста.

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

Стоимость ошибочного выигрыша

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

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

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

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

Среднее количество регистраций в неделю

2000

Насколько больше пользователей может приносить телефонная регистрация по сравнению с емейловой

5%

Насколько меньше пользователей может приносить телефонная-регистрация в случае ошибки

50%

Если всё отлично, то год работы с смс-регистрацией принесёт дополнительно пользователей

52 * 2000 * 5% = 5200

Если ошибочно внедрили смс-регистрации, то за год недосчитаемся пользователей

52 * 2000 * 50% = 52000

Выбранный граничный уровень значимоcти

95%

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

52 * 2000 = 104000

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

95% * 5200 - (100%-95%) * 52000 = 2340

Пользы от среднего подобного эксперимента в процентах роста аудитории

= 2340 / 104000 = 2.25%

Эту табличку можно найти в Google Sheets и скопировать себе.

Выбор уровня статистической значимости

Откуда же до начала эксперимента подставлять в такую таблицу ожидаемую пользу от справедливо выигравшего теста (5% в нашем примере) и возможный вред в случае ошибки (50% в нашем примере)? Лучше всего, конечно, опираться на историю подобных изменений. Если это далеко не первый эксперимент с улучшением воронки регистраций, и большинство из предыдущих увеличивало конверсию на пару процентов, то вряд ли даже очень значительная идея улучшит сильнее, чем на 5-10%.

Если истории подобных внедрений нет или она незначительна, то я не знаю метода лучше, чем экспертные оценки и страхи. Всё же вряд ли аж половина потенциальных пользователей не регистрируется, потому что не хотят / не могут вводить email, а телефон с радостью ввели бы. В лучшем случае миграция на номер телефона подтянет долю регистрирующихся с 7.7% может быть до 8% (улучшение на 5%). А вот если мы ошибаемся и пользователи на самом деле вообще не хотят доверять нам номер телефона, то можно/страшно потерять и половину регистраций.

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

Не уверены - проверяем строже

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

Например, давайте подставим в таблицу невероятно высокие страхи: 99% потери регистраций в случае ошибки (в нашей стране неожиданно входящие СМС оказались платными, и вообще никто не хочет регистрироваться по смс), а возможный положительный эффект оцениваем всего лишь в 2-3%. Даже в столь печальной ситуации, когда мы не уверены ни в рынке, ни в своих идеях, стоит нам выкрутить статистическую значимость на 99% - и волна подобных экспериментов всё же принесёт заметную пользу. Эксперименты станут занимать невероятно много времени, но если в воображаемом мире оно у нас есть - неработающие идеи будут внедряться крайне редко.

Небольшое улучшение, повторённое много раз, - большое улучшение

Дополнительные пара процента пользователей в год - не очень много, если только у вас уже не миллионы пользователей. Однако два десятка подобных скромных экспериментов проведённых один за другим уже принесут почти 50% дополнительных пользователей (20 повторений 2.25% улучшения из нашего примера принесут 56%: 1.0225^20 1.56 ).

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

Можно наблюдать и после принятия решений

Было бы здорово даже в случае выигравшего эксперимента понаблюдать за пользователями более долговременно. Например, если по результатам наших пяти недель регистрация по номеру телефона выиграла, мы можем включить её для 95% анонимных посетителей. Оставшимся пяти процентам можно предлагать по прежнему email и сравнить результаты различных вариантов не через пять, а через 25 недель.

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

Проверяйте идеи, имеющие смысл

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

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

Если вы знаете, что по результатам прошлых лет даже самые радикальные изменения не улучшают/ухудшают ситуацию больше, чем на несколько процентов (например, новые пользователи приходят в основном по сильной социальной рекомендации), то и в оценочную таблицу можно вписывать подобные цифры. Например, возможную пользу оценим в 2%, а возможный вред - в 5%. В результате снова окажется, что достаточно и 80% уровня значимости и эксперимент можно провести очень быстро.

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

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

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

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

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

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

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

    • "Эксперименты с возможной потерей клиентов тестируем с уровнем значимости 95%"

    • "Просто обычные небольшие улучшения тестируем с уровнем значимости 90%"

    • "Мелочи, вроде текстов и цветов в местах не касающихся оплаты товара, тестируем c 80%-тной значимостью и если калькулятор рекомендует длину эксперимента больше недели, то пропускаем тестирование совсем"

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

Благодарности

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

А как вы интерпретируете результаты AB-теста?

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

Подробнее..

Категории

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

© 2006-2021, personeltest.ru