[{"data":1,"prerenderedAt":916},["ShallowReactive",2],{"page-\u002Fadvanced-pytest-architecture-configuration\u002Fbuilding-custom-pytest-plugins\u002Ftesting-a-pytest-plugin-with-the-pytester-fixture\u002F":3},{"id":4,"title":5,"body":6,"description":880,"extension":881,"meta":882,"navigation":132,"path":912,"seo":913,"stem":914,"__hash__":915},"content\u002Fadvanced-pytest-architecture-configuration\u002Fbuilding-custom-pytest-plugins\u002Ftesting-a-pytest-plugin-with-the-pytester-fixture\u002Findex.md","Testing a pytest Plugin with the pytester Fixture",{"type":7,"value":8,"toc":869},"minimark",[9,22,28,33,58,62,65,88,276,417,421,445,462,466,516,520,558,561,619,623,635,648,664,677,745,749,752,755,762,783,787,804,815,828,832,860,865],[10,11,12,13,17,18,21],"p",{},"A pytest plugin — or the hooks and fixtures in a large ",[14,15,16],"code",{},"conftest.py"," — is code that changes how other tests are collected, run and reported. Testing it with ordinary unit tests is awkward, because the behaviour only exists inside a pytest run. The ",[14,19,20],{},"pytester"," fixture solves that by letting a test create a small throwaway project in a temporary directory, run pytest on it, and assert on what happened: how many tests passed, what was printed, which items were collected.",[10,23,24,25,27],{},"It is the tool pytest itself is tested with, and it makes plugin behaviour as testable as any other function. That matters more than it first appears, because plugin and ",[14,26,16],{}," code has an unusual failure profile: when it breaks, it rarely breaks loudly. A collection hook that stops skipping slow tests makes the suite slower rather than red; a report hook that stops attaching logs makes failures harder to diagnose rather than causing new ones; a fixture that stops cleaning up leaks resources that only become a problem weeks later. None of those produce a failing test on their own, which is exactly why they need tests of their own. The investment is small — a handful of tests per hook — and it pays back the first time a refactor changes a plugin's behaviour in a way that nothing else would have noticed.",[29,30,32],"h2",{"id":31},"prerequisites","Prerequisites",[34,35,36,46],"ul",{},[37,38,39,42,43,45],"li",{},[14,40,41],{},"pytest >= 8.0","; ",[14,44,20],{}," ships with it and needs only enabling.",[37,47,48,49,51,52,57],{},"A plugin or ",[14,50,16],{}," whose behaviour is worth pinning down — see ",[53,54,56],"a",{"href":55},"\u002Fadvanced-pytest-architecture-configuration\u002Fbuilding-custom-pytest-plugins\u002F","building custom pytest plugins",".",[29,59,61],{"id":60},"solution","Solution",[10,63,64],{},"Enable the fixture once, then write each scenario as a generated project and a run.",[66,67,72],"pre",{"className":68,"code":69,"language":70,"meta":71,"style":71},"language-python shiki shiki-themes github-light github-dark","# tests\u002Fconftest.py\npytest_plugins = [\"pytester\"]\n","python","",[14,73,74,82],{"__ignoreMap":71},[75,76,79],"span",{"class":77,"line":78},"line",1,[75,80,81],{},"# tests\u002Fconftest.py\n",[75,83,85],{"class":77,"line":84},2,[75,86,87],{},"pytest_plugins = [\"pytester\"]\n",[66,89,91],{"className":68,"code":90,"language":70,"meta":71,"style":71},"# tests\u002Ftest_run_slow_option.py\ndef test_slow_tests_are_skipped_by_default(pytester):\n    pytester.makeconftest(open(\"conftest.py\").read())      # the real hook under test\n    pytester.makepyfile(\n        \"\"\"\n        import pytest\n\n        @pytest.mark.slow\n        def test_heavy():\n            pass\n\n        def test_light():\n            pass\n        \"\"\"\n    )\n    result = pytester.runpytest(\"-rs\")\n    result.assert_outcomes(passed=1, skipped=1)\n    result.stdout.fnmatch_lines([\"*needs --run-slow*\"])\n\n\ndef test_run_slow_includes_them(pytester):\n    pytester.makeconftest(open(\"conftest.py\").read())\n    pytester.makepyfile(\n        \"\"\"\n        import pytest\n\n        @pytest.mark.slow\n        def test_heavy():\n            pass\n        \"\"\"\n    )\n    result = pytester.runpytest(\"--run-slow\")\n    result.assert_outcomes(passed=1)\n",[14,92,93,98,103,109,115,121,127,134,140,146,152,157,163,168,173,179,185,191,197,202,207,213,219,224,229,234,239,244,249,254,259,264,270],{"__ignoreMap":71},[75,94,95],{"class":77,"line":78},[75,96,97],{},"# tests\u002Ftest_run_slow_option.py\n",[75,99,100],{"class":77,"line":84},[75,101,102],{},"def test_slow_tests_are_skipped_by_default(pytester):\n",[75,104,106],{"class":77,"line":105},3,[75,107,108],{},"    pytester.makeconftest(open(\"conftest.py\").read())      # the real hook under test\n",[75,110,112],{"class":77,"line":111},4,[75,113,114],{},"    pytester.makepyfile(\n",[75,116,118],{"class":77,"line":117},5,[75,119,120],{},"        \"\"\"\n",[75,122,124],{"class":77,"line":123},6,[75,125,126],{},"        import pytest\n",[75,128,130],{"class":77,"line":129},7,[75,131,133],{"emptyLinePlaceholder":132},true,"\n",[75,135,137],{"class":77,"line":136},8,[75,138,139],{},"        @pytest.mark.slow\n",[75,141,143],{"class":77,"line":142},9,[75,144,145],{},"        def test_heavy():\n",[75,147,149],{"class":77,"line":148},10,[75,150,151],{},"            pass\n",[75,153,155],{"class":77,"line":154},11,[75,156,133],{"emptyLinePlaceholder":132},[75,158,160],{"class":77,"line":159},12,[75,161,162],{},"        def test_light():\n",[75,164,166],{"class":77,"line":165},13,[75,167,151],{},[75,169,171],{"class":77,"line":170},14,[75,172,120],{},[75,174,176],{"class":77,"line":175},15,[75,177,178],{},"    )\n",[75,180,182],{"class":77,"line":181},16,[75,183,184],{},"    result = pytester.runpytest(\"-rs\")\n",[75,186,188],{"class":77,"line":187},17,[75,189,190],{},"    result.assert_outcomes(passed=1, skipped=1)\n",[75,192,194],{"class":77,"line":193},18,[75,195,196],{},"    result.stdout.fnmatch_lines([\"*needs --run-slow*\"])\n",[75,198,200],{"class":77,"line":199},19,[75,201,133],{"emptyLinePlaceholder":132},[75,203,205],{"class":77,"line":204},20,[75,206,133],{"emptyLinePlaceholder":132},[75,208,210],{"class":77,"line":209},21,[75,211,212],{},"def test_run_slow_includes_them(pytester):\n",[75,214,216],{"class":77,"line":215},22,[75,217,218],{},"    pytester.makeconftest(open(\"conftest.py\").read())\n",[75,220,222],{"class":77,"line":221},23,[75,223,114],{},[75,225,227],{"class":77,"line":226},24,[75,228,120],{},[75,230,232],{"class":77,"line":231},25,[75,233,126],{},[75,235,237],{"class":77,"line":236},26,[75,238,133],{"emptyLinePlaceholder":132},[75,240,242],{"class":77,"line":241},27,[75,243,139],{},[75,245,247],{"class":77,"line":246},28,[75,248,145],{},[75,250,252],{"class":77,"line":251},29,[75,253,151],{},[75,255,257],{"class":77,"line":256},30,[75,258,120],{},[75,260,262],{"class":77,"line":261},31,[75,263,178],{},[75,265,267],{"class":77,"line":266},32,[75,268,269],{},"    result = pytester.runpytest(\"--run-slow\")\n",[75,271,273],{"class":77,"line":272},33,[75,274,275],{},"    result.assert_outcomes(passed=1)\n",[277,278,281,413],"figure",{"className":279},[280],"diagram",[282,283,290,291,290,295,290,299,290,317,290,325,290,334,290,343,290,348,290,357,290,361,290,366,290,370,290,377,290,382,290,386,290,390,290,394,290,398,290,402,290,406,290,410],"svg",{"viewBox":284,"role":285,"ariaLabelledBy":286,"xmlns":289},"0 0 820 262","img",[287,288],"pyt-t","pyt-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[292,293,294],"title",{"id":287},"A pytester test runs pytest inside pytest",[296,297,298],"desc",{"id":288},"The outer test creates a temporary project containing a conftest and a test file, runs an inner pytest session on it with chosen arguments, and receives a result object. It then asserts on the counts of outcomes and on lines of output, which is how the plugin's behaviour is verified without touching the real suite.",[300,301,302,303,290],"defs",{},"\n    ",[304,305,312],"marker",{"id":306,"viewBox":307,"refX":308,"refY":309,"markerWidth":310,"markerHeight":310,"orient":311},"pyt-a","0 0 10 10","9","5","7","auto-start-reverse",[313,314],"path",{"d":315,"fill":316},"M0 0 L10 5 L0 10 z","#3d405b",[318,319],"rect",{"x":320,"y":320,"width":321,"height":322,"rx":323,"fill":324},"0","820","262","14","#fffdf8",[326,327,333],"text",{"x":328,"y":329,"textAnchor":330,"fontSize":331,"fontWeight":332,"fill":316},"410","28","middle","16","700","A small project, a real run, assertions on the result",[318,335],{"x":336,"y":337,"width":338,"height":339,"rx":340,"fill":341,"stroke":316,"strokeWidth":342},"26","56","768","186","12","#f4f1de","1.8",[326,344,347],{"x":345,"y":346,"fontSize":340,"fontWeight":332,"fill":316},"46","80","outer test — test_slow_tests_are_skipped_by_default",[318,349],{"x":345,"y":350,"width":351,"height":352,"rx":353,"fill":354,"stroke":355,"strokeWidth":356},"94","210","120","11","#f7f0da","#f2cc8f","2",[326,358,360],{"x":359,"y":352,"textAnchor":330,"fontSize":340,"fontWeight":332,"fill":316},"151","temp project",[326,362,365],{"x":363,"y":364,"fontSize":353,"fill":316},"62","146","conftest.py (hook)",[326,367,369],{"x":363,"y":368,"fontSize":353,"fill":316},"168","test_x.py (2 tests)",[77,371],{"x1":372,"y1":373,"x2":374,"y2":373,"stroke":316,"strokeWidth":375,"markerEnd":376},"260","154","296","1.6","url(#pyt-a)",[318,378],{"x":379,"y":350,"width":351,"height":352,"rx":353,"fill":380,"stroke":381,"strokeWidth":356},"302","#e6f0ea","#81b29a",[326,383,385],{"x":384,"y":352,"textAnchor":330,"fontSize":340,"fontWeight":332,"fill":316},"407","inner pytest run",[326,387,389],{"x":388,"y":364,"fontSize":353,"fill":316},"318","runpytest(\"-rs\")",[326,391,393],{"x":388,"y":368,"fontSize":353,"fill":392},"#2a5f49","plugin active",[77,395],{"x1":396,"y1":373,"x2":397,"y2":373,"stroke":316,"strokeWidth":375,"markerEnd":376},"516","552",[318,399],{"x":400,"y":350,"width":401,"height":352,"rx":353,"fill":324,"stroke":316,"strokeWidth":375},"558","216",[326,403,405],{"x":404,"y":352,"textAnchor":330,"fontSize":340,"fontWeight":332,"fill":316},"666","RunResult",[326,407,409],{"x":408,"y":364,"fontSize":353,"fill":316},"574","assert_outcomes(…)",[326,411,412],{"x":408,"y":368,"fontSize":353,"fill":316},"stdout.fnmatch_lines",[414,415,416],"figcaption",{},"The inner run is a genuine pytest session with the plugin active, so the assertions test exactly what users will experience.",[29,418,420],{"id":419},"why-this-works","Why this works",[10,422,423,425,426,429,430,433,434,437,438,441,442,444],{},[14,424,20],{}," creates an isolated temporary directory for each test and changes into it, so files written with ",[14,427,428],{},"makepyfile",", ",[14,431,432],{},"makeconftest"," and ",[14,435,436],{},"makeini"," form a complete, self-contained project. ",[14,439,440],{},"runpytest"," then runs a full pytest session against that project — collection, hooks, fixtures, reporting — and returns a ",[14,443,405],{}," holding the exit code, the parsed outcome counts, and the captured stdout and stderr.",[10,446,447,448,450,451,429,454,457,458,461],{},"By default the inner run happens in-process, which is fast because it reuses the already-imported interpreter. The isolation is good enough for most plugins because ",[14,449,20],{}," snapshots and restores ",[14,452,453],{},"sys.path",[14,455,456],{},"sys.modules"," and the working directory around each run. ",[14,459,460],{},"runpytest_subprocess"," trades that speed for complete isolation, launching a fresh interpreter, and is the right choice when a plugin mutates global state that the snapshot does not cover.",[29,463,465],{"id":464},"edge-cases-and-failure-modes","Edge cases and failure modes",[34,467,468,479,491,499,505],{},[37,469,470,474,475,478],{},[471,472,473],"strong",{},"Asserting on exact output."," Terminal output changes between pytest versions and terminal widths. Use ",[14,476,477],{},"fnmatch_lines"," with wildcards, and assert on outcomes wherever possible.",[37,480,481,484,485,488,489,57],{},[471,482,483],{},"Plugin not loaded in the inner run."," A plugin registered through an entry point is active automatically; one living in a module is not. Pass ",[14,486,487],{},"-p myplugin"," to ",[14,490,440],{},[37,492,493,496,497,57],{},[471,494,495],{},"State leaking between in-process runs."," A plugin that caches at module level or registers atexit handlers can leak. Switch that test to ",[14,498,460],{},[37,500,501,504],{},[471,502,503],{},"Slow suites of plugin tests."," Each inner run is a full session. Keep generated projects minimal — two or three tests each — and prefer many small scenarios to one large one.",[37,506,507,510,511,515],{},[471,508,509],{},"Forgetting the negative case."," Test that the plugin does ",[512,513,514],"em",{},"not"," act when it should not — without its flag, on unmarked tests — as well as that it does.",[29,517,519],{"id":518},"what-to-test-in-a-plugin","What to test in a plugin",[10,521,522,523,526,527,530,531,534,535,538,539,542,543,546,547,551,552,555,556,57],{},"A plugin's surface is small but each part fails differently, and a short checklist covers it. ",[471,524,525],{},"Collection behaviour"," — which tests are selected, skipped, deselected or multiplied — is best tested with ",[14,528,529],{},"assert_outcomes"," and, where ids matter, with ",[14,532,533],{},"pytester.inline_run"," plus ",[14,536,537],{},"getreports"," to inspect item names. ",[471,540,541],{},"Fixtures"," the plugin provides are tested by generated tests that request them and assert on their values. ",[471,544,545],{},"Command-line options"," need the precedence checks described in ",[53,548,550],{"href":549},"\u002Fadvanced-pytest-architecture-configuration\u002Fbuilding-custom-pytest-plugins\u002Fadding-command-line-options-with-pytest-addoption\u002F","adding command-line options with pytest_addoption",". ",[471,553,554],{},"Report output"," — summary lines, headers, section titles — is tested with ",[14,557,477],{},[10,559,560],{},"The negative cases deserve the same attention as the positive ones. A plugin that adds a skip marker to slow tests must be tested both with and without its flag, and with a test that is not marked slow at all, because the bug that ships is rarely \"the feature does nothing\" and usually \"the feature does something to tests it should not touch\". Three generated test files with three runs each cover that matrix in well under a second.",[277,562,564,616],{"className":563},[280],[282,565,290,570,290,573,290,576,290,580,290,585,290,590,290,595,290,597,290,601,290,606,290,610,290,613],{"viewBox":566,"role":285,"ariaLabelledBy":567,"xmlns":289},"0 0 800 236",[568,569],"ck-t","ck-d",[292,571,572],{"id":568},"What to verify for each part of a plugin",[296,574,575],{"id":569},"Four rows. Collection behaviour is verified with assert_outcomes and report inspection. Provided fixtures are verified by generated tests that request them. Command-line options are verified with precedence scenarios. Report output is verified with wildcard line matching. Each row includes a negative case where the plugin should not act.",[318,577],{"x":320,"y":320,"width":578,"height":579,"rx":323,"fill":324},"800","236",[326,581,584],{"x":582,"y":329,"textAnchor":330,"fontSize":583,"fontWeight":332,"fill":316},"400","15.5","One assertion style per surface, plus the negative case",[318,586],{"x":336,"y":587,"width":588,"height":589,"rx":308,"fill":380,"stroke":381,"strokeWidth":342},"48","748","38",[326,591,594],{"x":345,"y":592,"fontSize":593,"fill":316},"72","11.5","collection → assert_outcomes · inline_run().getreports() for ids",[318,596],{"x":336,"y":350,"width":588,"height":589,"rx":308,"fill":354,"stroke":355,"strokeWidth":342},[326,598,600],{"x":345,"y":599,"fontSize":593,"fill":316},"118","fixtures → a generated test that requests the fixture and asserts on it",[318,602],{"x":336,"y":603,"width":588,"height":589,"rx":308,"fill":604,"stroke":605,"strokeWidth":342},"140","#fbe9e3","#e07a5f",[326,607,609],{"x":345,"y":608,"fontSize":593,"fill":316},"164","options → default, ini value, and flag, checked for precedence",[318,611],{"x":336,"y":339,"width":588,"height":589,"rx":308,"fill":341,"stroke":316,"strokeWidth":612},"1.5",[326,614,615],{"x":345,"y":351,"fontSize":593,"fill":316},"output → stdout.fnmatch_lines with wildcards, never exact matches",[414,617,618],{},"Every row should include a scenario where the plugin must leave the tests alone; that is where regressions in plugins usually hide.",[29,620,622],{"id":621},"inspecting-collection-and-reports-directly","Inspecting collection and reports directly",[10,624,625,627,628,631,632,634],{},[14,626,529],{}," answers \"how many passed, failed and skipped\", which covers most scenarios. Some plugin behaviour is about ",[512,629,630],{},"which"," items exist or what their reports contain, and ",[14,633,20],{}," exposes that too.",[10,636,637,640,641,644,645,647],{},[14,638,639],{},"pytester.inline_run(...)"," returns a ",[14,642,643],{},"HookRecorder"," rather than a ",[14,646,405],{},". It records every hook call during the inner session, so a test can retrieve the collected items, the reports for each phase, and their attributes. That makes it possible to assert that a hook generated items with particular ids, that a report carries a user property the plugin was supposed to attach, or that a deselection hook removed exactly the items it should have — none of which appear in the pass\u002Ffail counts.",[10,649,650,651,653,654,657,658,660,661,663],{},"The trade-off is fidelity to the user's view. ",[14,652,440],{}," checks what a person running the suite would see; ",[14,655,656],{},"inline_run"," checks internal state that may not be visible at all. Both are useful, and a good plugin test file uses ",[14,659,440],{}," for the behaviour users rely on and ",[14,662,656],{}," for the internal contracts other plugins or tools depend on, such as report properties consumed by a CI dashboard.",[10,665,666,667,669,670,673,674,676],{},"A practical rule for choosing: if the assertion would make sense in the plugin's documentation — \"tests marked slow are skipped with the reason 'needs --run-slow'\" — use ",[14,668,440],{}," and assert on output. If it only makes sense to someone reading the plugin's code — \"the report for each item carries a ",[14,671,672],{},"shard"," property\" — use ",[14,675,656],{}," and assert on the recorded reports.",[277,678,680,742],{"className":679},[280],[282,681,290,685,290,688,290,691,290,693,290,696,290,700,290,706,290,711,290,715,290,718,290,721,290,724,290,728,290,732,290,735,290,739],{"viewBox":566,"role":285,"ariaLabelledBy":682,"xmlns":289},[683,684],"ir-t","ir-d",[292,686,687],{"id":683},"runpytest versus inline_run",[296,689,690],{"id":684},"Two ways to run the inner session. runpytest returns outcome counts and captured output, suited to asserting on what a user sees. inline_run returns a hook recorder holding collected items and phase reports, suited to asserting on internal contracts such as report properties or generated ids.",[318,692],{"x":320,"y":320,"width":578,"height":579,"rx":323,"fill":324},[326,694,695],{"x":582,"y":329,"textAnchor":330,"fontSize":583,"fontWeight":332,"fill":316},"Assert on what users see, or on what tools consume",[318,697],{"x":336,"y":698,"width":699,"height":608,"rx":340,"fill":380,"stroke":381,"strokeWidth":356},"50","360",[326,701,705],{"x":702,"y":703,"textAnchor":330,"fontSize":704,"fontWeight":332,"fill":316},"206","76","12.5","runpytest → RunResult",[326,707,710],{"x":708,"y":709,"fontSize":353,"fill":316},"44","104","outcome counts, exit code",[326,712,714],{"x":708,"y":713,"fontSize":353,"fill":316},"126","captured stdout and stderr",[326,716,717],{"x":708,"y":608,"fontSize":353,"fontWeight":332,"fill":392},"documented behaviour",[326,719,720],{"x":708,"y":339,"fontSize":353,"fill":316},"\"slow tests are skipped with …\"",[318,722],{"x":723,"y":698,"width":699,"height":608,"rx":340,"fill":354,"stroke":355,"strokeWidth":356},"414",[326,725,727],{"x":726,"y":703,"textAnchor":330,"fontSize":704,"fontWeight":332,"fill":316},"594","inline_run → HookRecorder",[326,729,731],{"x":730,"y":709,"fontSize":353,"fill":316},"432","collected items and their ids",[326,733,734],{"x":730,"y":713,"fontSize":353,"fill":316},"reports per phase, properties",[326,736,738],{"x":730,"y":608,"fontSize":353,"fontWeight":332,"fill":737},"#8a5a00","internal contracts",[326,740,741],{"x":730,"y":339,"fontSize":353,"fill":316},"\"each report carries shard=…\"",[414,743,744],{},"Most plugin tests belong on the left. The right-hand side is for guarantees other tools depend on but users never see directly.",[29,746,748],{"id":747},"keeping-plugin-tests-fast-and-readable","Keeping plugin tests fast and readable",[10,750,751],{},"A plugin test file can easily become the slowest part of a suite, because every test runs a complete pytest session. Three habits keep it proportionate.",[10,753,754],{},"Keep each generated project tiny, and resist the urge to build one elaborate fixture project shared by many tests. Two or three generated tests are almost always enough to demonstrate a behaviour and its negative case; a scenario that needs twenty is usually two scenarios. The inner session's cost scales with what it collects, so small projects mean fast tests.",[10,756,757,758,761],{},"Share the conftest under test, not the generated projects. Reading the real hook from the repository once — as the examples above do with ",[14,759,760],{},"open(\"conftest.py\").read()"," — ensures the tests exercise the code that actually ships, while each test still writes its own minimal test file so its scenario is readable on its own.",[10,763,764,765,433,768,771,772,433,775,778,779,782],{},"Name tests after the behaviour, not the mechanism. ",[14,766,767],{},"test_slow_tests_are_skipped_by_default",[14,769,770],{},"test_run_slow_includes_them"," read as a specification of the option; ",[14,773,774],{},"test_plugin_1",[14,776,777],{},"test_plugin_2"," tell a reader nothing when one of them fails. Plugin tests are often the only documentation of a suite's custom behaviour, and naming them as statements about that behaviour makes them serve that role without any extra effort. When a new engineer asks what ",[14,780,781],{},"--run-slow"," does, the answer is a list of test names, and it is guaranteed to be current because the build fails when it is not.",[29,784,786],{"id":785},"frequently-asked-questions","Frequently Asked Questions",[10,788,789,792,793,796,797,799,800,803],{},[471,790,791],{},"How do I enable the pytester fixture?","\nAdd ",[14,794,795],{},"pytest_plugins = [\"pytester\"]"," to the root ",[14,798,16],{},", or pass ",[14,801,802],{},"-p pytester"," on the command line. It ships with pytest but is disabled by default because most suites do not test pytest itself.",[10,805,806,809,811,812,814],{},[471,807,808],{},"Should I use runpytest or runpytest_subprocess?",[14,810,440],{}," runs in-process and is much faster, which is right for most plugin tests. ",[14,813,460],{}," runs in a separate interpreter and is needed when the plugin changes global state that would leak between tests, or when testing command-line entry-point behaviour exactly.",[10,816,817,820,821,824,825,827],{},[471,818,819],{},"Can pytester test a conftest.py rather than a packaged plugin?","\nYes. ",[14,822,823],{},"pytester.makeconftest"," writes a ",[14,826,16],{}," into the temporary directory, so hooks and fixtures that live in a project's conftest can be copied or imported there and tested in isolation from the real suite.",[29,829,831],{"id":830},"related","Related",[34,833,834,840,846,853],{},[37,835,836,839],{},[53,837,838],{"href":55},"Building Custom pytest Plugins"," — the hooks these tests exercise.",[37,841,842,845],{},[53,843,844],{"href":549},"Adding Command-Line Options with pytest_addoption"," — options worth pinning down with pytester.",[37,847,848,852],{},[53,849,851],{"href":850},"\u002Fadvanced-pytest-architecture-configuration\u002Fassertion-introspection-and-reporting\u002Fcustomizing-failure-output-with-assertrepr-compare\u002F","Customizing Failure Output with assertrepr_compare"," — a hook whose output is ideal for fnmatch_lines tests.",[37,854,855,859],{},[53,856,858],{"href":857},"\u002Fadvanced-pytest-architecture-configuration\u002Fbuilding-custom-pytest-plugins\u002Fpackaging-a-pytest-plugin-with-entry-points\u002F","Packaging a pytest Plugin with Entry Points"," — how a tested plugin reaches users.",[10,861,862,863],{},"← Back to ",[53,864,838],{"href":55},[866,867,868],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":71,"searchDepth":84,"depth":84,"links":870},[871,872,873,874,875,876,877,878,879],{"id":31,"depth":84,"text":32},{"id":60,"depth":84,"text":61},{"id":419,"depth":84,"text":420},{"id":464,"depth":84,"text":465},{"id":518,"depth":84,"text":519},{"id":621,"depth":84,"text":622},{"id":747,"depth":84,"text":748},{"id":785,"depth":84,"text":786},{"id":830,"depth":84,"text":831},"Test pytest plugins, hooks and conftest code end to end with pytester: generated test files, assert_outcomes, output matching, in-process versus subprocess runs.","md",{"slug":883,"type":884,"breadcrumb":20,"datePublished":885,"dateModified":885,"faq":886,"howto":893},"testing-a-pytest-plugin-with-the-pytester-fixture","article","2026-09-18",[887,889,891],{"q":791,"a":888},"Add pytest_plugins = ['pytester'] to the root conftest.py, or pass -p pytester on the command line. It ships with pytest but is disabled by default because most suites do not test pytest itself.",{"q":808,"a":890},"runpytest runs in-process and is much faster, which is right for most plugin tests. runpytest_subprocess runs in a separate interpreter and is needed when the plugin changes global state that would leak between tests, or when testing command-line entry-point behaviour exactly.",{"q":819,"a":892},"Yes. pytester.makeconftest writes a conftest.py into the temporary directory, so hooks and fixtures that live in a project's conftest can be copied or imported there and tested in isolation from the real suite.",{"name":894,"description":895,"steps":896},"How to test a pytest plugin with pytester","Generate a small test project, run pytest on it with the plugin active, and assert on outcomes and output.",[897,900,903,906,909],{"name":898,"text":899},"Enable pytester","Add pytest_plugins = ['pytester'] to the root conftest so the fixture is available.",{"name":901,"text":902},"Write the project under test","Use makepyfile and makeconftest to create the tests and configuration the plugin should act on.",{"name":904,"text":905},"Run pytest on it","Call runpytest with the arguments the scenario needs, including -p to load the plugin if it is not auto-registered.",{"name":907,"text":908},"Assert on outcomes","Use assert_outcomes for counts of passed, failed, skipped and errors.",{"name":910,"text":911},"Assert on output where it matters","Use stdout.fnmatch_lines to check messages, headers or summary lines the plugin produces.","\u002Fadvanced-pytest-architecture-configuration\u002Fbuilding-custom-pytest-plugins\u002Ftesting-a-pytest-plugin-with-the-pytester-fixture",{"title":5,"description":880},"advanced-pytest-architecture-configuration\u002Fbuilding-custom-pytest-plugins\u002Ftesting-a-pytest-plugin-with-the-pytester-fixture\u002Findex","4wFHN6Lv2yn8Uu1SwoLrH0al9G7OEE_WVUwP0VA4l8c",1789718768496]