<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Pytest on Tarragon</title><link>https://tarrragon.github.io/blog/tags/pytest/</link><description>Recent content in Pytest on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 19 Oct 2026 00:00:00 +0800</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/pytest/index.xml" rel="self" type="application/rss+xml"/><item><title>pytest fixture（測試夾具）</title><link>https://tarrragon.github.io/blog/programming/python/basic/knowledge-cards/pytest-fixture/</link><pubDate>Mon, 19 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/programming/python/basic/knowledge-cards/pytest-fixture/</guid><description>&lt;p>pytest fixture 是 pytest 在測試執行前替它準備好的資源：一個加上 &lt;code>@pytest.fixture&lt;/code> 的函式，測試函式只要把它的名字寫成參數，pytest 就會先執行它、把回傳值傳進測試。fixture 用 &lt;code>yield&lt;/code> 交出資源時，&lt;code>yield&lt;/code> 之後的程式碼會在測試結束後執行，負責清理。pytest 內建的 fixture 裡最常用來替換依賴的是 &lt;a href="https://tarrragon.github.io/blog/programming/python/basic/knowledge-cards/monkeypatch/" data-link-title="monkeypatch（測試期間的替換）" data-link-desc="用 monkeypatch 或 unittest.mock.patch 替換了函式卻沒有生效、或把模組拆成套件之後原本的替換失效時：替換要對準程式執行時查找的那個名字">monkeypatch&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>fixture 承擔的是測試的準備與清理，與測試本身的斷言分開。unittest 用 &lt;code>setUp&lt;/code> 與 &lt;code>tearDown&lt;/code> 方法做同一件事，綁在測試類別上；pytest 的 fixture 是獨立的函式，由參數名稱宣告依賴，所以同一個 fixture 可以被不同檔案的測試共用（放在 &lt;code>conftest.py&lt;/code>），也可以依賴別的 fixture。pytest 內建一組 fixture：&lt;code>tmp_path&lt;/code> 給一個每次測試都不同的暫存目錄，&lt;code>capsys&lt;/code> 擷取寫到標準輸出與標準錯誤的內容，&lt;a href="https://tarrragon.github.io/blog/programming/python/basic/knowledge-cards/monkeypatch/" data-link-title="monkeypatch（測試期間的替換）" data-link-desc="用 monkeypatch 或 unittest.mock.patch 替換了函式卻沒有生效、或把模組拆成套件之後原本的替換失效時：替換要對準程式執行時查找的那個名字">monkeypatch&lt;/a> 在測試期間替換屬性、環境變數與字典項目，測試結束後還原。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kn">import&lt;/span> &lt;span class="nn">pytest&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="nd">@pytest.fixture&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">config_file&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">tmp_path&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="c1"># tmp_path 是 pytest 內建的 fixture，這個 fixture 依賴它&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="n">path&lt;/span> &lt;span class="o">=&lt;/span> &lt;span class="n">tmp_path&lt;/span> &lt;span class="o">/&lt;/span> &lt;span class="s2">&amp;#34;config.toml&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl"> &lt;span class="n">path&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">write_text&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;mode = &amp;#39;test&amp;#39;&lt;/span>&lt;span class="se">\n&lt;/span>&lt;span class="s2">&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> &lt;span class="k">yield&lt;/span> &lt;span class="n">path&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="c1"># yield 之後的程式碼在使用它的測試結束後執行&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="nb">print&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;清理&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">path&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">test_reads_mode&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">config_file&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="c1"># 參數名 config_file 對到上面的 fixture，沒有任何呼叫端傳入它&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="s2">&amp;#34;test&amp;#34;&lt;/span> &lt;span class="ow">in&lt;/span> &lt;span class="n">config_file&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">read_text&lt;/span>&lt;span class="p">()&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>讀別人的測試時，參數名稱沒有任何呼叫端傳入，就是 fixture；它的定義在同一個檔案、同一目錄或上層目錄的 &lt;code>conftest.py&lt;/code>，或是 pytest 內建的。&lt;code>pytest --fixtures&lt;/code> 會列出目前可用的全部 fixture 與它們定義的位置。&lt;/p>
&lt;p>&lt;code>@pytest.fixture(autouse=True)&lt;/code> 讓同一個檔案（或同一個 &lt;code>conftest.py&lt;/code> 涵蓋範圍）的每個測試都自動使用這個 fixture，不必寫成參數。它適合每個測試都要的固定設定，例如把一個取決於執行環境的函式替換成固定的結果；代價是讀單一測試時看不到它，要回頭找 fixture 的定義。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>fixture 的設計責任是讓每個測試拿到的資源與其他測試隔離：預設的 &lt;code>scope=&amp;quot;function&amp;quot;&lt;/code> 讓每個測試各自執行一次準備與清理，一個測試對資源的修改不會流到下一個測試。把 scope 放寬成 &lt;code>module&lt;/code> 或 &lt;code>session&lt;/code> 可以省下昂貴的準備（例如啟動一個資料庫），條件是測試不修改那份資源；會修改的話，測試結果會隨執行順序改變。&lt;/p></description><content:encoded><![CDATA[<p>pytest fixture 是 pytest 在測試執行前替它準備好的資源：一個加上 <code>@pytest.fixture</code> 的函式，測試函式只要把它的名字寫成參數，pytest 就會先執行它、把回傳值傳進測試。fixture 用 <code>yield</code> 交出資源時，<code>yield</code> 之後的程式碼會在測試結束後執行，負責清理。pytest 內建的 fixture 裡最常用來替換依賴的是 <a href="/blog/programming/python/basic/knowledge-cards/monkeypatch/" data-link-title="monkeypatch（測試期間的替換）" data-link-desc="用 monkeypatch 或 unittest.mock.patch 替換了函式卻沒有生效、或把模組拆成套件之後原本的替換失效時：替換要對準程式執行時查找的那個名字">monkeypatch</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>fixture 承擔的是測試的準備與清理，與測試本身的斷言分開。unittest 用 <code>setUp</code> 與 <code>tearDown</code> 方法做同一件事，綁在測試類別上；pytest 的 fixture 是獨立的函式，由參數名稱宣告依賴，所以同一個 fixture 可以被不同檔案的測試共用（放在 <code>conftest.py</code>），也可以依賴別的 fixture。pytest 內建一組 fixture：<code>tmp_path</code> 給一個每次測試都不同的暫存目錄，<code>capsys</code> 擷取寫到標準輸出與標準錯誤的內容，<a href="/blog/programming/python/basic/knowledge-cards/monkeypatch/" data-link-title="monkeypatch（測試期間的替換）" data-link-desc="用 monkeypatch 或 unittest.mock.patch 替換了函式卻沒有生效、或把模組拆成套件之後原本的替換失效時：替換要對準程式執行時查找的那個名字">monkeypatch</a> 在測試期間替換屬性、環境變數與字典項目，測試結束後還原。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">pytest</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nd">@pytest.fixture</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">def</span> <span class="nf">config_file</span><span class="p">(</span><span class="n">tmp_path</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="c1"># tmp_path 是 pytest 內建的 fixture，這個 fixture 依賴它</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">path</span> <span class="o">=</span> <span class="n">tmp_path</span> <span class="o">/</span> <span class="s2">&#34;config.toml&#34;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">path</span><span class="o">.</span><span class="n">write_text</span><span class="p">(</span><span class="s2">&#34;mode = &#39;test&#39;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">yield</span> <span class="n">path</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="c1"># yield 之後的程式碼在使用它的測試結束後執行</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;清理&#34;</span><span class="p">,</span> <span class="n">path</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">def</span> <span class="nf">test_reads_mode</span><span class="p">(</span><span class="n">config_file</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="c1"># 參數名 config_file 對到上面的 fixture，沒有任何呼叫端傳入它</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">assert</span> <span class="s2">&#34;test&#34;</span> <span class="ow">in</span> <span class="n">config_file</span><span class="o">.</span><span class="n">read_text</span><span class="p">()</span></span></span></code></pre></div><p>讀別人的測試時，參數名稱沒有任何呼叫端傳入，就是 fixture；它的定義在同一個檔案、同一目錄或上層目錄的 <code>conftest.py</code>，或是 pytest 內建的。<code>pytest --fixtures</code> 會列出目前可用的全部 fixture 與它們定義的位置。</p>
<p><code>@pytest.fixture(autouse=True)</code> 讓同一個檔案（或同一個 <code>conftest.py</code> 涵蓋範圍）的每個測試都自動使用這個 fixture，不必寫成參數。它適合每個測試都要的固定設定，例如把一個取決於執行環境的函式替換成固定的結果；代價是讀單一測試時看不到它，要回頭找 fixture 的定義。</p>
<h2 id="設計責任">設計責任</h2>
<p>fixture 的設計責任是讓每個測試拿到的資源與其他測試隔離：預設的 <code>scope=&quot;function&quot;</code> 讓每個測試各自執行一次準備與清理，一個測試對資源的修改不會流到下一個測試。把 scope 放寬成 <code>module</code> 或 <code>session</code> 可以省下昂貴的準備（例如啟動一個資料庫），條件是測試不修改那份資源；會修改的話，測試結果會隨執行順序改變。</p>
]]></content:encoded></item><item><title>monkeypatch（測試期間的替換）</title><link>https://tarrragon.github.io/blog/programming/python/basic/knowledge-cards/monkeypatch/</link><pubDate>Mon, 19 Oct 2026 00:00:00 +0800</pubDate><guid>https://tarrragon.github.io/blog/programming/python/basic/knowledge-cards/monkeypatch/</guid><description>&lt;p>monkeypatch 是 pytest 內建的 &lt;a href="https://tarrragon.github.io/blog/programming/python/basic/knowledge-cards/pytest-fixture/" data-link-title="pytest fixture（測試夾具）" data-link-desc="讀到測試函式的參數沒有任何呼叫端傳入、或要讓多個測試共用準備與清理步驟時：pytest 用參數名稱找出 fixture、在測試前後執行準備與清理">fixture&lt;/a>：在一個測試執行期間把某個物件上的屬性、某個環境變數或某個字典項目換成別的值，測試結束後自動還原。替換是否生效，取決於換掉的是否就是程式執行時實際去查的那個名字——這條規則同樣適用於 &lt;code>unittest.mock.patch&lt;/code>，用法見 &lt;a href="https://tarrragon.github.io/blog/programming/python/basic/05-error-testing/mock/" data-link-title="5.4 Mock 與測試隔離" data-link-desc="隔離外部依賴">Mock 與測試隔離&lt;/a>。&lt;/p>
&lt;h2 id="概念位置">概念位置&lt;/h2>
&lt;p>monkeypatch 屬於 &lt;a href="https://tarrragon.github.io/blog/programming/python/basic/knowledge-cards/pytest-fixture/" data-link-title="pytest fixture（測試夾具）" data-link-desc="讀到測試函式的參數沒有任何呼叫端傳入、或要讓多個測試共用準備與清理步驟時：pytest 用參數名稱找出 fixture、在測試前後執行準備與清理">pytest fixture&lt;/a> 的一種，責任是暫時改變被測程式的依賴，讓測試不受執行環境影響（時間、網路、哪些程序正在執行）。它改的是名字與物件之間的綁定：&lt;code>monkeypatch.setattr(模組, &amp;quot;名字&amp;quot;, 新值)&lt;/code> 把那個模組命名空間裡的「名字」改綁到新值。綁定在測試結束時還原，所以下一個測試看到的是原本的物件。&lt;/p>
&lt;h2 id="可觀察訊號與例子">可觀察訊號與例子&lt;/h2>
&lt;p>Python 的函式在執行時才去查它用到的名字，查的位置由匯入的寫法決定：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># app/clock.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">now&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="s2">&amp;#34;real&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># app/report.py&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">app.clock&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">now&lt;/span> &lt;span class="c1"># 在 report 的命名空間建立一個名字 now&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">app&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">clock&lt;/span> &lt;span class="c1"># 在 report 的命名空間建立一個名字 clock&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">stamp_from_import&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">now&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># 執行時查 report.now&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">stamp_via_module&lt;/span>&lt;span class="p">():&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl"> &lt;span class="k">return&lt;/span> &lt;span class="n">clock&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">now&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="c1"># 執行時查 report.clock，再查 clock.now&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-python" data-lang="python">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kn">from&lt;/span> &lt;span class="nn">app&lt;/span> &lt;span class="kn">import&lt;/span> &lt;span class="n">clock&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">report&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">test_patch_definition_site&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">monkeypatch&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> &lt;span class="n">monkeypatch&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">setattr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">clock&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;now&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">lambda&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;fake&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="n">report&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stamp_via_module&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;fake&amp;#34;&lt;/span> &lt;span class="c1"># 查的是 clock.now，換到了&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="n">report&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stamp_from_import&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;real&amp;#34;&lt;/span> &lt;span class="c1"># 查的是 report.now，沒換到&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="k">def&lt;/span> &lt;span class="nf">test_patch_lookup_site&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">monkeypatch&lt;/span>&lt;span class="p">):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl"> &lt;span class="n">monkeypatch&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">setattr&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">report&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="s2">&amp;#34;now&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="k">lambda&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;fake&amp;#34;&lt;/span>&lt;span class="p">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> &lt;span class="k">assert&lt;/span> &lt;span class="n">report&lt;/span>&lt;span class="o">.&lt;/span>&lt;span class="n">stamp_from_import&lt;/span>&lt;span class="p">()&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="s2">&amp;#34;fake&amp;#34;&lt;/span> &lt;span class="c1"># 換的正是被查找的名字&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>from app.clock import now&lt;/code> 在匯入當下把 &lt;code>report.now&lt;/code> 綁到原本的函式，之後替換 &lt;code>clock.now&lt;/code> 不會改變 &lt;code>report.now&lt;/code> 的綁定，所以 &lt;code>stamp_from_import&lt;/code> 仍然呼叫原本的函式。&lt;/p>
&lt;p>&lt;code>monkeypatch.setattr&lt;/code> 預設 &lt;code>raising=True&lt;/code>：要替換的名字不存在時直接拋出 &lt;code>AttributeError&lt;/code>。目標也可以寫成字串，例如 &lt;code>monkeypatch.setattr(&amp;quot;app.report.now&amp;quot;, ...)&lt;/code>。&lt;/p>
&lt;p>&lt;strong>拆模組時最容易違反這條規則。&lt;/strong> 一個模組改成同名套件之後，函式搬進子模組，它查找依賴的位置也跟著搬進那個子模組；原本對準舊模組的替換，在舊模組仍匯入那個名字時（例如為了相容而重新匯出）執行成功、不報錯，只是不再影響被測的函式，測試因此依執行環境而時好時壞；舊模組已經沒有那個名字時，&lt;code>raising=True&lt;/code> 讓它直接報錯。拆分時的處理方式在&lt;a href="https://tarrragon.github.io/blog/programming/python/basic/07-refactoring/python-module-split/" data-link-title="拆分 Python 模組：pytest 的特徵測試、同名套件與重新匯出、自動登記的分派表、避免循環匯入" data-link-desc="職責邊界已經從修改紀錄歸類出來之後，用 Python 拆分一個模組而不改變對外行為：用 pytest 寫特徵測試與 monkeypatch 的替換目標、改成同名套件並重新匯出、把巨型分支改成自動登記的分派表，以及拆出模組之間循環匯入在 Python 的症狀與解法">拆分 Python 模組&lt;/a>的〈用 pytest 寫特徵測試鎖住拆分前的行為〉。&lt;/p>
&lt;h2 id="設計責任">設計責任&lt;/h2>
&lt;p>寫替換之前先找出被測函式查找依賴的那一行：它寫的是 &lt;code>now()&lt;/code> 就替換被測函式所在模組的 &lt;code>now&lt;/code>，寫的是 &lt;code>clock.now()&lt;/code> 就替換 &lt;code>clock&lt;/code> 模組的 &lt;code>now&lt;/code>。程式碼搬家之後，替換目標跟著被測函式走，不跟著依賴的定義走。替換生效與否沒有任何錯誤訊息可以提示，所以替換之後加一條斷言確認被測函式拿到的是替換的值，比事後追查時好時壞的測試便宜。&lt;/p></description><content:encoded><![CDATA[<p>monkeypatch 是 pytest 內建的 <a href="/blog/programming/python/basic/knowledge-cards/pytest-fixture/" data-link-title="pytest fixture（測試夾具）" data-link-desc="讀到測試函式的參數沒有任何呼叫端傳入、或要讓多個測試共用準備與清理步驟時：pytest 用參數名稱找出 fixture、在測試前後執行準備與清理">fixture</a>：在一個測試執行期間把某個物件上的屬性、某個環境變數或某個字典項目換成別的值，測試結束後自動還原。替換是否生效，取決於換掉的是否就是程式執行時實際去查的那個名字——這條規則同樣適用於 <code>unittest.mock.patch</code>，用法見 <a href="/blog/programming/python/basic/05-error-testing/mock/" data-link-title="5.4 Mock 與測試隔離" data-link-desc="隔離外部依賴">Mock 與測試隔離</a>。</p>
<h2 id="概念位置">概念位置</h2>
<p>monkeypatch 屬於 <a href="/blog/programming/python/basic/knowledge-cards/pytest-fixture/" data-link-title="pytest fixture（測試夾具）" data-link-desc="讀到測試函式的參數沒有任何呼叫端傳入、或要讓多個測試共用準備與清理步驟時：pytest 用參數名稱找出 fixture、在測試前後執行準備與清理">pytest fixture</a> 的一種，責任是暫時改變被測程式的依賴，讓測試不受執行環境影響（時間、網路、哪些程序正在執行）。它改的是名字與物件之間的綁定：<code>monkeypatch.setattr(模組, &quot;名字&quot;, 新值)</code> 把那個模組命名空間裡的「名字」改綁到新值。綁定在測試結束時還原，所以下一個測試看到的是原本的物件。</p>
<h2 id="可觀察訊號與例子">可觀察訊號與例子</h2>
<p>Python 的函式在執行時才去查它用到的名字，查的位置由匯入的寫法決定：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># app/clock.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">def</span> <span class="nf">now</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">return</span> <span class="s2">&#34;real&#34;</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># app/report.py</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="kn">from</span> <span class="nn">app.clock</span> <span class="kn">import</span> <span class="n">now</span>     <span class="c1"># 在 report 的命名空間建立一個名字 now</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kn">from</span> <span class="nn">app</span> <span class="kn">import</span> <span class="n">clock</span>         <span class="c1"># 在 report 的命名空間建立一個名字 clock</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">def</span> <span class="nf">stamp_from_import</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">return</span> <span class="n">now</span><span class="p">()</span>               <span class="c1"># 執行時查 report.now</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="k">def</span> <span class="nf">stamp_via_module</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">return</span> <span class="n">clock</span><span class="o">.</span><span class="n">now</span><span class="p">()</span>         <span class="c1"># 執行時查 report.clock，再查 clock.now</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">from</span> <span class="nn">app</span> <span class="kn">import</span> <span class="n">clock</span><span class="p">,</span> <span class="n">report</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">def</span> <span class="nf">test_patch_definition_site</span><span class="p">(</span><span class="n">monkeypatch</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">monkeypatch</span><span class="o">.</span><span class="n">setattr</span><span class="p">(</span><span class="n">clock</span><span class="p">,</span> <span class="s2">&#34;now&#34;</span><span class="p">,</span> <span class="k">lambda</span><span class="p">:</span> <span class="s2">&#34;fake&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">assert</span> <span class="n">report</span><span class="o">.</span><span class="n">stamp_via_module</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;fake&#34;</span>   <span class="c1"># 查的是 clock.now，換到了</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">assert</span> <span class="n">report</span><span class="o">.</span><span class="n">stamp_from_import</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;real&#34;</span>  <span class="c1"># 查的是 report.now，沒換到</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">def</span> <span class="nf">test_patch_lookup_site</span><span class="p">(</span><span class="n">monkeypatch</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">monkeypatch</span><span class="o">.</span><span class="n">setattr</span><span class="p">(</span><span class="n">report</span><span class="p">,</span> <span class="s2">&#34;now&#34;</span><span class="p">,</span> <span class="k">lambda</span><span class="p">:</span> <span class="s2">&#34;fake&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">assert</span> <span class="n">report</span><span class="o">.</span><span class="n">stamp_from_import</span><span class="p">()</span> <span class="o">==</span> <span class="s2">&#34;fake&#34;</span>  <span class="c1"># 換的正是被查找的名字</span></span></span></code></pre></div><p><code>from app.clock import now</code> 在匯入當下把 <code>report.now</code> 綁到原本的函式，之後替換 <code>clock.now</code> 不會改變 <code>report.now</code> 的綁定，所以 <code>stamp_from_import</code> 仍然呼叫原本的函式。</p>
<p><code>monkeypatch.setattr</code> 預設 <code>raising=True</code>：要替換的名字不存在時直接拋出 <code>AttributeError</code>。目標也可以寫成字串，例如 <code>monkeypatch.setattr(&quot;app.report.now&quot;, ...)</code>。</p>
<p><strong>拆模組時最容易違反這條規則。</strong> 一個模組改成同名套件之後，函式搬進子模組，它查找依賴的位置也跟著搬進那個子模組；原本對準舊模組的替換，在舊模組仍匯入那個名字時（例如為了相容而重新匯出）執行成功、不報錯，只是不再影響被測的函式，測試因此依執行環境而時好時壞；舊模組已經沒有那個名字時，<code>raising=True</code> 讓它直接報錯。拆分時的處理方式在<a href="/blog/programming/python/basic/07-refactoring/python-module-split/" data-link-title="拆分 Python 模組：pytest 的特徵測試、同名套件與重新匯出、自動登記的分派表、避免循環匯入" data-link-desc="職責邊界已經從修改紀錄歸類出來之後，用 Python 拆分一個模組而不改變對外行為：用 pytest 寫特徵測試與 monkeypatch 的替換目標、改成同名套件並重新匯出、把巨型分支改成自動登記的分派表，以及拆出模組之間循環匯入在 Python 的症狀與解法">拆分 Python 模組</a>的〈用 pytest 寫特徵測試鎖住拆分前的行為〉。</p>
<h2 id="設計責任">設計責任</h2>
<p>寫替換之前先找出被測函式查找依賴的那一行：它寫的是 <code>now()</code> 就替換被測函式所在模組的 <code>now</code>，寫的是 <code>clock.now()</code> 就替換 <code>clock</code> 模組的 <code>now</code>。程式碼搬家之後，替換目標跟著被測函式走，不跟著依賴的定義走。替換生效與否沒有任何錯誤訊息可以提示，所以替換之後加一條斷言確認被測函式拿到的是替換的值，比事後追查時好時壞的測試便宜。</p>
]]></content:encoded></item></channel></rss>