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

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

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

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

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

Менеджерский лайфхак: 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. Вы великолепны.

Курс о профессиональном росте. Как расти самому, как растить команду

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

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

Именно так я и просрал первые 8 лет своей карьеры программиста. Я приходил на работу, пил кофеёк, разбирал почту, шёл на обед и садился программировать одно и то же на Джаве. Иногда после обеда играл в Lineage II (Миша, привет!) или Unreal Tournament (Боря, привет!). Результат — −8 лет жизни, +1 год опыта разработки на Джаве.

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

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

В основе курса — мой опыт, идеи и лайфхаки, которые я применяю последние 9 лет. Плюс опыт, лайфхаки и взгляд со стороны от Феди. Всё это приправлено историями из жизни, примерами из бразильского джиу-джитсу и дополнительными материалами: видосами, ссылками и книгами. В курсе четыре недели. Старт — 10 августа.

Записаться на курс:
https://education.borshev.com/growth

По промокоду iddqd дадут скидку в 10%.

Сервисы головного мозга

В понедельник встретил в коде такое:

# app/services/user_initials_extractor.rb
class UserInitialsExtractor
  def self.call(name)
    name
      .upcase
      .squeeze
      .split(" ")
      .map { |part| part[0] }
      .join(". ")
      .concat(".")
  end
end

# app/views/shared/_header.html.erb
<%= UserInitialsExtractor.call(current_user.name) %>

И меня тригернуло: это процедурное программирование, которое выдает себя за ООП, используя классы-помойки. Объекты — это живые организмы, которые обмениваются друг с другом сообщениями. UserInitialsExtractor — тупая процедура, завернутая в класс-сервис с убогим АПИ (#call, серьезно?).

Гораздо лучше было бы подумать, какой объект (существующий или новый) должен отвечать на #initials? Это мог бы быть User, декоратор для него или вообще отдельный объект, представляющий собой Имя:

class User
  # ...
  delegate :initials, to: :name
end

class UserName
  # ...
  def initials
  end
end

Чтобы не заниматься таким, советую прочитать: https://www.yegor256.com/2015/03/09/objects-end-with-er.html

«Что» и «Чтобы что» в пулреквестах

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

Пара примеров:

## Что?
Обновляем Рельсы до 6.1.3.2.

## Чтобы что?
Чтобы устранить проблемы с безопасностью: CVE-123123.
## Что?
Перед отслеживанием купона проверяем, что он действительно существует в БД.

## Чтобы что?
Чтобы не записывать в аналитику всякий шлак, типа utm_campaign, utm_source или utm_medium.

Заметил еще и положительный эффект для автора пулреквеста: если не можешь внятно сформулировать «Чтобы что?», то не понимаешь задачу; если в «Что?» есть «А также» и «А еще», то лучше пулреквест разбить на несколько.

Короче, советую попробовать.

Где проходит грань в изолированности объекта тестирования?

Вопрос:

Где проходит грань в изолированности класса (объекта тестирования)? Как ты принимаешь решение что тестировать напрямую, а что стабами?

Ситуация: есть аудитория, которую можно импортировать из csv-файла. Чтобы проверить, что файл валиден, нужно убедиться, что в csv-файле есть колонки с почтой и именем клиента:

class AudienceList
  def valid?
    csv_headers.include?("Email") && csv_headers.include?("Name")
  end

  private

  def csv_headers
    @csv_headers ||= csv_data.first.keys
  end

  def csv_data
    @csv_data ||= SmarterCSV.process(file)
  end
end

AudienceList зависит от внешнего метода-запроса SmarterCSV#process. По умолчанию внешние зависимости я стаблю: это позволяет получить полностью изолированный, по-настоящему модульный, тест:

allow(SmarterCSV).to receive(:process)
  .and_return(sample_csv_data)

# ...

expect(audience_list).to be_valid

Но если я чувствую себя неуверенно с получившимся тестом, если чувствую, что без стаба тест будет надежнее и полезнее, то тестирую напрямую. В данном случае я бы заготовил два csv-файла (валидный и невалидный) и положил бы их в spec/fixtures. Эти файлы послужили бы еще и отличным источником примеров, документации. Разработчики смогли бы заглянуть в них и подсмотреть реальные данные и их структуру.

Только то, что важно для проверки

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

context "when coupon is applied" do
  it "charges user $500" do
    allow(Cashier).to receive(:charge)

    purchase.perform

    expect(Cashier).to have_received(:charge)
      .with(
        user: user,
        sum: 500,
        description: "Покупка абонемента",
        coupon: coupon
      )
  end
end

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

context "when coupon is applied" do
  it "charges user $500" do
    allow(Cashier).to receive(:charge)

    purchase.perform

    expect(Cashier).to have_received(:charge)
      .with(hash_including(sum: 500))
  end
end

Меньше — лучше

Чем меньше пулреквест, тем лучше: легко отревьюить, легко задеплоить, легко искать ошибки. Чем больше пулреквест, тем сильнее дизмораль: увидел +700 -60 в Гитхабе и приуныл.

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

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

1. Открываем ветку rails-6-upgrade.

2. Делаем маленькие пулреквесты не к master, а к rails-6-upgrade.

3. Ревьюим, мержим их в rails-6-upgrade.

4. Когда апгрейд готов, мержим rails-6-upgrade в master.

Пора валить?

Худшее, что может сделать программист для своей карьеры — обменять десять лет своей жизни не на десять лет опыта, а на один год, повторенный десять раз. Вот сидишь ты в собственном кубикле, программируешь там, тут остановился и подумал: Ух, ты! Ну и псих же я: десять лет на одном месте с тем же самым Друпалом. Бывает такое?

Чтобы вовремя заметить такое болото, раз в полгода я задаю себе три вопроса:
1. Что интересного, значительного и важного я сделал за прошедшие полгода?

2. Что за навыки я получил или прокачал за прошедшие полгода? Они вообще мне нужны?

3. Как мне команда и компания? Что с доверием, ценностями и миссией компании? Я еще верю в них? Или уже все пронизано цинизмом и пессимизмом?

Если ответы по двум из трех вопросов удручающие, пора валить.

Еще по теме:
How to waste your career, one comfortable year at a time

Когда использовать double, а не instance_double?

Напомню разницу: instance_double может уронить тест, если застабленные методы отсутствуют в указанном классе, double на все пофиг.

По моему опыту double нужен в двух случаях:
1. Вместо объекта, который пока не существует в системе. Нет класса, значит, instance_double не на что опереться.

2. Вместо чего-то незначительного со стабильным АПИ. Например, для писем:

allow(DeadlineMailer)
  .to receive(:last_deadline_warning)
  .and_return(double(:email, deliver_later: true))

RSpec: before и after хуки

Почему-то сталкиваюсь с такими тестами:

describe "#foo" do
  before :each do
    # ...
  end
end

:each можно смело опускать: это поведение по умолчанию для before. Лучше так:

describe "#foo" do
  before do
    # ...
  end
end

И несколько интересных фактов о before и after хуках:

1. before :each и before :all — алиасы для before :example и before :context.

2. before :each выполняется перед каждым примером, it do...end. after :each — после.

3. before :all выполняется перед контекстом (context, describe). after :all — после.

4. В before :suite нельзя задавать переменные экземпляра (instance var, @foo)

5. Только в before :each можно мокать.

Как застабить переменные окружения в RSpec

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

# Если в коде ENV["CHARGES_TOKEN"]
allow(ENV)
  .to receive(:[])
  .with("CHARGES_TOKEN")
  .and_return("XXX")

# Если в коде ENV.fetch("CHARGES_TOKEN")
allow(ENV)
  .to receive(:fetch)
  .with("CHARGES_TOKEN")
  .and_return("XXX")

Если вы сторонник готовых гемов, возьмите для этой цели ClimateControl:

ClimateControl.modify CHARGES_TOKEN: "XXX" do
  # ...
end

Что не нужно писать в it

1. Бесполезные, общие слова, не несущие никакой конкретики:

it "adds certain value"
it "returns correct result"
it "fails"
it "returns formatted string"
it "returns correct url"
it "is ok"

2. Детали реализации:

it "changes @scheduled_on"
it "assigns @todos"

3. Ложь:

it "returns time in 24-hour format" do
  expect(...).to eq "9:25"
end

it "strips leading zeroes" do
  expect(foo(" 9:25 ")).to eq "9:25"
end

И, пожалуйста, не тестируйте конструкторы и attr_reader/writer/accessor: вы все равно их проверите, тестируя публичный АПИ.

RuboCop, RSpec и стайлгад

Годами стайлгайдом для RSpec был betterspecs.org. К сожалению, он годами не менялся, не развивался и частенько не работал.

Оказывается, 1,5 года назад Better Specs стал RSpec Style Guide и переехал в Rubocop HeadQuarters:
https://github.com/rubocop-hq/rspec-style-guide

И стайлгайд ожил и расцвел:
https://rspec.rubystyle.guide

И, конечно, есть плагин к Рубокопу для работы со спеками:
https://github.com/rubocop-hq/rubocop-rspec

Подключается в два счета:

# Gemfile
gem "rubocop-rspec", require: false

# .rubocop.yml
require:
  - ...
  - rubocop-rspec

Полный список копов:
https://docs.rubocop.org/rubocop-rspec/cops_rspec.html

Как протестировать конфиг whenever

Недавно я опечатался в конфиге whenever:

every 1.day, at: "03:30 am", roles: %i(backupable) do
  rake %(
    backup:db
    backup:assets
  ).join(" ")
end

При деплое whenever взорвался:

NoMethodError: undefined method `join' for "backup:db backup:assets":String

Чтобы в будущем такого не было, нужна хотя бы минимальная валидация конфига whenever. Решение оказалось простым: достаточно запустить на CI:

bundle exec whenever

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