Тестирование HTTP-запросов: инструмент выживания разработчика
Опубликовано: 2022-03-11Что делать, если набор для тестирования невозможен
Бывают случаи, когда мы — программисты и/или наши клиенты — имеем ограниченные ресурсы для написания как ожидаемого результата, так и автоматизированных тестов для этого результата. Когда приложение достаточно маленькое, вы можете срезать углы и пропускать тесты, потому что вы помните (в основном), что происходит в другом месте кода, когда вы добавляете функцию, исправляете ошибку или выполняете рефакторинг. Тем не менее, мы не всегда будем работать с небольшими приложениями, к тому же со временем они становятся больше и сложнее. Это делает ручное тестирование трудным и очень раздражающим.
Для моих последних нескольких проектов я был вынужден работать без автоматизированного тестирования, и, честно говоря, было неловко, когда клиент писал мне по электронной почте после отправки кода, чтобы сказать, что приложение ломается в тех местах, где я даже не коснулся кода.
Итак, в тех случаях, когда мой клиент либо не имел бюджета, либо собирался добавить какую-либо автоматизированную тестовую среду, я начинал тестировать базовую функциональность всего веб-сайта, отправляя HTTP-запрос на каждую отдельную страницу, анализируя заголовки ответов и ища «200». отклик. Звучит просто и ясно, но вы можете многое сделать, чтобы обеспечить точность без необходимости писать какие-либо тесты, модули, функциональные возможности или интеграцию.
Автоматизированное тестирование
В веб-разработке автоматизированные тесты включают три основных типа тестов: модульные тесты, функциональные тесты и интеграционные тесты. Мы часто комбинируем модульные тесты с функциональными и интеграционными тестами, чтобы убедиться, что все работает как единое целое. Когда эти тесты запускаются в унисон или последовательно (предпочтительно с помощью одной команды или клика), мы начинаем называть их автоматическими тестами, модульными или нет.
В значительной степени цель этих тестов (по крайней мере, в веб-разработке) состоит в том, чтобы убедиться, что все страницы приложения отображаются без проблем, без фатальных (остановка приложения) ошибок или ошибок.
Модульное тестирование
Модульное тестирование — это процесс разработки программного обеспечения, при котором мельчайшие части кода — модули — независимо проверяются на правильность работы. Вот пример на Руби:
test “should return active users” do active_user = create(:user, active: true) non_active_user = create(:user, active: false) result = User.active assert_equal result, [active_user] end
Функциональное тестирование
Функциональное тестирование — это метод, используемый для проверки функций и функций системы или программного обеспечения, предназначенный для охвата всех сценариев взаимодействия с пользователем, включая пути сбоя и пограничные случаи.
Примечание: все наши примеры написаны на Ruby.
test "should get index" do get :index assert_response :success assert_not_nil assigns(:object) end
Интеграционное тестирование
После модульного тестирования модулей они интегрируются один за другим, последовательно, чтобы проверить комбинированное поведение и убедиться, что требования реализованы правильно.
test "login and browse site" do # login via https https! get "/login" assert_response :success post_via_redirect "/login", username: users(:david).username, password: users(:david).password assert_equal '/welcome', path assert_equal 'Welcome david!', flash[:notice] https!(false) get "/articles/all" assert_response :success assert assigns(:articles) end
Испытания в идеальном мире
Тестирование широко распространено в отрасли, и оно имеет смысл; хорошие тесты позволяют:
- Качество гарантирует все ваше приложение с наименьшими человеческими усилиями
- Выявляйте ошибки легче, потому что вы точно знаете, где ваш код ломается из-за сбоев теста.
- Создайте автоматическую документацию для вашего кода
- Избегайте «кодового запора», который, по словам одного чувака из Stack Overflow, представляет собой юмористический способ сказать: «Когда вы не знаете, что писать дальше, или перед вами стоит сложная задача, начните с написания небольших ».
Я мог бы продолжать и продолжать о том, какие классные тесты, и как они изменили мир, и бла-бла-бла-бла, но суть вы поняли. Концептуально, тесты потрясающие.
Тесты в реальном мире
Хотя у всех трех типов тестирования есть свои достоинства, в большинстве проектов они не описываются. Почему? Что ж, позвольте мне разбить его:
Время/сроки
У всех есть дедлайны, и написание новых тестов может помешать их уложиться. Написание приложения и соответствующих тестов может занять полтора (или больше) времени. Некоторые из вас не согласны с этим, ссылаясь на сэкономленное в конечном итоге время, но я не думаю, что это так, и я объясню почему в разделе «Разница во мнениях».
Проблемы клиента
Часто клиент на самом деле не понимает, что такое тестирование и почему оно имеет значение для приложения. Клиенты, как правило, больше озабочены быстрой доставкой продукта и поэтому считают программное тестирование контрпродуктивным.
Или это может быть так же просто, как у клиента, не имеющего бюджета для оплаты дополнительного времени, необходимого для реализации этих тестов.
Отсутствие знаний
В реальном мире существует значительное племя разработчиков, которые не знают о существовании тестирования. На каждой конференции, митапе, концерте (даже во сне) я встречаю разработчиков, которые не умеют писать тесты, не знают, что тестировать, не знают, как настроить фреймворк для тестирования и т.д. на. Тестированию не учат в школах, и может возникнуть проблема с настройкой/изучением фреймворка, чтобы заставить их работать. Так что да, есть определенный барьер для входа.
«Много работы»
Написание тестов может быть непосильной задачей как для новичков, так и для опытных программистов, даже для тех гениев, которые меняют мир, и, в довершение всего, написание тестов не является захватывающим. Кто-то может подумать: «Зачем мне заниматься неинтересной работой, когда я могу реализовать важную функцию с результатами, которые впечатлят моего клиента?» Это жесткий аргумент.
Наконец, что не менее важно, сложно писать тесты, и студенты, изучающие информатику, не обучены этому.
О, и рефакторинг с модульными тестами — это не весело.
Разница во мнениях
На мой взгляд, модульное тестирование имеет смысл для алгоритмической логики, но не столько для координации живого кода.
Люди утверждают, что даже если вы заранее тратите дополнительное время на написание тестов, это экономит вам часы позже при отладке или изменении кода. Я позволю себе не согласиться и задать один вопрос: ваш код статичен или постоянно меняется?
Для большинства из нас он постоянно меняется. Если вы пишете успешное программное обеспечение, вы всегда добавляете функции, меняете существующие, удаляете их, съедаете их и так далее, и чтобы приспособиться к этим изменениям, вы должны постоянно изменять свои тесты, а изменение ваших тестов требует времени.
Но вам нужно какое-то тестирование
Никто не станет спорить, что отсутствие какого-либо тестирования — худший из возможных случаев. После внесения изменений в код необходимо убедиться, что он действительно работает. Многие программисты пытаются вручную проверить основы: отображается ли страница в браузере? Отправляется ли форма? Отображается ли правильный контент? И так далее, но на мой взгляд, это варварски, неэффективно и трудозатратно.
Что я использую вместо
Целью тестирования веб-приложения, будь то ручное или автоматизированное, является подтверждение того, что любая данная страница отображается в браузере пользователя без каких-либо фатальных ошибок и правильно отображает свое содержимое. Один из способов (и в большинстве случаев более простой) добиться этого — отправить HTTP-запросы на конечные точки приложения и проанализировать ответ. Код ответа говорит вам, была ли страница успешно доставлена. Контент легко проверить, проанализировав тело ответа HTTP-запроса и выполнив поиск определенных совпадений текстовой строки, или вы можете быть на шаг более изобретательны и использовать библиотеки веб-скрейпинга, такие как nokogiri.
Если для некоторых конечных точек требуется вход пользователя в систему, вы можете использовать библиотеки, предназначенные для автоматизации взаимодействий (идеальный вариант при проведении интеграционных тестов), например, автоматизировать вход в систему или переход по определенным ссылкам. Действительно, в общей картине автоматизированного тестирования это очень похоже на интеграционное или функциональное тестирование (в зависимости от того, как вы их используете), но написать его намного быстрее, и его можно включить в существующий проект или добавить в новый. , с меньшими усилиями, чем настройка всей среды тестирования. Точно!
Пограничные случаи представляют собой еще одну проблему при работе с большими базами данных с широким диапазоном значений; тестирование того, работает ли наше приложение гладко во всех ожидаемых наборах данных, может быть сложной задачей.

Один из способов сделать это — предвидеть все крайние случаи (что не просто сложно, а часто невозможно) и написать тест для каждого из них. Это может легко превратиться в сотни строк кода (представьте себе ужас) и обременительно в обслуживании. Тем не менее, с HTTP-запросами и всего одной строкой кода вы можете тестировать такие пограничные случаи непосредственно на данных из рабочей среды, загруженных локально на ваш компьютер для разработки или на промежуточный сервер.
Конечно, этот метод тестирования не является панацеей и имеет множество недостатков, как и любой другой метод, но я считаю, что эти типы тестов быстрее и проще писать и модифицировать.
На практике: тестирование с помощью HTTP-запросов
Поскольку мы уже установили, что написание кода без каких-либо сопутствующих тестов — плохая идея, мой самый простой тест для всего приложения — отправлять HTTP-запросы на все его страницы локально и анализировать заголовки ответа для 200
(или желаемый) код.
Например, если бы мы написали приведенные выше тесты (те, которые ищут определенный контент и фатальную ошибку) с HTTP-запросом вместо этого (на Ruby), это было бы примерно так:
# testing for fatal error http_code = `curl -X #{route[:method]} -s -o /dev/null -w "%{http_code}" #{Rails.application.routes.url_helpers.articles_url(host: 'localhost', port: 3000) }` if http_code !~ /200/ return “articles_url returned with #{http_code} http code.” end # testing for content active_user = create(:user, name: “user1”, active: true) non_active_user = create(:user, name: “user2”, active: false) content = `curl #{Rails.application.routes.url_helpers.active_user_url(host: 'localhost', port: 3000) }` if content !~ /#{active_user.name}/ return “Content mismatch active user #{active_user.name} not found in text body” #You can customise message to your liking end if content =~ /#{non_active_user.name}/ return “Content mismatch non active user #{active_user.name} found in text body” #You can customise message to your liking end
Строка curl -X #{route[:method]} -s -o /dev/null -w "%{http_code}" #{Rails.application.routes.url_helpers.articles_url(host: 'localhost', port: 3000) }
охватывает множество тестовых случаев; любой метод, вызывающий ошибку на странице статьи, будет перехвачен здесь, поэтому он эффективно покрывает сотни строк кода в одном тесте.
Вторая часть, которая конкретно перехватывает ошибку содержимого, может использоваться несколько раз для проверки содержимого на странице. (Более сложные запросы можно обрабатывать с помощью mechanize
, но это выходит за рамки этого блога.)
Теперь, если вы хотите проверить, работает ли конкретная страница с большим и разнообразным набором значений базы данных (например, ваш шаблон страницы статьи работает со всеми статьями в производственной базе данных), вы можете сделать следующее:
ids = Article.all.select { |post| `curl -s -o /dev/null -w “%{http_code}” #{Rails.application.routes.url_helpers.article_url(post, host: 'localhost', port: 3000) }`.to_i != 200).map(&:id) return ids
Это вернет массив идентификаторов всех статей в базе данных, которые не были обработаны, поэтому теперь вы можете вручную перейти на страницу конкретной статьи и проверить проблему.
Теперь я понимаю, что этот способ тестирования может не работать в определенных случаях, таких как тестирование автономного скрипта или отправка электронной почты, и он, несомненно, медленнее, чем модульные тесты, потому что мы делаем прямые вызовы конечной точки для каждого теста, но когда у вас не может быть ни модульных тестов, ни функциональных тестов, ни того и другого, это лучше, чем ничего.
Как бы вы структурировали эти тесты? В небольших несложных проектах вы можете записать все свои тесты в один файл и запускать этот файл каждый раз перед фиксацией изменений, но для большинства проектов потребуется набор тестов.
Обычно я пишу от двух до трех тестов на конечную точку, в зависимости от того, что я тестирую. Вы также можете попробовать протестировать отдельный контент (аналогично модульному тестированию), но я думаю, что это будет избыточно и медленно, поскольку вы будете делать HTTP-вызов для каждого модуля. Но, с другой стороны, они будут чище и понятны.
Я рекомендую поместить ваши тесты в вашу обычную тестовую папку, где каждая основная конечная точка имеет свой собственный файл (например, в Rails каждая модель/контроллер будет иметь по одному файлу), и этот файл можно разделить на три части в зависимости от того, что мы тестируют. У меня часто есть как минимум три теста:
Тест 1
Убедитесь, что страница возвращается без фатальных ошибок.
Обратите внимание, как я составил список всех конечных точек для Post
и перебрал его, чтобы убедиться, что каждая страница отображается без ошибок. Предполагая, что все прошло хорошо и все страницы отрендерились, вы увидите в терминале что-то вроде этого: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of failed url(s) -- []
Если какая-либо страница не отображается, вы увидите что-то вроде этого (в этом примере posts/index page
имеет ошибку и, следовательно, не отображается): ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of failed url(s) -- [{:url=>”posts_url”, :params=>[], :method=>”GET”, :http_code=>”500”}]
Второй тест
Подтвердите наличие всего ожидаемого содержимого:
Если весь контент, который мы ожидаем, найден на странице, результат будет выглядеть следующим образом (в этом примере мы удостоверимся, что posts/:id
имеет заголовок поста, описание и статус): ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of content(s) not found on Post#show page with post id: 1 -- []
Если какой-либо ожидаемый контент не найден на странице (здесь мы ожидаем, что страница будет отображать статус публикации — «Активно», если публикация активна, «Отключена», если публикация отключена), результат будет выглядеть следующим образом: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of content(s) not found on Post#show page with post id: 1 -- [“Active”]
Тест третий
Убедитесь, что страница отображается во всех наборах данных (если они есть):
Если все страницы рендерятся без ошибок, мы получим пустой список: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error in rendering -- []
Если содержимое некоторых записей имеет проблемы с рендерингом (в этом примере страницы с ID 2 и 5 выдают ошибку), результат будет выглядеть так: ➜ sample_app git:(master) ✗ ruby test/http_request/post_test.rb List of post(s) with error on rendering -- [2,5]
Если вы хотите повозиться с приведенным выше демонстрационным кодом, вот мой проект на github.
Так что лучше? Это зависит…
Тестирование HTTP-запросов может быть лучшим выбором, если:
- Вы работаете с веб-приложением
- У вас нехватка времени и вы хотите написать что-нибудь быстро
- Вы работаете с большим проектом, уже существующим проектом, в котором не были написаны тесты, но вам все еще нужен способ проверки кода.
- Ваш код включает в себя простой запрос и ответ
- Вы не хотите тратить большую часть своего времени на поддержку тестов (я где-то читал, что юнит-тест = ад обслуживания, и я частично согласен с ним/ней)
- Вы хотите проверить, работает ли приложение со всеми значениями в существующей базе данных.
Традиционное тестирование идеально, когда:
- Вы имеете дело не с веб-приложением, а с чем-то другим, например со скриптами.
- Вы пишете сложный алгоритмический код
- У вас есть время и бюджет, которые можно посвятить написанию тестов
- Бизнес требует отсутствия ошибок или низкого уровня ошибок (финансы, большая пользовательская база)
Спасибо за прочтение статьи; теперь у вас должен быть метод тестирования, который вы можете использовать по умолчанию, на который вы можете рассчитывать, когда у вас мало времени.
- Производительность и эффективность: работа с HTTP/3
- Держите его зашифрованным, держите его в безопасности: работа с ESNI, DoH и DoT