Как стать автором
Обновить

Комментарии 54

высокой нагрузке решаются пятью простыми способами

Ещё частичная денормализация, ей бывает п.2 (склейку) ускоряют. Позволяет быстрее получить все данные, но без гарантии целостности, иногда этого достаточно. Да и при склейке такие гарантии всё равно очень легко потерять.

Вы совершенно правы, денормализация является неплохим методом для улучшения производительности 👍

Я почему-то воспринимаю её как частный случай предпросчета (т.е. подготовки данных таким образом, чтобы потом было легко их доставать из БД и они уже в нужном формате), но наверное можно было вынести в отдельный пункт.

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

Измеримый, вы имеете в виду покрытие кода тестами?

Ну смотрите, ведь простота кода - тоже в общем-то измеримый показатель, то о чём я пишу в статье под заголовком "Maintainability Index". Так что, всё-таки рискну не согласиться, не единственный измеримый.

И кстати обе эти метрики, они не решают например проблему логических ошибок. В бизнес-приложениях, никто не знает полное количество use-case'ов, и поэтому невозможно оценить, написали ли мы достаточное количество интеграционных тестов.

Даже сам заказчик часто не знает, например, нужна ли та или иная кнопка или нет. Это надо напрямую с конечными пользователями общаться (т.е. с водителями, если брать систему из статьи), и нет никакой гарантии, что все они используют систему одинаково.

Помню, у нас был смешной случай (в другом продукте). Пользователи жалуются: система не работает, всё плохо. Покрытие кода тестами по этой фиче - гордые 100%. Тестируем вручную: всё отлично. Думаем, может это только на продакшене? Создаем тестовый аккаунт, проверяем - да всё нормально же. Ну думаем, наверное проблема в данных + продакшене. Копируем данные на тестовый аккаунт в продакшене, т.е. полная копия, идеально. И всё равно работает!

В конце концов, устраиваем звонок с пользователями, говорим, покажите пожалуйста баг. Они открывают браузер, зажимают Ctrl (я даже не знал, что так можно), и открывают 10 документов со страницы в 10 разных вкладках. К этому моменту у всей команды уже шевелятся волосы на всех частях тела...

В общем, метрики, они могут быть обманчивы. Конечно, лучше с ними, но доверять им стопроцентно, наверное не стоит.

Тест не доказывает надёжность работы программы. Тест просто фиксирует поведение программы на определённых входных данных.

Два раза прочитал статью, что бы не упустить мысли автора. Но так и не понял, что это за утилиты мониторинга, визуализирующая процессы. И как по этому выводу понять, это ошибка или корректное проведение. Хотя бы пример бы привели. Эта утилита автоматом понимает, что это ошибка именно в том модуле? Второй момент, юнит тесты помогают тестировать систему постоянно, нет ручной работы. Сделал коммит. Тесты запустились. Если тест упал, всегда понятно в каком именно модуле, классе и конкретном методе, что то сломалось. Интеграционные менее показательны в этом смысле. Так как они тестируют конкретный процесс который содержит много функционала, этапов и т.д к примеру регистрация юзера. Состоит из множества проверок, есть ли такой логин, почта, валиден ли логин, и т.д Потом запись в базу. При разделении кода для возможности написания юнит тестов, возможно проверить все этапы + интеграционные для записи в базу. Но если тестировать как единую операцию, не понятно, что сломалось. База, валидация и т.д Суровые вы ребята, без юнит тестов))

Спасибо, что прочитали статью дважды! К сожалению, система проприетарная, всё под NDA, привести скриншоты или совсем детальное описание, не представляется возможным.

Но утилита мониторинга, это просто средство отладки, средство для того, чтобы заглянуть внутрь системы и оценить её работоспособность с точки зрения задач, которые она решает.

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

Функционально похоже на Honeycomb, но более визуально, с заточкой под нашу конкретную систему.

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

Возможно я безвозвратно испорчен юнит тестами. Но окей есть дебаг режим есть логи. У вас есть эталонные логи, с которыми должны сходиться текущие логи? Это же тоже тест. Или вы именно когда юзер пишет, что то сломалось вы в ручную вычитывает логи и находите отклонение отталкиваясь от знания бизнес процессов и что вот конкретно на строке лога 20567 должно быть число 1 а не ноль. Кучу отладочных данных тяжело читать, понимать. Возможно вы управляете вселенной и для вас это на изи.))

Люди очень хороши в "pattern matching". Ручной мониторинг во время релиза, визуализация системы хотя бы через стандартные Graphana-дашбоарды - это мне кажется прям очень сильно увеличивает вероятность обнаружить ошибку. Даже если каждый индивидуальный график выглядит "ок", нет резких скачков метрик, и 0 ошибок в логах, человек всё равно может заметить, что что-то не так, по совокупности графиков, на уровне шестого чувства, и начать разбираться.

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

выглядит это так, что тесты бы всеравно не помешали. Но вы будто фокусируетесь на "ты хоть смотрел как это работает?". И отсутствием тестов принуждаете смотреть. Что кхм... тесты полезны. Думать головой и смотреть как оно работает - должно быть. Это как в Ералаш с бразильской системой. Не поймал - окно разбил. Не, на поле играть не будем. только под окном парикмахерской.

Сейчас Observability стала популярной темой. Например вот статья у NewRelic, где решения во многом перекликаются с подходами автора (ну может кроме отказа от юнит тестов). Вещь определенно полезная - у людей гораздо выше восприимчивость к тому, что они получают в непосредственных ощущениях. Это позволяет оперативнее реагировать на проблемы, не дожидаясь момента когда их кто-то увидит и зарепортает.

Моя гипотеза: кто-то у вас работал гибридом PM'а и QA'я. Я это наблюдал несколько раз - кому-то надо, чтобы софт работал, а ресурсов на QA и формализацию процессов нет. И он лично всё проверяет, и знает как должно быть. Супер быстро, супер точно, супер правильно.

Но!

а) человека надолго не хватает, и как только фокус смещается, всё идёт куда-то.

б) не масштабируется.

Нет, такого человека не было :)

Конечно, мы вручную проверяли после выгрузки, что визуально всё работает, но очень поверхностно. По сути, проверялось только несколько основных, самых критических страниц.

Кроме того, мы постоянно показывали систему заказчику на Sprint Demo и даже на daily, но это обычное локальное тестирование разработчиком фичей, которые он разрабатывает.

Тестировщик в команде был вроде около месяца, в самом начале проекта, но от него как-то быстро отказались.

Т.е. тестировали силами программистов и клиента. Тоже стратегия.

Если идёт работа с денежными средствами? Особо не потестируешь. Особенно если есть зубодробительная логика, по переводу, учёту, работой с датами, учитывая разные модификаторы и т.д Как написать юнит тесты и проверить кейсы понятно. Допустим извратиться можно проделать на интеграционных, просто не вызывая последний шаг, непосредственно запуск реальной транзакции. Я не представляю, что это должен быть за инструмент, баг режим, который позволит забить на тесты. Откройте секрет)

Все перечисленные в статье методы дополняют друг друга, дело далеко не только в мониторинг-утилите. Я бы сказал, Fault tree analysis намного сильнее улучшил для нас надежность кода.

Вот например, допустим у вас есть интеграция с финансовой системой. Вы хотите сделать её надежной. Вы покрыли её тестами, но в один прекрасный день эта финансовая система просто взяла и поменяла формат отдаваемых данных. Или просто упала. Как тесты помогут в этом случае? :)

А вот если был сделан fault tree analysis, архитектор подумал над всеми этими проблемами заранее, использовал например defensive programming для защиты от небольших изменений формата результатов, спроектировал механизм фоллбэка на резервную систему и алертов в случае полного отказа или же критичного изменения формата отдаваемых данных, и т.д.

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

Безусловно, в разных проектах - разные особенности, и конкретные решения будут разными.

Но сам факт того, что ситуация была заранее проанализирована и нужные меры были приняты превентивным образом, вот это важно :)

Превентивный подход тоже так себе. Обычно он приводит к оверинжиниринг и избыточному коду который не соответствует спекам (это же превентивное решение того, с чем не сталкивались), а значит удорожание обслуживание этого кода.

Вот всюду, где "логика", как раз тесты и нужны, и их обычно проще всего писать. Если сложно - код г-но.

Интеграционные тесты (особенно, с внешними сущностями), да, тяжело. e2e - тяжело. А уж логику изолировать в виде чистых функций и тестировать по самые помидоры - это ж азы нормального программирования.

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

>ресурсов на QA и формализацию процессов нет.
Не обязательно. Просто есть ресурсы, но они другие. Например и PM и QA в одном лице. Если это решает проблему — почему так не делать? То есть, я бы переформулировал гипотезу так — есть некоторые виды проектов, которые вполне могут жить и развиваться без тестов. Я такие тоже видел, причем несколько раз. У них было кое-что общее, в том числе, наверное, и наличие человека, который в состоянии держать проект в голове целиком, но не только это.

>а) человека надолго не хватает, и как только фокус смещается, всё идёт куда-то.
Ну, как бы тесты тоже нужно поддерживать, т.е. ресурсы на них тратятся постоянно. А эффект зачастую бывает сомнительным. И как только фокус смещается, тесты устаревают, перестают поддерживаться, и становятся скорее обузой, чем подмогой. Такое бывает с любыми инструментами и процессами.

При таком подходе, форма может быть вполне сломана и багрепортов как бы и нет))

Недавно слушал выступление на счёт языка F# и функционального программирования. Так вот автор утверждал, что использование F# позволило сократить количество ошибок в приложение за счёт более таких подходов как чистые функции, неизменяемость и в довесок сам язык позволяет выявить большее количество ошибок на стадии компиляции. Мне кажется, эта стать перекликается с его подходом к разработке.

Самые дорогие ошибки логические. К примеру перепутали условие И и Или. И все. Чистые функции и в других языках есть, это подход к написанию, не самый эффективный конечно. Сам язык ищет системные ошибки, типа выход за границы массива, нулевые указатели и т.д Приятно конечно, но. Если то произошло, сервис упал, исправили запустили. Но если в логике Ошибка и даже тестов нет. Это прям беда. Ошибку выявить возможно, но только непосредственном мониторинге на рабочей системе. И одно дело юзер не смог зарегистрироваться на сайте знакомств. Другое дело лярд рублей немного не тому юзеру ушел или выплаты миллионам человек, ушли другим.

не самый эффективный конечно

Почему?


Но если в логике Ошибка и даже тестов нет.

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

Неэффективность заключается при обработке тяжёлых структур. Вместо модификации входной структуры, возвращается новая копия. Лишний new. Если GC туп, будет адовая фрагментация. Особенно если new в цикле. Возможно продвинутая система типов, может решить часть проблем. Но это уже требует рассмотрения конкретного случая.

Вместо модификации входной структуры, возвращается новая копия.

Если вы не используете старую версию структуры, то компилятор нередко может соптимизировать это в in-place-мутации.


Если GC туп

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

Это проще, чем писать тесты?

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

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

Давайте на примере! Допустим, у нас есть банк, но у нас есть система отслеживания подозрительных платежей. Так вот платёж считается подозрительным, если платеж превышает 100'000 у.е. И/ИЛИ тот, кто отправлял деньги, совершил за день платежей на 1'000'000 у.е. Такие платежи должны проходить ручную модерацию, соответственно, нам их надо замечать. Допустим, программист устал, вчера вечером и ночью в клубе был, сегодня на работе у него завал был, а эту часть кода писал сильно вечером, потому что сроки горят. Программист написал "(ЭтотПлатёж > 100'000) ИЛИ/И (Сумма(Платежи, ЭтотДень) > 1'000'000)" (в общем, перепутал И и ИЛИ. Требованием было написать И, он написал ИЛИ, или, наоборот, просили ИЛИ, а он написал И)

Как защититься от таких ошибок?

Я предложу своё решение на Haskell для случая, когда просили И, а программист написал ИЛИ.

Решение!

Очевидно, в силу вступает произведение типов (для продвинутых можно было бы, наверное, пойти в уровень kinds, но и моя реализация, на мой взгляд, спасает от ошибок). Для лучшего эффекта надо вынести некоторые конструкторы в отдельный файл и не экспоритровать их, чтобы не допустить ошибки (случайно не создать их). Извините за длинные названия и за ужасное качество кода, но думаю, что я смог минизировать ошибку перепутать И с ИЛИ. Кстати, заметьте, здесь нет ни &&, ни ||!

Вот возникает вопрос по поводу второго случая (когда надо ИЛИ, а программист пишет по ошибке И). Это можно реализовать, использовав тип-сумму, но у меня вопрос: допустим, нам надо отправить пользователю причину, по которой его платёж оказался на ручной модерации. Если платёж превышает 100'000, то причина "платёж превышает 100'000". Если сумма за день больше 1'000'000, то причина "сумма за день больше 1'000'000". А если и то, и другое, то "платёж превышает 100'000" и "сумма за день больше 1'000'000". Вот как это изящно отразить последний случай в системе типов? Не писать же data Reason = A | B | A_And_B?

module Main where

import Text.Read ( readMaybe )
import System.Environment ( getArgs )

main :: IO ()
main = do
  args <- getArgs
  case parseArgs args of
      Just (aop, somftd) -> 
          case isUserSuspicious aop somftd of
            Just _ -> putStrLn "The user is suspicious"
            Nothing -> putStrLn "The user is NOT suspicious"
      Nothing -> putStrLn "Usage: ./program <amount of payment> <sum of money for this day>"

parseArgs :: [String] -> Maybe (AmountOfPayment, SumOfMoneyForThisDay)
parseArgs [aop, somftd] = (,) <$> (AmountOfPayment <$> readMaybe aop) <*> (SumOfMoneyForThisDay <$> readMaybe somftd)
parseArgs _ = Nothing


newtype AmountOfPayment = AmountOfPayment Int deriving (Eq, Show)

newtype SumOfMoneyForThisDay = SumOfMoneyForThisDay Int deriving (Eq, Show)

-- The constructor should be hidden
data ThisPaymentIsSuspicious = ThisPaymentIsSuspicious deriving (Eq, Show)

isThisPaymentSuspicious :: AmountOfPayment -> Maybe ThisPaymentIsSuspicious
isThisPaymentSuspicious (AmountOfPayment x)
  | x > 100000 = Just ThisPaymentIsSuspicious
  | otherwise = Nothing

-- The constructor should be hidden
data SumOfMoneyForThisDayIsSuspicious = SumOfMoneyForThisDayIsSuspicious deriving (Eq, Show)

isSumOfMoneyForThisDaySuspicious :: SumOfMoneyForThisDay -> Maybe SumOfMoneyForThisDayIsSuspicious
isSumOfMoneyForThisDaySuspicious (SumOfMoneyForThisDay x)
  | x > 1000000 = Just SumOfMoneyForThisDayIsSuspicious
  | otherwise = Nothing


data TheUserIsSuspicious = TheUserIsSuspicious ThisPaymentIsSuspicious SumOfMoneyForThisDayIsSuspicious deriving (Eq, Show)

isUserSuspicious :: AmountOfPayment -> SumOfMoneyForThisDay -> Maybe TheUserIsSuspicious
isUserSuspicious aop somftd = TheUserIsSuspicious <$> isThisPaymentSuspicious aop <*> isSumOfMoneyForThisDaySuspicious somftd

Тут, увы, всё самое интересное (проверки на суммы и всё такое) всё равно на уровне термов. Более того, если бы была какая-нибудь гипотетическая функция


payIfNotSuspicious :: User -> ??? -> IO ()

то совершенно непонятно, как на уровне типов проверить, что она вызывается только для неподозрительных товарищей (и что конструктор, например, спрятан и не экспортируется из модуля).


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


data User : Type where ...
data Payment : Type where ...
data TxHistory : User → Type where ...

data Suspicious : (u : User) → Payment → TxHistory u → Type where
  PaymentTooBig : (p : Payment) → (value p ≥ 10000) → Suspicious u p h
  SumTooBig : (h : TxHistory u) → (sum h ≥ 1000000) → Suspicious u p h

decideSuspicious : (u : User)
                 → (p : Payment)
                 → (h : TxHistory u)
                 → Either (Suspicious u p h) (Suspicious u p h → Void)
decideSuspicious = ...

pay : (u : User)
    → (h : TxHistory u)
    → (p : Payment)
    → (Suspicious u p h → Void)
    → IO ()

Здесь Suspicious u p h — свидетельство (которое можно получить одним из двух способов), что пользователь u, пытающийся сделать оплату p с историей транзакций h, является подозрительным. Suspicious u p h → Void же означает, что эта тройка точно не подозрительная (потому что иначе можно было бы получить значение типа Void, а для консистентных систем типов это невозможно).


Теперь мне достаточно одного взгляда на тип pay (и на определение типа Suspicious), чтобы понять, что эту функцию можно вызвать, только если пользователь доказуемо не подозрителен.

Более того, формулировка в терминах типов позволяет протестировать тесты проверить, что ваши типы удовлетворяют некоторым разумным предположениям.


Скажем, что можно ожидать от любого предиката Suspicious? Например, что если какая-то оплата отмечается как подозрительная, то и большая оплата помечается как подозрительная:


susMonoPayment : (u : User)
               → (p1 p2 : Payment)
               → (value p2 ≥ value p1)
               → (h : TxHistory u)
               → Suspicious u p1 h
               → Suspicious u p2 h

— это теперь теорема, которую можно доказать и убедиться, что ваше определение разумно, и вы, например, не перепутали ≥ 10000 и ≤ 10000.


Или, аналогично, если оплата подозрительна с какой-то историей транзакций, то разумно ожидать, что она подозрительна и с более длинной историей транзакций:


susMonoTx : (u : User)
          → (p : Payment)
          → (h1 h2 : TxHistory u)
          → (h1 `IsSubsequenceOf` h2)
          → Suspicious u p h1
          → Suspicious u p h2

С отрицанием, конечно, ситуация становится много интереснее, но я не могу понять, как можно сконстуировать тип Either a (a -> Void)? Это же закон исключённого третьего, с которым в интуиционистской логике, есть какие-то подоводные камни. И ещё: как, например, считать данные откуда-то, если, например, у value p тип не условныйInt, а непосредственно само число? То есть надо грязное значение из IO представить в виде чистого типа.

С отрицанием, конечно, ситуация становится много интереснее, но я не могу понять, как можно сконстуировать тип Either a (a -> Void)? Это же закон исключённого третьего, с которым в интуиционистской логике, есть какие-то подоводные камни.

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


aom : (a : Type)
    → (P : a → Type)
    → (x : a)
    → Either (P x) (P x → Void)

действительно не существует, потому что он эквивалентен аксиоме исключённого третьего — он умеет для любого типа a и предиката P на этом типе определять, выполняется ли P на произвольном x.


Но это не мешает существовать аналогичному терму для тех конкретных P, которые разрешимы. Например, (не)равенство натуральных чисел разрешимо (и, как следствие, разрешим Suspicious выше). А вот неравенство [вычислимых] вещественных уже неразрешимо. Лишний повод не представлять деньги вещественными числами.


И ещё: как, например, считать данные откуда-то, если, например, у value p тип не условныйInt, а непосредственно само число? То есть надо грязное значение из IO представить в виде чистого типа.

Ну как обычно, вытаскиваем из IO внутри >>= и передаём в уже чистые функции.

Fault tree analysis позволяет сконцентрироваться на действительно критичных участках и провести полный, системный анализ возможных отказов на этих участках

Можете рассказать подробнее как это у вас встроено в процесс разработки? Сколько всего человек причастны к разработке, что у них на входе, что на выходе, кто за все отвечает и как контролируется результат.

На старте, в те четыре месяца перед первым продакшеном, в команде было, если мне не изменяет память, 6 человек - архитектор и ведущий разработчик (ваш покорный слуга) - отвечал за архитектуру и помогал с разработкой, фронтендер и бэкендер писали веб-морду, ещё один программист - мобильное приложение, которое использовалось водителями, также в команде были дизайнер и менеджер проекта (он же выполнял функции скрам мастера). Где-то на месяц к нам присоединялся тестировщик.

Также, активно учавствовал в процессе представитель заказчика (в качестве Product Owner'а).

Я как архитектор был ответственнен за планирование технической части, в том числе за обеспечение отказоустойчивости. Совместно с Product Owner, мы выделили критический функционал и где-то за месяц до дедлайна я произвёл анализ по модели fault tree analysis и добавил в бэклог задачи призванные повысить отказоустойчивость системы. Мы их приоритизировали и взяли в следующий спринт.

После первого продакшена, размер команды разработки был снижен до двух человек, которые обеспечивали багфиксы, доработку и поддержку внедрения. После успешного внедрения, заказчик оценил бизнес-KPI (которые мы предварительно разработали), запланировал вторую итерацию, и т.д. Впоследствии, команда разработки редко когда переваливала за 2-3 разработчиков, project manager был на проекте part-time и помогал только на Scrum-церемониях. Это довольно типичная ситуация для консалтинг-компаний.

Я пропустил вторую итерацию (был задействован на другом проекте), но учавствовал в третьей, в процессе которой я предложил дальнейшие улучшения по отказоустойчивости, в том числе написание той самой мониторинг-утилиты, которая позволила разобраться в некоторых накопившихся к тому времени проблемах с производительностью. Кроме того, был произведен повторный анализ критического функционала системы по методу fault tree analysis, и, как результат, мы добавили оффлайн режим и ещё кое-какие улучшения.

Если говорить не про консалтинг, а про продуктовую разработку, обеспечение отказоустойчивости в команде - это обычно задача техлида, а практики и рекомендации по улучшению отказоустойчивости разрабатываются архитекторами и/или командой платформы и внедряются через Global Technical Roadmap.

Эти все подходы это конечно замечательно, но все равно непонятно как

же избавились от юнит тестов. Допустим в расчете стоимости у вас плавающая ошибка в пару тысяч рублей или в расчете расстояния перевозки 10-20 километров. Юнит тест обычно ловит это элементарно и главное автоматически проверяет каждый новый коммит. Здесь же только если повезет и при ручном тестировании кто-то попадется въедливый,

и главное такой человек должен появляться регулярно, но у вас даже QA нет.

А не один из описанных методов не защищает от опечаток в арифметических выражениях.

Насколько я понял, проект писался с нуля 4 месяца вчетвером (фронтенд, бекенд, мобайл и тимлид). Каждый знает свою область, в юнит тестирование не умеют, к моменту релиза все требования ещё умещаются в одной голове. Есть один замотивированный на результат клиент. Можно спокойно обойтись без автотестов и регрессии, делая проверки вручную. Раньше веб студии только так и работали, и все были довольны.

Не понял совершенно, как без тестов можно гарантировать правильность поведения сложной логики, например?

Пользователь не был на сайте неделю, и у него рейтинг больше, чем 3 - тогда шлем ему красивый пуш. Но если при этом пользователь не пользовался комментариями или напокупал за последний месяц на 10тр - то шлем другой и некрасивый пуш.

Как вы подобное будете без тестов проверять? В той же ЕРП подобной логики - вагон и маленькая тележка. А если рефакторинг прошел - опять все перепроверять? Базу руками подсовывать и в постмане ручки дергать?

А если код на стейдже даже запустить проблематично (чтобы 1 в 1 было с реальностью)? Ну например те же пуши, которые ходят по расписанию разными списками для разных типов пользователей. Да тут без тестов просто сединой покроешься... А если у вас 31го декабря должен отправляться особый праздничный пуш.. то вы будете время на компьютере двигать туда-сюда при проверке?) Ну 1 раз подвигаете, потом забъете и 12го января в БД будут выявлены артефакты в данных..

Логика со сложными условиями - имхо первый кандидат на покрытие тестами, ничего другого тут ну просто не придумано.

А если например в проекте несколько языков программирования и куча народа пилит код? Я вот например понятия не имею, чтот там в PHP произойдет, если вместо пустой строки в джейсоне прилетит явный null (условно). Я в таких случаях просто пишу тест и не думаю про это...

А если вы захотите версию языка или библиотек обновить? И что делать, если вообще ничего тестами не покрыто? Полнейший регресс делать?

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

Например, вы смотрели Modelling Time Эрика Эванса? Это же просто удивительный подход "outside of the box", на мой взгляд. И вместе с тем, очень разумный.

Кстати, если бы меня спросили, "назови самую сложную проблему для программиста", я бы без раздумий назвал временные зоны. Любая запутанная бизнес-логика отдыхает по сравнению с взрывом мозга, который происходит, когда работаешь с таймзонами. Даже в тестах легко запутаться с ними.

Кстати, все почему-то забывают, что есть разряд ошибок, которые очень легко совершить. Все же знают про оптические иллюзии (например, вот эти). Неважно, сколько раз смотреть на оптическую иллюзию, всё равно будет казаться неправильно. Человеческий мозг так устроен. Мы не роботы.

Если вы сделали ошибку в программе, вы можете сделать такую же ошибку на code review, и вы можете сделать такую же ошибку в тестах. От этого никто не застрахован, и на моём опыте я такое видел неоднократно. Если код запутанный, то и правильный тест для него - очень сложно написать.

А сколько раз было, что программист уронил тест, а потом подумал, ну да, у меня-то всё правильно, это наверное тест неправильный был, и поправил тест, а не свой код.

Или другой классический случай. Функция с двумя строковыми параметрами. Код полностью покрыт тестами, тесты проходят, всё идеально. Но при вызове функции из реального кода, параметры перепутали местами. У нас был такой баг в продакшене, причём оно не падало, а вело к неверному поведению в некоторых случаях, и отловить эту проблему было просто невероятно сложно. Хотя статическая типизация + линтер с правилом на недопущение параметров одинаковых примитивных типов, решают весь этот класс проблем со стопроцентной вероятностью.

Что я пытаюсь сказать: тесты, это всего лишь один инструмент из множества. Не идеальный. Я не против тестов (наоборот, двумя руками за, и вот прямо сейчас в моей текущей компании я руковожу процессом улучшения покрытия), но мне кажется, что нужно знать и другие приёмы.

Добрый день! Это прекрасно! Именно РОВНО ТАК ЖЕ и работаю со всеми проектами уже лет пять. От юнит-тестов совсем не отказался, оставил только для тестирования абсолютно самодостаточных кусков что работают с датами и с числами, потому что эта часть точно не меняется раз в неделю. Остальное все, из за того что у бизнеса хотелки всё время разные с прыжками в ширину, покрывать тестами бесполезно. Стек немножко другой - Котлин на бэке, но на самом деле роли не играет. Запросы по возможности все строить с KProperty, количество стринговых запросов и тд свести к минимуму, или убрать совсем. и всё... замечательно развивается и поддерживается. Спасибо за статью - просто вся моя боль и непонимание переусложнения проектов структурированы и разложены по полочкам.

Спасибо за отзыв!

Котлин на бэке, но на самом деле роли не играет

Я думаю, любой язык с хорошей типизацией подойдёт. Стартап у меня на Typescript, использую ровно такие же приёмы как в статье, всё прекрасно работает.

Более того, в плане интеллисенса, реализовал полное покрытие - фронтенд (+JSX для темплейтов), бэкенд, ORM (MongoDB запросы в TS полностью покрываются интеллисенсом из коробки), и даже коммуникацию между фронтендом и бэкендом, т.е. нет magic strings в виде "/api/users", и даже можно нажать F12 из фронтенда и прыгнуть в бэкенд. Это очень круто кстати, правда требует, чтобы и бэкенд и фронтенд были написаны на одном языке (ну или нужен какой-то плагин к IDE, что уже сложнее, т.к. разные люди используют разные IDE).

Хех, у нас в Big Tech fault tree analysis это просто часть ежедневной работы каждого программиста. Начинается на уровне дизайна системы, а потом постоянно на уровне кода, в каждом коммите. Если нормально все точки отказа не проработаешь, код просто не пройдет code review. И это не отменяет юнит тестов, наоборот - большинство нестандартных ошибок в тестовом кластере могут не встретится никогда ("пожар в дата центре"), и хоть как то убедится что код делает то что надо можно с помощью тестов (ну может за исключением совсем тривиальных случаев).

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

Если нормально все точки отказа не проработаешь, код просто не пройдет code review.

Как этого достигаете? Есть чеклист? Можете его показать? Действительно интересно.

Обычно в каждом подразделении / продукте своя специфика, свой док по код стайлу и код ревью, где в том или ином виде могут быть чеклисты для ошибок/отказов, best practices для retry, какие-то специфичные для домена вещи итп. Строгого регламента обычно нет, скорее это все сайд-эффект высокой инженерной культуры, никого не надо пинать, народ сам инициативу проявляет по улучшению процессов.

В статье подробно расписаны технические методы работы с кодом. Это и правда полезно, спасибо. Но, к сожалению, не совсем раскрыта "бизнесовая" сторона вопроса. Если бизнес-требования меняются быстро и в разных местах, то высока вероятность того, что ожидаемое поведение системы для новой фичи может начать конфликтовать с ожидаемым поведением для какой-то из старых. При наличии правильных (business-faced) автотестов эту ситуацию можно отлавливать на раннем этапе. А как вы справлялись без них? Было бы интересно узнать.

Несмотря на кучу pivot-ов и глобальных рефакторингов, очень стабильная система, и я в состоянии поддерживать и развивать её в одиночку

В одиночку поддерживать консистентность кода как раз легче, чем в команде. Чем больше команда, тем выше риски расхождения в понимании тех или иных аспектов работы системы. Я в своих личных проектах тоже помню всевозможные нюансы, которые важны для корректной работы, но не обязательно явным образом выражены в коде или доках. Это знание позволяет принимать правильные решения. Но если в этот код придёт кто-то другой, очень сомневаюсь, что в его голове появится такое же понимание. Чем больше людей в команде, тем больше этих "чёрных ящиков" - голов, в которых принимаются решения, порой по неясным для других голов принципам. Быстрый и автоматизированный фидбек - это действенный способ отделить неправильные решения от правильных и скорректировать поведение "чёрных ящиков". Настроенный intellisense и "статический анализ" (хотя по описанию это больше похоже на architectural fitness functions, имхо) - это дело полезное. Но лично я бы не рискнул полагаться только на них в команде из нескольких человек. Особенно распределённой или меняющейся.

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

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

Вот например, Malte Ubl из Google, пишет, что основной задачей программистов уровня выше Senior, является проектирование codebase таким образом, чтобы в этом codebase было крайне сложно допустить ошибку и чтобы качество кода сохранялось естественным образом.

В статье, я именно про это пишу в пункте "Maintainability Index". Это отличная метрика, которая позволяет не допустить (или, как минимум, замедлить) натуральный процесс деградации качества кода "over time". Конечно, это желательно совмещать с правильно поставленным code review (но это отдельная большая тема).

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

"я в состоянии поддерживать и развивать её в одиночку" вот с этого и надо было начинать, а не расписывать почему тесты не нужны. Мы тут все такие кто свой проект может вести свой проект без тестов, потому что не надо, спека и код в принципе в одной голове и простыми мануальными тестами можно легко обойтись. Если есть достаточно времени чтоб ещё и писать правильно, модульно и просто то оно сработает.

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

"в одиночку" - это про совершенно другой проект.

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

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

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

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.