Василий Половнёв

Ведущий разработчик.

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

Связаться: vasily@polovnyov.ru или телеграм.

Блог Курс о тестах в Руби и Рельсах

Мониторинг ключевых метрик приложения на Рельсах в Графане

Иногда у приложения есть какие-то ключевые метрики, которые могут триггерить алерты, ахтунги и жопы. Например, в условном биллинге это может быть процент неудачных списаний за последний час. Если он высокий, скажем, более 75%, пора заводить инцидент и вызванивать инженеров. Самый толковый способ мониторить такое в приложениях на Рельсах — экспорт ключевой метрики в Графану через Прометеус и Ябеду.

Вот как-то так:

require "yabeda"

Yabeda.configure do
  # Где мы
  group :billing

  # Что мы экспортируем
  gauge :failed_charges_percentage, tags: [], comment: "The percent of failed charges in last hour"

  # Как собираем данные
  collect do
    failed_charges_percentage.set({}, ChargeMetrics.failed_charges_percentage)
  end
end

Лайфхак: буферы для совещаний

Придумал лайфхак, чтобы снизить тревогу и «урон» от совещаний, созвонов и встреч: если созвон с 12 до 13, то в календарь вношу 11:45—13:15.

Буфер в 15 минут до совещания трачу на то, чтобы собраться с мыслями, подготовить повестку и перенастроиться с программирования на разговоры. Без этого буфера первые 5-10 минут совещания провожу в каком-то лимбе: мысли еще в коде, а уже нужно внимательно слушать и как-то реагировать.

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

Мне помогло, попробуйте и вы.

Проблема в занятости, а не в лени

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

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

Отдельно парит, что общество порицает лень, а занятость превозносит. Конечно, в каких-то компаниях важно выглядеть занятым, к таким претензий нет. Но чаще всего занятость — это проблема: посмотрел Gary Vee и 10x programmers, набрал себе работы на выходные, чтобы коллеги видели, как я GET AFTER IT, и сгорел в угли.

Что думаете? Как у вас?

before + after → around

Часто встречаю в тестах такое:

before do
  Timecop.freeze(Time.zone.local(2022, 4, 21))
end

after do
  Timecop.return
end

Или с ActiveSupport::Testing::TimeHelpers:

before do
  travel_to(Time.zone.local(2022, 4, 21))
end

after do
  travel_back
end

Такие конструкции из before с «заморозкой» времени и after с возвратом лучше упрощать с помощью around:

around do |example|
  Timecop.freeze(now) { example.run }
  # или
  travel_to(now) { example.run }
end

Лайфхак: талоны за дипворк

Дипворк (deep work) — это промежуток времени (1,5 часа в моем случае), во время которого я сфокусирован, сосредоточен и не отвлекаюсь на соцсети, месенджеры, телефон и разговоры. Дипворк — это сессии работы над самыми сложными задачами, требующими максимум внимания, ума и сообразительности. Противоположность дипворка — работа, не требующая максимальных усилий: ответы на почту, совещания и созвоны, разговоры по телефону, заполнение форм и документов. Термин я украл у Кэла Ньюпорта из одноименной книги.

На фотке моя обычная неделя: пачка задач на неделю в ЛВУ + списки задач по дням. Возле дат нарисованы «талоны за дипворк», которые я выдаю сам себе, чтобы отслеживать производительность. Отработал 1,5 часа над чем-то сложным — выписал талон, просидел весь день на созвонах и стендапах — ничего не получил.

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

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

inbox.txt

Со мной часто бывало такое: программируешь интересное, а в голову вдруг приходит СРОЧНАЯ и ВАЖНАЯ идея. Если отвлечься на это, голова начнет раскручивать и продумывать идею, что-то планировать, писать письма и гуглить. К коду вернуться сложно: голова занята другим.

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

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

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

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

RSpec & Rails: как проверить deliver_later(wait_until: ...)

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

after_action :send_onboarding_email, only: :create

def send_onboarding_email
  ClientsMailer
    .with(recipient: user)
    .onboarding_email
    .deliver_later(wait_until: 3.days.from_now)
end

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

1. Цепочка из моков-стабов:

mailer = instance_double(ClientsMailer)
email = double(:onboarding_email, deliver_later: true)

allow(ClientsMailer)
  .to receive(:with).with(recipient: user)
  .and_return(email)

# ...

expect(email)
  .to have_received(:deliver_later)
  .with(wait_until: 3.days.from_now)

2. Встроенный матчер и тестовый адаптер для ActiveJob из rspec-rails:

# Это, конечно, лучше спрятать в хелперы
def with_test_active_job_adapter
  last_adapter = ActiveJob::Base.queue_adapter
  ActiveJob::Base.queue_adapter = :test

  yield

  ActiveJob::Base.queue_adapter = last_adapter
end

around do |example|
  with_test_active_job_adapter { example.run }
end

it "schedules onboarding email in 3 days from now" do
  expect { ... }
    .to have_enqueued_job(ActionMailer::MailDeliveryJob)
    .with("ClientsMailer", "onboarding_email", "deliver_now")
    .at(3.days.from_now)
end

P. S. Использовать 3.days.from_now без Таймкопа опасно: могут быть ложноотрицательные тесты.

Стабы и моки юникс-сокетов

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

Excon.get("unix:///ping", socket: "/tmp/unicorn.sock")

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

stub_request(:get, "http://unix/ping").to_return(status: 200)

А у нас Faker

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

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

Во-вторых, рандомные данные замедляют тесты. Отдельный ад — Faker в фабриках: я работал с проектом, в котором переход с Faker на sequence {} в фабрике ускорил тесты на 12%.

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

it "returns initials"
  user = described_class.new(name: Faker::Name.name)
  expected = user.name
    .upcase
    .split(" ")
    .map { |part| part[0] }
    .join("")

  expect(user.initials).to eq(expected)
end

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

it "returns initials"
  user = described_class.new(name: "Daniel Craig Jones")

  expect(user.initials).to eq("DCJ")
end

Я, конечно, ни на что не намекаю, но советую присмотреться к Faker: точно ли он вам нужен?

Еще по теме:

Фейковые данные в тестах

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

В тестах лучше использовать данные, максимально приближенные к реальным: User → Иван Семенов, “ip” → “82.100.200.3”, пустой файл → картинка с котиком. Так мы получим неплохую документацию и хорошие примеры использования нашего АПИ.

Чтобы тесты в проекте были согласованы, советую для тестовых данных выбирать одну доменную область. Например, я часто использую:
1. Персонажей и цитаты из Симпсонов:
User.new(name: "Bart Simpson", email: "bart@simpson.dev")

2. Персонажей и цитаты из боевиков 90-х:
comment: "Dead or alive... you're coming with me"

3. Кусочки и персонажей из песен Эминема или Бейонсе (не спрашивайте):
do_request(text: "In my shoes, just to see what it's like to be me")

А у вас что?

Как тестировать код, работающий с внешним АПИ. Заглушка на Синатре

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

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

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

Один из вариантов — фейковый сервис на Синатре. Например, такой:

let(:fake_api) do
  Class.new Sinatra::Base do
    get "/users/:user_id/card" do
      content_type :json

      { number: "4111...1111" }.to_json
    end
  end
end

before do
  stub_request(:any, /api.nanocashier.com/).to_rack(fake_api)
end

В коде выше два интересных момента. Во-первых, Class.new с родителем Sinatra::Base в let, чтобы не добавлять глобальную константу из теста. Во-вторых, stub_request Вебмока, который роутит все запросы в rack-приложение.

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

На каких задачах сфокусироваться

Вот есть методы тайм-менеджмента: Помодоро, матрица Эйзенхауэра, GTD, ZTD, Канбан, своя-чужая работа, Zero Inbox и прочие. Они все хороши, все работают.

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

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

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

Такая идея: если задач слишком много, сфокусируйся на тех, что кратно увеличат твое полезное действие. Что думаете?

Как читать больше в эпоху цифрового максимализма

Я всегда любил читать. Когда в школе задавали список литературы на лето, я справлялся с ним к концу июня и искал, чего бы еще почитать в бабушкиной библиотеке или у друзей. Хуже того, некоторые книги я перечитывал несколько раз за лето: например, «Тихий Дон» Шолохова или «Золотой теленок» Ильфа и Петрова.

К сожалению, в последние годы я читал (и перечитывал) все меньше. Дошло до того, что в 2020 я прочитал всего десяток книг.

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

Решил эту проблему в лоб, разрешив себе читать книги как угодно. Если книга мне не нравится, могу полистать ее и без зазрения совести удалить. Если книга норм, но сегодня не хочется ее читать, без проблем переключаюсь: почитал Ruby Science, а пока он «переваривается» в голове, почитаю Лукьяненко. В результате за полгода прочитал 18 книг, почти в два раза больше, чем за весь 2020.

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

Менеджерский лайфхак: 1:1 в Телеграме

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

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

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

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

3. Саму встречу (например, аудио) и её конспект бережно хранит Телеграм. Легко собирать, легко возвращаться.

Задавать вопросы, а не перебирать варианты

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

Это, конечно, полная херня: у меня не завелась машина, я садился в нее и с пассажирского, и с водительского сиденья, но она все равно не заводится. Как поправить?

Чтобы вырасти как программист, нужно научиться решать проблемы не перебором, гаданием и случайными манипуляциями в коде, а железобетонным научным методом: задавать себе вопрос «Почему?» до тех пор, пока не станет понятна суть проблемы. Например:
— Я поменял вот эту строчку, чтобы починить X, а теперь у меня в консоли полно 404 ошибок.

— Почему полно 404 ошибок?

— Потому что мы делаем запрос, в ответ на который приходит 404.

— Почему приходит 404?

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

— Окей, обработчики точно в порядке.

— Почему урл неправильный?

— Потому что мы забыли /scope/ в начале.

— Почему мы забыли /scope/ в урле, почему его там не оказалось?

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

Бум! Вот и причина проблемы, теперь решение будет легко найти.

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

См. также:
https://bureau.ru/soviet/20180621/

Ищу толкового стажера на Рельсах

Ищу толкового стажера, который вместе со мной сделает интересную бэкендерскую задачу — систему аудита в Learning Management System. Конечно, если все пойдет хорошо, мы поработаем и над другими задачами, а стажер вырастет в сеньора или лида.

Я ищу стажёра, который:

  • отличает GET от POST, SELECT от UPDATE;
  • отличает git rebase от git merge, пулреквест от реверта;
  • знаком с Рельсами на уровне «написал блог за 15 минут, а потом пилил его по выходным»;
  • слышал про RSpec, модульные и интеграционные тесты;
  • не боится ни легаси, ни ПХП, ни фронтенда.

За полтора месяца научу стажёра:

  • проверять свою работу модульными и интеграционными тестами;
  • писать тесты быстрее, чем код;
  • ревьюить, парно программировать и не унывать;
  • быстро работать с Гитхабом, Рельсами и Семафором;
  • находить и исправлять «запахи» в коде: от Long Method до Shotgun Surgery.

Для этого стажёр будет:

  • работать 20 часов в неделю, часть времени — в паре со мной;
  • тратить 2-3 часа в неделю на теорию и домашние задания;
  • переписывать код снова и снова после ревью.

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

Если написанное выше про вас, напишите мне письмо с рассказом о себе: vasily@polovnyov.ru

Достроить, чтобы обойти

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

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

<h1>h1</h1>
wtf

<h1>h1</h1>

wtf???

Даёт такой ХТМЛ (в первом случае параграфа нет, а хотелось бы):

<h1>h1</h1>
wtf

<h1>h1</h1>
<p>wtf???</p>

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

Бум! Можно просто достроить исходные данные так, чтобы баг никогда не случался.

Заглушка из Rack-приложения в одну строчку

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

# config/routes.rb
get "wp-login", to: -> (env) { [200, {}, [""]] }

Лайфхак: поработать в машине

Кажется, я изобрёл лайфхак для МАКСИМАЛЬНОЙ ПРОДУКТИВНОСТИ. Беру машину, паркую её в тени на парковке у местного катка. Пересаживаюсь на пассажирское сиденье, расшариваю интернет с телефона и работаю. За час успеваю сделать столько же, сколько сделал бы за четыре часа дома или в кофейне.

Это работает, потому что:
1. Интернет с телефона хорош, но недостаточно: его хватает для работы, но Ютюб уже не посмотришь. Меньше Ютюба — больше работы.

2. Нет соблазна встать, сходить на кухню или к баристе и приготовить чаёк, кофеёк или поесть. Сидишь и работаешь, идти некуда.

3. Никаких хипстеров и стартаперов за соседними столиками. Ты по-настоящему один: чаще всего в радиусе 30-40 метров никого нет.

Это, конечно, лайфхак уровня /b/, но реально работает ¯_(ツ)_/¯

Линтим Руби в Виме

Чтобы Вим фоном линтил текущий файл и показывал ошибки прямо в редакторе, нужно:

1. Установить ALE: https://github.com/dense-analysis/ale

2. Настроить его:

" Что и чем линтим
let g:ale_linters = {
\   'javascript': ['eslint'],
\   'ruby': ['rubocop']
\}

let g:ale_linters_explicit = 1

" Линтим Руби бинстабом, который использует Spring
let g:ale_ruby_rubocop_executable = "bin/rubocop"

" Настраиваем значки
let g:ale_sign_error = "\u2022"
let g:ale_sign_warning = "\u2022"
" Оставляем колонку под значки, чтобы левай край окна не «дрожал»
let g:ale_sign_column_always = 1

3. Вы великолепны.