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

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

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

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

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

Просто не делай херни

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

В последний год я научился справляться с такими желаниями с помощью мантры «Просто не делай херни и…». Жопа на работе — просто не делай херни и подумай, как разгрести. Жопа в семье — просто не делай херни и подумай, как восстановить отношения. Не успеваешь с проектом — просто не делай херни и поговори с клиентом. Хочешь форспушнуть в мастер — просто не делай херни и разрезолви конфликт. Короче, просто не делай херни.

Знающие люди подсказывают, что это называется «Профессиональное самоубийство».

Как конкретно разгребать ситуации, кажущиеся неподъемными и ужасными — тема отдельного поста. Если коротко, то нужны две вещи: уверенность в том, что проблемы решаются делом, и «паззлы».

Датчик есть, но его как бы нет

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

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

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

Что с этим можно поделать? Во-первых, убедиться, что датчики проверяют то, что нужно. Во-вторых, убедиться, что датчиков хватает. В-третьих, ввести регулярные «проверки связи» и sanity checks.

Большая маленькая ложь

Самая большая ложь в разработке — фраза «сделаем когда-нибудь». Конечно, никто ничего никогда не сделает. Это прекрасная двойная ложь: врем себе и остальным, будто задача важна, и врем себе, что обязательно до нее доберемся.

Если у задачи нет дедлайна, считайте, что и задачи нет. Поэтому любое «сделаем когда-нибудь» нужно либо фиксировать дедлайном, либо отпускать с миром: значит, не очень-то оно нам и нужно.

P. S. Даже если добавить «после пуска», «после майских» или «после отпусков», ситуация не изменится: никто ничего не сделает, все забудут.

Карма тестировщика

Бывают люди с кармой тестировщика: что бы они не использовали, находят самые невероятные баги, заусенцы и нестыковки. Взялся пинговать ya.ru, сломал ping ¯\_(ツ)\_/¯. Самые суровые из них одарены еще и бесконечной энергией: 5 багрепортов за 10 минут для них не проблема.

Конечно, когда я был начинающим программистом, я всегда их недолюбливал: ну емана, неужели тяжело НОРМАЛЬНО пинговать? Зачем это исправлять, почему нельзя последовательно нажать A, B и C, но с минимальной задержкой в 300 мс?

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

irb: echo on assignment

Недавно заметил, что при запуске рельсовой консоли пропало «эхо» при присвоении. Раньше было так:

> subscription = Subscription.find(123)
=>
#<Subscription:0x00007fe17d6b6e38
 id: 123,
 product_id: "xxx",
 status: "active">

А стало так (некруто):

> subscription = Subscription.find(123)
=>
#<Subscription:0x00007fb3f4d06588
...

Полез разбираться и узнал пару интересных вещей. Во-первых, с марта railties бандлит irb.

Это нужно для того, чтобы ребята со старым Руби все равно получили новый irb вместо «системного» старья.

Во-вторых, новый irb по умолчанию обрезает «эхо» при присвоении:

В-третьих, чтобы вернуть «эхо», достаточно подкрутить конфигурацию irb в ~/.irbrc:

IRB.conf[:ECHO_ON_ASSIGNMENT] = true

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

IRB.conf[:USE_AUTOCOMPLETE] = false

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

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

class Banner
  attr_reader :probability

  def initialize(probability:)
    @probability = probability
  end

  def visible?
    probability > Kernel.rand
  end
end

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

banner = described_class.new(probability: 0.7)

# Проводим 1000 испытаний, результаты — в массив
trials = Array.new(1000) { banner.visible? }
# Сколько раз visible? вернул true
successful_trials = trials.select { |trial| trial }
# Какой процент
successful_trials_percentage = successful_trials.length.to_f / trials.length.to_f

# Проверяем получившийся процент
expect(successful_trials_percentage).to be_within(0.05).of(0.7)

Этот вариант кажется логичным и математичным. Но с ним есть проблемы: «мигания» и производительность. Тест будет иногда падать в зависимости от количества испытаний: чем меньше испытаний, тем чаще. Чтобы сделать тест стабильным, придётся увеличить количество испытаний и завернуть тест в rspec-retry.

С другой стороны, можно избавиться от рандома. Например, застабив Kernel#rand:

allow(Kernel).to receive(:rand).and_return(0.25)

banner = described_class.new(probability: 0.7)

expect(banner).to be_visible

Или зафиксировав сид:

# Осторожно, магия!
# rand с таким сидом первым числом возвращает 0.37
srand(42)

banner = described_class.new(probability: 0.7)

expect(banner).to be_visible

Ещё по теме:

Лайфхак: процедура завершения работы

Украл лайфхак, чтобы снизить тревогу после работы. Ну вы знаете: рабочий день давно закончился, пора бы уснуть, а ты лежишь в кровати до 2-3 ночи и стрессуешь, бесконечно возвращаясь к работе и задачам. А вот надо было Мише вот так ответить. Блин, на что переходить с Яндекс.Почты? Или лучше заплатить им? Но мы не ведем переговоры с террористами! А когда надо счётчики на воду поверить? Как в билинге учесть, что подарков может быть несколько? А если ещё и одновременно? Зачем я вообще пошёл в программисты?

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

1. Разбираю инбоксы — почту, телегу и inbox.txt — и распихиваю всё по задачам.

2. Смотрю список дел на сегодня. Всё, что не успел, переношу на завтра или выкидываю.

3. Смотрю список дел на завтра и остаток недели.

4. Разбираю незакрытые вопросы в голове: вытаскиваю в задачи или заметку с идеями «на подумать».

5. Говорю себе: «Теперь питание компьютера можно отключить».

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

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

RSpec: to be_within().of()

Как вы прекрасно знаете, в комплуктерах 0,1 + 0,2 ≠ 0,3. Аналогичные проблемы есть и в любых других вычислениях, связанных с дробями (считай, с числами с плавающей запятой). Поэтому в тестах никогда нельзя напрямую сравнивать дробные результаты вычислений.

Используйте be_within(delta).of(expected) — матчер, проверяющий, что полученное число находится в окрестности радиусом delta от нужного:

expect(score).to be_within(0.0001).of(2.19)

Аналогичная история и со временем: в Руби Time имеет точность до наносекунд, а БД — до микросекунд или секунд. В таких случаях приходится сравнивать приведением к юникстайму:

expect(deadline.to_i).to eq Time.new(2022, 7, 7).to_i

Или тем же матчером:

expect(deadline).to be_within(1.second).of(Time.new(2022, 7, 7))

И конечно, если работаете с деньгами, берите BigDecimal.

SPA на Реакте без Вебпака

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

Оказалось, это ограничение решается довольно легко. Все можно сделать на клиенте:

1. Берем react и react-dom с CDN:

<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>

2. Берем babel с CDN:

<script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script>

3. Добавляем рутовый элемент, в который отрендерим приложение:

<div id="root"></div>

4. Добавляем <script> в конец body, который babel скомпилирует в рантайме:

<script type="text/babel">

4. Пишем приложение внутри этого скрипта:

const App = (<h1>Ola!</h1>)

ReactDOM.render(<App />, document.getElementById('root'))

5. Вы великолепны!

RSpec: skip и pending

Ситуация: пришли дизайнеры, говорят, СРОЧНО меняем механизм рекомендаций к статье. Вы меняете код, запускаете тесты, получаете кучу ошибок: рекомендации теперь другие, результаты не соответствуют действительности. Дизайнеры и маркетологи стоят у вас над душой и просят выкатить все НЕМЕДЛЕННО. Что делать с «битыми» тестами?

В RSpec есть пара тегов для «пропуска» битых тестов: skip и pending. Добавляем тег к context, describe или it и указываем причину:

it "returns posts with tags commonly used together", skip: "Broken in #45"

it "returns posts with tags commonly used together", pending: "Blocked by #46"

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

1) returns posts with tags commonly used together FIXED
     Expected pending 'Blocked by #46' to fail. No error was raised.

Короче, если на собеседовании вас спросят, чем отличается skip от pending, смело отвечайте: pending может завалить тесты, а skip — нет.

SSRF

Недавно я узнал об интересном типе атак на веб-приложения: SSRF — server-side request forgery. Идея простая: если ваше приложение принимает ссылки от пользователей, что-то по ним скачивает и отображает, то злоумышленник может скормить ссылку, например, на приватный ресурс в локалке, и получить доступ к данным. Например, в AWS можно использовать http://169.254.169.254/latest/meta-data/, чтобы получить креденшиалы.

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

Чтобы погрузиться в потенциальные варианты атак с помощью SSRF, взгляните на исходники ssrf_filter: https://github.com/arkadiyt/ssrf_filter/blob/main/lib/ssrf_filter/ssrf_filter.rb

Тут всё: локальные айпишники, ipv6, левые URI, бесконечные редиректы, CRLF инъекции, ДНС и публичные адреса.

Range в ActiveRecord

Полюбил открытые и закрытые интервалы в запросах в ActiveRecord. Было:

Charge.where("created_at >= ?", 5.minutes.ago)

Subscription.where("valid_until <= ?", Time.now)

User.where("created_at >= ? and created_at <= ?", 2.weeks.ago, 1.week.ago)

Стало:

Charge.where(created_at: 5.minutes.ago..)

Subscription.where(valid_until: ..Time.now)

User.where(created_at: (2.weeks.ago..1.week.ago))

Не надо везде лепить восклицательные знаки!

Вижу такое:

def finish!
  update(status: :finished)
end

В Руби принято добавлять восклицательный знак к «небезопасным» методам, которые модифицируют оригинальный объект, а не возращают новый:

email = "foo@bar.com"

email.gsub(/foo/, "baz") # вернет новую строку, email останется тем же
email.gsub!(/foo/, "baz") # поменяет email и вернет его

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

user.save # при ошибках вернет false
user.save! # при ошибках выкинет ActiveRecord::RecordInvalid

Оттого, что мы добавили восклицательный знак к finish, лучше АПИ не стало. Наоборот, мы запутали сами себя: безопасного finish не существует, а finish! вряд ли выкинет исключение. Соответственно, метод лучше переименовать тупо в finish.

Планирование преемственности

Читаю сейчас «Элегантный пазл» Уила Ларсона — книгу о системном подходе в управлении и развитии инженерной части организации. Наткнулся на интересное упражнение, succession planning, планирование преемественности.

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

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

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

3. Взять в работу и распланировать на год весь список «прямо сейчас» и одну-две рискованных штуки.

4. Повторить упражнение через год.

yakuake в iTerm

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

Оказывается, iTerm из коробки умеет выезжать сверху. Ребята спрятали эту фичу здесь: Preferences — Keys — Hotkey — Create a Dedicated Hotkey Window…

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

Иногда у приложения есть какие-то ключевые метрики, которые могут триггерить алерты, ахтунги и жопы. Например, в условном биллинге это может быть процент неудачных списаний за последний час. Если он высокий, скажем, более 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 часа над чем-то сложным — выписал талон, просидел весь день на созвонах и стендапах — ничего не получил.

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

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