Сегодня я собираюсь обсудить абсолютно новую для многих пользователей (особенно для питонистов) идею: интеграцию тестов в ваше приложение. Итак, давайте начнем. На сегодняшний день проблема взаимосвязи исходного кода и тестов стоит таким образом, что вы отправляете исходный код пользователям своей библиотеки и чаще всего вообще не включаете в нее ваши тесты. Иногда люди еще прикрепляют к релизу папку с тестами, так сказать, на всякий случай. В большинстве случаев они бесполезны для конечного пользователя. Однако наиболее критично то, что наши пользователи часто оказываются в ситуации, когда им приходится заново переписывать некоторые тесты связанного с библиотекой функционала. Приведу пример: у вас есть Django View, предназначенное только для авторизованных пользователей. Итак, нам потребовалось бы написать как минимум два теста: Для варианта успешной авторизации и нашей бизнес-логики Для варианта неудачной авторизации Разве не лучше было бы, если бы мы могли просто пропустить второй вариант и использовать доступную тестовую логику, которую можно повторно использовать непосредственно из библиотеки? Представьте себе API наподобие: А затем – вуаля – мы получаем второй сценарий использования, который тестируем с помощью всего одной строчки кода! И это еще не все. Например, в Django может быть несколько декораторов функции для выполнения нескольких задач. Представьте себе такую ситуацию: Итак, API можно немного усовершенствовать посредством включения тестов для всех возможных случаев: Потенциально он может выполнять: Тесты для неразрешенных методов HTTP Тесты для разрешенных методов HTTP Тестирование на наличие заголовка Cache-Controlс соответствующим верным значением Тестирование на запрет доступа для неавторизованных пользователей Другие варианты тестирования Все, что вам нужно сделать, — это протестировать созданный вами «зеленый путь» с возможностью настройки отдельных сгенерированных сценариев тестирования, например, возврата настраиваемого HTTP-кода для неавторизованных пользователей. Плохая новость заключается в том, что обсуждаемого API не существует. И, вероятно, он никогда и не появится в Django. Однако есть и другие менее известные проекты (которые я помогаю поддерживать), в которых уже реализованы эти функции. Давайте рассмотрим их возможности! deal — это библиотека для контрактного программирования. Иными словами, она позволяет добавлять в функции и классы определенные проверки, которые невозможно представить в типах (по крайней мере, в языке Python). Допустим, у вас есть функция для деления двух положительных целых чисел (которые в Python являются просто int): Вся контрактная информация содержится здесь в определении функции: @deal.pre(lambda a, b: a >= 0 and b >= 0)проверяет, что переданные аргументы являются положительными @deal.raises(ZeroDivisionError)позволяет этой функции в прямой форме вызвать ZeroDivisionErrorбез нарушения контракта, при этом по-умолчанию функции не могут вызывать никакие исключения Примечание. Аннотации типов, такие как (a: int, b: int) -> float, все еще не проверяются во время выполнения кода: следует использовать mypyдля выявления ошибок типов. Использование (помните, что это все еще просто функция!):  Хорошо, с простым сценарием использования все ясно. А теперь давайте специально добавим ошибку в эту функцию: К счастью, dealсоответствует основной идее этой статьи и включает в себя тесты. Все, что нам нужно для их запуска, — написать всего один тестовый сценарий: Мы получим вот такой результат: Как видите, наши тесты обнаружили ошибку! Но как? Тут возникает множество вопросов: Откуда мы получили данные для теста? Они были взяты из другой замечательной библиотеки под названием hypothesis. Она позволяет генерировать множество различных тестовых данных в соответствии с определенными правилами, которые мы определяем. В рассматриваемом случае у нас есть два правила. Первое правило генерирует два аргумента типа int, как определено в def div(a: int, b: int). Второе правило заключается в том, что эти целые числа должны быть >= 0, как определено в @deal.pre(lambda a, b: a >= 0 and b >= 0). Мы можем определять количество генерируемых примеров, а также выполнять другие небольшие настройки. Подробнее см. в документации. Почему ошибка ZeroDivisionErrorне привела к падению теста, в отличие от необработанного Exception? Потому что именно так работает контрактное программирование: вы четко определяете все возможные случаи. Если происходит что-то странное — контракт нарушен. В нашем примере ZeroDivisionErrorявляется условием контракта через декоратор deal.raises. Итак, мы знаем, что это может произойти (и произойдет). Вот почему мы не рассматриваем это как падение теста, но необработанный Exception не является частью нашего контракта, и мы рассматриваем его как явное падение. Найдет ли он все ошибки в моем коде? Это самый интересный вопрос. И ответ на него — нет. Печально, но факт. В вашем коде может быть бесконечное количество сценариев использования, логики, комбинаций и ошибок. И я точно знаю, что невозможно выявить их все. В действительности, он обнаружит много ошибок. На мой взгляд, это того стоит. Мы можем пойти еще дальше и представить наши контракты как Теоремы, которые нужно доказать. Например, у deal есть сопутствующий исследовательский проект — deal-solver, который может в этом помочь. Но это тема для отдельной статьи, так что давайте продолжим. dry-python/returns — это библиотека с примитивами, которая упрощает типизированное функциональное программирование на Python. В ней имеется ряд интерфейсов, которые наши пользователи могут расширять для создания собственных примитивов/объектов. В недавней статье о Типах высших порядков я показал, как это можно сделать типо-безопасным способом. Сейчас я собираюсь продемонстрировать, что одного этого недостаточно. И, скорее всего, вам понадобятся дополнительные правила в отношении того, как должны себя вести ваши объекты. Мы называем это свойство «Законы как значения». Давайте начнем с самого простого из имеющихся у нас интерфейсов высшего порядка: Equable. Этот интерфейс позволяет выполнять типобезопасные проверки эквивалентности. Потому что в Python вы можете использовать == для всего. Однако наш метод .equals()позволит проверить только объект того же типа, внутри которого есть реальные значения. Например: Вот как это выглядит сейчас: Допустим, мы хотим создать плохую реализацию для данного интерфейса (из научных соображений): Это явно неверно, потому что он всегда возвращает значение Falseбез фактической проверки inner_valueобъекта. При этом он по-прежнему удовлетворяет определению интерфейса: он будет выполнять проверку соответствия типа. Таким образом мы можем сказать, что одного лишь интерфейса недостаточно. Нам также нужно протестировать реализацию. Однако, равенство имеет математические законы, чтобы отслеживать такие сценарии: Закон рефлексивности: значение должно быть равно самому себе Закон симметрии: a.equals(b) == b.equals(a) Закон транзитивности: если a равно b, а bравно c, то aравно c Конечно, при написании кода для реализации данного интерфейса можем заодно написать и тест, позволяющий установить соответствие нашей реализации данным законам. Или мы можем забыть об этом. Или допустить ошибку в нашей тестовой логике. Вот почему авторам библиотек важно думать о своих пользователях и предусматривать тесты в своих приложениях. Например, мы объявляем законы непосредственно в определении интерфейса: Это то, что я называю «сделать тесты частью приложения»! Теперь, когда у нас есть законы, остается обеспечить их соблюдение. Но для этого нам нужны некоторые данные. К счастью, у нас есть библиотека hypothesis, которая позволяет нам генерировать множество случайных данных. Итак, вот что мы собираемся делать: Мы передадим определение класса, в котором определено свойство _laws hypothesis получит все наши законы Для каждого закона мы сгенерируем уникальный тестовый сценарий Для каждого тестового сценария мы сгенерируем множество входных данных, чтобы удостовериться, что закон выполняется в отношении всех возможных входных данных Исходный код для тех, кому интересны детали реализации. Мы должны предоставить простой API, чтобы конечный пользователь мог сделать все это за один лишь вызов функции! Вот что мы придумали: И вот результат: Как мы видим, test_Example_equable_reflexive_lawпадает, так как equalsв нашем классе Exampleвсегда возвращает значение False, а reflexive_law, который указывает, что (a == a) is True не выполняется. Мы можем отрефакторить Example чтобы использовать правильную логику с фактической проверкой inner_value:  И снова запускаем наши тесты: Однако по факту мы не написали ни одного теста для Example. Вместо этого мы один раз написали законы для всех будущих реализаций! Вот как выглядит забота о пользователях. И снова нам помогает потрясающая библиотека hypothesisпосредством генерирования случайных данных, которые мы будем использовать в наших тестах (поэтому пакет и называется returns.contrib.hypothesis.laws. Конечно, Equable— не единственный интерфейс, который у нас есть в dry-python/returns у нас много таких, которые охватывают большинство традиционных функциональных инстансов; прочтите нашу документацию, если вам интересно. Эти интерфейсы помогут людям, если им любопытно, что такое Monad на самом деле, и какие у нее законы. К определению большинства интерфейсов предусмотрены законы. Благодаря чему, они за минимальное число шагов могут удостовериться в том, что их реализации верны. В некоторых сценариях использования дополнение приложений тестами и специальными API может быть крайне полезной возможностью. При этом сценарии использования действительно крайне разнообразны! Как я продемонстрировал, они могут варьироваться от платформ веб-приложений до инструментов архитектуры и (около-)математических библиотек. В будущем мне хотелось бы видеть больше таких инструментов! Надеюсь, мне удалось рассказать о возможных преимуществах для нынешних и будущих авторов библиотек. CTO at wemake.services