Ведущий разработчик.
Специализация — Рельсы, фронтенд, автоматизация тестирования и разработки.
Связаться: vasily@polovnyov.ru или телеграм.
Ведущий разработчик.
Специализация — Рельсы, фронтенд, автоматизация тестирования и разработки.
Связаться: vasily@polovnyov.ru или телеграм.
Самая большая ложь в разработке — фраза «сделаем когда-нибудь». Конечно, никто ничего никогда не сделает. Это прекрасная двойная ложь: врем себе и остальным, будто задача важна, и врем себе, что обязательно до нее доберемся.
Если у задачи нет дедлайна, считайте, что и задачи нет. Поэтому любое «сделаем когда-нибудь» нужно либо фиксировать дедлайном, либо отпускать с миром: значит, не очень-то оно нам и нужно.
P. S. Даже если добавить «после пуска», «после майских» или «после отпусков», ситуация не изменится: никто ничего не сделает, все забудут.
Бывают люди с кармой тестировщика: что бы они не использовали, находят самые невероятные баги, заусенцы и нестыковки. Взялся пинговать ya.ru, сломал ping ¯\_(ツ)\_/¯. Самые суровые из них одарены еще и бесконечной энергией: 5 багрепортов за 10 минут для них не проблема.
Конечно, когда я был начинающим программистом, я всегда их недолюбливал: ну емана, неужели тяжело НОРМАЛЬНО пинговать? Зачем это исправлять, почему нельзя последовательно нажать A, B и C, но с минимальной задержкой в 300 мс?
Со временем до меня дошло, что такие люди — дар, а не проблема. Как программисты мы склонны использовать продукты так, как это делали бы другие программы: в строгом соответствии с инструкциями и командами. Ребята с кармой тестировщика помогают делать отличные продукты, которые учитывают человеческие слабости, а не бьют за них по рукам. Короче, если среди ваших коллег есть ребята с кармой тестировщика, обнимайте их и почаще слушайте.
Недавно заметил, что при запуске рельсовой консоли пропало «эхо» при присвоении. Раньше было так:
> 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. Говорю себе: «Теперь питание компьютера можно отключить».
Важный момент: фразу можно использовать любую, главное, доверять ей. Если сказал, значит, всё: рабочий день закончен, всё аккуратно учтено, рассортировано и записано, можно отдыхать.
Мне помогло, попробуйте и вы.
Как вы прекрасно знаете, в комплуктерах 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
.
В конце сентября у меня резко ухудшилось зрение в правом глазу: даже верхнюю строчку таблицы Сивцева видел с трудом. Врачи установили ложную близорукость, назначили лекарства, ограничение зрительных нагрузок (ха!) и гимнастику для глаз «по Аветисову». Конечно, ни одно из приложений с гимнастикой для глаз мне не понравилось, и я написал свое. Чтобы было интереснее, добавил ограничений: Реакт, одна страница и никакого Вебпака.
Оказалось, это ограничение решается довольно легко. Все можно сделать на клиенте:
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
. Добавляем тег к 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 — 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 инъекции, ДНС и публичные адреса.
Полюбил открытые и закрытые интервалы в запросах в 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 из коробки умеет выезжать сверху. Ребята спрятали эту фичу здесь: 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 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, всегда открытый в Саблайме. Появилась идея, опасение, вопрос или что-то, что нельзя забыть, записал в инбокс и продолжил программировать, не отвлекаясь.
Чтобы инбокс работал, мозгу нужны гарантии, что все записанное в инбокс не потеряется. Поэтому у меня есть повторяющаяся задача «Разобрать инбокс», которая стоит сразу после разбора почты в конце рабочего дня. Сделал дело — разбирай инбоксы смело.
Короче, советую: если отвлекаетесь от работы на всякое, закидывайте его в инбокс и забывайте до вечера.
P. S. Использую Саблайм, потому что он не теряет несохраненные данные. Если не сохраню изменения в инбоксе и перезапущу комп, Саблайм их не потеряет.
Ситуация: клиенты регистрируются, а через три дня получают письмо с онбордингом. Скажем, таким кодом:
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
без Таймкопа опасно: могут быть ложноотрицательные тесты.