И вот тут как раз обычно и начинаются проблемы, потому что границы модуля тестирования определяются на основе деталей реализации. И как правило, в таких случаях тесты становятся хрупкими и с ними больно работать.
Я не очень люблю обсуждать абстрактно, как оно бывает. Ваш пример, хотя бы и минимальный, на самом деле хорошо иллюстрирует реальные сценарии. Поэтому буду отталкиваться от него.
То, что DiscountService использует как внешнюю зависимость PromoService - это деталь реализации. С точки зрения поведения и API класса DiscountService предоставляет метод для вычисления стоимости определенного продукта для определенного пользователя с определенным промокодом. У сервиса есть естественные границы - он откуда-то должен получать данные пользователя и продукта, и скорее всего будет внешняя зависимость для проверки промокодов. С этим ничего не сделаешь. А вот то, что DiscountService нуждается в коллабораторе для вычисления скидки - это уже деталь реализации. Бизнес-правила вообще могут быть такими, что с определенным промокодом все будет стоить 20 или в бесплатно, и тогда отдельный класс для вычисления скидки может оказаться излишним - а может, и не излишним. Поэтому наиболее естественный и простой для понимания тест - это вычисление стоимости продукта у которого есть промокод на 20% с реальной реализацией DiscountService и PromoService. А если мы выделили PromoService в отдельный класс и хотим более гранулярные тесты, чтобы знать из-за чего пофейлилась - то пишем отдельный тест для PromoService.calculateDiscount().
Такой подход хорошо масштабируется для сколь угодно сложных систем.
Не, интеграционный тест - это если у вас еще репозитории работают с БД (реальной или хотя бы in-memory) и входная точка это как минимум API endpoint или обработчик сообщения очереди сообщений. То, что я описал - это как раз юнит-тест в классическом подходе (так называемая Детройтская школа). То, что описываете вы - это юнит-тест Лондонской школы.
Какого будет искать потом по всей бизнес логики, а что именно пошло не так? Достаточно просто - есть изолированное поведение - подсчет скидки по промокоду. Если тест сломался - значит, именно здесь ошибка. Если для анализа изолированного поведения вам нужно перелопатить 20 файлов - значит, у вас архитектурная проблема: скорее всего нужно будет посмотреть две функции по 5 строк кода, но эти две функции будут полностью описывать необходимое поведение, необходимое для понимание. Плюс вы теперь знаете, что ваша система правильно считает скидку по промокоду, а не только правильно вычитает из тысячи двести. Вы можете, глядя на тест, обсудить поведение с аналитиком/продакт-менеджером.
И это все гораздо проще, чем менять десятки моков по всей кодовой базе при простом рефакторинге или разбираться, сломанные тесты - это ошибка в логике или у вас просто еще одна зависимость появилась при рефакторинге.
Моки - это просто симптом, а не проблема. Как мне видится, основная проблема - это плохое определение границы модулей, которые мы тестируем в модульном тестировании. Почему-то кто-то решил, а все остальные поверили, что тестовый и продуктовый код должны соответствовать друг другу и что каждый класс нужно тестировать в изоляции. Тестировать класс в изоляции не имеет смысла - имеет смысл тестировать поведение в изоляции. В примере DiscountService и PromoService описывают одну логику/поведение - как считать скидку, и поведение которое мы тестируем - это скидка при наличии промокода (а не то, что дискаунт сервер умеет из 1000 вычитать 200). Поэтому в хорошем тесте должны использоваться настоящие реализации DiscountService и PromoService. А то, что находится за границей бизнес-логики (репозитории, например) - то можно мокать - стабами, моками или фейками - не важно.
И теперь если вдруг кому-то захочется объединить DiscountService и PromoService или вытащить из DiscountService какой-то еще класс, не изменяя поведение - то тесты останутся рабочими (золотое правило: меняется поведение - меняются тесты, поведение неизменное - тесты не меняются).
LLM-ка - это иногда не просто чат-бот, который генерирует ответы на вопросы, ответы на которые вы знаете. Иногда LLM-ка в продакшне может решать, подходит ли ваше резюме на такую-то должность, есть ли подозрительные записи в логах. Системный промпт/контекст LLM может содержать чувствительную информацию и инструкции ни в коем случае не раскрывать её. Смысл подхода - что злоумышленник разрабатывает атаку и тестирует её в безопасной среде, где его не забанят, а применяет в окружении, где для него есть что-то ценное.
Документировать релизный процесс, чтобы не только тим-лид мог релизить и не приходилось импровизировать
master и release ветки сделать protected, force-push в любую ветку, которой пользуются больше одного человека - зло. Если нужно пересобрать релиз-ветку заново, то делать новую релиз-ветку или ревертить коммиты.
Разрешение экрана не является юридически значимой информацией, но вместе с другими параметрами позволяет сконструировать fingerprint, который можно использовать для корреляции действий с разных ip адресов.
У клиента на другой платформе при каждом изменении файла и шифровании будет использоваться одно и то же IV, и этого достаточно, чтобы ослабить стойкость шифрования: подробности можете посмотреть в первой ссылке моего изначального комментария, но как бы то, что злоумышленник может увидеть в каком блоке было изменение - это уже нехорошо.
По поводу AES CBC - главный недостаток, это то, что нет аутентификации, то есть злоумышленник может пытаться подменить шифротекст, поменяв какие-то биты, и смотреть, что получится. А если говорить про CBC, то может попытаться сделать padding oracle attack - а вы, наверняка, не тестировали, что будет, если в вашей реализации при расшифровке на вход передать неправильный padding.
Если выбирать из семейства AES, то почти всегда единственным безопасным алгоритмом будет AES GCM (если нет очень веских причин выбрать другой алгоритм) - но вы почему-то его вообще не рассматриваете. Если не из семейства AES, то с xchacha20-poly1305 сложнее накосячить. Но вообще правильное использование низкоуровневых криптографических библиотек - дело достаточное хитрое, и, чтобы не ошибиться ненароком, есть более высокоуровневые библиотеки типа NaCl/libsodium, используя которые можно избежать многих подводных камней.
По моему опыту (не Канада, Европа), не найти работу за два месяца - нормально. С отсутствием опыта работы в новой стране проживания я бы предположил, что найти работу за полгода - хорошо. Если по законодательству от компании требуется бюрократия или денежные вложения для оформления сотруднику визы/внж, то полгода - это даже очень хорошо.
По резюме, я не специалист в рекрутинге, но если бы я был hiring manager, то насторожился бы пересечению фриланса и работе по найму. И мне кажется, что сотруднику с опытом 7+ лет указывать образование Udemy - это подозрительно. Возможно Минского университета было бы достаточно (но опять же я не специалист в рекрутинге).
- Название модуля в go.mod: обычно оно не src, а github.com/blahblah/blahblahblah. Это имеет значение, если кто-то будет импортировать ваш код. И совсем не обязательно помещать весь код в src.
- Вы начали использовать пакет internal для кода, который не должен переиспользоваться вне проекта, но при этом по какой-то загадочной причине контроллер и роутер не попадают в internal. По-хорошему, практически весь код, кроме main, может (и должен) быть в internal.
- Я очень понимаю желание использовать DI-контейнеры после многолетнего опыта работы с .NET, но по сути для микросервисов (если они, конечно, микро) большой пользы они не приносят - достаточно просто построить дерево зависимостей вручную.
- Если уж использовать ООП, то зависимости контроллеров, роутеров итп должны быть от интерфейсов, а не от реализаций. Ну, и тестов нет :-( Зачем городить многослойность контроллер-сервис-репозиторий, если нет ни бизнес-логики, ни доменной модели, ни тестов? А если и делать многослойную архитектуру (с расчетом, что когда-то микросервис станет гигантским монолитом), то почему вдруг сервис зависит от моделей API?
- Зачем экспортировать все поля в структурах таких, как Router, ControllerRoute, Server?
- В методе CreateBook контроллера отсутствует обработка ошибок - ошибки попросту игнорируются, что нехорошо. То же в методе Run сервера, который по-хорошему должен был бы возвращать ошибку.
- В методе getEnvAsInt: код на Go читается гораздо удобнее, если сверху вниз - happy path, магистральный сценарий, а все if обрабатывают ошибочные ситуации. Если поменять инвертировать if err == nil, чтобы стало if err != nil, то код станет каноничнее.
И вот тут как раз обычно и начинаются проблемы, потому что границы модуля тестирования определяются на основе деталей реализации. И как правило, в таких случаях тесты становятся хрупкими и с ними больно работать.
Я не очень люблю обсуждать абстрактно, как оно бывает. Ваш пример, хотя бы и минимальный, на самом деле хорошо иллюстрирует реальные сценарии. Поэтому буду отталкиваться от него.
То, что DiscountService использует как внешнюю зависимость PromoService - это деталь реализации. С точки зрения поведения и API класса DiscountService предоставляет метод для вычисления стоимости определенного продукта для определенного пользователя с определенным промокодом. У сервиса есть естественные границы - он откуда-то должен получать данные пользователя и продукта, и скорее всего будет внешняя зависимость для проверки промокодов. С этим ничего не сделаешь. А вот то, что DiscountService нуждается в коллабораторе для вычисления скидки - это уже деталь реализации. Бизнес-правила вообще могут быть такими, что с определенным промокодом все будет стоить 20 или в бесплатно, и тогда отдельный класс для вычисления скидки может оказаться излишним - а может, и не излишним. Поэтому наиболее естественный и простой для понимания тест - это вычисление стоимости продукта у которого есть промокод на 20% с реальной реализацией DiscountService и PromoService. А если мы выделили PromoService в отдельный класс и хотим более гранулярные тесты, чтобы знать из-за чего пофейлилась - то пишем отдельный тест для PromoService.calculateDiscount().
Такой подход хорошо масштабируется для сколь угодно сложных систем.
Не, интеграционный тест - это если у вас еще репозитории работают с БД (реальной или хотя бы in-memory) и входная точка это как минимум API endpoint или обработчик сообщения очереди сообщений. То, что я описал - это как раз юнит-тест в классическом подходе (так называемая Детройтская школа). То, что описываете вы - это юнит-тест Лондонской школы.
Какого будет искать потом по всей бизнес логики, а что именно пошло не так? Достаточно просто - есть изолированное поведение - подсчет скидки по промокоду. Если тест сломался - значит, именно здесь ошибка. Если для анализа изолированного поведения вам нужно перелопатить 20 файлов - значит, у вас архитектурная проблема: скорее всего нужно будет посмотреть две функции по 5 строк кода, но эти две функции будут полностью описывать необходимое поведение, необходимое для понимание. Плюс вы теперь знаете, что ваша система правильно считает скидку по промокоду, а не только правильно вычитает из тысячи двести. Вы можете, глядя на тест, обсудить поведение с аналитиком/продакт-менеджером.
И это все гораздо проще, чем менять десятки моков по всей кодовой базе при простом рефакторинге или разбираться, сломанные тесты - это ошибка в логике или у вас просто еще одна зависимость появилась при рефакторинге.
Моки - это просто симптом, а не проблема. Как мне видится, основная проблема - это плохое определение границы модулей, которые мы тестируем в модульном тестировании. Почему-то кто-то решил, а все остальные поверили, что тестовый и продуктовый код должны соответствовать друг другу и что каждый класс нужно тестировать в изоляции. Тестировать класс в изоляции не имеет смысла - имеет смысл тестировать поведение в изоляции. В примере DiscountService и PromoService описывают одну логику/поведение - как считать скидку, и поведение которое мы тестируем - это скидка при наличии промокода (а не то, что дискаунт сервер умеет из 1000 вычитать 200). Поэтому в хорошем тесте должны использоваться настоящие реализации DiscountService и PromoService. А то, что находится за границей бизнес-логики (репозитории, например) - то можно мокать - стабами, моками или фейками - не важно.
И теперь если вдруг кому-то захочется объединить DiscountService и PromoService или вытащить из DiscountService какой-то еще класс, не изменяя поведение - то тесты останутся рабочими (золотое правило: меняется поведение - меняются тесты, поведение неизменное - тесты не меняются).
LLM-ка - это иногда не просто чат-бот, который генерирует ответы на вопросы, ответы на которые вы знаете. Иногда LLM-ка в продакшне может решать, подходит ли ваше резюме на такую-то должность, есть ли подозрительные записи в логах. Системный промпт/контекст LLM может содержать чувствительную информацию и инструкции ни в коем случае не раскрывать её. Смысл подхода - что злоумышленник разрабатывает атаку и тестирует её в безопасной среде, где его не забанят, а применяет в окружении, где для него есть что-то ценное.
Документировать релизный процесс, чтобы не только тим-лид мог релизить и не приходилось импровизировать
master и release ветки сделать protected, force-push в любую ветку, которой пользуются больше одного человека - зло. Если нужно пересобрать релиз-ветку заново, то делать новую релиз-ветку или ревертить коммиты.
Полюбить фича-флаги
Разрешение экрана не является юридически значимой информацией, но вместе с другими параметрами позволяет сконструировать fingerprint, который можно использовать для корреляции действий с разных ip адресов.
У клиента на другой платформе при каждом изменении файла и шифровании будет использоваться одно и то же IV, и этого достаточно, чтобы ослабить стойкость шифрования: подробности можете посмотреть в первой ссылке моего изначального комментария, но как бы то, что злоумышленник может увидеть в каком блоке было изменение - это уже нехорошо.
По поводу AES CBC - главный недостаток, это то, что нет аутентификации, то есть злоумышленник может пытаться подменить шифротекст, поменяв какие-то биты, и смотреть, что получится. А если говорить про CBC, то может попытаться сделать padding oracle attack - а вы, наверняка, не тестировали, что будет, если в вашей реализации при расшифровке на вход передать неправильный padding.
Если выбирать из семейства AES, то почти всегда единственным безопасным алгоритмом будет AES GCM (если нет очень веских причин выбрать другой алгоритм) - но вы почему-то его вообще не рассматриваете. Если не из семейства AES, то с xchacha20-poly1305 сложнее накосячить. Но вообще правильное использование низкоуровневых криптографических библиотек - дело достаточное хитрое, и, чтобы не ошибиться ненароком, есть более высокоуровневые библиотеки типа NaCl/libsodium, используя которые можно избежать многих подводных камней.
Простите, несколько вопросов по используемой криптографии.
1) Я так понял вы один раз генерируете соль и IV, линкуете и они переиспользуются при каждом шифровании? Если это правда, то так делать нельзя. Например, вот почему: https://derekwill.com/2021/01/01/aes-cbc-mode-chosen-plaintext-attack/
2) А почему AES CBC, собственно? Ведь в нем же нет аутентификации. https://alicegg.tech/2019/06/23/aes-cbc
По моему опыту (не Канада, Европа), не найти работу за два месяца - нормально. С отсутствием опыта работы в новой стране проживания я бы предположил, что найти работу за полгода - хорошо. Если по законодательству от компании требуется бюрократия или денежные вложения для оформления сотруднику визы/внж, то полгода - это даже очень хорошо.
По резюме, я не специалист в рекрутинге, но если бы я был hiring manager, то насторожился бы пересечению фриланса и работе по найму. И мне кажется, что сотруднику с опытом 7+ лет указывать образование Udemy - это подозрительно. Возможно Минского университета было бы достаточно (но опять же я не специалист в рекрутинге).
Я вот тоже когда-то переходил из .NET в Go.
Несколько случайных замечаний:
- Название модуля в go.mod: обычно оно не src, а github.com/blahblah/blahblahblah. Это имеет значение, если кто-то будет импортировать ваш код. И совсем не обязательно помещать весь код в src.
- Вы начали использовать пакет internal для кода, который не должен переиспользоваться вне проекта, но при этом по какой-то загадочной причине контроллер и роутер не попадают в internal. По-хорошему, практически весь код, кроме main, может (и должен) быть в internal.
- Я очень понимаю желание использовать DI-контейнеры после многолетнего опыта работы с .NET, но по сути для микросервисов (если они, конечно, микро) большой пользы они не приносят - достаточно просто построить дерево зависимостей вручную.
- Если уж использовать ООП, то зависимости контроллеров, роутеров итп должны быть от интерфейсов, а не от реализаций. Ну, и тестов нет :-( Зачем городить многослойность контроллер-сервис-репозиторий, если нет ни бизнес-логики, ни доменной модели, ни тестов? А если и делать многослойную архитектуру (с расчетом, что когда-то микросервис станет гигантским монолитом), то почему вдруг сервис зависит от моделей API?
- Зачем экспортировать все поля в структурах таких, как Router, ControllerRoute, Server?
- В методе CreateBook контроллера отсутствует обработка ошибок - ошибки попросту игнорируются, что нехорошо. То же в методе Run сервера, который по-хорошему должен был бы возвращать ошибку.
- В методе getEnvAsInt: код на Go читается гораздо удобнее, если сверху вниз - happy path, магистральный сценарий, а все if обрабатывают ошибочные ситуации. Если поменять инвертировать if err == nil, чтобы стало if err != nil, то код станет каноничнее.
Удачи!