<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>alchemmist — Testing</title><link>https://alchemmist.xyz/ru/tags/testing/</link><description>Последние записи в блоге alchemmist</description><generator>Hugo 0.163.3</generator><language>ru</language><atom:link href="https://alchemmist.xyz/ru/tags/testing/index.xml" rel="self" type="application/rss+xml"/><lastBuildDate>Thu, 12 Feb 2026 11:07:00 +0300</lastBuildDate><item><title>Тестирование на Python</title><link>https://alchemmist.xyz/ru/articles/testing-in-python/</link><pubDate>Thu, 12 Feb 2026 11:07:00 +0300</pubDate><dc:creator>alchemmist</dc:creator><guid>https://alchemmist.xyz/ru/articles/testing-in-python/</guid><description>Эта статья-интенсив по тестированию на Python подробно объясняет, зачем нужны автотесты, какие виды тестов существуют, и как строится пирамида тестирования. Вы познакомитесь с подходом TDD и циклом Red -&amp;gt; Green -&amp;gt; Refactor, узнаете, когда использовать мок-объекты, а когда нет, и получите практические навыки работы с pytest — от юнит-тестов до интеграционных и E2E-тестов. Примеры кода, рекомендации по структуре тестов и советы по качественной архитектуре тестового покрытия помогут вам начать писать надёжный и поддерживаемый Python-код с тестами.</description><content:encoded><![CDATA[<p>Эта статья-интенсив по тестированию на Python подробно объясняет,
зачем нужны автотесты, какие виды тестов существуют, и как
строится пирамида тестирования. Вы познакомитесь с подходом TDD
и циклом Red -&gt; Green -&gt; Refactor, узнаете, когда использовать
мок-объекты, а когда нет, и получите практические навыки работы
с pytest — от юнит-тестов до интеграционных и E2E-тестов. Примеры
кода, рекомендации по структуре тестов и советы по качественной
архитектуре тестового покрытия помогут вам начать писать надёжный
и поддерживаемый Python-код с тестами.</p>

<h2 id="правильное-отношение-к-тестам">
  <a class="link" href="#%d0%bf%d1%80%d0%b0%d0%b2%d0%b8%d0%bb%d1%8c%d0%bd%d0%be%d0%b5-%d0%be%d1%82%d0%bd%d0%be%d1%88%d0%b5%d0%bd%d0%b8%d0%b5-%d0%ba-%d1%82%d0%b5%d1%81%d1%82%d0%b0%d0%bc">
    #
  </a>
  Правильное отношение к тестам
</h2>

<p>Вокруг тестов в Python, да и не только, много лишней драматургии.
Обычно есть два крайних мнения. Первое звучит как: &ldquo;без
стопроцентного покрытия вы не разработчик&rdquo;. Второе как: &ldquo;мы
стартап, нам не до тестов&rdquo;. Оба подхода, конечно, имеют право на
существование, но ведут, как правило не туда. В первом случае,
погоня за стопроцентным покрытим с высокой вероятностью может
привести к мусорным тестам, так как метрика покрытия (<em>как бы она
не вычисляллась</em>) не идеальна. Например, никто не запрещяет через
рефликисю получить все возможные объекты в коде, инстанцировать
их, и вообще ничего не проверять, получив стопроцентное покрытие.
Во втором случае, со временем процесс разработки будет становится
все сложнее, а DE (developer experience) ухудшаться из-за рочных
проверок &ldquo;на каждый чих&rdquo;.</p>
<p>Зрелый взгляд на тестирование куда проще. У нас есть система,
которая меняется. У этой системы есть цена ошибки. У этой же
системы есть стоимость проверки. Мы хотим снизить цену ошибки при
вменяемой стоимости проверки. Автотесты решают именно эту задачу.</p>
<p>Важно понимать, что тесты не делают код безошибочным. Они делают
ошибки быстрыми, локализуемыми и дешевыми в исправлении. Когда
ошибка ловится на ноутбуке разработчика через двадцать секунд после
изменения, это одна экономика. Когда ошибка ловится через два дня
в продакшене у реальных пользователей, это почти всегда дороже.</p>

<h2 id="зачем-автотесты-на-практике">
  <a class="link" href="#%d0%b7%d0%b0%d1%87%d0%b5%d0%bc-%d0%b0%d0%b2%d1%82%d0%be%d1%82%d0%b5%d1%81%d1%82%d1%8b-%d0%bd%d0%b0-%d0%bf%d1%80%d0%b0%d0%ba%d1%82%d0%b8%d0%ba%d0%b5">
    #
  </a>
  Зачем автотесты на практике
</h2>

<p>Чаще всего называют три причины. Защита от регрессий, документация
поведения и уверенность при рефакторинге. Это правильные слова,
но чтобы они не были абстрактными, нужно приземлить их на реальные
сценарии.</p>
<p>Представим сервис заказов. В него добавляют поддержку промокодов.
Разработчик изменяет логику расчета итоговой цены и случайно
ломает правило бесплатной доставки для VIP-клиентов. Если тестов
нет, баг уезжает в прод, поддержка получает жалобы, менеджер
поднимает инцидент, команда срочно откатывает релиз. Если тесты
есть, падает конкретный тест расчета доставки, и проблема
решается до релиза.</p>
<p>Документация поведения в тестах особенно заметна в долгоживущих
проектах. Через полгода после запуска фичи никто уже не помнит,
почему именно в случае отмененного заказа и частичного возврата
комиссия пересчитывается определенным образом. Тест с понятным
названием и явным assertion отвечает на этот вопрос быстрее, чем
любой коммит в истории.</p>
<p>Уверенность при рефакторинге вообще стоит считать отдельной
инженерной валютой. Код без рефакторинга стареет и тянет проект
вниз. Код с рефакторингом без тестов превращается в поле
экспериментов с высоким риском. Код с тестами позволяет улучшать
архитектуру постепенно и безопасно.</p>

<h2 id="что-именно-мы-тестируем-уровни-и-назначение">
  <a class="link" href="#%d1%87%d1%82%d0%be-%d0%b8%d0%bc%d0%b5%d0%bd%d0%bd%d0%be-%d0%bc%d1%8b-%d1%82%d0%b5%d1%81%d1%82%d0%b8%d1%80%d1%83%d0%b5%d0%bc-%d1%83%d1%80%d0%be%d0%b2%d0%bd%d0%b8-%d0%b8-%d0%bd%d0%b0%d0%b7%d0%bd%d0%b0%d1%87%d0%b5%d0%bd%d0%b8%d0%b5">
    #
  </a>
  Что именно мы тестируем: уровни и назначение
</h2>

<!-- TODO: replace link to einglish wiki -->
<p>Слово &ldquo;тест&rdquo; без указания уровня мало что означает. Один тест
может проверять <a href="https://ru.wikipedia.org/wiki/%D0%A7%D0%B8%D1%81%D1%82%D0%BE%D1%82%D0%B0_%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B8">чистую
функцию</a>
за миллисекунды, другой гонять браузерный сценарий несколько
минут. И это нормально, если мы понимаем, зачем каждый из них
нужен.</p>
<p>Юнит-тест проверяет маленький фрагмент поведения в изоляции.
Обычно это функция или небольшой класс без обращения к сети,
диску и базе данных. Главная ценность юнита в скорости и точности
сигнала. Если он упал, область поиска проблемы очень узкая.</p>
<p>Интеграционный тест проверяет стык нескольких модулей или слоев.
Обычно здесь уже участвуют реальные адаптеры, БД, очередь,
внешний HTTP-клиент через тестовый стенд. Такой тест медленнее,
но он ловит ошибки контрактов и конфигураций, которые юнитами не
поймать.</p>
<p>E2E тест проверяет сквозной пользовательский путь. Это не тест
метода. Это тест бизнес-сценария целиком. Например,
&ldquo;регистрация -&gt; подтверждение почты -&gt; создание заказа -&gt;
оплата&rdquo;. Такие тесты самые дорогие в поддержке, но на даже
небольшое их количество, покрывающее критические участки системы,
дают большую увернность в надёжности.</p>
<p>Регрессионные тесты не отдельный технический уровень,
а назначение. Любой тест становится регрессионным, когда он
защищает уже согласованное поведение от повторной поломки.</p>

<h2 id="пирамида-тестирования-как-модель-затрат">
  <a class="link" href="#%d0%bf%d0%b8%d1%80%d0%b0%d0%bc%d0%b8%d0%b4%d0%b0-%d1%82%d0%b5%d1%81%d1%82%d0%b8%d1%80%d0%be%d0%b2%d0%b0%d0%bd%d0%b8%d1%8f-%d0%ba%d0%b0%d0%ba-%d0%bc%d0%be%d0%b4%d0%b5%d0%bb%d1%8c-%d0%b7%d0%b0%d1%82%d1%80%d0%b0%d1%82">
    #
  </a>
  Пирамида тестирования как модель затрат
</h2>

<p>Пирамида тестирования это рабочая модель стоимости проверки.
Внизу много дешевых и быстрых проверок, выше меньше и дороже, на
вершине совсем немного самых дорогих проверок на уровне бизнеса.</p>
<p>Если команда пишет почти одни E2E, она быстро сталкивается
с медленным feedback loop. Каждый запуск долгий, окружение
хрупкое, тесты нестабильны. Разработчики начинают реже запускать
их локально, а CI превращается в место сюрпризов.</p>
<p>Если команда пишет только юниты, она теряет защиту на стыках.
Модули по отдельности могут быть безупречны, но в интеграции
сломаться из-за формата данных, конфигурации или миграций.</p>
<p>Здоровая стратегия почти всегда выглядит как широкий фундамент из
юнитов, умеренный слой интеграционных проверок и ограниченный, но
тщательно подобранный набор E2E на критический путь продукта. Но
в зависимости от специфики проекта, количество и соотношение всех
видов тестов может менятсья и это нормально.</p>

<h2 id="архитектура-и-тестируемость-связаны-напрямую">
  <a class="link" href="#%d0%b0%d1%80%d1%85%d0%b8%d1%82%d0%b5%d0%ba%d1%82%d1%83%d1%80%d0%b0-%d0%b8-%d1%82%d0%b5%d1%81%d1%82%d0%b8%d1%80%d1%83%d0%b5%d0%bc%d0%be%d1%81%d1%82%d1%8c-%d1%81%d0%b2%d1%8f%d0%b7%d0%b0%d0%bd%d1%8b-%d0%bd%d0%b0%d0%bf%d1%80%d1%8f%d0%bc%d1%83%d1%8e">
    #
  </a>
  Архитектура и тестируемость связаны напрямую
</h2>

<p>Тестируемость редко появляется случайно. Она почти всегда является
следствием архитектурных решений. Чем лучше разделены
ответственности в коде, тем проще писать тесты и тем дешевле их
поддерживать.</p>
<p>Плохой пример обычно выглядит как склеивание бизнес-логики,
инфраструктуры и побочных эффектов в одном методе.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PaymentService</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">pay</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">amount</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">db</span> <span class="o">=</span> <span class="n">PostgresClient</span><span class="p">(</span><span class="s2">&#34;postgres://prod&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">gateway</span> <span class="o">=</span> <span class="n">StripeClient</span><span class="p">(</span><span class="s2">&#34;secret&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">metrics</span> <span class="o">=</span> <span class="n">MetricsClient</span><span class="p">(</span><span class="s2">&#34;statsd://metrics&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">user</span> <span class="o">=</span> <span class="n">db</span><span class="o">.</span><span class="n">fetch_user</span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="o">.</span><span class="n">active</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;inactive user&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">gateway</span><span class="o">.</span><span class="n">charge</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">card_token</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">db</span><span class="o">.</span><span class="n">save_payment</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">metrics</span><span class="o">.</span><span class="n">increment</span><span class="p">(</span><span class="s2">&#34;payments.success&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>Здесь тесту сложно выбрать уровень. Для юнит-теста слишком много
внешнего мира. Для интеграционного слишком много разных
ответственностей сразу.</p>
<p>Лучший пример отделяет бизнес-решение от инфраструктуры и
внедряет зависимости извне.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">PaymentService</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">users_repo</span><span class="p">,</span> <span class="n">payments_repo</span><span class="p">,</span> <span class="n">gateway</span><span class="p">,</span> <span class="n">metrics</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">users_repo</span> <span class="o">=</span> <span class="n">users_repo</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">payments_repo</span> <span class="o">=</span> <span class="n">payments_repo</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">gateway</span> <span class="o">=</span> <span class="n">gateway</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">metrics</span> <span class="o">=</span> <span class="n">metrics</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">pay</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">amount</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">user</span> <span class="o">=</span> <span class="bp">self</span><span class="o">.</span><span class="n">users_repo</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">user_id</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="o">.</span><span class="n">active</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">            <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;inactive user&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">gateway</span><span class="o">.</span><span class="n">charge</span><span class="p">(</span><span class="n">user</span><span class="o">.</span><span class="n">card_token</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">payments_repo</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">amount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">metrics</span><span class="o">.</span><span class="n">increment</span><span class="p">(</span><span class="s2">&#34;payments.success&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>Теперь юнит-тест может проверять решения домена через фейковые
зависимости, а интеграционные тесты отдельно проверят репозиторий,
шлюз и метрики. Такой код проще развивать и проще покрывать.</p>

<h2 id="red---green---refactor">
  <a class="link" href="#red---green---refactor">
    #
  </a>
  Red -&gt; Green -&gt; Refactor
</h2>

<p>Red означает, что мы сначала формулируем ожидаемое поведение через
падающий тест. Green означает, что мы пишем минимальный код,
достаточный для прохождения. Refactor означает, что мы улучшаем
структуру кода, не меняя поведение.</p>
<p>Рассмотрим короткий пример. Пусть нужна функция,
которая разбивает сумму по месяцам и последнюю копейку отдает
в последний месяц.</p>
<p>На фазе Red можно начать так:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_allocate_even_months_with_remainder</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># 1000 cents over 3 months -&gt; 333, 333, 334</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">allocate_cents</span><span class="p">(</span><span class="mi">1000</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span> <span class="o">==</span> <span class="p">[</span><span class="mi">333</span><span class="p">,</span> <span class="mi">333</span><span class="p">,</span> <span class="mi">334</span><span class="p">]</span>
</span></span></code></pre></div><p>На фазе Green пишем минимальную реализацию:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">allocate_cents</span><span class="p">(</span><span class="n">total</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">months</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="n">base</span> <span class="o">=</span> <span class="n">total</span> <span class="o">//</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">remainder</span> <span class="o">=</span> <span class="n">total</span> <span class="o">%</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="p">[</span><span class="n">base</span><span class="p">]</span> <span class="o">*</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">result</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">+=</span> <span class="n">remainder</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">result</span>
</span></span></code></pre></div><p>На фазе Refactor добавляем защиту от некорректного ввода,
делаем имя переменных яснее, добавляем тесты на крайние случаи.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">allocate_cents</span><span class="p">(</span><span class="n">total</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">months</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">int</span><span class="p">]:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">months</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;months must be positive&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">total</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;total must be non-negative&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">base</span> <span class="o">=</span> <span class="n">total</span> <span class="o">//</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">remainder</span> <span class="o">=</span> <span class="n">total</span> <span class="o">%</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">schedule</span> <span class="o">=</span> <span class="p">[</span><span class="n">base</span><span class="p">]</span> <span class="o">*</span> <span class="n">months</span>
</span></span><span class="line"><span class="cl">    <span class="n">schedule</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">+=</span> <span class="n">remainder</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">schedule</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_allocate_one_month</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">allocate_cents</span><span class="p">(</span><span class="mi">500</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span> <span class="o">==</span> <span class="p">[</span><span class="mi">500</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_allocate_zero_total</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">allocate_cents</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span> <span class="o">==</span> <span class="p">[</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_allocate_invalid_months</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">pytest</span><span class="o">.</span><span class="n">raises</span><span class="p">(</span><span class="ne">ValueError</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">allocate_cents</span><span class="p">(</span><span class="mi">100</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
</span></span></code></pre></div><p>Именно так TDD удерживает фокус на контракте и предотвращает
переусложнение раньше времени.</p>

<h2 id="скорость-обратной-связи">
  <a class="link" href="#%d1%81%d0%ba%d0%be%d1%80%d0%be%d1%81%d1%82%d1%8c-%d0%be%d0%b1%d1%80%d0%b0%d1%82%d0%bd%d0%be%d0%b9-%d1%81%d0%b2%d1%8f%d0%b7%d0%b8">
    #
  </a>
  Скорость обратной связи
</h2>

<p>Команда ускоряется не тогда, когда быстрее печатает код.
Команда ускоряется тогда, когда быстрее получает надежный ответ
на вопрос &ldquo;изменение корректно или нет&rdquo;.</p>
<p>Маленькие изолированные тесты дают сигнал за секунды.
Тяжелые интеграционные проверки дают сигнал за минуты.
E2E иногда дают сигнал за десятки минут. Все уровни нужны,
но последовательность их использования критична.</p>
<p>В рабочем режиме разработчик обычно запускает локально быстрый
пакет, потом нужные интеграционные тесты по задаче, и уже после
этого отдает изменения в CI, где гоняется полный набор.</p>
<p>Когда эта дисциплина есть, количество &ldquo;красных&rdquo; сюрпризов в CI
существенно падает. А значит, падает и время ожидания,
и количество контекстных переключений.</p>

<h2 id="london-school-и-classicist-school-в-реальном-проекте">
  <a class="link" href="#london-school-%d0%b8-classicist-school-%d0%b2-%d1%80%d0%b5%d0%b0%d0%bb%d1%8c%d0%bd%d0%be%d0%bc-%d0%bf%d1%80%d0%be%d0%b5%d0%ba%d1%82%d0%b5">
    #
  </a>
  London school и classicist school в реальном проекте
</h2>

<p>Спор между школами тестирования существует давно и часто подается
как выбор одного лагеря. На практике полезнее понимать, в чем
сила каждой школы и где ее границы.</p>
<p>London school чаще использует моки и проверяет взаимодействия.
Если ваш код это оркестратор нескольких внешних вызовов,
этот подход дает очень точный контроль над тем, кто и как был
вызван.</p>
<p>Classicist school чаще проверяет состояние и поведение через
реальные объекты без агрессивного мокинга. Если у вас много
чистой доменной логики, такой стиль обычно делает тесты более
устойчивыми к рефакторингу.</p>
<p>Давайте посмотрим на один и тот же кейс двумя стилями.</p>
<p>В mock-heavy подходе:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">unittest.mock</span> <span class="kn">import</span> <span class="n">Mock</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">register_user</span><span class="p">(</span><span class="n">repo</span><span class="p">,</span> <span class="n">email_sender</span><span class="p">,</span> <span class="n">email</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">user_id</span> <span class="o">=</span> <span class="n">repo</span><span class="o">.</span><span class="n">create</span><span class="p">(</span><span class="n">email</span><span class="o">=</span><span class="n">email</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">email_sender</span><span class="o">.</span><span class="n">send_welcome</span><span class="p">(</span><span class="n">user_id</span><span class="o">=</span><span class="n">user_id</span><span class="p">,</span> <span class="n">email</span><span class="o">=</span><span class="n">email</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_register_user_calls_dependencies</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">repo</span> <span class="o">=</span> <span class="n">Mock</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">repo</span><span class="o">.</span><span class="n">create</span><span class="o">.</span><span class="n">return_value</span> <span class="o">=</span> <span class="mi">101</span>
</span></span><span class="line"><span class="cl">    <span class="n">email_sender</span> <span class="o">=</span> <span class="n">Mock</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">register_user</span><span class="p">(</span><span class="n">repo</span><span class="p">,</span> <span class="n">email_sender</span><span class="p">,</span> <span class="s2">&#34;alice@example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">repo</span><span class="o">.</span><span class="n">create</span><span class="o">.</span><span class="n">assert_called_once_with</span><span class="p">(</span><span class="n">email</span><span class="o">=</span><span class="s2">&#34;alice@example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">email_sender</span><span class="o">.</span><span class="n">send_welcome</span><span class="o">.</span><span class="n">assert_called_once_with</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="n">user_id</span><span class="o">=</span><span class="mi">101</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">email</span><span class="o">=</span><span class="s2">&#34;alice@example.com&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span></code></pre></div><p>В state-oriented подходе:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">InMemoryRepo</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">items</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">create</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">email</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">user_id</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="bp">self</span><span class="o">.</span><span class="n">items</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">items</span><span class="o">.</span><span class="n">append</span><span class="p">({</span><span class="s2">&#34;id&#34;</span><span class="p">:</span> <span class="n">user_id</span><span class="p">,</span> <span class="s2">&#34;email&#34;</span><span class="p">:</span> <span class="n">email</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">user_id</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">InMemoryEmailSender</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">sent</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">send_welcome</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">user_id</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">email</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">sent</span><span class="o">.</span><span class="n">append</span><span class="p">({</span><span class="s2">&#34;user_id&#34;</span><span class="p">:</span> <span class="n">user_id</span><span class="p">,</span> <span class="s2">&#34;email&#34;</span><span class="p">:</span> <span class="n">email</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_register_user_changes_state</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">repo</span> <span class="o">=</span> <span class="n">InMemoryRepo</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">sender</span> <span class="o">=</span> <span class="n">InMemoryEmailSender</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">register_user</span><span class="p">(</span><span class="n">repo</span><span class="p">,</span> <span class="n">sender</span><span class="p">,</span> <span class="s2">&#34;alice@example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">repo</span><span class="o">.</span><span class="n">items</span> <span class="o">==</span> <span class="p">[{</span><span class="s2">&#34;id&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&#34;email&#34;</span><span class="p">:</span> <span class="s2">&#34;alice@example.com&#34;</span><span class="p">}]</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">sender</span><span class="o">.</span><span class="n">sent</span> <span class="o">==</span> <span class="p">[{</span><span class="s2">&#34;user_id&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">&#34;email&#34;</span><span class="p">:</span> <span class="s2">&#34;alice@example.com&#34;</span><span class="p">}]</span>
</span></span></code></pre></div><p>Оба теста полезны. Первый лучше контролирует контракт вызовов.
Второй лучше переживает рефакторинг внутренней реализации.</p>

<h2 id="моки-и-стабы-когда-помогают-а-когда-ломают-картину">
  <a class="link" href="#%d0%bc%d0%be%d0%ba%d0%b8-%d0%b8-%d1%81%d1%82%d0%b0%d0%b1%d1%8b-%d0%ba%d0%be%d0%b3%d0%b4%d0%b0-%d0%bf%d0%be%d0%bc%d0%be%d0%b3%d0%b0%d1%8e%d1%82-%d0%b0-%d0%ba%d0%be%d0%b3%d0%b4%d0%b0-%d0%bb%d0%be%d0%bc%d0%b0%d1%8e%d1%82-%d0%ba%d0%b0%d1%80%d1%82%d0%b8%d0%bd%d1%83">
    #
  </a>
  Моки и стабы: когда помогают, а когда ломают картину
</h2>

<p>Мок нужен там, где без него тест становится нестабильным,
дорогим или слишком широким. Хороший признак полезного мокинга
это изоляция внешнего мира, а не изоляция собственной логики.</p>
<p>Разберем пример с внешним API курса валют. В production-коде мы
делаем HTTP-запрос, но в юнит-тесте хотим проверить реакцию на
ответы API без реальной сети.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">requests</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">fetch_rate</span><span class="p">(</span><span class="n">base</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">quote</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">resp</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">        <span class="s2">&#34;https://rates.example.com/latest&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">        <span class="n">params</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;base&#34;</span><span class="p">:</span> <span class="n">base</span><span class="p">,</span> <span class="s2">&#34;quote&#34;</span><span class="p">:</span> <span class="n">quote</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">        <span class="n">timeout</span><span class="o">=</span><span class="mi">3</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">data</span> <span class="o">=</span> <span class="n">resp</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">float</span><span class="p">(</span><span class="n">data</span><span class="p">[</span><span class="s2">&#34;rate&#34;</span><span class="p">])</span>
</span></span></code></pre></div><p>Юнит-тест с mock:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">unittest.mock</span> <span class="kn">import</span> <span class="n">Mock</span><span class="p">,</span> <span class="n">patch</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_fetch_rate_parses_response</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">fake_response</span> <span class="o">=</span> <span class="n">Mock</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">fake_response</span><span class="o">.</span><span class="n">json</span><span class="o">.</span><span class="n">return_value</span> <span class="o">=</span> <span class="p">{</span><span class="s2">&#34;rate&#34;</span><span class="p">:</span> <span class="s2">&#34;95.4&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">patch</span><span class="p">(</span><span class="s2">&#34;requests.get&#34;</span><span class="p">,</span> <span class="n">return_value</span><span class="o">=</span><span class="n">fake_response</span><span class="p">)</span> <span class="k">as</span> <span class="n">get_mock</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">rate</span> <span class="o">=</span> <span class="n">fetch_rate</span><span class="p">(</span><span class="s2">&#34;USD&#34;</span><span class="p">,</span> <span class="s2">&#34;RUB&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">rate</span> <span class="o">==</span> <span class="mf">95.4</span>
</span></span><span class="line"><span class="cl">    <span class="n">get_mock</span><span class="o">.</span><span class="n">assert_called_once</span><span class="p">()</span>
</span></span></code></pre></div><p>Тут мок оправдан. Мы не тестируем <code>requests</code>, мы тестируем,
как наша функция обрабатывает ответ.</p>
<p>Но если мы начнем мокать каждый метод собственного репозитория,
каждую внутреннюю функцию и каждый <code>if</code>, тесты станут хрупкими.
Они будут падать от технического рефакторинга, даже если
поведение не изменилось.</p>

<h2 id="unittest-pytest-nose2-что-выбирать-в-2026-году">
  <a class="link" href="#unittest-pytest-nose2-%d1%87%d1%82%d0%be-%d0%b2%d1%8b%d0%b1%d0%b8%d1%80%d0%b0%d1%82%d1%8c-%d0%b2-2026-%d0%b3%d0%be%d0%b4%d1%83">
    #
  </a>
  unittest, pytest, nose2: что выбирать в 2026 году
</h2>

<p>Встроенный <code>unittest</code> остается рабочим вариантом, особенно там,
где важна минимизация зависимостей и строгая стандартизация.</p>
<p><code>pytest</code> в повседневной разработке обычно выигрывает за счет
читаемости, параметризации, фикстур и удобной экосистемы.
Для большинства новых Python-проектов это практический выбор по
умолчанию.</p>
<p><code>nose</code> стоит рассматривать как исторический этап экосистемы.
<code>nose2</code> жив, но в новых кодовых базах он встречается редко,
потому что <code>pytest</code> решает типичные задачи проще и с большей
поддержкой сообщества.</p>

<h2 id="aaa-и-структура-теста-которую-удобно-читать-через-год">
  <a class="link" href="#aaa-%d0%b8-%d1%81%d1%82%d1%80%d1%83%d0%ba%d1%82%d1%83%d1%80%d0%b0-%d1%82%d0%b5%d1%81%d1%82%d0%b0-%d0%ba%d0%be%d1%82%d0%be%d1%80%d1%83%d1%8e-%d1%83%d0%b4%d0%be%d0%b1%d0%bd%d0%be-%d1%87%d0%b8%d1%82%d0%b0%d1%82%d1%8c-%d1%87%d0%b5%d1%80%d0%b5%d0%b7-%d0%b3%d0%be%d0%b4">
    #
  </a>
  AAA и структура теста, которую удобно читать через год
</h2>

<p>Одна из самых простых техник, которая сильно повышает
поддерживаемость тестов, это явная структура Arrange, Act,
Assert. Даже без комментариев она удерживает тест коротким
и линейным.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">calculate_total</span><span class="p">(</span><span class="n">price</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">quantity</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">discount</span><span class="p">:</span> <span class="nb">float</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">subtotal</span> <span class="o">=</span> <span class="n">price</span> <span class="o">*</span> <span class="n">quantity</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="nb">int</span><span class="p">(</span><span class="n">subtotal</span> <span class="o">*</span> <span class="p">(</span><span class="mi">1</span> <span class="o">-</span> <span class="n">discount</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_calculate_total_with_discount</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">price</span> <span class="o">=</span> <span class="mi">500</span>
</span></span><span class="line"><span class="cl">    <span class="n">quantity</span> <span class="o">=</span> <span class="mi">2</span>
</span></span><span class="line"><span class="cl">    <span class="n">discount</span> <span class="o">=</span> <span class="mf">0.1</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">calculate_total</span><span class="p">(</span><span class="n">price</span><span class="p">,</span> <span class="n">quantity</span><span class="p">,</span> <span class="n">discount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">result</span> <span class="o">==</span> <span class="mi">900</span>
</span></span></code></pre></div><p>Здесь тест легко сканируется глазами. Подготовка данных,
действие, проверка. Ничего лишнего.</p>
<p>Если тест содержит слишком много действий и проверок, это часто
сигнал, что он пытается проверить слишком много разных сценариев
сразу. Лучше разделить такой тест на несколько маленьких.</p>

<h2 id="фикстуры-setupteardown-и-контроль-над-состоянием">
  <a class="link" href="#%d1%84%d0%b8%d0%ba%d1%81%d1%82%d1%83%d1%80%d1%8b-setupteardown-%d0%b8-%d0%ba%d0%be%d0%bd%d1%82%d1%80%d0%be%d0%bb%d1%8c-%d0%bd%d0%b0%d0%b4-%d1%81%d0%be%d1%81%d1%82%d0%be%d1%8f%d0%bd%d0%b8%d0%b5%d0%bc">
    #
  </a>
  Фикстуры, setup/teardown и контроль над состоянием
</h2>

<p>Фикстуры в <code>pytest</code> решают ключевую проблему дублирования
подготовки данных и окружения. Но их легко превратить в
непрозрачную магию, если прятать внутри слишком много логики.</p>
<p>Сравним плохой и хороший подход.</p>
<p>Плохо, когда фикстура делает слишком много:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.fixture</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">app_everything</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># создает БД, поднимает сервис, загружает данные,</span>
</span></span><span class="line"><span class="cl">    <span class="c1"># настраивает очередь, запускает фоновые воркеры</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;warning&#34;</span><span class="p">:</span> <span class="s2">&#34;fixture is too broad&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>Хорошо, когда фикстуры узкие и ясные по назначению:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.fixture</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">user_email</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;alice@example.com&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.fixture</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">in_memory_repo</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">class</span> <span class="nc">Repo</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">items</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="k">def</span> <span class="nf">save</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">email</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">            <span class="bp">self</span><span class="o">.</span><span class="n">items</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">email</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">Repo</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_repo_save</span><span class="p">(</span><span class="n">in_memory_repo</span><span class="p">,</span> <span class="n">user_email</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">in_memory_repo</span><span class="o">.</span><span class="n">save</span><span class="p">(</span><span class="n">user_email</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">in_memory_repo</span><span class="o">.</span><span class="n">items</span> <span class="o">==</span> <span class="p">[</span><span class="s2">&#34;alice@example.com&#34;</span><span class="p">]</span>
</span></span></code></pre></div><p>Чем яснее фикстуры, тем проще понимать, откуда в тесте взялись
данные и почему они именно такие.</p>

<h2 id="рекомендации-по-юнит-тестам-через-конкретные-примеры">
  <a class="link" href="#%d1%80%d0%b5%d0%ba%d0%be%d0%bc%d0%b5%d0%bd%d0%b4%d0%b0%d1%86%d0%b8%d0%b8-%d0%bf%d0%be-%d1%8e%d0%bd%d0%b8%d1%82-%d1%82%d0%b5%d1%81%d1%82%d0%b0%d0%bc-%d1%87%d0%b5%d1%80%d0%b5%d0%b7-%d0%ba%d0%be%d0%bd%d0%ba%d1%80%d0%b5%d1%82%d0%bd%d1%8b%d0%b5-%d0%bf%d1%80%d0%b8%d0%bc%d0%b5%d1%80%d1%8b">
    #
  </a>
  Рекомендации по юнит-тестам через конкретные примеры
</h2>

<p>Сильный юнит-тест обычно проверяет один смысловой сценарий.
Рассмотрим функцию нормализации телефона.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">re</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">normalize_phone</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">digits</span> <span class="o">=</span> <span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="sa">r</span><span class="s2">&#34;\D&#34;</span><span class="p">,</span> <span class="s2">&#34;&#34;</span><span class="p">,</span> <span class="n">value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">digits</span><span class="p">)</span> <span class="o">==</span> <span class="mi">11</span> <span class="ow">and</span> <span class="n">digits</span><span class="o">.</span><span class="n">startswith</span><span class="p">(</span><span class="s2">&#34;8&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">digits</span> <span class="o">=</span> <span class="s2">&#34;7&#34;</span> <span class="o">+</span> <span class="n">digits</span><span class="p">[</span><span class="mi">1</span><span class="p">:]</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">digits</span><span class="p">)</span> <span class="o">!=</span> <span class="mi">11</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ValueError</span><span class="p">(</span><span class="s2">&#34;invalid phone&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="s2">&#34;+&#34;</span> <span class="o">+</span> <span class="n">digits</span>
</span></span></code></pre></div><p>Плохой тест на такую функцию часто пытается проверить сразу
пять форматов и две ошибки в одном кейсе. Хороший набор тестов
разделяет сценарии.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_normalize_phone_ru_local_prefix</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">normalize_phone</span><span class="p">(</span><span class="s2">&#34;8 (999) 000-00-00&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;+79990000000&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_normalize_phone_already_international</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">normalize_phone</span><span class="p">(</span><span class="s2">&#34;+7 999 000 00 00&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;+79990000000&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_normalize_phone_invalid_length</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">pytest</span><span class="o">.</span><span class="n">raises</span><span class="p">(</span><span class="ne">ValueError</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">normalize_phone</span><span class="p">(</span><span class="s2">&#34;123&#34;</span><span class="p">)</span>
</span></span></code></pre></div><p>Теперь при падении точно видно, какой именно сценарий сломан.</p>

<h2 id="как-организовать-test-suite-проекта-чтобы-им-реально-пользовались">
  <a class="link" href="#%d0%ba%d0%b0%d0%ba-%d0%be%d1%80%d0%b3%d0%b0%d0%bd%d0%b8%d0%b7%d0%be%d0%b2%d0%b0%d1%82%d1%8c-test-suite-%d0%bf%d1%80%d0%be%d0%b5%d0%ba%d1%82%d0%b0-%d1%87%d1%82%d0%be%d0%b1%d1%8b-%d0%b8%d0%bc-%d1%80%d0%b5%d0%b0%d0%bb%d1%8c%d0%bd%d0%be-%d0%bf%d0%be%d0%bb%d1%8c%d0%b7%d0%be%d0%b2%d0%b0%d0%bb%d0%b8%d1%81%d1%8c">
    #
  </a>
  Как организовать test suite проекта, чтобы им реально пользовались
</h2>

<p>Рабочий test suite начинается с понятной структуры каталогов и
режимов запуска. Когда уровни тестов разделены, разработчику проще
выбирать нужный пакет под конкретную задачу.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">project/
</span></span><span class="line"><span class="cl">  app/
</span></span><span class="line"><span class="cl">    domain/
</span></span><span class="line"><span class="cl">    application/
</span></span><span class="line"><span class="cl">    infrastructure/
</span></span><span class="line"><span class="cl">  tests/
</span></span><span class="line"><span class="cl">    unit/
</span></span><span class="line"><span class="cl">    integration/
</span></span><span class="line"><span class="cl">    e2e/
</span></span><span class="line"><span class="cl">  pyproject.toml
</span></span></code></pre></div><p>В <code>pyproject.toml</code> удобно зафиксировать маркеры и общие настройки
<code>pytest</code>, чтобы команда запускала тесты единообразно.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">tool</span><span class="p">.</span><span class="nx">pytest</span><span class="p">.</span><span class="nx">ini_options</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">addopts</span> <span class="p">=</span> <span class="s2">&#34;-ra -q&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">testpaths</span> <span class="p">=</span> <span class="p">[</span><span class="s2">&#34;tests&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">markers</span> <span class="p">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;integration: tests with real infrastructure&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;e2e: end-to-end scenarios&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span></code></pre></div><p>Тогда локально можно запускать быстрые тесты отдельно от тяжелых.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sh" data-lang="sh"><span class="line"><span class="cl">pytest tests/unit
</span></span><span class="line"><span class="cl">pytest -m integration
</span></span><span class="line"><span class="cl">pytest -m e2e
</span></span></code></pre></div><p>Ключевой момент в том, что suite должен помогать разработчику,
а не наказывать его долгими непредсказуемыми прогонами.</p>

<h2 id="интеграционные-тесты-на-примере-api-и-базы-данных">
  <a class="link" href="#%d0%b8%d0%bd%d1%82%d0%b5%d0%b3%d1%80%d0%b0%d1%86%d0%b8%d0%be%d0%bd%d0%bd%d1%8b%d0%b5-%d1%82%d0%b5%d1%81%d1%82%d1%8b-%d0%bd%d0%b0-%d0%bf%d1%80%d0%b8%d0%bc%d0%b5%d1%80%d0%b5-api-%d0%b8-%d0%b1%d0%b0%d0%b7%d1%8b-%d0%b4%d0%b0%d0%bd%d0%bd%d1%8b%d1%85">
    #
  </a>
  Интеграционные тесты на примере API и базы данных
</h2>

<p>Разберем практический пример с FastAPI и SQLite,
где мы хотим проверить полный путь &ldquo;HTTP-запрос -&gt; запись в БД -&gt;
HTTP-ответ&rdquo;.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">fastapi</span> <span class="kn">import</span> <span class="n">FastAPI</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">pydantic</span> <span class="kn">import</span> <span class="n">BaseModel</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sqlite3</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">app</span> <span class="o">=</span> <span class="n">FastAPI</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">ItemIn</span><span class="p">(</span><span class="n">BaseModel</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">name</span><span class="p">:</span> <span class="nb">str</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">get_conn</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span> <span class="o">=</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="s2">&#34;test.db&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;CREATE TABLE IF NOT EXISTS items(name TEXT)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">conn</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@app.post</span><span class="p">(</span><span class="s2">&#34;/items&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">create_item</span><span class="p">(</span><span class="n">payload</span><span class="p">:</span> <span class="n">ItemIn</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span> <span class="o">=</span> <span class="n">get_conn</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;INSERT INTO items(name) VALUES (?)&#34;</span><span class="p">,</span> <span class="p">(</span><span class="n">payload</span><span class="o">.</span><span class="n">name</span><span class="p">,))</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">commit</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;ok&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">}</span>
</span></span></code></pre></div><p>Интеграционный тест:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">fastapi.testclient</span> <span class="kn">import</span> <span class="n">TestClient</span>
</span></span><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">sqlite3</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="n">client</span> <span class="o">=</span> <span class="n">TestClient</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_create_item_persists_to_db</span><span class="p">(</span><span class="n">tmp_path</span><span class="p">,</span> <span class="n">monkeypatch</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">db_path</span> <span class="o">=</span> <span class="n">tmp_path</span> <span class="o">/</span> <span class="s2">&#34;items.db&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">get_test_conn</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">        <span class="n">conn</span> <span class="o">=</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">db_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">conn</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;CREATE TABLE IF NOT EXISTS items(name TEXT)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">conn</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">monkeypatch</span><span class="o">.</span><span class="n">setattr</span><span class="p">(</span><span class="s2">&#34;main.get_conn&#34;</span><span class="p">,</span> <span class="n">get_test_conn</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="s2">&#34;/items&#34;</span><span class="p">,</span> <span class="n">json</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;book&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s2">&#34;ok&#34;</span><span class="p">:</span> <span class="kc">True</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">conn</span> <span class="o">=</span> <span class="n">sqlite3</span><span class="o">.</span><span class="n">connect</span><span class="p">(</span><span class="n">db_path</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">rows</span> <span class="o">=</span> <span class="n">conn</span><span class="o">.</span><span class="n">execute</span><span class="p">(</span><span class="s2">&#34;SELECT name FROM items&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">fetchall</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="n">conn</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">rows</span> <span class="o">==</span> <span class="p">[(</span><span class="s2">&#34;book&#34;</span><span class="p">,)]</span>
</span></span></code></pre></div><p>Этот тест уже не юнит. Он проверяет интеграцию HTTP-слоя,
валидации и хранилища.</p>

<h2 id="e2e-на-примере-playwright">
  <a class="link" href="#e2e-%d0%bd%d0%b0-%d0%bf%d1%80%d0%b8%d0%bc%d0%b5%d1%80%d0%b5-playwright">
    #
  </a>
  E2E на примере Playwright
</h2>

<p>E2E полезно показывать на коротких, но бизнес-значимых сценариях.
Например, логин пользователя.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">playwright.sync_api</span> <span class="kn">import</span> <span class="n">sync_playwright</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_login_e2e</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">sync_playwright</span><span class="p">()</span> <span class="k">as</span> <span class="n">p</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">browser</span> <span class="o">=</span> <span class="n">p</span><span class="o">.</span><span class="n">chromium</span><span class="o">.</span><span class="n">launch</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">page</span> <span class="o">=</span> <span class="n">browser</span><span class="o">.</span><span class="n">new_page</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">page</span><span class="o">.</span><span class="n">goto</span><span class="p">(</span><span class="s2">&#34;http://localhost:8000/login&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">page</span><span class="o">.</span><span class="n">fill</span><span class="p">(</span><span class="s2">&#34;input[name=&#39;email&#39;]&#34;</span><span class="p">,</span> <span class="s2">&#34;alice@example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">page</span><span class="o">.</span><span class="n">fill</span><span class="p">(</span><span class="s2">&#34;input[name=&#39;password&#39;]&#34;</span><span class="p">,</span> <span class="s2">&#34;qwerty&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">page</span><span class="o">.</span><span class="n">click</span><span class="p">(</span><span class="s2">&#34;button[type=&#39;submit&#39;]&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">page</span><span class="o">.</span><span class="n">wait_for_url</span><span class="p">(</span><span class="s2">&#34;**/dashboard&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">assert</span> <span class="n">page</span><span class="o">.</span><span class="n">locator</span><span class="p">(</span><span class="s2">&#34;text=Welcome&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">is_visible</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">        <span class="n">browser</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span></code></pre></div><p>В этом тесте мы не проверяем внутреннюю реализацию логина.
Мы проверяем, что пользователь действительно может пройти
критический путь.</p>

<h2 id="база-данных-в-тестах-миграции-фикстуры-rollback">
  <a class="link" href="#%d0%b1%d0%b0%d0%b7%d0%b0-%d0%b4%d0%b0%d0%bd%d0%bd%d1%8b%d1%85-%d0%b2-%d1%82%d0%b5%d1%81%d1%82%d0%b0%d1%85-%d0%bc%d0%b8%d0%b3%d1%80%d0%b0%d1%86%d0%b8%d0%b8-%d1%84%d0%b8%d0%ba%d1%81%d1%82%d1%83%d1%80%d1%8b-rollback">
    #
  </a>
  База данных в тестах: миграции, фикстуры, rollback
</h2>

<p>С БД в тестах есть одно правило, которое нельзя нарушать.
Каждый тест должен быть независим по состоянию.</p>
<p>Один из рабочих шаблонов для SQLAlchemy это транзакция на тест,
которая откатывается после выполнения.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">sqlalchemy</span> <span class="kn">import</span> <span class="n">create_engine</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">sqlalchemy.orm</span> <span class="kn">import</span> <span class="n">sessionmaker</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.fixture</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">db_session</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">engine</span> <span class="o">=</span> <span class="n">create_engine</span><span class="p">(</span><span class="s2">&#34;sqlite:///:memory:&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">Session</span> <span class="o">=</span> <span class="n">sessionmaker</span><span class="p">(</span><span class="n">bind</span><span class="o">=</span><span class="n">engine</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">session</span> <span class="o">=</span> <span class="n">Session</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">tx</span> <span class="o">=</span> <span class="n">session</span><span class="o">.</span><span class="n">begin</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">yield</span> <span class="n">session</span>
</span></span><span class="line"><span class="cl">    <span class="k">finally</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="n">tx</span><span class="o">.</span><span class="n">rollback</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">        <span class="n">session</span><span class="o">.</span><span class="n">close</span><span class="p">()</span>
</span></span></code></pre></div><p>Такой подход дает чистое состояние на каждый тест и сохраняет
высокую скорость прогонов.</p>
<p>Если в проекте используются миграции, их нужно включать в процесс
подготовки тестовой БД, иначе тесты начнут жить в схеме,
которая отличается от production.</p>

<h2 id="приватные-методы-когда-тестировать-напрямую-а-когда-нет">
  <a class="link" href="#%d0%bf%d1%80%d0%b8%d0%b2%d0%b0%d1%82%d0%bd%d1%8b%d0%b5-%d0%bc%d0%b5%d1%82%d0%be%d0%b4%d1%8b-%d0%ba%d0%be%d0%b3%d0%b4%d0%b0-%d1%82%d0%b5%d1%81%d1%82%d0%b8%d1%80%d0%be%d0%b2%d0%b0%d1%82%d1%8c-%d0%bd%d0%b0%d0%bf%d1%80%d1%8f%d0%bc%d1%83%d1%8e-%d0%b0-%d0%ba%d0%be%d0%b3%d0%b4%d0%b0-%d0%bd%d0%b5%d1%82">
    #
  </a>
  Приватные методы: когда тестировать напрямую, а когда нет
</h2>

<p>По умолчанию лучше тестировать поведение через публичный API.
Это делает тесты устойчивее к рефакторингу.</p>
<p>Рассмотрим пример.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">TaxCalculator</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">calculate</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">amount</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="n">amount</span> <span class="o">+</span> <span class="bp">self</span><span class="o">.</span><span class="n">_tax</span><span class="p">(</span><span class="n">amount</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">_tax</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">amount</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="nb">int</span><span class="p">(</span><span class="n">amount</span> <span class="o">*</span> <span class="mf">0.2</span><span class="p">)</span>
</span></span></code></pre></div><p>Стабильный тест проверяет <code>calculate</code>, а не <code>_tax</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_calculate_adds_tax</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="n">calc</span> <span class="o">=</span> <span class="n">TaxCalculator</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">calc</span><span class="o">.</span><span class="n">calculate</span><span class="p">(</span><span class="mi">100</span><span class="p">)</span> <span class="o">==</span> <span class="mi">120</span>
</span></span></code></pre></div><p>Если завтра <code>_tax</code> будет заменен на сложную стратегию
с льготами, тест публичного поведения останется валидным,
если контракт не изменился.</p>
<p>Прямое тестирование приватного метода может быть временно
оправдано в legacy-коде, но как постоянная практика оно
обычно повышает хрупкость тестов.</p>

<h2 id="время-в-тестах-и-детерминизм">
  <a class="link" href="#%d0%b2%d1%80%d0%b5%d0%bc%d1%8f-%d0%b2-%d1%82%d0%b5%d1%81%d1%82%d0%b0%d1%85-%d0%b8-%d0%b4%d0%b5%d1%82%d0%b5%d1%80%d0%bc%d0%b8%d0%bd%d0%b8%d0%b7%d0%bc">
    #
  </a>
  Время в тестах и детерминизм
</h2>

<p>Время это одна из самых частых причин flaky-тестов.
Если тест зависит от <code>datetime.now()</code>, он может вести себя
по-разному в разные дни, часы и timezone.</p>
<p><code>freezegun</code> решает это элегантно.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">datetime</span> <span class="kn">import</span> <span class="n">datetime</span>
</span></span><span class="line"><span class="cl"><span class="kn">from</span> <span class="nn">freezegun</span> <span class="kn">import</span> <span class="n">freeze_time</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">current_period_start</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">now</span> <span class="o">=</span> <span class="n">datetime</span><span class="o">.</span><span class="n">now</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">now</span><span class="o">.</span><span class="n">replace</span><span class="p">(</span><span class="n">day</span><span class="o">=</span><span class="mi">1</span><span class="p">,</span> <span class="n">hour</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">minute</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">second</span><span class="o">=</span><span class="mi">0</span><span class="p">,</span> <span class="n">microsecond</span><span class="o">=</span><span class="mi">0</span><span class="p">)</span><span class="o">.</span><span class="n">isoformat</span><span class="p">()</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@freeze_time</span><span class="p">(</span><span class="s2">&#34;2026-02-12 10:00:00&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_current_period_start</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">current_period_start</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;2026-02-01T00:00:00&#34;</span>
</span></span></code></pre></div><p>Такой тест детерминирован и не зависит от машины запуска.</p>

<h2 id="плотный-блок-про-pytest-от-базы-до-полезных-встроенных-фикстур">
  <a class="link" href="#%d0%bf%d0%bb%d0%be%d1%82%d0%bd%d1%8b%d0%b9-%d0%b1%d0%bb%d0%be%d0%ba-%d0%bf%d1%80%d0%be-pytest-%d0%be%d1%82-%d0%b1%d0%b0%d0%b7%d1%8b-%d0%b4%d0%be-%d0%bf%d0%be%d0%bb%d0%b5%d0%b7%d0%bd%d1%8b%d1%85-%d0%b2%d1%81%d1%82%d1%80%d0%be%d0%b5%d0%bd%d0%bd%d1%8b%d1%85-%d1%84%d0%b8%d0%ba%d1%81%d1%82%d1%83%d1%80">
    #
  </a>
  Плотный блок про pytest: от базы до полезных встроенных фикстур
</h2>

<p>Начнем с базового примера с <code>assert</code>.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">multiply</span><span class="p">(</span><span class="n">a</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">int</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">a</span> <span class="o">*</span> <span class="n">b</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_multiply_basic</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">multiply</span><span class="p">(</span><span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">)</span> <span class="o">==</span> <span class="mi">12</span>
</span></span></code></pre></div><p>Параметризация позволяет расширять покрытие без копипаста.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.mark.parametrize</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;a,b,expected&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">6</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">10</span><span class="p">,</span> <span class="mi">0</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="o">-</span><span class="mi">6</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_multiply_parametrized</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">,</span> <span class="n">expected</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">multiply</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">)</span> <span class="o">==</span> <span class="n">expected</span>
</span></span></code></pre></div><p>Проверка исключений:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">divide</span><span class="p">(</span><span class="n">a</span><span class="p">:</span> <span class="nb">int</span><span class="p">,</span> <span class="n">b</span><span class="p">:</span> <span class="nb">int</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">float</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">b</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">raise</span> <span class="ne">ZeroDivisionError</span><span class="p">(</span><span class="s2">&#34;division by zero&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">a</span> <span class="o">/</span> <span class="n">b</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_divide_zero_raises</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">with</span> <span class="n">pytest</span><span class="o">.</span><span class="n">raises</span><span class="p">(</span><span class="ne">ZeroDivisionError</span><span class="p">,</span> <span class="k">match</span><span class="o">=</span><span class="s2">&#34;division by zero&#34;</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">        <span class="n">divide</span><span class="p">(</span><span class="mi">10</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
</span></span></code></pre></div><p><code>tmp_path</code> полезен для файловых сценариев.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">save_report</span><span class="p">(</span><span class="n">path</span><span class="p">,</span> <span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="n">path</span><span class="o">.</span><span class="n">write_text</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_save_report</span><span class="p">(</span><span class="n">tmp_path</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">report</span> <span class="o">=</span> <span class="n">tmp_path</span> <span class="o">/</span> <span class="s2">&#34;report.txt&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">save_report</span><span class="p">(</span><span class="n">report</span><span class="p">,</span> <span class="s2">&#34;ok&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">report</span><span class="o">.</span><span class="n">read_text</span><span class="p">(</span><span class="n">encoding</span><span class="o">=</span><span class="s2">&#34;utf-8&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="s2">&#34;ok&#34;</span>
</span></span></code></pre></div><p><code>monkeypatch</code> помогает временно менять окружение и атрибуты.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">os</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">service_url</span><span class="p">()</span> <span class="o">-&gt;</span> <span class="nb">str</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">os</span><span class="o">.</span><span class="n">getenv</span><span class="p">(</span><span class="s2">&#34;SERVICE_URL&#34;</span><span class="p">,</span> <span class="s2">&#34;https://prod.example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_service_url_from_env</span><span class="p">(</span><span class="n">monkeypatch</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="n">monkeypatch</span><span class="o">.</span><span class="n">setenv</span><span class="p">(</span><span class="s2">&#34;SERVICE_URL&#34;</span><span class="p">,</span> <span class="s2">&#34;https://test.example.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">service_url</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;https://test.example.com&#34;</span>
</span></span></code></pre></div><p>В реальных проектах такие встроенные инструменты снимают много
болезненного ручного кода в тестах.</p>

<h2 id="интеграция-типизации-и-тестов-две-сетки-безопасности-лучше-одной">
  <a class="link" href="#%d0%b8%d0%bd%d1%82%d0%b5%d0%b3%d1%80%d0%b0%d1%86%d0%b8%d1%8f-%d1%82%d0%b8%d0%bf%d0%b8%d0%b7%d0%b0%d1%86%d0%b8%d0%b8-%d0%b8-%d1%82%d0%b5%d1%81%d1%82%d0%be%d0%b2-%d0%b4%d0%b2%d0%b5-%d1%81%d0%b5%d1%82%d0%ba%d0%b8-%d0%b1%d0%b5%d0%b7%d0%be%d0%bf%d0%b0%d1%81%d0%bd%d0%be%d1%81%d1%82%d0%b8-%d0%bb%d1%83%d1%87%d1%88%d0%b5-%d0%be%d0%b4%d0%bd%d0%be%d0%b9">
    #
  </a>
  Интеграция типизации и тестов: две сетки безопасности лучше одной
</h2>

<p>Тесты и статический анализ закрывают разные области риска.
Type checker ловит типовые несоответствия до запуска.
Тесты ловят поведенческие и интеграционные ошибки при выполнении.</p>
<p>Например, type checker может заранее подсветить ситуацию,
когда функция обещает вернуть <code>int</code>, а фактически возвращает
<code>float</code> или <code>None</code>. Тесты в этот момент еще даже не запускались.</p>
<p>В CI зрелого проекта обычно запускают и type checker,
и pytest. Это дает плотный и быстрый контур качества.
Сначала отсекаются структурные ошибки, затем проверяется
поведение.</p>

<h2 id="cicd-как-сделать-тесты-частью-процесса-а-не-пожеланием">
  <a class="link" href="#cicd-%d0%ba%d0%b0%d0%ba-%d1%81%d0%b4%d0%b5%d0%bb%d0%b0%d1%82%d1%8c-%d1%82%d0%b5%d1%81%d1%82%d1%8b-%d1%87%d0%b0%d1%81%d1%82%d1%8c%d1%8e-%d0%bf%d1%80%d0%be%d1%86%d0%b5%d1%81%d1%81%d0%b0-%d0%b0-%d0%bd%d0%b5-%d0%bf%d0%be%d0%b6%d0%b5%d0%bb%d0%b0%d0%bd%d0%b8%d0%b5%d0%bc">
    #
  </a>
  CI/CD: как сделать тесты частью процесса, а не пожеланием
</h2>

<p>Если тесты живут только локально, их ценность ограничена.
Кто-то запускает их, кто-то нет, а качество становится вопросом
дисциплины отдельных людей. CI снимает этот риск, потому что
проверки становятся обязательным этапом потока.</p>
<p>Для GitHub Actions минимальный рабочий пайплайн может выглядеть так:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">tests</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pull_request</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">push</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">main]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/setup-python@v5</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">python-version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3.12&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">pip install -r requirements.txt</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">pip install pytest pytest-cov pyright</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">pyright</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">pytest tests/unit tests/integration --cov=app --cov-fail-under=80</span><span class="w">
</span></span></span></code></pre></div><p>Для GitLab CI логика та же:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">stages</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span>- <span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">stage</span><span class="p">:</span><span class="w"> </span><span class="l">test</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">python:3.12</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">script</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">pip install -r requirements.txt</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">pip install pytest pytest-cov pyright</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">pyright</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="l">pytest tests/unit tests/integration --cov=app --cov-fail-under=80</span><span class="w">
</span></span></span></code></pre></div><p>Ключевая инженерная мысль здесь простая. Неважно, какая платформа
CI выбрана. Важно, что правила качества автоматизированы и
единообразны для всех.</p>

<h2 id="частые-анти-паттерны-из-за-которых-тесты-теряют-ценность">
  <a class="link" href="#%d1%87%d0%b0%d1%81%d1%82%d1%8b%d0%b5-%d0%b0%d0%bd%d1%82%d0%b8-%d0%bf%d0%b0%d1%82%d1%82%d0%b5%d1%80%d0%bd%d1%8b-%d0%b8%d0%b7-%d0%b7%d0%b0-%d0%ba%d0%be%d1%82%d0%be%d1%80%d1%8b%d1%85-%d1%82%d0%b5%d1%81%d1%82%d1%8b-%d1%82%d0%b5%d1%80%d1%8f%d1%8e%d1%82-%d1%86%d0%b5%d0%bd%d0%bd%d0%be%d1%81%d1%82%d1%8c">
    #
  </a>
  Частые анти-паттерны, из-за которых тесты теряют ценность
</h2>

<p>Один из самых неприятных анти-паттернов это тестирование внутренней
реализации вместо внешнего поведения. Такой тест привязывается
к тому, как код написан сейчас, и ломается при любом рефакторинге,
даже если контракт не менялся.</p>
<p>Второй анти-паттерн это смешивание уровней в одном тесте.
Тест называется юнитом, но внутри идет в сеть, использует
реальную БД, зависит от текущего времени и случайности.
Результат предсказуем: долгий и нестабильный прогон.</p>
<p>Третий анти-паттерн это flaky-тесты, которые иногда красные,
иногда зеленые без изменений в коде. Как только команда привыкает
к мысли &ldquo;этот тест иногда сам по себе падает&rdquo;, доверие к тестовому
пакету рушится.</p>
<p>Четвертый анти-паттерн это попытка покрыть абсолютно все подряд,
включая чужие библиотеки и тривиальные прокси-методы без
бизнес-смысла. Покрытие растет, но полезный сигнал не растет.</p>

<h2 id="реальный-сценарий-red---green---refactor-на-короткой-функции">
  <a class="link" href="#%d1%80%d0%b5%d0%b0%d0%bb%d1%8c%d0%bd%d1%8b%d0%b9-%d1%81%d1%86%d0%b5%d0%bd%d0%b0%d1%80%d0%b8%d0%b9-red---green---refactor-%d0%bd%d0%b0-%d0%ba%d0%be%d1%80%d0%be%d1%82%d0%ba%d0%be%d0%b9-%d1%84%d1%83%d0%bd%d0%ba%d1%86%d0%b8%d0%b8">
    #
  </a>
  Реальный сценарий Red -&gt; Green -&gt; Refactor на короткой функции
</h2>

<p>Сделаем еще один компактный TDD-сценарий, на этот раз про валидацию
пароля. Требование такое: пароль должен быть длиной не меньше
восьми символов, содержать хотя бы одну цифру и хотя бы одну
букву.</p>
<p>Сначала Red:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_validate_password_ok</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">validate_password</span><span class="p">(</span><span class="s2">&#34;abc12345&#34;</span><span class="p">)</span> <span class="ow">is</span> <span class="kc">True</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_validate_password_too_short</span><span class="p">():</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">validate_password</span><span class="p">(</span><span class="s2">&#34;a1&#34;</span><span class="p">)</span> <span class="ow">is</span> <span class="kc">False</span>
</span></span></code></pre></div><p>Green, минимальная реализация:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">validate_password</span><span class="p">(</span><span class="n">value</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">bool</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="o">&lt;</span> <span class="mi">8</span><span class="p">:</span>
</span></span><span class="line"><span class="cl">        <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="cl">    <span class="n">has_digit</span> <span class="o">=</span> <span class="nb">any</span><span class="p">(</span><span class="n">ch</span><span class="o">.</span><span class="n">isdigit</span><span class="p">()</span> <span class="k">for</span> <span class="n">ch</span> <span class="ow">in</span> <span class="n">value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">has_alpha</span> <span class="o">=</span> <span class="nb">any</span><span class="p">(</span><span class="n">ch</span><span class="o">.</span><span class="n">isalpha</span><span class="p">()</span> <span class="k">for</span> <span class="n">ch</span> <span class="ow">in</span> <span class="n">value</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">return</span> <span class="n">has_digit</span> <span class="ow">and</span> <span class="n">has_alpha</span>
</span></span></code></pre></div><p>Refactor и расширение тестов:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nd">@pytest.mark.parametrize</span><span class="p">(</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;value,expected&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="p">[</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="s2">&#34;abc12345&#34;</span><span class="p">,</span> <span class="kc">True</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="s2">&#34;abcdefgh&#34;</span><span class="p">,</span> <span class="kc">False</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="s2">&#34;12345678&#34;</span><span class="p">,</span> <span class="kc">False</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">        <span class="p">(</span><span class="s2">&#34;ab12&#34;</span><span class="p">,</span> <span class="kc">False</span><span class="p">),</span>
</span></span><span class="line"><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">test_validate_password_parametrized</span><span class="p">(</span><span class="n">value</span><span class="p">,</span> <span class="n">expected</span><span class="p">):</span>
</span></span><span class="line"><span class="cl">    <span class="k">assert</span> <span class="n">validate_password</span><span class="p">(</span><span class="n">value</span><span class="p">)</span> <span class="ow">is</span> <span class="n">expected</span>
</span></span></code></pre></div><p>Это простой пример, но он хорошо показывает идею.
Тесты сначала формулируют контракт, потом поддерживают
безопасные изменения.</p>

<h2 id="что-делать-уже-завтра-в-реальном-проекте">
  <a class="link" href="#%d1%87%d1%82%d0%be-%d0%b4%d0%b5%d0%bb%d0%b0%d1%82%d1%8c-%d1%83%d0%b6%d0%b5-%d0%b7%d0%b0%d0%b2%d1%82%d1%80%d0%b0-%d0%b2-%d1%80%d0%b5%d0%b0%d0%bb%d1%8c%d0%bd%d0%be%d0%bc-%d0%bf%d1%80%d0%be%d0%b5%d0%ba%d1%82%d0%b5">
    #
  </a>
  Что делать уже завтра в реальном проекте
</h2>

<p>Если в проекте тестов мало или они хаотичны, не нужно пытаться
переписать все за одну итерацию. Практичнее начать с маленького,
но системного шага. Выделить критический бизнес-путь и закрыть его
базовым набором проверок на трех уровнях: быстрые юниты,
несколько интеграционных тестов на ключевые контракты и один-два
E2E сценария на пользовательскую ценность.</p>
<p>Параллельно стоит зафиксировать минимальные правила качества в CI.
Запуск юнитов, запуск интеграционных тестов, запуск type checker,
порог покрытия, блокировка merge при красном пайплайне. Это уже
создает каркас процесса, который можно постепенно усиливать.</p>
<p>По мере роста кода нужно следить не только за числом тестов,
но и за их полезностью. Каждый тест должен отвечать на вопрос,
какой риск он закрывает. Когда тесты начинают жить своей жизнью
и проверять не риск, а случайные детали, их ценность быстро
снижается.</p>

<h2 id="вместо-финала">
  <a class="link" href="#%d0%b2%d0%bc%d0%b5%d1%81%d1%82%d0%be-%d1%84%d0%b8%d0%bd%d0%b0%d0%bb%d0%b0">
    #
  </a>
  Вместо финала
</h2>

<p>Тестирование в Python не является бюрократией, если к нему
подходить как к инженерному инструменту, а не как к формальному
требованию. Тесты помогают быстрее двигаться, потому что дают
предсказуемый сигнал, удерживают систему в рабочем состоянии
и позволяют безопасно менять код, который уже приносит бизнесу
ценность.</p>
<p>Команда, которая умеет писать понятные юниты, осмысленные
интеграционные тесты и ограниченный набор E2E на критический путь,
обычно выпускает изменения чаще и спокойнее. Не потому, что у нее
&ldquo;больше тестов&rdquo;, а потому что у нее лучше управляется риск.</p>
<p>И в этом главный смысл всей темы. Тесты нужны не ради тестов.
Они нужны ради управляемой разработки.</p>
]]></content:encoded><category>python</category><category>testing</category><category>eosp</category></item></channel></rss>