Pull to refresh
18
0
Арсений Жижелев @primetalk

Scala архитектор

Send message

С успехом применял Jsonnet для сложных конфигураций. Очень простой язык в функциональном стиле, тотальный (гарантированно возвращающий одинаковый результат при тех же входных данных). Является надмножеством json. В частности, хорошо поддерживается k8s.

  1. Негативная коннотация в словах "захламляется", "фейк" ... Видимо, тяжёлый опыт?.. Могу только посочувствовать.

  2. На практике квитанции объединяются в пакеты квитанций и не сильно обременяют код. Гарантии, которые получаются в результата - оказывают волшебное влияние на качество и снижение количества необходимых тестов.

Квитанции используются вместе с эффектами F[_]. Например, IO[LogReceipt]. Значение квитанции будет получено только в том случае, если исключений не было. Если были исключения, то они будут представлены средствами IO, и, естественно, никакой квитанции уже не будет.

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

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

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

Да. Всё верно. Обычно люди пытаются получить косвенные свидетельства с помощью тестов.
Здесь речь идёт о гарантиях, предоставляемых компилятором. Это, как мне кажется, несколько убедительнее.

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

Мне кажется, автор достаточно удачно провёл границу между тем, что включить в статью, а что оставить за рамками. Предложенные Вами примеры сами по себе неплохие, но, на мой взгляд, не очень вписываются в стиль статьи. В частности, instanceOf, перекос (bias) Either, исключения - это низкоуровневые детали реализации, несущественные с точки зрения рассматриваемых понятий.

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

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

  2. Мутационное тестирование. У меня аналогичные впечатления - применяется энтузиастами, широкого признания не получило.

  3. Использование типов. Не все типы одинаковы :) Некоторые типы особенно хороши. Например, алгебраические типы данных делают возможным "make illegal state unrepresentable". Этого очень не хватает, если приходится писать на языках без ADT.

  4. "Но корректное поведение программы и желаемое поведение - увы, это разные вещи." Я как раз исхожу из того, что корректное поведение == соответствие требованиям == желаемое поведение.

  5. Тесты - документация. Нормальный use case для тестов. Я об этом тоже пишу - документирование-api.

  6. "тесты - это еще своего рода песочница в которой можно поиграться с кодом и лучше понять что и как работает". Тоже - Юнит-тесты как упражнение.

  7. "пример с сериализацией". См.п.1. Минимизируем число тестов.

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

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

  9. Типы помогают уменьшить количество юнит-тестов. Да. Тесты, приведённые в первой части, как раз могут быть ликвидированы/упрощены за счёт использования типов. И я не предлагаю совершенно избавиться от тестов. Надо подходить к ним исходя из рациональных соображений. Если польза перевешивает, то надо писать. Если много стереотипных тестов, то, может, стоит заменить их на один универсальный? Я постарался привести несколько категорий тестов, которые полезны даже если активно использовать типы данных - Применимость юнит-тестов.

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

  1. Некоторые тесты полезны, например, те, что упомянуты в следующей части.

  2. Действительно, property-based тесты (использующие генераторы) - весьма сильная штука. Они позволяют существенно, в сотни раз, увеличить мощность тестового множества. Классический пример - наличие прямой и обратной функции (сериализация/десериализация; запись в БД/чтение). Их композиция должна давать identity, которое легко проверить - достаточно нагенерировать данных с одной стороны, потом прогнать через эту identity и убедиться, что всё ок. К сожалению, в общем случае обнаружить/придумать свойство, которое можно протестировать, не так-то просто. Более того, если такое свойство обнаружено, то, весьма вероятно, оно может быть выражено в типах явным образом.

  3. Я во всех примерах исхожу из того, что используемые библиотеки, компилятор, виртуальные машины и т.д. работают корректно. Если это не так, то мы вступаем на тонкий лёд непредсказуемого окружения и там может быть всё что угодно. В таком окружении тесты, проверяющие корректность работы/использования библиотек - обычное дело. Я лично писал такого рода тест на языке Go при использовании библиотеки google FHIR, потому что эта библиотека обладала неожиданной "фичёй" - при сериализации объект портился и некоторые свойства пропадали. При сериализации! Представить себе такое в Scala - крайне сложно.

  4. Работу с БД я также обычно проверяю тестами на реальной БД в контейнере. Это гораздо полезнее моков и позволяет обнаружить в том числе отличия между версиями СУБД. Другое дело, что мне достаточно написать один тест на одну сущность и убедиться, что весь generic-код для этой сущности работает корректно. Этого одного теста достаточно для того, чтобы быть уверенным, что все остальные сущности также будут обрабатываться этим generic-кодом корректно.

  5. Про логи и метрики. К сожалению, обычно логирование и запись метрик происходит в форме не наблюдаемых побочных эффектов. Поэтому протестировать, что логирование действительно происходит, может быть непросто. Обычно ограничиваются тем, что визуально, при запуске тестов, видны сообщения в логах. Если требуется проверить корректность логирования/измерения метриц, то, по-видимому, необходимо выносить их явным образом на уровень типов. В одном проекте, например, я использовал специальную обёртку, чтобы собирать метрики в явном виде. И в тестах проверялась корректность измерений.

  6. Про мутационное тестирование. Интересно, в реальных проектах его применяют?

  7. Типы vs тесты. Я предполагаю, что 99% читателей согласились бы с Вами и также выбрали бы проект с тестами, вместо того, чтобы разбираться со "сложными типами" :). Вся серия заметок направлена на популяризацию использования типов. Внесение изменений в корректно-типизированную систему, на мой взгляд, приводит к более предсказуемому результату. В частности, после изменения код перестаёт компилироваться. А вот после исправления всех точек, где ругается компилятор, мы можем быть почти на 100% уверены в том, что программа снова корректно работает. Тесты в принципе не могут приблизиться к такому уровню уверенности. Корректно-типизированный код изменять не "страшно". Сложно добиться того, чтобы программа потом компилировалась:) Зато если это получилось, то мы снова имеем гарантированно работающий код.

Если честно, я не сталкивался в реальных проектах с успешным применением мутационного тестирования.
Для кода, в котором полно операторов if, такое тестирование, по-видимому, можно реализовать.
Я здесь выступаю за то, чтобы минимизировать количество if'ов и по-максимуму использовать развитые типы данных. Я затрудняюсь предположить, что может "мутировать" автоматика в таком коде:

val f: [A] => A => A = [A] => (a: A) => a

?

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

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

Хм, похоже, это ошибка.
Обычно такой случай перехватывается с помощью переменной. Если бы мы были в пространстве значений, было бы так:

...
  case (x, 0) => x

В пространстве типов, похоже, тоже работает:

type AbsDiff[a, b] = (a, b) match
  case (S[a1], S[b1]) =>
    AbsDiff[a1, b1]
  case (a, Zero) =>
    a
  case (Zero, b) =>
    b
  case _ => Nothing

Предложение с var dx := AbsDiff[ax, bx];. Я бы тоже так хотел. Но не похоже, что такая возможность предусмотрена.

  1. Я хочу обратить внимание на определённый аспект этих технологий. А именно, возможность построения гибких реконфигурируемых систем. Возможности Helm'а как менеджера пакетов с этой точки зрения не являются существенными, на мой взгляд. (Впрочем, я об этом тоже упомянул.)

  2. Ksonnet, вроде как, не поддерживается. На его место пришла Tanka, которую поддерживает Grafana.

Может, добавить ссылку на какую-нибудь библиотеку наподобие https://typelevel.org/cats/typeclasses/monoid.html (только для .NET)?

А кому продаёт свой продукт эта чешская компания и в какой валюте? (https://www.jetbrains.com/lp/annualreport-2020/)

Разве это не чешская компания, которая приобретает труд программистов в России?

Не знал…
Похоже, надо делать ещё одну публикацию "DataArt всё"...

Во-первых, есть компании, продающие труд разработчиков на запад, которые пока не уходят с российского рынка труда (DataArt, JetBrains, Toptal, Reksoft). Думаю, с удовольствием предложат свои вакансии освободившимся работникам.
Во-вторых, способ организации взаимодействия между клиентами и разработчиками через "биржу ИТ-проектов с комиссией платформе" — приносит доход и является рынком, а значит, может быть занят конкурентами (https://freelance.habr.com/ ?).
В-третьих, появятся посредники в нейтральных странах (Турция, Индия), например, iSpace, Datametica, готовые предложить вакансии российским разработчикам и продавать их услуги на запад.

Information

Rating
Does not participate
Location
Воронеж, Воронежская обл., Россия
Registered
Activity

Specialization

Backend Developer, Software Architect
Lead
From 700,000 ₽
Git
Linux
Docker
PostgreSQL
Golang
Scala