Ведущий разработчик.
Специализация — Рельсы, фронтенд, автоматизация тестирования и разработки.
Связаться: vasily@polovnyov.ru или телеграм.
Ведущий разработчик.
Специализация — Рельсы, фронтенд, автоматизация тестирования и разработки.
Связаться: vasily@polovnyov.ru или телеграм.
Бывает, приложение общается с внешним сервисом не через интернет-сокеты, а через юникс-сокеты, считай, через локальный файл:
Excon.get("unix:///ping", socket: "/tmp/unicorn.sock")
Чтобы застабить такой запрос Вебмоком, нужно чуть извернуться:
stub_request(:get, "http://unix/ping").to_return(status: 200)
Пара ребят в комментариях к предыдущему посту ответили, что у них 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. Собрали повестку, в том же чате созвонились.
2. Повестка полностью асинхронная. Можно накидывать вопросы и делиться интересным когда угодно, не нужно ждать встречи. Больше того, часть вопросов можно разобрать ещё до встречи: если вопрос короткий и простой, можно сразу ответить на него текстом.
3. Саму встречу (например, аудио) и её конспект бережно хранит Телеграм. Легко собирать, легко возвращаться.
Часто начинающие разработчики останавливаются на первой же проблеме и пытаются решить ее перебором:
— Я поменял вот эту строчку, чтобы починить баг X, а теперь у нас тесты не проходят. Я переставлял и менял эту строчку так и так, но тесты все еще падают. Как поправить?
Это, конечно, полная херня: у меня не завелась машина, я садился в нее и с пассажирского, и с водительского сиденья, но она все равно не заводится. Как поправить?
Чтобы вырасти как программист, нужно научиться решать проблемы не перебором, гаданием и случайными манипуляциями в коде, а железобетонным научным методом: задавать себе вопрос «Почему?» до тех пор, пока не станет понятна суть проблемы. Например:
— Я поменял вот эту строчку, чтобы починить X, а теперь у меня в консоли полно 404 ошибок.
— Почему полно 404 ошибок?
— Потому что мы делаем запрос, в ответ на который приходит 404.
— Почему приходит 404?
— Потому что мы либо собрали неправильный урл, либо по этому адресу запросы никто не обрабатывает.
— Окей, обработчики точно в порядке.
— Почему урл неправильный?
— Потому что мы забыли /scope/ в начале.
— Почему мы забыли /scope/ в урле, почему его там не оказалось?
— Потому что я поменял строчку с идентификатором страницы, из которого собирается и урл для запросов к АПИ.
Бум! Вот и причина проблемы, теперь решение будет легко найти.
Пожалуйста, не тратьте время на бесполезную перестановку строчек кода, задавайте вопросы и проникайте в суть проблемы.
См. также:
https://bureau.ru/soviet/20180621/
Ищу толкового стажера, который вместе со мной сделает интересную бэкендерскую задачу — систему аудита в Learning Management System. Конечно, если все пойдет хорошо, мы поработаем и над другими задачами, а стажер вырастет в сеньора или лида.
Я ищу стажёра, который:
За полтора месяца научу стажёра:
Для этого стажёр будет:
После стажировки мы либо продолжим работать вместе, либо стажер уйдёт на новогодние каникулы с планом роста и списком рекомендуемой литературы.
Если написанное выше про вас, напишите мне письмо с рассказом о себе: vasily@polovnyov.ru
В советах о дебаге я писал, что если не получается поправить баг, можно побороться с его последствиями или заменить решение. Сегодня понял, что есть и третий вариант: достроить и обойти баг.
Ситуация: Commonmark не заворачивает текст в параграф, когда он идёт с одним переводом строки после блочных элементов (заголовки, дивы и прочие). Такой код:
<h1>h1</h1>
wtf
<h1>h1</h1>
wtf???
Даёт такой ХТМЛ (в первом случае параграфа нет, а хотелось бы):
<h1>h1</h1>
wtf
<h1>h1</h1>
<p>wtf???</p>
Если чинить проблему в самом Commonmark дорого и долго, то можно достраивать исходный текст так, чтобы баг не случался. В случае выше достаточно препроцессить исходный текст, добавляя ещё один перевод строки в ситуации с блочным тегом, одним переводом строки и последующим текстом.
Бум! Можно просто достроить исходные данные так, чтобы баг никогда не случался.
Бывает, нужно поставить заглушку на определенный роут, а контроллер для этого делать не хочется. В таком случае подойдет роутинг в 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