<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Nyakio Muriuki J</title>
    <description>The latest articles on DEV Community by Nyakio Muriuki J (@nyakio).</description>
    <link>https://dev.to/nyakio</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2895889%2F5a5adef1-7b4d-4393-a687-cbba202064e9.jpeg</url>
      <title>DEV Community: Nyakio Muriuki J</title>
      <link>https://dev.to/nyakio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nyakio"/>
    <language>en</language>
    <item>
      <title>Pytest in Practice</title>
      <dc:creator>Nyakio Muriuki J</dc:creator>
      <pubDate>Wed, 23 Sep 2026 20:31:53 +0000</pubDate>
      <link>https://dev.to/nyakio/pytest-in-practice-4oda</link>
      <guid>https://dev.to/nyakio/pytest-in-practice-4oda</guid>
      <description>&lt;p&gt;I've been learning Python by building a video clipping CLI application, and along the way I've ended up learning a lot more than I expected.&lt;/p&gt;

&lt;p&gt;Some of it was straightforward. Some of it made me stop and think, especially when I first started writing tests.&lt;/p&gt;

&lt;p&gt;Things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How does pytest even know which functions are tests?&lt;/li&gt;
&lt;li&gt;Why does everyone keep putting things in &lt;code&gt;conftest.py&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;When should I use a fixture?&lt;/li&gt;
&lt;li&gt;What's the difference between &lt;code&gt;patch&lt;/code&gt; and &lt;code&gt;monkeypatch&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;Why did my mock not work when I was &lt;em&gt;sure&lt;/em&gt; I patched the right thing?&lt;/li&gt;
&lt;li&gt;When should I mock something, and when should I just run the real thing?&lt;/li&gt;
&lt;li&gt;What's the point of parametrization when I could just use a loop?&lt;/li&gt;
&lt;li&gt;How do I debug one failing test without running the entire suite every time?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are the kinds of things that can feel like arbitrary rules when you're first encountering them.&lt;/p&gt;

&lt;p&gt;Once I understood the reasoning behind them, though, pytest started making a lot more sense.&lt;/p&gt;

&lt;p&gt;So this isn't meant to be a complete pytest reference or an explanation of every feature pytest has. It's more like a collection of the things that finally "clicked" for me while testing a real Python project.&lt;/p&gt;

&lt;p&gt;The examples are based on my video clipping CLI, so you'll see things like FFmpeg, video metadata, audio extraction, transcription, temporary files, external dependencies, and slow tests.&lt;/p&gt;

&lt;p&gt;Hopefully, if you're also learning Python by building something rather than just following tutorials, some of these lessons save you a few of the "why does it work like &lt;em&gt;that&lt;/em&gt;?" moments I had.&lt;/p&gt;




&lt;h2&gt;
  
  
  Installing pytest
&lt;/h2&gt;

&lt;p&gt;If you're using &lt;code&gt;uv&lt;/code&gt;, installing pytest and coverage support is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add &lt;span class="nt"&gt;--dev&lt;/span&gt; pytest pytest-cov
uv run pytest &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You now have everything needed to start writing and running tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your First Test
&lt;/h2&gt;

&lt;p&gt;A pytest test can be nothing more than a function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/core/test_video_inspector.py
&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;clipper.core.video_inspector&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;get_video_metadata&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_get_video_metadata_returns_none_on_missing_file&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_video_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;does_not_exist.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it.&lt;/p&gt;

&lt;p&gt;There is no base class, no registration, and no decorator required.&lt;/p&gt;

&lt;p&gt;pytest finds the test because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the file starts with &lt;code&gt;test_&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the function starts with &lt;code&gt;test_&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That naming convention is the mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Does pytest Find Your Tests?
&lt;/h2&gt;

&lt;p&gt;pytest automatically walks the current directory and its subdirectories looking for test files.&lt;/p&gt;

&lt;p&gt;By default, it looks for files matching patterns such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_*.py
*_test.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside those files, it looks for functions matching:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_*
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and, inside classes whose names start with &lt;code&gt;Test*&lt;/code&gt;, methods that also match &lt;code&gt;test_*&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tests/
└── core/
    └── test_video_inspector.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is enough for pytest to discover the file automatically. You don't need to register the test anywhere.&lt;/p&gt;

&lt;p&gt;This is also why commands such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run pytest &lt;span class="nt"&gt;--cov&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;work without explicitly listing every test file. pytest's discovery mechanism does the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assertions: Just Use &lt;code&gt;assert&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;One of the nicest things about pytest is that you use Python's built-in &lt;code&gt;assert&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_fps_math&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;29.970029970029969&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;29.97&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't need a collection of methods like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="nf"&gt;assertTrue&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="nf"&gt;assertFalse&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;span class="nf"&gt;assertIn&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, pytest rewrites &lt;code&gt;assert&lt;/code&gt; statements when running tests so that failures contain useful information about what each side evaluated to. That makes a failed assertion much more informative than simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Assertion failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Comparing Floats with &lt;code&gt;pytest.approx&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Don't compare floating-point calculations using exact equality.&lt;/p&gt;

&lt;p&gt;Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_duration_seconds&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="mf"&gt;125.5&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;approx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;125.500001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Floating-point calculations rarely land exactly where you expect. This matters especially when dealing with things like frame rates, durations, timestamps, or calculations involving &lt;code&gt;Fraction&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Without &lt;code&gt;pytest.approx&lt;/code&gt;, you might end up writing this everywhere:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.001&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pytest.approx&lt;/code&gt; gives you the tolerance without repeating that boilerplate.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;pytest.approx&lt;/code&gt; Works with Nested Structures
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;pytest.approx&lt;/code&gt; isn't limited to individual numbers. You can use it when comparing nested structures:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_transcribe_matches_golden_output&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sample_speech_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;sample_transcript&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;transcribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_speech_path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;approx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_transcript&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is particularly useful for regression or "golden file" tests. Imagine a transcript like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;segments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.123456&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;end&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.987654&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;words&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;word&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.123&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;word&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;world&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;start&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.456&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't want to write a loop comparing every timestamp manually. &lt;code&gt;pytest.approx&lt;/code&gt; can recursively compare numeric values with tolerance while still using normal equality for strings and other values — so it's practical to compare an entire expected transcript while allowing for harmless floating-point differences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Testing That an Error Is Actually Raised
&lt;/h2&gt;

&lt;p&gt;When the expected behavior is for a function to raise an exception, use &lt;code&gt;pytest.raises&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_extract_audio_raises_on_bad_codec&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;extract_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out.wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;codec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;not_a_real_codec&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test verifies two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The code raises an exception.&lt;/li&gt;
&lt;li&gt;The exception is the expected type.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The test fails if the function doesn't raise anything. It also fails if it raises a different exception type. Both outcomes are useful information.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;pytest.raises&lt;/code&gt; Only Works When the Exception Escapes
&lt;/h3&gt;

&lt;p&gt;There's an important distinction here. Suppose your function catches its own exception:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_audio&lt;/span&gt;&lt;span class="p"&gt;(...):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exception never reaches the caller. Therefore, this won't work:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;extract_audio&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's nothing left for &lt;code&gt;pytest.raises&lt;/code&gt; to catch. Instead, test the function's actual contract:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_audio&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And, if logging is part of the behavior you care about, use &lt;code&gt;caplog&lt;/code&gt; to verify that the expected error was logged.&lt;/p&gt;

&lt;p&gt;A useful rule of thumb:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If the function returns &lt;code&gt;None&lt;/code&gt; from its &lt;code&gt;except&lt;/code&gt; block, test the return value rather than expecting an exception.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Parametrize: One Test, Many Inputs
&lt;/h2&gt;

&lt;p&gt;Suppose you need to test several timestamp formats. You could write three tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_timestamp_one&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_timestamp_two&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_timestamp_three&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the test logic is identical. That's exactly what &lt;code&gt;pytest.mark.parametrize&lt;/code&gt; is for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.mark.parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp,expected_seconds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;00:00:01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;00:01:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;60.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;01:00:00&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3600.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_validate_timestamp_parses_valid_formats&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;expected_seconds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;validate_timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;approx&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected_seconds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of it like a form letter: the first argument names the blanks to fill in (&lt;code&gt;timestamp&lt;/code&gt;, &lt;code&gt;expected_seconds&lt;/code&gt;), and the second argument is a stack of filled-in versions. pytest prints one out and runs the test for each one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_validate_timestamp_parses_valid_formats[00:00:01-1.0]
test_validate_timestamp_parses_valid_formats[00:01:00-60.0]
test_validate_timestamp_parses_valid_formats[01:00:00-3600.0]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each case can pass or fail independently.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why independence matters
&lt;/h3&gt;

&lt;p&gt;You could achieve something similar with a loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_timestamps&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cases&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;validate_timestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;expected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But there's a major difference: a normal loop stops at the first failure. With parametrization, pytest runs every case. So one test run tells you exactly which inputs are broken instead of merely telling you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;test_timestamps failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When should you use it?
&lt;/h3&gt;

&lt;p&gt;The signal is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you're about to write several nearly identical test functions that differ only in their input values, consider &lt;code&gt;parametrize&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It works just as well for valid inputs as it does for malformed or invalid ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Grouping Tests in a Class
&lt;/h2&gt;

&lt;p&gt;pytest supports test classes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestGetVideoMetadata&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_returns_none_for_missing_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;get_video_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_has_audio_true_when_audio_stream_present&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But classes aren't mandatory. Use them when they provide something useful, such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;shared setup&lt;/li&gt;
&lt;li&gt;fixtures scoped to the group&lt;/li&gt;
&lt;li&gt;applying a marker to multiple tests&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Don't use a class purely because it feels more organized. A well-named test file already provides plenty of organization.&lt;/p&gt;

&lt;h3&gt;
  
  
  An important detail about test classes
&lt;/h3&gt;

&lt;p&gt;Each test method gets a fresh instance of the class. So this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TestSomething&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_two&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not valid. &lt;code&gt;test_two&lt;/code&gt; doesn't receive the instance used by &lt;code&gt;test_one&lt;/code&gt;. That's intentional — tests should be isolated from one another. Depending on mutable state from another test creates order-dependent and difficult-to-debug failures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixtures
&lt;/h2&gt;

&lt;p&gt;Fixtures are one of pytest's most useful features. A fixture is a function that provides something a test needs — think of it like an ingredient a recipe asks for by name. You don't prep it inside the recipe itself; you just say "I need &lt;code&gt;sample_video_path&lt;/code&gt;" and pytest hands it to you already made.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/conftest.py
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;


&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sample_video_path&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;__file__&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fixtures&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sample.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A test can then request it simply by putting the fixture name in its parameters:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# tests/core/test_video_inspector.py
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_metadata_against_real_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_video_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_video_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_video_path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pytest sees &lt;code&gt;sample_video_path&lt;/code&gt; in the test signature, finds the fixture with that name, runs it, and passes the result to the test. No import is required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why &lt;code&gt;conftest.py&lt;/code&gt; Matters
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;conftest.py&lt;/code&gt; is automatically discovered by pytest. Fixtures defined there are available to test files in the same directory and its subdirectories — it works like a shared pantry that any test file in that part of the tree can pull from without asking for a delivery.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tests/
├── conftest.py
├── core/
│   └── test_video_inspector.py
└── api/
    └── test_transcription.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A fixture defined in &lt;code&gt;tests/conftest.py&lt;/code&gt; is available to both &lt;code&gt;tests/core/&lt;/code&gt; and &lt;code&gt;tests/api/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can also have a more specific &lt;code&gt;conftest.py&lt;/code&gt; deeper in the tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tests/
├── conftest.py
└── core/
    ├── conftest.py
    └── test_video_inspector.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deeper fixture configuration can then be used specifically by that subtree. A useful convention is: root &lt;code&gt;conftest.py&lt;/code&gt; for shared fixtures, nested &lt;code&gt;conftest.py&lt;/code&gt; files for fixtures that are genuinely local to a particular test area.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixture Scope
&lt;/h2&gt;

&lt;p&gt;Fixtures don't necessarily have to run for every test. You can control their lifetime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;function&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# default
&lt;/span&gt;&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;module&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;# once per test file
&lt;/span&gt;&lt;span class="nd"&gt;@pytest.fixture&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;session&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# once per entire test run
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The default is &lt;code&gt;scope="function"&lt;/code&gt;, which means a fresh fixture is created for every test. Prefer the default unless there is a good reason to reuse the fixture.&lt;/p&gt;

&lt;p&gt;A broader scope can make sense when setup is genuinely expensive and safe to share. For example, generating a synthetic sample video once per session may be much cheaper than generating it once for every test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built-in Fixtures Worth Knowing
&lt;/h2&gt;

&lt;p&gt;pytest provides several useful fixtures out of the box.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;tmp_path&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Provides a unique temporary directory for each test. Great for anything that needs to write files.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;caplog&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Captures logging output. Useful when you want to verify that something logged an error or warning.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;capsys&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Captures stdout and stderr. Useful for testing CLI output.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;monkeypatch&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Temporarily changes attributes, environment variables, dictionary entries, and other values. pytest automatically restores the changes after the test.&lt;/p&gt;

&lt;p&gt;You can inspect available fixtures with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run pytest &lt;span class="nt"&gt;--fixtures&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;code&gt;tmp_path&lt;/code&gt; in Practice
&lt;/h2&gt;

&lt;p&gt;Suppose you want to verify that audio extraction creates an actual file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_extract_audio_writes_real_file&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sample_video_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;output_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tmp_path&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output.wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_video_path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;output_path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is an integration-style test. It actually runs FFmpeg and checks that a real file was created. But because &lt;code&gt;tmp_path&lt;/code&gt; is temporary, the test doesn't leave generated files scattered around your project — pytest creates the directory and handles its cleanup.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mocking: Two Tools, Different Jobs
&lt;/h2&gt;

&lt;p&gt;Python gives you &lt;code&gt;unittest.mock&lt;/code&gt;. pytest gives you &lt;code&gt;monkeypatch&lt;/code&gt;. They overlap, but each one shines in a different situation.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;unittest.mock.patch&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;patch&lt;/code&gt; when you want a more capable mock, particularly when you need to inspect how something was called.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;unittest.mock&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;patch&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_metadata_no_audio&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;fake_probe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;format&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;filename&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;silent.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duration&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;streams&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="p"&gt;{&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;codec_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;video&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;r_frame_rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;25/1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clipper.core.video_inspector.ffmpeg.probe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;return_value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fake_probe&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_video_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;silent.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;has_audio&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;patch&lt;/code&gt; temporarily replaces the target with a mock. It is particularly useful when you want to assert things like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;mock_input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_called_once_with&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;code&gt;monkeypatch&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;monkeypatch&lt;/code&gt; is pytest's own fixture. It's particularly convenient for simple temporary substitutions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_uses_default_bitrate_env_var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;setenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CLIPPER_DEFAULT_BITRATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;256k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;get_default_bitrate&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;256k&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's no context manager and no manual cleanup — pytest automatically restores the environment variable after the test. You can also replace attributes with &lt;code&gt;monkeypatch.setattr(...)&lt;/code&gt; or dictionary entries with &lt;code&gt;monkeypatch.setitem(...)&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;patch&lt;/code&gt; vs &lt;code&gt;monkeypatch&lt;/code&gt;: A Practical Mental Model
&lt;/h2&gt;

&lt;p&gt;Imagine your code is a toy robot and you want to test what happens when one of its components behaves differently. You don't want to actually break the component — you just want to temporarily pretend that it behaves differently.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;monkeypatch&lt;/code&gt; is like putting a temporary sticky note over the component: "For this test, pretend this value is different." pytest removes the note when the test finishes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;patch&lt;/code&gt; is another version of the same idea, but with a much more powerful mock object behind it. &lt;code&gt;patch&lt;/code&gt; can easily track call count, arguments, return values, exceptions, and chained calls.&lt;/p&gt;

&lt;p&gt;Both clean themselves up reliably when used correctly. With &lt;code&gt;patch&lt;/code&gt;, the common form in this project is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(...):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The mock is restored automatically when the block ends, even if the test fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Which One Should You Use?
&lt;/h3&gt;

&lt;p&gt;A practical rule:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Need&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Assert how something was called&lt;/td&gt;
&lt;td&gt;&lt;code&gt;patch&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check call count or exact arguments&lt;/td&gt;
&lt;td&gt;&lt;code&gt;patch&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mock a chained/fluent API&lt;/td&gt;
&lt;td&gt;&lt;code&gt;patch&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replace an environment variable&lt;/td&gt;
&lt;td&gt;&lt;code&gt;monkeypatch&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporarily change one attribute&lt;/td&gt;
&lt;td&gt;&lt;code&gt;monkeypatch.setattr(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Temporarily change a dictionary entry&lt;/td&gt;
&lt;td&gt;&lt;code&gt;monkeypatch.setitem(...)&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Replace an external dependency and inspect the mock&lt;/td&gt;
&lt;td&gt;&lt;code&gt;patch&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important thing isn't to memorize a strict rule. It's to recognize the distinction:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you need to inspect how the dependency was used, &lt;code&gt;patch&lt;/code&gt; is usually the natural choice. If you just need to temporarily replace a value or attribute, &lt;code&gt;monkeypatch&lt;/code&gt; is often simpler.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;side_effect&lt;/code&gt;: Make a Mock Fail
&lt;/h2&gt;

&lt;p&gt;Mocks aren't only useful for returning fake successful results. They can also simulate failures.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;return_value&lt;/code&gt; means "always return this." &lt;code&gt;side_effect&lt;/code&gt; means "raise this exception instead."&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clipper.core.video_inspector.ffmpeg.probe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;side_effect&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ffprobe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bad file&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_video_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bad.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets you exercise failure paths without needing to create a genuinely broken video file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mock the Boundary, Not Your Own Logic
&lt;/h2&gt;

&lt;p&gt;This is probably the most important mocking rule in the entire guide:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Mock the boundary, not the logic you're trying to test.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Suppose your code does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;probe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting logic is what your application does with the result. So mock &lt;code&gt;ffmpeg.probe&lt;/code&gt; and provide realistic fake data — don't mock the parsing logic itself. A test that mocks everything, including the thing you're trying to test, can end up proving nothing more than: "my mocks returned what I told them to return."&lt;/p&gt;

&lt;p&gt;Put another way — you're testing your recipe, not reinventing the oven. External boundaries are good mocking candidates:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FFmpeg&lt;/li&gt;
&lt;li&gt;HTTP APIs&lt;/li&gt;
&lt;li&gt;databases&lt;/li&gt;
&lt;li&gt;filesystem operations&lt;/li&gt;
&lt;li&gt;cloud services&lt;/li&gt;
&lt;li&gt;external processes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Your own business logic is usually what you want to execute for real.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Most Common Mocking Mistake: Patching the Wrong Path
&lt;/h2&gt;

&lt;p&gt;This catches a lot of people. Suppose your module contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# video_inspector.py
&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ffmpeg&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_video_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;probe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might think you should patch &lt;code&gt;ffmpeg.probe&lt;/code&gt;. But that's not necessarily the correct target — the code under test looks up &lt;code&gt;ffmpeg&lt;/code&gt; in its own module namespace. Therefore, patch where the name is looked up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clipper.core.video_inspector.ffmpeg.probe&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The general rule is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Patch where the object is looked up, not where it was originally defined.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you patch the wrong path, the real dependency may still run. That can make a test unexpectedly hit FFmpeg, the network, a database, or some other external system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mocking a Chained Call
&lt;/h2&gt;

&lt;p&gt;Some APIs aren't a single function call. For example, &lt;code&gt;ffmpeg-python&lt;/code&gt; can use a fluent chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;input&lt;/span&gt;&lt;span class="p"&gt;(...).&lt;/span&gt;&lt;span class="nf"&gt;output&lt;/span&gt;&lt;span class="p"&gt;(...).&lt;/span&gt;&lt;span class="nf"&gt;overwrite_output&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mocking that requires walking the chain. &lt;code&gt;MagicMock&lt;/code&gt; automatically creates mocks for attributes you access, so &lt;code&gt;mock_input.return_value&lt;/code&gt; represents the result of &lt;code&gt;ffmpeg.input(...)&lt;/code&gt;, &lt;code&gt;.output.return_value&lt;/code&gt; represents the next link in the chain, and so on until you reach &lt;code&gt;.run&lt;/code&gt; — the actual operation you want to make fail.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_extract_audio_sad_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caplog&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;fake_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ffmpeg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ffmpeg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;stderr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;encoding failed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;clipper.core.video_inspector.ffmpeg.input&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;mock_input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;mock_run&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;mock_input&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;overwrite_output&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;return_value&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;mock_run&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;side_effect&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fake_error&lt;/span&gt;

        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_audio&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;in.mp4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out.wav&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;caplog&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Walking the chain into its own variable (&lt;code&gt;mock_run&lt;/code&gt;) before setting &lt;code&gt;side_effect&lt;/code&gt; keeps the assignment itself simple and unambiguous — trying to set an attribute in the middle of a long parenthesized chain is a common source of syntax mistakes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Organizing Your Test Files
&lt;/h2&gt;

&lt;p&gt;A simple and predictable test layout goes a long way. If your source looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
└── clipper/
    └── core/
        └── video_inspector.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;mirror it under &lt;code&gt;tests/&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tests/
└── core/
    └── test_video_inspector.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes it immediately obvious where the tests for a source module live.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep &lt;code&gt;tests/&lt;/code&gt; Outside &lt;code&gt;src/&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Use:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;project/
├── src/
├── tests/
└── pyproject.toml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;rather than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
├── clipper/
└── tests/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keeping tests outside the package helps avoid accidentally shipping them with your built distribution and keeps the distinction between application code and test code clear.&lt;/p&gt;

&lt;h3&gt;
  
  
  You Usually Don't Need &lt;code&gt;__init__.py&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;pytest doesn't require your test directories to be Python packages. So this is perfectly fine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tests/
├── core/
│   └── test_video_inspector.py
└── api/
    └── test_transcription.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No &lt;code&gt;__init__.py&lt;/code&gt; required. There can be edge cases where you intentionally want package semantics, but don't add &lt;code&gt;__init__.py&lt;/code&gt; to test directories just because you think pytest requires it. It doesn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  Keep Fixtures Separate from Test Code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tests/
├── conftest.py
├── fixtures/
│   └── sample.mp4
└── core/
    └── test_video_inspector.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This makes the distinction obvious: &lt;code&gt;test_*.py&lt;/code&gt; contains test logic, &lt;code&gt;fixtures/&lt;/code&gt; contains test data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mark Slow and External Tests
&lt;/h2&gt;

&lt;p&gt;Not every test costs the same amount to run. A test that calls real FFmpeg is different from a test that checks a pure Python function.&lt;/p&gt;

&lt;p&gt;You can mark integration tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.mark.integration&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_extract_audio_against_real_file&lt;/span&gt;&lt;span class="p"&gt;(...):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then register the marker in &lt;code&gt;pyproject.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.pytest.ini_options]&lt;/span&gt;
&lt;span class="py"&gt;markers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"integration: calls real ffmpeg/ffprobe against a fixture file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you can run the fast suite without integration tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"not integration"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And run everything with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives you a useful distinction between fast unit tests and slower integration tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging pytest Without Sprinkling &lt;code&gt;print()&lt;/code&gt; Everywhere
&lt;/h2&gt;

&lt;p&gt;pytest captures output by default. That can be surprising when you're debugging — a &lt;code&gt;print(result)&lt;/code&gt; that produces nothing on screen. Here are the options you'll reach for most often.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;-s&lt;/code&gt;: Show stdout/stderr
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run pytest &lt;span class="nt"&gt;-s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This disables output capturing, so your &lt;code&gt;print()&lt;/code&gt; statements become visible. It's useful, although there are usually better debugging tools than adding prints everywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;-k&lt;/code&gt;: Run Tests by Name
&lt;/h3&gt;

&lt;p&gt;If you're working on audio extraction, there's no reason to run 200 unrelated tests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run pytest &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="s2"&gt;"extract_audio"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pytest runs tests whose names match the expression. This is one of the most useful options during active development.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;-x&lt;/code&gt;: Stop at the First Failure
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run pytest &lt;span class="nt"&gt;-x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of getting a wall of failures, pytest stops after the first one. This is particularly useful when fixing a cascading failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;--pdb&lt;/code&gt;: Drop Into the Debugger
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run pytest &lt;span class="nt"&gt;--pdb&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When a test fails, pytest drops you into an interactive debugger at the point of failure. Inside the debugger, you can inspect variables and control execution — &lt;code&gt;c&lt;/code&gt; continues, &lt;code&gt;q&lt;/code&gt; quits. This can be much faster than trying to predict where a bug is before running the test.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;-vv&lt;/code&gt;: More Verbose Output
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run pytest &lt;span class="nt"&gt;-vv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful when assertions involve large dictionaries, long strings, or parametrized tests and you want more detail in the output.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;--lf&lt;/code&gt;: Run Only the Last Failures
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run pytest &lt;span class="nt"&gt;--lf&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--lf&lt;/code&gt; means "last failed." It reruns tests that failed during the previous run instead of running the entire suite. This is particularly useful when you're fixing several failures one by one.&lt;/p&gt;

&lt;h3&gt;
  
  
  A Practical Debugging Command
&lt;/h3&gt;

&lt;p&gt;These options can be combined:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run pytest &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="s2"&gt;"extract_audio"&lt;/span&gt; &lt;span class="nt"&gt;-x&lt;/span&gt; &lt;span class="nt"&gt;--pdb&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means: run only tests matching &lt;code&gt;extract_audio&lt;/code&gt;, stop at the first failure, and open the debugger at the failure. That's a very practical development loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running a Specific Test
&lt;/h2&gt;

&lt;p&gt;pytest doesn't natively use line numbers to select a test. This isn't standard pytest test selection:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest test_file.py:29
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead, select by node ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest tests/core/test_video_inspector.py::test_metadata_no_audio
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use &lt;code&gt;-k&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;-k&lt;/span&gt; &lt;span class="s2"&gt;"metadata_no_audio"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Editors such as VS Code and PyCharm can provide "run this test" buttons. They translate your selection into pytest's name-based test selection behind the scenes. There are plugins that provide literal line-number selection, but you generally don't need one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quickly Inspecting a Return Value
&lt;/h2&gt;

&lt;p&gt;Sometimes you just want to see what a function actually returned. One quick technique is to deliberately make an assertion fail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_scratch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_video_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;get_video_metadata&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sample_video_path&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;{}&lt;/code&gt; isn't the actual result, pytest's failure output will show you the value. Delete the temporary test afterward.&lt;/p&gt;

&lt;p&gt;Another option is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run pytest &lt;span class="nt"&gt;--showlocals&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or the shorthand &lt;code&gt;-l&lt;/code&gt;, which shows local variables when a test fails.&lt;/p&gt;

&lt;h2&gt;
  
  
  Dealing with Slow Tests
&lt;/h2&gt;

&lt;p&gt;Don't guess which tests are slow. Measure them.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run pytest &lt;span class="nt"&gt;--durations&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;10
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reports the ten slowest tests after the run. You may discover that one or two tests account for most of the delay. Optimizing everything before measuring can waste the very time you're trying to save.&lt;/p&gt;

&lt;h3&gt;
  
  
  Mark Slow Tests
&lt;/h3&gt;

&lt;p&gt;Tests that load a real model or perform real inference are good candidates for a &lt;code&gt;slow&lt;/code&gt; marker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest.mark.slow&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_transcribe_real_audio_produces_valid_shape&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;sample_speech_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Register the marker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[tool.pytest.ini_options]&lt;/span&gt;
&lt;span class="py"&gt;markers&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s"&gt;"integration: calls real ffmpeg/ffprobe against a fixture file"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"slow: tests that load a real model or run real transcription"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then your normal development loop can skip them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pytest &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"not slow"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And your complete suite can still run with &lt;code&gt;pytest&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Caching Can Already Solve Some Performance Problems
&lt;/h3&gt;

&lt;p&gt;Suppose your application loads a model once at module level, and subsequent calls reuse it. Then only the first test that needs the model may pay the expensive loading cost — the others can reuse the already-loaded instance.&lt;/p&gt;

&lt;p&gt;You can verify whether this is actually happening with &lt;code&gt;uv run pytest --durations=10&lt;/code&gt;. If the first model-related test is slow and subsequent tests are much faster, your existing caching may already be doing the job. If every test is slow, something may be reloading the model when it shouldn't. Measure first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Parallelizing the Suite
&lt;/h3&gt;

&lt;p&gt;If the entire suite is slow, rather than just a handful of expensive tests, parallel execution may help.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv add &lt;span class="nt"&gt;--dev&lt;/span&gt; pytest-xdist
uv run pytest &lt;span class="nt"&gt;-n&lt;/span&gt; auto
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-n auto&lt;/code&gt; lets xdist determine a useful number of workers. But don't reach for parallelization automatically — for many projects, simply excluding slow tests during development (&lt;code&gt;pytest -m "not slow"&lt;/code&gt;) is enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coverage Is a Flashlight, Not a Scoreboard
&lt;/h2&gt;

&lt;p&gt;Coverage is useful. It tells you which lines of code weren't exercised by your tests.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;uv run pytest &lt;span class="nt"&gt;--cov&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This can help you find code paths you haven't thought about. But coverage doesn't tell you whether your tests are good. You can achieve 100% coverage with tests that assert almost nothing meaningful:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_everything&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="nf"&gt;my_function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code executed. Coverage increases. But the test may tell you almost nothing about whether the behavior is correct. So treat coverage as a flashlight — use it to illuminate parts of the code you haven't tested. Don't treat it as a scoreboard you have to maximize.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Simple Checklist for Every New Test
&lt;/h2&gt;

&lt;p&gt;When adding tests for a new function or feature, ask:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. What's the happy path?
&lt;/h4&gt;

&lt;p&gt;What should happen when everything is valid?&lt;/p&gt;

&lt;h4&gt;
  
  
  2. What's at least one edge case?
&lt;/h4&gt;

&lt;p&gt;Consider: empty input, missing fields, boundary values, unexpected but valid combinations.&lt;/p&gt;

&lt;h4&gt;
  
  
  3. What's the failure mode?
&lt;/h4&gt;

&lt;p&gt;What should happen when input is invalid, an external dependency fails, a file doesn't exist, or an API returns an unexpected result?&lt;/p&gt;

&lt;h4&gt;
  
  
  4. What should be mocked?
&lt;/h4&gt;

&lt;p&gt;Usually mock things that are external, slow, non-deterministic, or side-effecting. But let your own logic run for real.&lt;/p&gt;

&lt;h4&gt;
  
  
  5. Is the test asking one question?
&lt;/h4&gt;

&lt;p&gt;If the test name contains the word "and," that's sometimes a sign that you're testing two behaviors at once. A test should ideally answer one clear question.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Mental Model I Ended Up With
&lt;/h2&gt;

&lt;p&gt;pytest can look like a large collection of features when you're first learning it. But after working with it for a while, I found that most of what I actually needed came down to a few ideas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tests are ordinary Python functions.&lt;/strong&gt; pytest discovers them through naming conventions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Assertions are ordinary Python &lt;code&gt;assert&lt;/code&gt; statements.&lt;/strong&gt; pytest makes their failures informative.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fixtures provide reusable things that tests need.&lt;/strong&gt; &lt;code&gt;conftest.py&lt;/code&gt; makes shared fixtures available without imports.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Parametrization lets you run the same test logic against multiple inputs.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mocks let you replace boundaries you don't want to exercise for real.&lt;/strong&gt; Things like FFmpeg, APIs, databases, and other external systems are often good candidates — while the logic you're actually testing should usually run normally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Markers let you separate tests by purpose or cost.&lt;/strong&gt; And the debugging tools make it much easier to figure out why something failed without resorting to &lt;code&gt;print()&lt;/code&gt; everywhere.&lt;/p&gt;

&lt;p&gt;Most importantly, I stopped thinking of testing as "how do I get my coverage number up?" and started thinking more in terms of "what question am I trying to answer with this test?" For me, that shift made pytest much easier to understand.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The syntax is the easy part. The useful part is learning what behavior is worth testing, what should be isolated, what should run for real, and how to make a failure tell you something useful.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>testing</category>
      <category>tutorial</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Learning `just`: A Practical Task Runner</title>
      <dc:creator>Nyakio Muriuki J</dc:creator>
      <pubDate>Thu, 03 Sep 2026 13:30:33 +0000</pubDate>
      <link>https://dev.to/nyakio/learning-just-a-practical-task-runner-2lk1</link>
      <guid>https://dev.to/nyakio/learning-just-a-practical-task-runner-2lk1</guid>
      <description>&lt;p&gt;I've been learning &lt;code&gt;just&lt;/code&gt; recently.&lt;/p&gt;

&lt;p&gt;I came across it while setting up a Python project and looking for a simple way to collect all the commands I use during development.&lt;/p&gt;

&lt;p&gt;This started with something I got used to in Elixir.&lt;/p&gt;

&lt;p&gt;One of the things I really like about Elixir projects is that you can put the common development tasks behind simple commands/aliases:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix format
mix &lt;span class="nb"&gt;test
&lt;/span&gt;mix ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't have to remember every command that runs underneath. The project gives you a small set of commands and you use those.&lt;/p&gt;

&lt;p&gt;I wanted something similar for a Python project.&lt;/p&gt;

&lt;p&gt;For example, instead of remembering all the different checks I need to run, I wanted to be able to do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and let the project take care of the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  My first thought was Make
&lt;/h2&gt;

&lt;p&gt;Make was the obvious choice.&lt;/p&gt;

&lt;p&gt;I've used Make a lot, and it works. It's installed almost everywhere, most developers have seen a Makefile before, and there is a huge amount of existing knowledge around it.&lt;/p&gt;

&lt;p&gt;So I started there.&lt;/p&gt;

&lt;p&gt;The setup was pretty small:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;format&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    uv run ruff format &lt;span class="nt"&gt;--check&lt;/span&gt; .

&lt;span class="nl"&gt;lint&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    uv run ruff check .

&lt;span class="nl"&gt;typecheck&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    uv run mypy src/

&lt;span class="nl"&gt;test&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
    uv run pytest &lt;span class="nt"&gt;--cov&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;src &lt;span class="nt"&gt;--cov-report&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;term-missing

&lt;span class="nl"&gt;ci&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;format lint typecheck test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That gave me exactly what I wanted.&lt;/p&gt;

&lt;p&gt;I could run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;make format
make lint
make typecheck
make &lt;span class="nb"&gt;test
&lt;/span&gt;make ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing complicated.&lt;/p&gt;

&lt;p&gt;But while doing this, I kept seeing &lt;code&gt;just&lt;/code&gt; mentioned in newer projects.&lt;/p&gt;

&lt;p&gt;That made me curious.&lt;/p&gt;

&lt;p&gt;Not because I thought Make was bad. I still use it a lot, and I have no problem reaching for it.&lt;/p&gt;

&lt;p&gt;I just wondered what &lt;code&gt;just&lt;/code&gt; was doing differently.&lt;/p&gt;

&lt;p&gt;Was it basically Make with different syntax? Was it easier to use? Did it solve some of the small annoyances that come with Make?&lt;/p&gt;

&lt;p&gt;So I decided to rebuild the same thing in &lt;code&gt;just&lt;/code&gt; and see.&lt;/p&gt;

&lt;h2&gt;
  
  
  The &lt;code&gt;just&lt;/code&gt; version
&lt;/h2&gt;

&lt;p&gt;The nice thing was that I didn't have to change any of the actual commands.&lt;/p&gt;

&lt;p&gt;I just changed the wrapper around them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;format:
    uv run ruff format --check .

lint:
    uv run ruff check .

typecheck:
    uv run mypy src/

test:
    uv run pytest --cov=src --cov-report=term-missing

ci: format lint typecheck test
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's pretty much the whole file.&lt;/p&gt;

&lt;p&gt;Now I can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just format
just lint
just typecheck
just &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or everything with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ci&lt;/code&gt; recipe depends on the other four recipes, so &lt;code&gt;just ci&lt;/code&gt; runs them in order and stops when one fails.&lt;/p&gt;

&lt;p&gt;For what I was trying to do, it felt very familiar.&lt;/p&gt;

&lt;p&gt;And that was probably the first thing I noticed. I didn't really have to learn a new way of thinking about the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  A few things I liked about &lt;code&gt;just&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The basic syntax is very similar to Make.&lt;/p&gt;

&lt;p&gt;You have a recipe, optional dependencies, and then the commands to run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;recipe_name: dependency1 dependency2
    command to run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But there are a few differences that make sense when you're using it mainly as a task runner.&lt;/p&gt;

&lt;h3&gt;
  
  
  No &lt;code&gt;.PHONY&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;With Make, you often have something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight make"&gt;&lt;code&gt;&lt;span class="nl"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;format lint typecheck test ci&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's because Make is also a build system and cares about files and timestamps.&lt;/p&gt;

&lt;p&gt;For these development commands, I don't really need that behaviour.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;just&lt;/code&gt; doesn't use file timestamps to decide whether a recipe needs to run, so there is no &lt;code&gt;.PHONY&lt;/code&gt; declaration to maintain.&lt;/p&gt;

&lt;h3&gt;
  
  
  No tab problem
&lt;/h3&gt;

&lt;p&gt;Then there is the classic Make issue where a command needs to be indented with a tab.&lt;/p&gt;

&lt;p&gt;I've definitely lost time to that one.&lt;/p&gt;

&lt;p&gt;With &lt;code&gt;just&lt;/code&gt;, you don't have that same tab requirement. You use normal indentation.&lt;/p&gt;

&lt;p&gt;It might seem like a small thing, but I like small improvements in tools I use.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;just --list&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This was another nice surprise.&lt;/p&gt;

&lt;p&gt;You can run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just &lt;span class="nt"&gt;--list&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and get a list of the recipes available in the project.&lt;/p&gt;

&lt;p&gt;You can add comments above recipes too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Format check
format:
    uv run ruff format --check .

# Run tests
test:
    uv run pytest --cov=src --cov-report=term-missing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those comments can then show up when listing the recipes.&lt;/p&gt;

&lt;p&gt;You can also make &lt;code&gt;just&lt;/code&gt; itself show the list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;default:
    @just --list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So a developer can clone the project and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to see what is available.&lt;/p&gt;

&lt;p&gt;I like that because the task runner becomes a small piece of documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  But is &lt;code&gt;just&lt;/code&gt; actually better than Make?
&lt;/h2&gt;

&lt;p&gt;I'm not sure that's the right question.&lt;/p&gt;

&lt;p&gt;Make is still a great choice in plenty of situations.&lt;/p&gt;

&lt;p&gt;If I'm working on a project that already has a Makefile, I'm not going to suggest replacing it with &lt;code&gt;just&lt;/code&gt; just because &lt;code&gt;just&lt;/code&gt; is newer.&lt;/p&gt;

&lt;p&gt;And if I'm writing something that needs to work in an environment where Make is already installed, that is a pretty good reason to use Make.&lt;/p&gt;

&lt;p&gt;The thing I found interesting about &lt;code&gt;just&lt;/code&gt; is that it feels more focused on the thing I was actually trying to do.&lt;/p&gt;

&lt;p&gt;I wasn't building anything based on file timestamps.&lt;/p&gt;

&lt;p&gt;I just wanted names for common commands and a way to combine them.&lt;/p&gt;

&lt;p&gt;For that use case, &lt;code&gt;just&lt;/code&gt; is pretty nice.&lt;/p&gt;

&lt;p&gt;But it also comes with a trade-off. Unlike Make, you probably have to install it first.&lt;/p&gt;

&lt;p&gt;So the comparison for me looks something like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;Make&lt;/th&gt;
&lt;th&gt;just&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Already installed&lt;/td&gt;
&lt;td&gt;Often&lt;/td&gt;
&lt;td&gt;Usually not&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task runner&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Build system&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.PHONY&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Often needed&lt;/td&gt;
&lt;td&gt;Not needed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tab requirement&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Task listing&lt;/td&gt;
&lt;td&gt;DIY&lt;/td&gt;
&lt;td&gt;Built in&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-platform&lt;/td&gt;
&lt;td&gt;Possible&lt;/td&gt;
&lt;td&gt;Designed for it&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Neither one wins across the board.&lt;/p&gt;

&lt;p&gt;Make has ubiquity.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;just&lt;/code&gt; has a nice developer experience.&lt;/p&gt;

&lt;p&gt;Which one makes more sense depends on the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  There are other options too
&lt;/h2&gt;

&lt;p&gt;Once I started looking into this, I found quite a few other tools doing roughly the same thing.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Task&lt;/code&gt; (go-task) was one I came across. It uses a YAML &lt;code&gt;Taskfile.yml&lt;/code&gt; and has some useful features around variables and templating.&lt;/p&gt;

&lt;p&gt;For Python, &lt;code&gt;poethepoet&lt;/code&gt; was another interesting option. It lets you put tasks in &lt;code&gt;pyproject.toml&lt;/code&gt;, which can be nice if you don't want another configuration file.&lt;/p&gt;

&lt;p&gt;And if you're working in Node, npm scripts already give you a similar setup through &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;They all solve a similar problem.&lt;/p&gt;

&lt;p&gt;You have a bunch of commands that developers need to run, and you give those commands names so people don't have to remember all the details.&lt;/p&gt;

&lt;p&gt;The differences are mostly around syntax, ecosystem, and how much functionality you want.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, am I switching to &lt;code&gt;just&lt;/code&gt;?
&lt;/h2&gt;

&lt;p&gt;For everything? No.&lt;/p&gt;

&lt;p&gt;I'm still going to use Make.&lt;/p&gt;

&lt;p&gt;If I open a project and there is a Makefile, I'll use it. If I'm working on infrastructure where Make is already expected, I'll probably use it there too.&lt;/p&gt;

&lt;p&gt;But for projects I start myself, I can see myself reaching for &lt;code&gt;just&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The thing I liked most was how little there was to learn.&lt;/p&gt;

&lt;p&gt;My existing commands stayed exactly the same. I just put a small, readable interface around them.&lt;/p&gt;

&lt;p&gt;That brought me back to why I started looking at this in the first place.&lt;/p&gt;

&lt;p&gt;In Elixir, I'm used to having commands like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mix ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;that give the project a simple interface for common development tasks.&lt;/p&gt;

&lt;p&gt;With Python, I found myself wanting the same thing.&lt;/p&gt;

&lt;p&gt;Make was one way to do it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;just&lt;/code&gt; is another.&lt;/p&gt;

&lt;p&gt;I'm glad I spent the time learning it. Not because it replaced Make, but because now I know another tool that fits this particular problem really well.&lt;/p&gt;

&lt;p&gt;And sometimes that's enough of a reason to try something new.&lt;/p&gt;

</description>
      <category>python</category>
      <category>ci</category>
      <category>tutorial</category>
      <category>learning</category>
    </item>
  </channel>
</rss>
