惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
Microsoft Azure Blog
Microsoft Azure Blog
Cloudbric
Cloudbric
I
InfoQ
V
V2EX
博客园_首页
The Register - Security
The Register - Security
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
S
Secure Thoughts
Vercel News
Vercel News
Forbes - Security
Forbes - Security
云风的 BLOG
云风的 BLOG
PCI Perspectives
PCI Perspectives
L
LINUX DO - 最新话题
D
DataBreaches.Net
H
Hacker News: Front Page
Application and Cybersecurity Blog
Application and Cybersecurity Blog
B
Blog RSS Feed
A
About on SuperTechFans
N
News and Events Feed by Topic
Apple Machine Learning Research
Apple Machine Learning Research
Help Net Security
Help Net Security
Attack and Defense Labs
Attack and Defense Labs
N
Netflix TechBlog - Medium
Spread Privacy
Spread Privacy
F
Full Disclosure
Recorded Future
Recorded Future
AWS News Blog
AWS News Blog
博客园 - 【当耐特】
The Cloudflare Blog
T
Threatpost
T
Tor Project blog
Google DeepMind News
Google DeepMind News
C
CXSECURITY Database RSS Feed - CXSecurity.com
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
Recent Announcements
Recent Announcements
M
MIT News - Artificial intelligence
A
Arctic Wolf
C
Check Point Blog
Stack Overflow Blog
Stack Overflow Blog
T
Threat Research - Cisco Blogs
Security Archives - TechRepublic
Security Archives - TechRepublic
Hacker News - Newest:
Hacker News - Newest: "LLM"
WordPress大学
WordPress大学
Cyberwarzone
Cyberwarzone
小众软件
小众软件
C
Cyber Attacks, Cyber Crime and Cyber Security
P
Proofpoint News Feed
Security Latest
Security Latest
The Last Watchdog
The Last Watchdog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Моки без боли
goshacodes · 2026-05-03 · via Все публикации подряд на Хабре

Моки — достаточно крутой инструмент, если использовать его правильно.

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

Задачку сделал. Остались только тесты (c)

Задачку сделал. Остались только тесты (c)

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


Предыстория

В 2023 году, когда Scala 3 уже существовала, но ещё почти никем не использовалась - я заинтересовался метапрограммированием. Туториалов не было, поддержки IDE почти не существовало — но именно это и было интересно. Я решил взять одну из ещё не переведённых на Scala 3 библиотек и портировать её. Выбор пал на scalamock — как раз то, что использовалось у нас на проекте.

Примерно тогда команда начала миграцию с Future на ZIO, а в дальнейшем планировала переход на Scala 3. Сразу выяснилось: scalamock с ZIO работает плохо. Попробовали zio-mock — интересная идея, но @mockable не была портирована на Scala 3, и использование превращалось в мучение. Перевод scalamock на Scala 3 в какой-то момент был завершён, но второй проблемы это не решало. Подружить классический scalamock с функциональными системами эффектов казалось задачей нерешаемой.

В итоге я начал писать стабы руками. Нужны были только две вещи: задать результат и потом проверить аргументы. Работало — но надоело быстро. И я точно знал, что я не один такой. Поэтому начал создавать решение, используя опыт, полученный при миграции scalamock на Scala 3. Так получился проект backstub — позднее переросший в scalamockStubs. А я стал мейнтейнером scalamock.


Что не так с классическим подходом

В scalamock есть два "разных" варианта мокирования - stub[A], и mock[A].

Оба подхода позволяют делать следующее:
1. Устанавливать возвращаемый методом результат
2. Устанавливать ожидания на аргументы, с которыми метод был вызван
3. Устанавливать ожидания на порядок вызовов разных методов.

Но у обоих есть общая концептуальная проблема - установка результата и установка ожиданий на аргументы смешаны в кучу. Хотя установка результата может спокойно существовать без установки ожиданий на аргументы. Даже больше - установку ожиданий на аргументы следует использовать далеко не всегда.

Для mock это выглядит так:

myTrait.twoArgs.expects(1, "hello").returns("world")

otherTrait.oneArg.expects("foo").returns(1)

Для stub — почти то же самое, только через when вместо expects и отдельное использование verify.

Здесь - уже лучше, установка результата отделена от проверки ожиданий. Но зачем здесь .when(*, *)?

myTrait.twoArgs.when(*, *).returns("world")
//...
myTrait.twoArgs.verify(1, "hello").once()

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

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


Идея нового API: разделить ответственность

Новый API строится вокруг одного принципа: установка результата, проверка аргументов и проверка порядка — три отдельные, независимые операции.

Задать результат      →  .returnsWith / .returns
Проверить аргументы   →  .calls / .times          (опционально)
Проверить порядок     →  .isBefore / .isAfter       (опционально)

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


Пример

trait ProductRepository:
  def findById(id: ProductId): Option[Product]

trait NotificationService:
  def notify(email: Email, order: Order): Unit

class OrderService(
  products: ProductRepository, 
  notifications: NotificationService
):
  def placeOrder(
    productId: ProductId,
    quantity: Int, 
    email: Email
  ): Either[OrderError, Order] =
    products.findById(productId) match
      case None          => Left(OrderError.ProductNotFound)
      case Some(product) =>
        if product.stock < quantity then Left(OrderError.InsufficientStock)
        else
          val order = Order(productId, quantity, email)
          notifications.notify(email, order)
          Right(order)

Паттерн Env и задание результата

Стабы всегда создаются внутри класса-окруженияEnv / Wiring / etc. Это стандартный паттерн при использовании моков в Scala — каждый тест-кейс получает собственный, изолированный экземпляр всех зависимостей:

//> using dep org.scalamock::scalamock::7.5.5

import org.scalamock.stubs.Stubs
import munit.FunSuite

class OrderServiceSpec extends FunSuite, Stubs:

  class Env:
    val products      = stub[ProductRepository]
    val notifications = stub[NotificationService]
    val service       = OrderService(products, notifications)

returnsWith — результат без оглядки на аргументы

Самый частый случай. Метод всегда возвращает одно и то же:

  test("товар не найден"):
    val env = Env()

    env.products.findById.returnsWith(None)

    val result = env.service.placeOrder(
      productId = ProductId("123"), 
      quantity = 1, 
      email = Email("user@example.com")
    )

    assertEquals(result, Left(OrderError.ProductNotFound)
)

Причемnotify здесь не настроен.

Если placeOrder попробует его вызвать — получит NotImplementedError с понятным описанием метода в стектрейсе.

returns — результат зависит от аргументов

Когда нужно задать разное поведение для разных аргументов, используем returns с pattern matching:

  test("недостаточно товара на складе"):
    val env = Env()
    val pid = ProductId("123")

    env.products.findById.returns:
      case `pid` => Some(Product(pid, stock = 5))
      case _     => None

    val result = env.service.placeOrder(
      productId = ProductId("123"), 
      quantity = 10, 
      email = Email("user@example.com")
    )

    assertEquals(result, Left(OrderError.InsufficientStock))

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


Проверка аргументов и количества вызовов

Когда важно убедиться, что метод был вызван — и с правильными данными — используем calls и times:

  test("отправляет уведомление после успешного заказа"):
    val product = Product(ProductId("123"), stock = 10)
    val email   = Email("user@example.com")
    val env     = Env()

    env.products.findById.returnsWith(Some(product))
    env.notifications.notify.returnsWith(())

    env.service.placeOrder(
      productId = ProductId("123"), 
      quantity = 1,
      email = email
    )

    assertEquals(env.notifications.notify.times, 1)
    assertEquals(
      env.notifications.notify.calls,
      List(
        (
          email,
          Order(productId, quantity, email)
        )
      )
    )

calls возвращает список аргументов всех вызовов:

  • List[Unit] — если аргументов нет

  • List[A] — если один аргумент типа A

  • List[(A, B, ...)] — если несколько (tuple)

Ключевое отличие от классического подхода: задание результата и проверка аргументов — происходят отдельно. Можно задать результат и никогда не проверять аргументы. Можно проверить аргументы, не указывая их при настройке результата. Изменение сигнатуры метода затронет только те тесты, которые действительно проверяют аргументы.


Проверка порядка вызовов

Самая редкая потребность — и она вынесена в отдельный явный механизм. Нужен CallLog:

  test("сначала ищем клиента, только потом сохраняем"):
    given CallLog = CallLog()

    val knownClient = ClientRecord("client-123", allowedScopes = Set("read"))
    val env = Env()

    env.repo.findByClientId.returnsWith(Some(knownClient))
    env.repo.save.returnsWith(())

    env.issuer.issue("client-123", Set("read"))
    // в данном случае это бессмысленная проверка, порядок следует неявно
    // без product нет order
    assert(env.repo.findByClientId.isBefore(env.repo.save))

CallLog создаётся вручную только там, где проверка порядка нужна. Тесты, которым это не важно, не знают о его существовании.


Как это работает под капотом

Вот черновик раздела, посмотри:


Как это работает под капотом

Хорошо, вот обновлённая версия абзаца:


Как это работает под капотом

scalamock использует два механизма Scala — макросы и неявные преобразования.

Когда ты пишешь stub[ProductRepository], макрос генерирует реализацию трейта. Для каждого метода создаётся StubbedMethod[Args, Result] с кэшом — она хранит настроенный результат и записывает все входящие вызовы с аргументами. Когда метод вызывается, он обращается к StubbedMethod: берёт результат или бросает NotImplementedError, если результат не настроен.

Когда ты пишешь env.products.findById.returnsWith(...) — здесь происходит ETA расширение: findById автоматически преобразуется из метода в функцию ProductId => Option[Product], а затем неявное преобразование ищет соответствующий методуStubbedMethod[ProductId, Option[Product]].

Можно вызвать это преобразование явно используя методstubbed:

val findById: StubbedMethod[ProductId, Option[Product]] =
  stubbed(env.products.findById)

findById.returnsWith(None)

// или указав тип явно
val findById: StubbedMethod[ProductId, Option[Product]] =
  env.products.findById

Это может быть полезно, если хочешь переиспользовать ссылку на StubbedMethod в нескольких местах теста.


Параметризованные тест-кейсы

Это место, где новый API раскрывается по-настоящему. Поведение задаётся один раз в Env, тест-кейсы только проверяют результат:

class OrderServiceSpec extends FunSuite, Stubs:

  val product = Product(ProductId("123"), stock = 10)
  val email   = Email("user@example.com")

  class Env:
    val products      = stub[ProductRepository]
    val notifications = stub[NotificationService]
    val service       = OrderService(products, notifications)

    products.findById.returns:
      case ProductId("123") => Some(product)
      case _                => None

    notifications.notify.returnsWith(())

  case class Verify(notifyCalledTimes: Int = 0)

  def testCase(
    description: String,
    productId: ProductId,
    quantity: Int,
    expected: Either[OrderError, Order],
    verify: Verify = Verify()
  ): Unit =
    test(description):
      val env    = Env()
      val result = env.service.placeOrder(productId, quantity, email)

      assertEquals(result, expected)
      assertEquals(env.notifications.notify.times, verify.notifyCalledTimes)

  testCase(
    description = "товар не найден",
    productId = ProductId("999"), 
    quantity = 1,
    expected = Left(OrderError.ProductNotFound)
  )

  testCase(
    description = "недостаточно товара",
    productId = ProductId("123"),
    quantity = 99, 
    expected = Left(OrderError.InsufficientStock)
  )

  testCase(
    description = "успешное оформление заказа",
    productId = ProductId("123"),
    quantity = 1, 
    expected = Right(Order(ProductId("123"), 1, email)),
    verify = Verify(notifyCalledTimes = 1)
  )

Интеграция с функциональными системами эффектов

ZIO

//> using dep org.scalamock::scalamock-zio::7.5.5

import org.scalamock.stubs.ZIOStubs
import zio.test.*

trait TokenRepository:
  def findByClientId(clientId: String): IO[RepositoryError, Option[ClientRecord]]
  def save(token: IssuedToken): IO[RepositoryError, Unit]

class TokenIssuerSpec extends ZIOSpecDefault, ZIOStubs:

  class Env:
    val repo   = stub[TokenRepository]
    val issuer = TokenIssuer(repo)

  override def spec = suite("TokenIssuer")(
    test("неизвестный клиент"):
      val env = Env()
      for
        _      <- env.repo.findByClientId.succeedsWith(None)
        result <- env.issuer.issue("unknown", Set("read"))
      yield assertTrue(result == Left(AuthError.UnknownClient))
    ,
    test("успешная выдача"):
      val env = Env()
      for
        _      <- env.repo.findByClientId.succeedsWith(Some(knownClient))
        _      <- env.repo.save.succeedsWith(())
        result <- env.issuer.issue("client-123", Set("read"))
        times  <- env.repo.save.timesZIO
      yield assertTrue(result.isRight, times == 1)
  )

succeedsWith / failsWith / diesWith возвращают ZIO и удобно встраиваются в for-comprehension. Если нужно поведение от аргументов — returnsZIO:

env.repo.findByClientId.returnsZIO:
  case "client-123" => ZIO.succeed(Some(knownClient))
  case _            => ZIO.succeed(None)

cats-effect

//> using dep org.scalamock::scalamock-cats-effect::7.5.5

import org.scalamock.stubs.CatsEffectStubs
import munit.CatsEffectSuite

class TokenIssuerSpec extends CatsEffectSuite, CatsEffectStubs:

  class Env:
    val repo   = stub[TokenRepository]
    val issuer = TokenIssuer(repo)

  test("успешная выдача"):
    val env = Env()
    for
      _      <- env.repo.findByClientId.succeedsWith(Some(knownClient))
      _      <- env.repo.save.succeedsWith(())
      result <- env.issuer.issue("client-123", Set("read"))
      times  <- env.repo.save.timesIO
    yield assertEquals(times, 1)

Данный проект родился из моей боли связанной с мок-тестированием. Надеюсь и вам это сэкономит время и нервы.
И я буду рад обратной связи.

Документация: scalamock.org/stubs

Вопросы, идеи, баги