-
With-http: a Clojure library for testing HTTP
The with-http library provides a macro to stub HTTP calls with a local Jetty server. it’s declarative, flexible, and extremely useful. I’ve been copying it through many projects, and now it’s time to ship it as a standalone library.
ToC
- Installation
- About
- The App routes
- Default handler
- Basic test
- JSON
- Slow responses
- Files & Resources
- Capturing requests
Installation
Lein:
[com.github.igrishaev/with-http "0.1.1"]
Deps.edn
{com.github.igrishaev/with-http {:mvn/version "0.1.1"}}
Pay attention: since the library is primarily used for tests, put the dependency in the corresponding profile or alias. Storing it in global dependencies is not a good idea as it becomes a part of the production code otherwise.
About
The library provides a
with-http
macro of the following form:(with-http [port app] ...body)
The
port
is the number (1..65535) and theapp
is a map of routes. When entering the macro, it spawns a local Jetty server on that port in the background. Theapp
map tells the server how to respond to calls.Now that you have a running server, point your HTTP API clients to
http://localhost:<port>
to imitate real network interaction. For example, for prod, a third-party base URL is https://api.some.cool.service but for tests, it is http://localhost:8088. This can be done using environment variables or the Aero library.Why not use
with-redefs
, would you ask? Well, althoughwith-redefs
looks like a solution at first glance, it’s questionable. Usingwith-redefs
means lying to yourself. You temporarily mute some pieces of the codebase pretending it’s OK, but it’s not.Often, bugs lurk in the code that you actually substitute using
with-redefs
, namely:-
you’ve messed up with MD5/SHA/etc algorithms to sign a request. Calling localhost would trigger that code and lead to an exception, but
with-redefs
would not. -
you process the response poorly, e.g. not taking Content-Type header or non-200 status code into account.
-
you cannot imitate delays and timeout exceptions when interacting with HTTP API.
The good news is, that the
with-http
macro can test all the cases mentioned above and much more.The App routes
The
app
parameter is a two-level map of the form:{path {method response}}
For example:
{"/foo" {:get {:status 200 :body "it was GET"} :post {:status 201 :body "it was POST"}}}
Calling
GET /foo
andPOST /foo
would return 200 and 201 status codes with different messages.The response might be:
- a Ring map;
- a Ring handler function that accepts a request map and returns a response map;
- an instance of
java.io.File
; - a resource:
(clojure.java.io/resource "some/file.txt")
; - a string.
Other examples:
{"/foo" {:get (fn [{:keys [params]}] (log/infof "Params: %s" params) {:status 200 :body "OK"})}} {"/some/json" {:get (io/resource "file.json")}}
The path might be a vector as well. During the preparation step, it will be compiled into a string, for example:
{["/foo/bar/" 42 "/test"] {:get {:status 200 :body "hello"}}} ;; becomes {"/foo/bar/42/test" {:get {:status 200 :body "hello"}}}
This is useful when the paths contain parameters.
The
make-url
function helps to build a local URL likehttp://localhost:<port>/<path>
. Its secondpath
argument is either a string or a vector which gets compiled into a string:(make-url PORT "/foo?a=1&b=2") ;; http://localhost:8899/foo?a=1&b=2 (make-url 8899 ["/users/" 42 "/reports/" 99999]) ;; http://localhost:8899/users/42/reports/99999
Default handler
The App mapping has a default handler which gets triggered when the client calls a non-existing route. By default, it’s 404 status page with the following JSON payload:
(def NOT-FOUND {:status 404 :body {:error "with-http: route not found"}})
You can override it by adding the
:default
key to the app map. The value might be a map, a function, a file and so on.{"/foo" {:get {:status 200 :body "hello"}} :default {:status 202 :body "I'm the default!"}}
Basic test
A simple test to ensure the macro works:
(deftest test-with-http-test-json (let [app {"/foo" {:get {:status 200 :body "test"}}} url (make-url PORT "/foo") {:keys [status body]} (with-http [PORT app] (client/get url))] (is (= 200 status)) (is (= "test" body))))
JSON
The Ring handler function produced from the
app
mapping is wrapped withwrap-json-response
andwrap-json-params
middleware layers. It means the body of the response might be a collection that gets dumped into JSON:(deftest test-with-http-test-json (let [body {:hello [1 "test" true]} app {"/foo" {:get {:status 200 :body body}}} url (make-url PORT "/foo") {:keys [status body]} (with-http [PORT app] (client/get url {:as :json}))] (is (= 200 status)) (is (= {:hello [1 "test" true]} body))))
Slow responses
To imitate slow responses, provide a function that sleeps for a certain amount of time:
{"/foo" {:get (fn [_] (Thread/sleep 10000) {:status 200 :body "OK"})}}
Then ensure you pass the timeout limit into your API call.
Files & Resources
Storing JSON responses in files is a good idea. Here is how you can serve them with the macro:
{"/foo" {:get (io/file "dev-resources/test.txt")}}
or
{"/foo" {:get (io/file "dev-resources/test.json")}}
Capturing requests
Another trick to improve your tests: ensure you pass the right parameters or headers to the HTTP API. Provide an atom and a handler function closed over that atom. Each time you receive a request, save it’s data to the atom and then validate them:
(deftest test-with-http-capture-params (let [capture! (atom nil) app {"/foo" {:get (fn [{:keys [params]}] (reset! capture! params) {:status 200 :body "OK"})}} url (make-url PORT "/foo?a=1&b=2") {:keys [status body]} (with-http [PORT app] (client/get url))] (is (= 200 status)) (is (= "OK" body)) (is (= {:a "1" :b "2"} @capture!))))
-
Скриншоты на винде
На винде есть утилита для скриншотов рабочего стола, называется “Ножницы” или вроде того (clip). И надо сказать, программа люто, бешено тупая.
Делаю скриншот и хочу сохранить — открывает диалог с именем “безымянный.PNG” (в английской версии untitled.PNG). Не будем комментировать расширение капсом; но блин, почему файл безымянный? Что мешало назвать его текущей датой с точность до минуты?
Далее, если в папке уже есть “безымянный” с прошлого раза, программа предложит его перезаписать. Что мешает проверить, что такой файл уже есть и хотя бы поставить единичку (двойку, тройку)?
В итоге как дурак дописываешь в конец чушь вроде 123, чтобы сохранить скриншот и куда-то отправить.
Вот как на Маке: когда делаешь скриншот, он сперва улетает в угол, и в течении 10 секунд его можно куда-то перетащить без сохранения на диск. А если не успел, он сохранится на рабочий стол с датой и временем в названии. Не нужно париться, сохранил или нет, перезаписал или нет.
Что мешало сделать также Микрософту? Они вообще своими программами пользуются?
-
Мы заслужили
Выражение “X, который мы заслужили” стало затасканным штампом.
- Киберпанк, который мы заслужили.
- Футбол, который мы заслужили.
- Релиз, который мы заслужили.
- Пайплайн, который мы заслужили.
Блин, кто мы? Чем и как заслужили?
Лет пять назад это было туда-сюда, но сегодня терпеть уже невозможно.
-
Картинки в Телеграме
Телеграм бесит ещё одной вещью. Если добавить к тексту картинку, текст поджимается под её ширину. Зачем, какой смысл? Текст превращается в мышиный хвост, справа сплошная пустота. Половина полезного места уходит в унитаз. Всё это ради чего? Чтобы что?
-
Пасхалки
Пожалуй, худшим фильмом, что я смотрел за последние годы, можно назвать “Петровы в гриппе”. В двух словах его можно описать как бессвязный треш. В те времена я читал Медузу, наслушался критика Долина и побежал на предпоказ как последний дурачок.
Фильм — набор флешбеков без какого-либо действия. Женщина убивает мужчину на детской площадке. Это происходит во дворе девятиэтажки, но никто этого не видит. Потом заболевает гриппом мальчик, ее сын. С работы приходит батя-алкаш. У него начинаются приходы и флешбеки со снегурочкой. Потом фильм рассказывает про снегурочку: нищета, театр, случайный секс, беременность. В конце сцена про алкашей и оживший труп.
Я не досидел до конца и ушел из зала, но потом вернулся за забытой вещью. К тому времени свет включили, но люди не расходились. Оказалось, к фильму был приставлен специально обученный человек, который после показа работал со зрителями: отвечал на вопросы “что хотел сказать автор”.
Вот оно, современное искусство: тебе не только продают говно, но ещё тратятся на человека, который объяснит, что это не говно, а режиссёр — тонкий художник.
Но пост не о плохом фильме — их и так хватает, — а о большом числе “пасхалок” в нём. Каждые пять минут в кадре появляются какие-нибудь слова на заборе, которые что-то значат. Это сделано столь топорно, что просто закрываешь глаза. Режиссёр как будто тычет с экрана: смотри, это пасхалочка, скрытый смысл, понимаешь? Улавливаешь?
От одной из сцен захотелось просто выругаться. Парень и девушка идут вдоль огромной инсталляции с буквами в человеческий рост. Буквы показаны наполовину, но распознать их можно. И когда пара шла мимо них, я подумал: наверняка буквы складываются в какой-то “мэссадж”, но читать было лень, потому что от пасхалок я уже устал.
И что вы думаете? Позже я прочёл в интернете, что те буквы складываются в “тебе пизда”. Очень оригинально! Какой талант режиссёра! Наверное, много думал над этой пасхалкой — больше, чем над сюжетом и персонажами.
Если серьёзно, то непонятно, зачем эта пасхалка и все другие. Какую задачу они решают? Фильм как был говном, так остался им со всеми пасхалками, разгаданными или нет. Если накрыть говно бантиком, получится говно с бантиком, верно?
Пожелаю режиссёру скорей понять это.
-
Куки
К сожалению, нам не везёт с веб-дизайнерами. Я имею в виду нам всем, не только мне. Открываешь любой сайт — и тебе стреляют плашкой, что сайт использует куки. Даже если нажмёшь ОК, через пару дней сайт забудет, что ты согласился и выстрелит опять. Или зайдешь с другого устройства и снова терпи. На телефоне вообще беда: плашка занимает в лучшем случае полэкрана, в худшем — весь.
Нам не повезло, потому что не нашлось дизайнера, который бы нормально встроил сообщение о куках в дизайн. Под “нормально” я имею в виду, чтобы оно не выскакивало, не выпадало, а было написано без анимаций и JavaScript.
Я уже говорил, что хорошие дизайнеры должны начинать с печатного дизайна. Есть лист бумаги, и нужно уместить на нём дизайн со всеми требованиями. Никаких тебе выпадашек и анимашек.
А требования бывают разные. Если алкоголь или табак — предупреждение Минздрава занимает не менее 10% от площади макета. Если банк — генеральная лицензия кеглем не менее стольки пунктов. Если кандидат на выборах — лицензия на участие, номер и прочие вещи. В роликах то же самое: показываешь БАД — внизу должна быть надпись “не является лекарством, проконсультируйтесь с врачом”. И не в первую секунду, а на протяжении всего ролика.
И дизайнеры как-то справляются с этим — выделяют площадь, увеличивают кегль, согласуют с юристами и надзорными органами. И только веб-дизайнеры, эти обезьянки с курсами HTTP/CSS/JavaScript, не могут доработать дизайн так, чтобы требование чиновников (тоже обезьянок) выполнялось без выпадашки.
Казалось бы: нужно уведомить посетителя, что сайт использует куки. Так напиши чуть ниже шапки: “этот сайт использует куки”. И ссылку на отдельную страницу, где подробно написано, что такое куки, а ниже форма с чекбоксами, какие куки хранить. Что мешает так сделать?
Но нет, дизайнер быдлокодит всплывающую форму, скрипты, стили. Получил инструменты, а пользуется ими во вред.
Чтоб у него в штанах что-нибудь выпало.
-
Бан в XBox
У меня нет приставки, и я почти ни во что не играю (разве что в старые игры на эмуляторе). Однако я не мог пройти мимо такой картинки в интернете. Это скриншот из интерфейса XBox, где написано, что вас забанили.
Удивляет пиктограмма рукопожатия справа. Что было в голове у дизайнера, который ее добавил? Мало того, что забанили клиента без объяснения причины; так еще нарисовали рукопожатие, мол, дружище, без обид, держись там. Бан пройдет, и мы снова будем друзьями.
Тупизна какого-то вселенского масштаба. В точности как все у Микрософта.
-
Warn
Не допускайте, чтобы в коде встречались логи с уровнем warn(ing).
Во-первых, warn, предупреждение, — это ни то, ни сё. Вроде важнее обычного info, но и не ошибка. Что делать? Продолжать вести наблюдение?
Во-вторых, warn — отличный способ отстрелить ногу. В текущем проекте много фоновых задач, и для них используется библиотека-аналог крона. Каждую задачу она оборачивает в try/catch, чтобы не падать, но при этом логирует исключение с уровнем warn. Мол, тут что-то упало, но не беспокойся. Авось в следующий раз повезёт.
Наш логгер устроен так, что только error попадает в Sentry; остальное идёт в файл, который никто не читает. В результате мы прошляпили кучу ошибок в фоновых задачах — просто потому, что автор библиотеки не считает их чем-то важным. Крон работает и хорошо. Подумаешь, половина задач валится, главное, что у меня всё в порядке.
Не надо так.
-
Фрагментация Телеграма
Когда я пользовался VK, меня удивляла фрагментация сущностей. Заметка, пост, фотография, видео и прочее — всё уже не упомню. Были записи на стене, сама стена, группы, форум. Я никогда не мог запомнить, что и где создавать и удивлялся — почему нельзя свести этот зоопарк к “сообщению”? Да, пусть у него будет много полей: гео-теги, фото, видео, ссылки… но это лучше, чем дюжина разных сущностей! Причём не только для программистов, но и для пользователей, потому что заморочки бекенда не будут на них проецироваться.
Ясно, что ВК был клоном Фейсбука (запрещенная организация) и унаследовал его родовые травмы. В свою очередь в Фейсбуке была та же фрагментация, потому что изначально это был закрытый форум. А на форумах до сих пор такая фигня: создать заметку, фотографию, видео.
Учетку VK, ровно как и других соцсетей, я удалил лет семь назад и ни разу не пожалел.
Удвиляет, что при всем идиотизме этой фрагментации она продолжает жить в мессенджерах. Скажем, я написал заметку на лист А4, она прекрасно помещается в Телеграме. Но стоит добавить картинку или видео — размер сокращается в разы, буквально 150 символов или около. Почему? Потому что это уже не “заметка”, а “картинка”. А в чём проблема показать и картинку, и текст? Очевидно, там какие-то заморочки на бекенде — Эрланг, Mnesia, — но какое они имеют значение для меня как пользователя? Никакого.
Чтобы исправить этот косяк, Телеграм сделал Телеграф — сервис анонимных заметок, в котором текст чередуется с картинками. Телеграфные ссылки открываются в Телеграме без перехода в браузер, но в целом это костыль — решили симптом, проблема осталась.
А с недавними сторисами вышел смех и грех. Совершенно бесполезная функция, которую нельзя отключить. Мало того, что это буквальное неуважение к пользователю, так еще чувствуется вторичность и запоздалое заимствование. Телеграм — хороший месаджер, при чём тут какие-то сторис? У нас что, Инстаграм? Полная нелепица.
В итоге происходит следующее: люди уходят из соцсетей в месаджеры, потому что устали от фрагментации контента. Не хочется групп, стен, форумов. Хочется единого потока сообщений. Для некоторых целей это не подходит, но в целом удобно. И тут опять приходит фрагментация. Деление каналов на топики — как же они глючили и кувыркались в первые недели работы! — и сторизы.
Конечно, найдется тот, кто скажет: не нравится — не пользуйся. Но так не работает. Уже построены связи, диалоги, люди стучатся в Телеграм, и слезть с него так просто не выйдет.
Словом, Телеграм плавно становится комбайном, который “может все”, как китайский Ви-чат. Это немного, но огорчает: мне не нужно всё.
Мне нужно чуть-чуть.
-
Parasite Eve
На первой PlayStation была замечательная игра Parasite Eve. И была там музыкальная шкатулка с красивой темой:
Оказывается, это Бах. Всего-то 23 года прошло, прежде чем я это понял.
Кто играл в эту замечательную игру, почтите ее прослушиванием саундтрека.