[{"data":1,"prerenderedAt":1997},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Ftest-processing-algorithms-with-pytest-qgis":3},{"id":4,"title":5,"body":6,"description":1987,"extension":1988,"meta":1989,"navigation":372,"path":1993,"seo":1994,"stem":1995,"__hash__":1996},"docs\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Ftest-processing-algorithms-with-pytest-qgis\u002Findex.md","Test Processing Algorithms with pytest-qgis",{"type":7,"value":8,"toc":1972},"minimark",[9,13,22,31,179,184,216,220,223,257,293,302,309,313,320,456,469,473,476,652,673,677,680,981,993,1086,1090,1093,1343,1352,1356,1359,1592,1609,1613,1616,1719,1739,1743,1749,1829,1833,1882,1886,1892,1896,1906,1916,1926,1935,1939,1968],[10,11,5],"h1",{"id":12},"test-processing-algorithms-with-pytest-qgis",[14,15,16,17,21],"p",{},"A Processing algorithm is the easiest part of a QGIS plugin to test properly: it takes a parameter dictionary, produces outputs, and touches no interface. What normally stops people is the boilerplate around it — initialising the application, registering providers, and tearing it all down without crashing. ",[18,19,20],"code",{},"pytest-qgis"," handles that, leaving tests that read like ordinary Python.",[14,23,24,25,30],{},"This recipe belongs to ",[26,27,29],"a",{"href":28},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002F","Testing & CI for Plugins",". It covers installing and configuring the plugin, the fixtures worth knowing, building input layers in memory, asserting on geometry and attributes, and running the whole thing headlessly.",[14,32,33],{},[34,35,40,44,48,55,72,81,91,97,102,107,111,116,123,129,133,136,139,142,145,149,154,158,161,165,169,173],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 300","img","A test building an in-memory input layer, running the algorithm, and asserting on the returned output layer, with no files and no interface involved","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"A Processing test with nothing on disk",[45,46,47],"desc",{},"The test constructs an input layer in memory with known geometry and attributes, calls the algorithm with a parameter dictionary and a temporary output, and asserts on the feature count, geometry and attribute values of the result. No files are written and no interface is created.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","300","#f6f3ea",[56,57,58],"defs",{},[59,60,67],"marker",{"id":61,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"tstArrow","0 0 10 10","8","5","7","auto-start-reverse",[68,69],"path",{"d":70,"fill":71},"M0 0 L10 5 L0 10 z","#2f3b35",[73,74,80],"text",{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Known input, one call, specific assertions",[49,82],{"x":83,"y":84,"width":85,"height":86,"rx":87,"fill":88,"stroke":89,"style":90},"20","66","212","150","10","#eff3ff","#2563eb","stroke-width:2.5",[73,92,96],{"x":93,"y":94,"style":95,"fill":89,"textAnchor":79},"126","94","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","memory layer",[73,98,101],{"x":93,"y":99,"style":100,"fill":71,"textAnchor":79},"120","text-anchor:middle;font-size:10px;font-family:monospace","Point?crs=EPSG:27700",[73,103,106],{"x":93,"y":104,"style":105,"fill":71,"textAnchor":79},"144","text-anchor:middle;font-size:10px;font-family:sans-serif","3 features, known values",[73,108,110],{"x":93,"y":109,"style":105,"fill":71,"textAnchor":79},"168","built in the test",[73,112,115],{"x":93,"y":113,"style":105,"fill":114,"textAnchor":79},"196","#15803d","nothing on disk",[117,118],"line",{"x1":119,"y1":120,"x2":121,"y2":120,"stroke":71,"style":122},"238","140","272","stroke-width:2;marker-end:url(#tstArrow)",[49,124],{"x":125,"y":84,"width":126,"height":86,"rx":87,"fill":127,"stroke":128,"style":90},"280","200","#eef7f4","#0f766e",[73,130,132],{"x":75,"y":94,"style":131,"fill":128,"textAnchor":79},"text-anchor:middle;font-size:11px;font-weight:bold;font-family:monospace","processing.run()",[73,134,135],{"x":75,"y":99,"style":105,"fill":71,"textAnchor":79},"your algorithm id",[73,137,138],{"x":75,"y":104,"style":100,"fill":71,"textAnchor":79},"TEMPORARY_OUTPUT",[73,140,141],{"x":75,"y":109,"style":105,"fill":71,"textAnchor":79},"with a feedback object",[73,143,144],{"x":75,"y":113,"style":105,"fill":114,"textAnchor":79},"still nothing on disk",[117,146],{"x1":147,"y1":120,"x2":148,"y2":120,"stroke":71,"style":122},"486","520",[49,150],{"x":151,"y":84,"width":152,"height":86,"rx":87,"fill":153,"stroke":114,"style":90},"528","208","#edf8e9",[73,155,157],{"x":156,"y":94,"style":95,"fill":114,"textAnchor":79},"632","assertions",[73,159,160],{"x":156,"y":99,"style":105,"fill":71,"textAnchor":79},"feature count",[73,162,164],{"x":156,"y":163,"style":105,"fill":71,"textAnchor":79},"142","geometry, to a tolerance",[73,166,168],{"x":156,"y":167,"style":105,"fill":71,"textAnchor":79},"164","attribute values",[73,170,172],{"x":156,"y":171,"style":105,"fill":71,"textAnchor":79},"192","and the warnings list",[73,174,178],{"x":75,"y":175,"style":176,"fill":177,"textAnchor":79},"264","text-anchor:middle;font-size:11px;font-family:sans-serif","#59645f","a test like this runs in milliseconds and needs no display",[180,181,183],"h2",{"id":182},"prerequisites","Prerequisites",[185,186,187,200,207],"ul",{},[188,189,190,194,195,199],"li",{},[191,192,193],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer, importable by the Python running pytest — either the system install or a ",[26,196,198],{"href":197},"\u002Fpyqgis-fundamentals-environment-setup\u002Fvirtual-environments-for-gis\u002Fuse-pyqgis-with-conda-and-mamba\u002F","conda environment",".",[188,201,202,203,199],{},"A custom algorithm to test; see ",[26,204,206],{"href":205},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Fwrite-custom-processing-algorithm-pyqgis\u002F","writing a custom processing algorithm",[188,208,209,212,213,215],{},[18,210,211],{},"pytest"," and ",[18,214,20],{}," installed into the same environment.",[180,217,219],{"id":218},"install-and-configure","Install and configure",[14,221,222],{},"Two lines of configuration make the fixtures available everywhere.",[224,225,230],"pre",{"className":226,"code":227,"language":228,"meta":229,"style":229},"language-bash shiki shiki-themes github-dark","python3 -m pip install pytest pytest-qgis\n","bash","",[18,231,232],{"__ignoreMap":229},[233,234,236,240,244,248,251,254],"span",{"class":117,"line":235},1,[233,237,239],{"class":238},"svObZ","python3",[233,241,243],{"class":242},"sDLfK"," -m",[233,245,247],{"class":246},"sU2Wk"," pip",[233,249,250],{"class":246}," install",[233,252,253],{"class":246}," pytest",[233,255,256],{"class":246}," pytest-qgis\n",[224,258,262],{"className":259,"code":260,"language":261,"meta":229,"style":229},"language-ini shiki shiki-themes github-dark","# pytest.ini\n[pytest]\nqgis_qui_enabled = false\nqgis_canvas_enabled = false\naddopts = -q\n","ini",[18,263,264,269,275,281,287],{"__ignoreMap":229},[233,265,266],{"class":117,"line":235},[233,267,268],{},"# pytest.ini\n",[233,270,272],{"class":117,"line":271},2,[233,273,274],{},"[pytest]\n",[233,276,278],{"class":117,"line":277},3,[233,279,280],{},"qgis_qui_enabled = false\n",[233,282,284],{"class":117,"line":283},4,[233,285,286],{},"qgis_canvas_enabled = false\n",[233,288,290],{"class":117,"line":289},5,[233,291,292],{},"addopts = -q\n",[14,294,295,298,299,301],{},[191,296,297],{},"Breakdown:"," Disabling the interface and the canvas is what makes the suite fast and headless — the fixtures still initialise the QGIS application, which is what registers providers and loads the PROJ database, but no widgets are created. ",[18,300,20],{}," starts the application once for the whole session and shuts it down at the end, which matters because initialising QGIS repeatedly is slow and exiting it repeatedly is unstable.",[14,303,304,305,308],{},"On a headless machine, ",[18,306,307],{},"QT_QPA_PLATFORM=offscreen"," is still worth setting in the environment, because some Qt builds construct a platform plugin regardless of whether widgets are used.",[180,310,312],{"id":311},"register-your-provider-once","Register your provider once",[14,314,315,316,319],{},"An algorithm has to be in the registry before ",[18,317,318],{},"processing.run"," can find it.",[224,321,325],{"className":322,"code":323,"language":324,"meta":229,"style":229},"language-python shiki shiki-themes github-dark","# conftest.py\nimport pytest\nfrom qgis.core import QgsApplication\nfrom my_plugin.provider import MyProvider\n\n\n@pytest.fixture(scope=\"session\", autouse=True)\ndef provider(qgis_app):\n    provider = MyProvider()\n    QgsApplication.processingRegistry().addProvider(provider)\n    yield provider\n    QgsApplication.processingRegistry().removeProvider(provider)\n","python",[18,326,327,333,343,356,368,374,379,412,424,435,441,450],{"__ignoreMap":229},[233,328,329],{"class":117,"line":235},[233,330,332],{"class":331},"sjoCn","# conftest.py\n",[233,334,335,339],{"class":117,"line":271},[233,336,338],{"class":337},"snl16","import",[233,340,342],{"class":341},"s95oV"," pytest\n",[233,344,345,348,351,353],{"class":117,"line":277},[233,346,347],{"class":337},"from",[233,349,350],{"class":341}," qgis.core ",[233,352,338],{"class":337},[233,354,355],{"class":341}," QgsApplication\n",[233,357,358,360,363,365],{"class":117,"line":283},[233,359,347],{"class":337},[233,361,362],{"class":341}," my_plugin.provider ",[233,364,338],{"class":337},[233,366,367],{"class":341}," MyProvider\n",[233,369,370],{"class":117,"line":289},[233,371,373],{"emptyLinePlaceholder":372},true,"\n",[233,375,377],{"class":117,"line":376},6,[233,378,373],{"emptyLinePlaceholder":372},[233,380,382,385,388,392,395,398,401,404,406,409],{"class":117,"line":381},7,[233,383,384],{"class":238},"@pytest.fixture",[233,386,387],{"class":341},"(",[233,389,391],{"class":390},"s9osk","scope",[233,393,394],{"class":337},"=",[233,396,397],{"class":246},"\"session\"",[233,399,400],{"class":341},", ",[233,402,403],{"class":390},"autouse",[233,405,394],{"class":337},[233,407,408],{"class":242},"True",[233,410,411],{"class":341},")\n",[233,413,415,418,421],{"class":117,"line":414},8,[233,416,417],{"class":337},"def",[233,419,420],{"class":238}," provider",[233,422,423],{"class":341},"(qgis_app):\n",[233,425,427,430,432],{"class":117,"line":426},9,[233,428,429],{"class":341},"    provider ",[233,431,394],{"class":337},[233,433,434],{"class":341}," MyProvider()\n",[233,436,438],{"class":117,"line":437},10,[233,439,440],{"class":341},"    QgsApplication.processingRegistry().addProvider(provider)\n",[233,442,444,447],{"class":117,"line":443},11,[233,445,446],{"class":337},"    yield",[233,448,449],{"class":341}," provider\n",[233,451,453],{"class":117,"line":452},12,[233,454,455],{"class":341},"    QgsApplication.processingRegistry().removeProvider(provider)\n",[14,457,458,460,461,464,465,468],{},[191,459,297],{}," Depending on the ",[18,462,463],{},"qgis_app"," fixture is what orders this after the application exists — without it the registry may not be ready. Session scope means the provider is added once rather than per test, which is both faster and closer to how it behaves in QGIS. Keeping a reference by yielding the provider rather than a bare ",[18,466,467],{},"yield"," prevents it being garbage collected while registered, which produces a crash rather than an error. Removing it afterwards keeps the session clean if other tests inspect the registry.",[180,470,472],{"id":471},"build-input-in-memory","Build input in memory",[14,474,475],{},"Reading a fixture file makes tests slow and makes failures ambiguous. Constructing the input in the test makes the expected result obvious.",[224,477,479],{"className":322,"code":478,"language":324,"meta":229,"style":229},"from qgis.core import QgsVectorLayer, QgsFeature, QgsGeometry, QgsPointXY, QgsField\nfrom qgis.PyQt.QtCore import QVariant\n\n\ndef make_points(coords, values):\n    layer = QgsVectorLayer(\"Point?crs=EPSG:27700\", \"input\", \"memory\")\n    layer.dataProvider().addAttributes([QgsField(\"value\", QVariant.Double)])\n    layer.updateFields()\n\n    features = []\n    for (x, y), value in zip(coords, values):\n        feature = QgsFeature(layer.fields())\n        feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y)))\n        feature.setAttribute(\"value\", value)\n        features.append(feature)\n\n    layer.dataProvider().addFeatures(features)\n    layer.updateExtents()\n    return layer\n",[18,480,481,492,504,508,512,522,547,558,563,567,577,593,603,609,620,626,631,637,643],{"__ignoreMap":229},[233,482,483,485,487,489],{"class":117,"line":235},[233,484,347],{"class":337},[233,486,350],{"class":341},[233,488,338],{"class":337},[233,490,491],{"class":341}," QgsVectorLayer, QgsFeature, QgsGeometry, QgsPointXY, QgsField\n",[233,493,494,496,499,501],{"class":117,"line":271},[233,495,347],{"class":337},[233,497,498],{"class":341}," qgis.PyQt.QtCore ",[233,500,338],{"class":337},[233,502,503],{"class":341}," QVariant\n",[233,505,506],{"class":117,"line":277},[233,507,373],{"emptyLinePlaceholder":372},[233,509,510],{"class":117,"line":283},[233,511,373],{"emptyLinePlaceholder":372},[233,513,514,516,519],{"class":117,"line":289},[233,515,417],{"class":337},[233,517,518],{"class":238}," make_points",[233,520,521],{"class":341},"(coords, values):\n",[233,523,524,527,529,532,535,537,540,542,545],{"class":117,"line":376},[233,525,526],{"class":341},"    layer ",[233,528,394],{"class":337},[233,530,531],{"class":341}," QgsVectorLayer(",[233,533,534],{"class":246},"\"Point?crs=EPSG:27700\"",[233,536,400],{"class":341},[233,538,539],{"class":246},"\"input\"",[233,541,400],{"class":341},[233,543,544],{"class":246},"\"memory\"",[233,546,411],{"class":341},[233,548,549,552,555],{"class":117,"line":381},[233,550,551],{"class":341},"    layer.dataProvider().addAttributes([QgsField(",[233,553,554],{"class":246},"\"value\"",[233,556,557],{"class":341},", QVariant.Double)])\n",[233,559,560],{"class":117,"line":414},[233,561,562],{"class":341},"    layer.updateFields()\n",[233,564,565],{"class":117,"line":426},[233,566,373],{"emptyLinePlaceholder":372},[233,568,569,572,574],{"class":117,"line":437},[233,570,571],{"class":341},"    features ",[233,573,394],{"class":337},[233,575,576],{"class":341}," []\n",[233,578,579,582,585,588,591],{"class":117,"line":443},[233,580,581],{"class":337},"    for",[233,583,584],{"class":341}," (x, y), value ",[233,586,587],{"class":337},"in",[233,589,590],{"class":242}," zip",[233,592,521],{"class":341},[233,594,595,598,600],{"class":117,"line":452},[233,596,597],{"class":341},"        feature ",[233,599,394],{"class":337},[233,601,602],{"class":341}," QgsFeature(layer.fields())\n",[233,604,606],{"class":117,"line":605},13,[233,607,608],{"class":341},"        feature.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(x, y)))\n",[233,610,612,615,617],{"class":117,"line":611},14,[233,613,614],{"class":341},"        feature.setAttribute(",[233,616,554],{"class":246},[233,618,619],{"class":341},", value)\n",[233,621,623],{"class":117,"line":622},15,[233,624,625],{"class":341},"        features.append(feature)\n",[233,627,629],{"class":117,"line":628},16,[233,630,373],{"emptyLinePlaceholder":372},[233,632,634],{"class":117,"line":633},17,[233,635,636],{"class":341},"    layer.dataProvider().addFeatures(features)\n",[233,638,640],{"class":117,"line":639},18,[233,641,642],{"class":341},"    layer.updateExtents()\n",[233,644,646,649],{"class":117,"line":645},19,[233,647,648],{"class":337},"    return",[233,650,651],{"class":341}," layer\n",[14,653,654,656,657,660,661,664,665,668,669,672],{},[191,655,297],{}," The URI string carries the geometry type and CRS, so a memory layer needs no file and no provider setup. ",[18,658,659],{},"updateFields()"," after ",[18,662,663],{},"addAttributes()"," is mandatory — the layer caches its field list and a feature built before the refresh gets the wrong field count. ",[18,666,667],{},"updateExtents()"," at the end matters for any algorithm that reads the extent, and its absence produces an empty result that looks like an algorithm bug. Adding features in one ",[18,670,671],{},"addFeatures()"," call rather than one at a time is both faster and the only way to get a single provider transaction.",[180,674,676],{"id":675},"write-the-test","Write the test",[14,678,679],{},"With the input in hand, the test is three lines of setup and specific assertions.",[224,681,683],{"className":322,"code":682,"language":324,"meta":229,"style":229},"import processing\nfrom qgis.core import QgsProcessingFeedback\n\n\ndef test_buffer_by_value_produces_expected_areas():\n    layer = make_points([(0, 0), (100, 0)], [10.0, 20.0])\n    feedback = QgsProcessingFeedback()\n\n    result = processing.run(\n        \"myplugin:bufferbyvalue\",\n        {\"INPUT\": layer, \"FIELD\": \"value\", \"OUTPUT\": \"TEMPORARY_OUTPUT\"},\n        feedback=feedback,\n    )\n\n    output = result[\"OUTPUT\"]\n    assert output.featureCount() == 2\n\n    areas = sorted(f.geometry().area() for f in output.getFeatures())\n    assert areas[0] == pytest.approx(3.14159 * 10 ** 2, rel=0.01)\n    assert areas[1] == pytest.approx(3.14159 * 20 ** 2, rel=0.01)\n",[18,684,685,692,703,707,711,721,760,770,774,784,792,824,834,839,843,858,872,876,900,944],{"__ignoreMap":229},[233,686,687,689],{"class":117,"line":235},[233,688,338],{"class":337},[233,690,691],{"class":341}," processing\n",[233,693,694,696,698,700],{"class":117,"line":271},[233,695,347],{"class":337},[233,697,350],{"class":341},[233,699,338],{"class":337},[233,701,702],{"class":341}," QgsProcessingFeedback\n",[233,704,705],{"class":117,"line":277},[233,706,373],{"emptyLinePlaceholder":372},[233,708,709],{"class":117,"line":283},[233,710,373],{"emptyLinePlaceholder":372},[233,712,713,715,718],{"class":117,"line":289},[233,714,417],{"class":337},[233,716,717],{"class":238}," test_buffer_by_value_produces_expected_areas",[233,719,720],{"class":341},"():\n",[233,722,723,725,727,730,732,734,736,739,742,744,746,749,752,754,757],{"class":117,"line":376},[233,724,526],{"class":341},[233,726,394],{"class":337},[233,728,729],{"class":341}," make_points([(",[233,731,51],{"class":242},[233,733,400],{"class":341},[233,735,51],{"class":242},[233,737,738],{"class":341},"), (",[233,740,741],{"class":242},"100",[233,743,400],{"class":341},[233,745,51],{"class":242},[233,747,748],{"class":341},")], [",[233,750,751],{"class":242},"10.0",[233,753,400],{"class":341},[233,755,756],{"class":242},"20.0",[233,758,759],{"class":341},"])\n",[233,761,762,765,767],{"class":117,"line":381},[233,763,764],{"class":341},"    feedback ",[233,766,394],{"class":337},[233,768,769],{"class":341}," QgsProcessingFeedback()\n",[233,771,772],{"class":117,"line":414},[233,773,373],{"emptyLinePlaceholder":372},[233,775,776,779,781],{"class":117,"line":426},[233,777,778],{"class":341},"    result ",[233,780,394],{"class":337},[233,782,783],{"class":341}," processing.run(\n",[233,785,786,789],{"class":117,"line":437},[233,787,788],{"class":246},"        \"myplugin:bufferbyvalue\"",[233,790,791],{"class":341},",\n",[233,793,794,797,800,803,806,809,811,813,816,818,821],{"class":117,"line":443},[233,795,796],{"class":341},"        {",[233,798,799],{"class":246},"\"INPUT\"",[233,801,802],{"class":341},": layer, ",[233,804,805],{"class":246},"\"FIELD\"",[233,807,808],{"class":341},": ",[233,810,554],{"class":246},[233,812,400],{"class":341},[233,814,815],{"class":246},"\"OUTPUT\"",[233,817,808],{"class":341},[233,819,820],{"class":246},"\"TEMPORARY_OUTPUT\"",[233,822,823],{"class":341},"},\n",[233,825,826,829,831],{"class":117,"line":452},[233,827,828],{"class":390},"        feedback",[233,830,394],{"class":337},[233,832,833],{"class":341},"feedback,\n",[233,835,836],{"class":117,"line":605},[233,837,838],{"class":341},"    )\n",[233,840,841],{"class":117,"line":611},[233,842,373],{"emptyLinePlaceholder":372},[233,844,845,848,850,853,855],{"class":117,"line":622},[233,846,847],{"class":341},"    output ",[233,849,394],{"class":337},[233,851,852],{"class":341}," result[",[233,854,815],{"class":246},[233,856,857],{"class":341},"]\n",[233,859,860,863,866,869],{"class":117,"line":628},[233,861,862],{"class":337},"    assert",[233,864,865],{"class":341}," output.featureCount() ",[233,867,868],{"class":337},"==",[233,870,871],{"class":242}," 2\n",[233,873,874],{"class":117,"line":633},[233,875,373],{"emptyLinePlaceholder":372},[233,877,878,881,883,886,889,892,895,897],{"class":117,"line":639},[233,879,880],{"class":341},"    areas ",[233,882,394],{"class":337},[233,884,885],{"class":242}," sorted",[233,887,888],{"class":341},"(f.geometry().area() ",[233,890,891],{"class":337},"for",[233,893,894],{"class":341}," f ",[233,896,587],{"class":337},[233,898,899],{"class":341}," output.getFeatures())\n",[233,901,902,904,907,909,912,914,917,920,923,926,929,932,934,937,939,942],{"class":117,"line":645},[233,903,862],{"class":337},[233,905,906],{"class":341}," areas[",[233,908,51],{"class":242},[233,910,911],{"class":341},"] ",[233,913,868],{"class":337},[233,915,916],{"class":341}," pytest.approx(",[233,918,919],{"class":242},"3.14159",[233,921,922],{"class":337}," *",[233,924,925],{"class":242}," 10",[233,927,928],{"class":337}," **",[233,930,931],{"class":242}," 2",[233,933,400],{"class":341},[233,935,936],{"class":390},"rel",[233,938,394],{"class":337},[233,940,941],{"class":242},"0.01",[233,943,411],{"class":341},[233,945,947,949,951,954,956,958,960,962,964,967,969,971,973,975,977,979],{"class":117,"line":946},20,[233,948,862],{"class":337},[233,950,906],{"class":341},[233,952,953],{"class":242},"1",[233,955,911],{"class":341},[233,957,868],{"class":337},[233,959,916],{"class":341},[233,961,919],{"class":242},[233,963,922],{"class":337},[233,965,966],{"class":242}," 20",[233,968,928],{"class":337},[233,970,931],{"class":242},[233,972,400],{"class":341},[233,974,936],{"class":390},[233,976,394],{"class":337},[233,978,941],{"class":242},[233,980,411],{"class":341},[14,982,983,985,986,988,989,992],{},[191,984,297],{}," ",[18,987,138],{}," returns a layer object rather than a path, so nothing touches the disk and the assertions run against the result directly. ",[18,990,991],{},"pytest.approx"," with a relative tolerance is essential for any geometry assertion — a buffer is a polygon approximation of a circle, so the area is close but never exact, and the segment count affects it. Sorting the areas removes any dependence on feature order, which no algorithm guarantees. Passing a feedback object even when nothing reads it keeps the call identical to production and makes it easy to add warning assertions later.",[14,994,995],{},[34,996,999,1002,1005,1008,1011,1015,1020,1024,1027,1031,1035,1039,1042,1045,1048,1051,1054,1057,1060,1066,1070,1073,1076,1079,1083],{"viewBox":997,"role":37,"ariaLabel":998,"xmlns":39},"0 0 760 288","Assertion styles ranked by how well they localise a failure: exact equality on counts, approximate comparison on geometry, and set comparison on attributes",[41,1000,1001],{},"Assert on the thing that would actually change",[45,1003,1004],{},"Counts and attribute sets can be compared exactly. Geometry must be compared with a tolerance because buffers, transforms and simplification all produce floating point results that are close rather than identical. Comparing whole exported files is brittle and localises failures poorly.",[49,1006],{"x":51,"y":51,"width":52,"height":1007,"fill":54},"288",[73,1009,1010],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"A failing assertion should name what changed",[49,1012],{"x":83,"y":1013,"width":1014,"height":126,"rx":87,"fill":153,"stroke":114,"style":90},"56","228",[73,1016,1019],{"x":1017,"y":1018,"style":95,"fill":114,"textAnchor":79},"134","84","counts and sets",[73,1021,1023],{"x":1017,"y":1022,"style":100,"fill":71,"textAnchor":79},"112","featureCount() == 2",[73,1025,1026],{"x":1017,"y":1017,"style":100,"fill":71,"textAnchor":79},"fields() names == …",[73,1028,1030],{"x":1017,"y":1029,"style":105,"fill":71,"textAnchor":79},"162","exact comparison",[73,1032,1034],{"x":1017,"y":1033,"style":105,"fill":114,"textAnchor":79},"188","fails loudly and precisely",[73,1036,1038],{"x":1017,"y":1037,"style":105,"fill":177,"textAnchor":79},"222","assert these first",[49,1040],{"x":1041,"y":1013,"width":1014,"height":126,"rx":87,"fill":88,"stroke":89,"style":90},"266",[73,1043,1044],{"x":75,"y":1018,"style":95,"fill":89,"textAnchor":79},"geometry",[73,1046,1047],{"x":75,"y":1022,"style":100,"fill":71,"textAnchor":79},"area == approx(…)",[73,1049,1050],{"x":75,"y":1017,"style":100,"fill":71,"textAnchor":79},"equals(other, 1e-6)",[73,1052,1053],{"x":75,"y":1029,"style":105,"fill":71,"textAnchor":79},"always a tolerance",[73,1055,1056],{"x":75,"y":1033,"style":105,"fill":89,"textAnchor":79},"segment count changes it",[73,1058,1059],{"x":75,"y":1037,"style":105,"fill":177,"textAnchor":79},"never assert exact WKT",[49,1061],{"x":1062,"y":1013,"width":1063,"height":126,"rx":87,"fill":1064,"stroke":1065,"style":90},"512","224","#fdf2e2","#b91c1c",[73,1067,1069],{"x":1068,"y":1018,"style":95,"fill":1065,"textAnchor":79},"624","whole-file compare",[73,1071,1072],{"x":1068,"y":1022,"style":105,"fill":71,"textAnchor":79},"write and diff a GeoPackage",[73,1074,1075],{"x":1068,"y":120,"style":105,"fill":1065,"textAnchor":79},"breaks on GDAL updates",[73,1077,1078],{"x":1068,"y":1029,"style":105,"fill":1065,"textAnchor":79},"breaks on timestamps",[73,1080,1082],{"x":1068,"y":1081,"style":105,"fill":1065,"textAnchor":79},"190","says only \"files differ\"",[73,1084,1085],{"x":1068,"y":1037,"style":105,"fill":177,"textAnchor":79},"avoid",[180,1087,1089],{"id":1088},"parametrise-instead-of-repeating","Parametrise instead of repeating",[14,1091,1092],{},"Most algorithms have a handful of interesting cases, and pytest's parametrisation turns them into one test with a table.",[224,1094,1096],{"className":322,"code":1095,"language":324,"meta":229,"style":229},"import pytest\n\n\n@pytest.mark.parametrize(\n    \"values,expected_count,note\",\n    [\n        ([10.0, 20.0], 2, \"both positive\"),\n        ([10.0, None], 1, \"null is skipped\"),\n        ([10.0, -5.0], 1, \"negative is skipped\"),\n        ([0.0, 0.0], 0, \"zero produces nothing\"),\n    ],\n)\ndef test_skipping_rules(values, expected_count, note):\n    layer = make_points([(0, 0), (100, 0)][: len(values)], values)\n    result = processing.run(\n        \"myplugin:bufferbyvalue\",\n        {\"INPUT\": layer, \"FIELD\": \"value\", \"OUTPUT\": \"TEMPORARY_OUTPUT\"},\n        feedback=QgsProcessingFeedback(),\n    )\n    assert result[\"OUTPUT\"].featureCount() == expected_count, note\n",[18,1097,1098,1104,1108,1112,1120,1127,1132,1157,1179,1204,1226,1231,1235,1245,1276,1284,1290,1314,1323,1327],{"__ignoreMap":229},[233,1099,1100,1102],{"class":117,"line":235},[233,1101,338],{"class":337},[233,1103,342],{"class":341},[233,1105,1106],{"class":117,"line":271},[233,1107,373],{"emptyLinePlaceholder":372},[233,1109,1110],{"class":117,"line":277},[233,1111,373],{"emptyLinePlaceholder":372},[233,1113,1114,1117],{"class":117,"line":283},[233,1115,1116],{"class":238},"@pytest.mark.parametrize",[233,1118,1119],{"class":341},"(\n",[233,1121,1122,1125],{"class":117,"line":289},[233,1123,1124],{"class":246},"    \"values,expected_count,note\"",[233,1126,791],{"class":341},[233,1128,1129],{"class":117,"line":376},[233,1130,1131],{"class":341},"    [\n",[233,1133,1134,1137,1139,1141,1143,1146,1149,1151,1154],{"class":117,"line":381},[233,1135,1136],{"class":341},"        ([",[233,1138,751],{"class":242},[233,1140,400],{"class":341},[233,1142,756],{"class":242},[233,1144,1145],{"class":341},"], ",[233,1147,1148],{"class":242},"2",[233,1150,400],{"class":341},[233,1152,1153],{"class":246},"\"both positive\"",[233,1155,1156],{"class":341},"),\n",[233,1158,1159,1161,1163,1165,1168,1170,1172,1174,1177],{"class":117,"line":414},[233,1160,1136],{"class":341},[233,1162,751],{"class":242},[233,1164,400],{"class":341},[233,1166,1167],{"class":242},"None",[233,1169,1145],{"class":341},[233,1171,953],{"class":242},[233,1173,400],{"class":341},[233,1175,1176],{"class":246},"\"null is skipped\"",[233,1178,1156],{"class":341},[233,1180,1181,1183,1185,1187,1190,1193,1195,1197,1199,1202],{"class":117,"line":426},[233,1182,1136],{"class":341},[233,1184,751],{"class":242},[233,1186,400],{"class":341},[233,1188,1189],{"class":337},"-",[233,1191,1192],{"class":242},"5.0",[233,1194,1145],{"class":341},[233,1196,953],{"class":242},[233,1198,400],{"class":341},[233,1200,1201],{"class":246},"\"negative is skipped\"",[233,1203,1156],{"class":341},[233,1205,1206,1208,1211,1213,1215,1217,1219,1221,1224],{"class":117,"line":437},[233,1207,1136],{"class":341},[233,1209,1210],{"class":242},"0.0",[233,1212,400],{"class":341},[233,1214,1210],{"class":242},[233,1216,1145],{"class":341},[233,1218,51],{"class":242},[233,1220,400],{"class":341},[233,1222,1223],{"class":246},"\"zero produces nothing\"",[233,1225,1156],{"class":341},[233,1227,1228],{"class":117,"line":443},[233,1229,1230],{"class":341},"    ],\n",[233,1232,1233],{"class":117,"line":452},[233,1234,411],{"class":341},[233,1236,1237,1239,1242],{"class":117,"line":605},[233,1238,417],{"class":337},[233,1240,1241],{"class":238}," test_skipping_rules",[233,1243,1244],{"class":341},"(values, expected_count, note):\n",[233,1246,1247,1249,1251,1253,1255,1257,1259,1261,1263,1265,1267,1270,1273],{"class":117,"line":611},[233,1248,526],{"class":341},[233,1250,394],{"class":337},[233,1252,729],{"class":341},[233,1254,51],{"class":242},[233,1256,400],{"class":341},[233,1258,51],{"class":242},[233,1260,738],{"class":341},[233,1262,741],{"class":242},[233,1264,400],{"class":341},[233,1266,51],{"class":242},[233,1268,1269],{"class":341},")][: ",[233,1271,1272],{"class":242},"len",[233,1274,1275],{"class":341},"(values)], values)\n",[233,1277,1278,1280,1282],{"class":117,"line":622},[233,1279,778],{"class":341},[233,1281,394],{"class":337},[233,1283,783],{"class":341},[233,1285,1286,1288],{"class":117,"line":628},[233,1287,788],{"class":246},[233,1289,791],{"class":341},[233,1291,1292,1294,1296,1298,1300,1302,1304,1306,1308,1310,1312],{"class":117,"line":633},[233,1293,796],{"class":341},[233,1295,799],{"class":246},[233,1297,802],{"class":341},[233,1299,805],{"class":246},[233,1301,808],{"class":341},[233,1303,554],{"class":246},[233,1305,400],{"class":341},[233,1307,815],{"class":246},[233,1309,808],{"class":341},[233,1311,820],{"class":246},[233,1313,823],{"class":341},[233,1315,1316,1318,1320],{"class":117,"line":639},[233,1317,828],{"class":390},[233,1319,394],{"class":337},[233,1321,1322],{"class":341},"QgsProcessingFeedback(),\n",[233,1324,1325],{"class":117,"line":645},[233,1326,838],{"class":341},[233,1328,1329,1331,1333,1335,1338,1340],{"class":117,"line":946},[233,1330,862],{"class":337},[233,1332,852],{"class":341},[233,1334,815],{"class":246},[233,1336,1337],{"class":341},"].featureCount() ",[233,1339,868],{"class":337},[233,1341,1342],{"class":341}," expected_count, note\n",[14,1344,1345,1347,1348,1351],{},[191,1346,297],{}," The ",[18,1349,1350],{},"note"," column is not decoration — pytest prints it on failure, so a red test says \"null is skipped\" rather than only naming an index. Each row documents one rule of the algorithm's contract, which makes the table a readable specification as well as a test. Adding a case is one line, which is what keeps the edge cases getting tested as they are discovered rather than fixed and forgotten.",[180,1353,1355],{"id":1354},"test-the-failures-too","Test the failures too",[14,1357,1358],{},"An algorithm's error handling is as much its behaviour as its happy path.",[224,1360,1362],{"className":322,"code":1361,"language":324,"meta":229,"style":229},"import pytest\nfrom qgis.core import QgsProcessingException\n\n\ndef test_empty_input_reports_a_warning():\n    layer = make_points([], [])\n    feedback = CollectingFeedback()\n\n    processing.run(\n        \"myplugin:bufferbyvalue\",\n        {\"INPUT\": layer, \"FIELD\": \"value\", \"OUTPUT\": \"TEMPORARY_OUTPUT\"},\n        feedback=feedback,\n    )\n\n    assert any(\"no features\" in w.lower() for w in feedback.warnings)\n\n\ndef test_missing_field_raises():\n    layer = make_points([(0, 0)], [1.0])\n    with pytest.raises(QgsProcessingException):\n        processing.run(\n            \"myplugin:bufferbyvalue\",\n            {\"INPUT\": layer, \"FIELD\": \"nonexistent\", \"OUTPUT\": \"TEMPORARY_OUTPUT\"},\n        )\n",[18,1363,1364,1370,1381,1385,1389,1398,1407,1416,1420,1425,1431,1455,1463,1467,1471,1499,1503,1507,1516,1537,1545,1551,1559,1586],{"__ignoreMap":229},[233,1365,1366,1368],{"class":117,"line":235},[233,1367,338],{"class":337},[233,1369,342],{"class":341},[233,1371,1372,1374,1376,1378],{"class":117,"line":271},[233,1373,347],{"class":337},[233,1375,350],{"class":341},[233,1377,338],{"class":337},[233,1379,1380],{"class":341}," QgsProcessingException\n",[233,1382,1383],{"class":117,"line":277},[233,1384,373],{"emptyLinePlaceholder":372},[233,1386,1387],{"class":117,"line":283},[233,1388,373],{"emptyLinePlaceholder":372},[233,1390,1391,1393,1396],{"class":117,"line":289},[233,1392,417],{"class":337},[233,1394,1395],{"class":238}," test_empty_input_reports_a_warning",[233,1397,720],{"class":341},[233,1399,1400,1402,1404],{"class":117,"line":376},[233,1401,526],{"class":341},[233,1403,394],{"class":337},[233,1405,1406],{"class":341}," make_points([], [])\n",[233,1408,1409,1411,1413],{"class":117,"line":381},[233,1410,764],{"class":341},[233,1412,394],{"class":337},[233,1414,1415],{"class":341}," CollectingFeedback()\n",[233,1417,1418],{"class":117,"line":414},[233,1419,373],{"emptyLinePlaceholder":372},[233,1421,1422],{"class":117,"line":426},[233,1423,1424],{"class":341},"    processing.run(\n",[233,1426,1427,1429],{"class":117,"line":437},[233,1428,788],{"class":246},[233,1430,791],{"class":341},[233,1432,1433,1435,1437,1439,1441,1443,1445,1447,1449,1451,1453],{"class":117,"line":443},[233,1434,796],{"class":341},[233,1436,799],{"class":246},[233,1438,802],{"class":341},[233,1440,805],{"class":246},[233,1442,808],{"class":341},[233,1444,554],{"class":246},[233,1446,400],{"class":341},[233,1448,815],{"class":246},[233,1450,808],{"class":341},[233,1452,820],{"class":246},[233,1454,823],{"class":341},[233,1456,1457,1459,1461],{"class":117,"line":452},[233,1458,828],{"class":390},[233,1460,394],{"class":337},[233,1462,833],{"class":341},[233,1464,1465],{"class":117,"line":605},[233,1466,838],{"class":341},[233,1468,1469],{"class":117,"line":611},[233,1470,373],{"emptyLinePlaceholder":372},[233,1472,1473,1475,1478,1480,1483,1486,1489,1491,1494,1496],{"class":117,"line":622},[233,1474,862],{"class":337},[233,1476,1477],{"class":242}," any",[233,1479,387],{"class":341},[233,1481,1482],{"class":246},"\"no features\"",[233,1484,1485],{"class":337}," in",[233,1487,1488],{"class":341}," w.lower() ",[233,1490,891],{"class":337},[233,1492,1493],{"class":341}," w ",[233,1495,587],{"class":337},[233,1497,1498],{"class":341}," feedback.warnings)\n",[233,1500,1501],{"class":117,"line":628},[233,1502,373],{"emptyLinePlaceholder":372},[233,1504,1505],{"class":117,"line":633},[233,1506,373],{"emptyLinePlaceholder":372},[233,1508,1509,1511,1514],{"class":117,"line":639},[233,1510,417],{"class":337},[233,1512,1513],{"class":238}," test_missing_field_raises",[233,1515,720],{"class":341},[233,1517,1518,1520,1522,1524,1526,1528,1530,1532,1535],{"class":117,"line":645},[233,1519,526],{"class":341},[233,1521,394],{"class":337},[233,1523,729],{"class":341},[233,1525,51],{"class":242},[233,1527,400],{"class":341},[233,1529,51],{"class":242},[233,1531,748],{"class":341},[233,1533,1534],{"class":242},"1.0",[233,1536,759],{"class":341},[233,1538,1539,1542],{"class":117,"line":946},[233,1540,1541],{"class":337},"    with",[233,1543,1544],{"class":341}," pytest.raises(QgsProcessingException):\n",[233,1546,1548],{"class":117,"line":1547},21,[233,1549,1550],{"class":341},"        processing.run(\n",[233,1552,1554,1557],{"class":117,"line":1553},22,[233,1555,1556],{"class":246},"            \"myplugin:bufferbyvalue\"",[233,1558,791],{"class":341},[233,1560,1562,1565,1567,1569,1571,1573,1576,1578,1580,1582,1584],{"class":117,"line":1561},23,[233,1563,1564],{"class":341},"            {",[233,1566,799],{"class":246},[233,1568,802],{"class":341},[233,1570,805],{"class":246},[233,1572,808],{"class":341},[233,1574,1575],{"class":246},"\"nonexistent\"",[233,1577,400],{"class":341},[233,1579,815],{"class":246},[233,1581,808],{"class":341},[233,1583,820],{"class":246},[233,1585,823],{"class":341},[233,1587,1589],{"class":117,"line":1588},24,[233,1590,1591],{"class":341},"        )\n",[14,1593,1594,985,1596,1599,1600,1604,1605,1608],{},[191,1595,297],{},[18,1597,1598],{},"CollectingFeedback"," is the small subclass described in ",[26,1601,1603],{"href":1602},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Fhandle-processing-feedback-and-errors-pyqgis\u002F","handling processing feedback and errors",", collecting messages into lists. Asserting on a substring rather than the exact message keeps the test from breaking when the wording is improved, while still checking that something relevant was said. A parameter that does not validate raises before the algorithm runs, which is why the second test uses ",[18,1606,1607],{},"pytest.raises"," rather than inspecting feedback.",[180,1610,1612],{"id":1611},"run-it-in-ci","Run it in CI",[14,1614,1615],{},"The suite needs QGIS present and a display absent, which a container gives directly.",[224,1617,1621],{"className":1618,"code":1619,"language":1620,"meta":229,"style":229},"language-yaml shiki shiki-themes github-dark","jobs:\n  test:\n    runs-on: ubuntu-latest\n    container: qgis\u002Fqgis:release-3_34\n    steps:\n      - uses: actions\u002Fcheckout@v4\n      - run: pip install pytest pytest-qgis\n      - run: xvfb-run -a pytest tests\u002F\n        env:\n          QT_QPA_PLATFORM: offscreen\n","yaml",[18,1622,1623,1632,1639,1649,1659,1666,1679,1691,1702,1709],{"__ignoreMap":229},[233,1624,1625,1629],{"class":117,"line":235},[233,1626,1628],{"class":1627},"s4JwU","jobs",[233,1630,1631],{"class":341},":\n",[233,1633,1634,1637],{"class":117,"line":271},[233,1635,1636],{"class":1627},"  test",[233,1638,1631],{"class":341},[233,1640,1641,1644,1646],{"class":117,"line":277},[233,1642,1643],{"class":1627},"    runs-on",[233,1645,808],{"class":341},[233,1647,1648],{"class":246},"ubuntu-latest\n",[233,1650,1651,1654,1656],{"class":117,"line":283},[233,1652,1653],{"class":1627},"    container",[233,1655,808],{"class":341},[233,1657,1658],{"class":246},"qgis\u002Fqgis:release-3_34\n",[233,1660,1661,1664],{"class":117,"line":289},[233,1662,1663],{"class":1627},"    steps",[233,1665,1631],{"class":341},[233,1667,1668,1671,1674,1676],{"class":117,"line":376},[233,1669,1670],{"class":341},"      - ",[233,1672,1673],{"class":1627},"uses",[233,1675,808],{"class":341},[233,1677,1678],{"class":246},"actions\u002Fcheckout@v4\n",[233,1680,1681,1683,1686,1688],{"class":117,"line":381},[233,1682,1670],{"class":341},[233,1684,1685],{"class":1627},"run",[233,1687,808],{"class":341},[233,1689,1690],{"class":246},"pip install pytest pytest-qgis\n",[233,1692,1693,1695,1697,1699],{"class":117,"line":414},[233,1694,1670],{"class":341},[233,1696,1685],{"class":1627},[233,1698,808],{"class":341},[233,1700,1701],{"class":246},"xvfb-run -a pytest tests\u002F\n",[233,1703,1704,1707],{"class":117,"line":426},[233,1705,1706],{"class":1627},"        env",[233,1708,1631],{"class":341},[233,1710,1711,1714,1716],{"class":117,"line":437},[233,1712,1713],{"class":1627},"          QT_QPA_PLATFORM",[233,1715,808],{"class":341},[233,1717,1718],{"class":246},"offscreen\n",[14,1720,1721,1723,1724,212,1727,1729,1730,1733,1734,1738],{},[191,1722,297],{}," The official QGIS container images carry a matching Python with the bindings already on the path, so no environment setup is needed beyond the test dependencies. ",[18,1725,1726],{},"xvfb-run",[18,1728,307],{}," are belt and braces — either alone usually suffices, and having both costs nothing and saves a confusing failure on an image where one is missing. Pinning the container to a release tag rather than ",[18,1731,1732],{},"latest"," is what makes a green build mean something six months later; see ",[26,1735,1737],{"href":1736},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Frun-qgis-plugin-tests-in-github-actions\u002F","running QGIS plugin tests in GitHub Actions"," for the full workflow.",[180,1740,1742],{"id":1741},"qgis-version-compatibility","QGIS version compatibility",[14,1744,1745,1746,1748],{},"The examples target ",[191,1747,193],{}," (Python 3.12).",[1750,1751,1752,1768],"table",{},[1753,1754,1755],"thead",{},[1756,1757,1758,1762,1765],"tr",{},[1759,1760,1761],"th",{},"QGIS version",[1759,1763,1764],{},"Python",[1759,1766,1767],{},"Notes",[1769,1770,1771,1785,1796,1808,1819],"tbody",{},[1756,1772,1773,1777,1780],{},[1774,1775,1776],"td",{},"3.16 LTR",[1774,1778,1779],{},"3.7",[1774,1781,1782,1784],{},[18,1783,20],{}," supports this line; fixture names as described.",[1756,1786,1787,1790,1793],{},[1774,1788,1789],{},"3.22 LTR",[1774,1791,1792],{},"3.9",[1774,1794,1795],{},"Memory provider URI options unchanged.",[1756,1797,1798,1801,1803],{},[1774,1799,1800],{},"3.28 LTR",[1774,1802,1792],{},[1774,1804,1805,1807],{},[18,1806,138],{}," returns layer objects consistently across native algorithms.",[1756,1809,1810,1813,1816],{},[1774,1811,1812],{},"3.34 LTR",[1774,1814,1815],{},"3.12",[1774,1817,1818],{},"Baseline for this page.",[1756,1820,1821,1824,1826],{},[1774,1822,1823],{},"3.40+",[1774,1825,1815],{},[1774,1827,1828],{},"Container images continue per release; check the tag exists before pinning.",[180,1830,1832],{"id":1831},"troubleshooting","Troubleshooting",[185,1834,1835,1843,1849,1859,1867,1876],{},[188,1836,1837,1840,1841,199],{},[191,1838,1839],{},"The algorithm id is not found."," The provider was not registered, or the fixture did not depend on ",[18,1842,463],{},[188,1844,1845,1848],{},[191,1846,1847],{},"The suite crashes at teardown."," The provider object was collected while still registered. Yield it from the fixture.",[188,1850,1851,985,1854,1856,1857,199],{},[191,1852,1853],{},"Features have the wrong field count.",[18,1855,659],{}," was not called after ",[18,1858,663],{},[188,1860,1861,985,1864,1866],{},[191,1862,1863],{},"An algorithm returns nothing from a memory layer.",[18,1865,667],{}," was skipped, so the layer reports an empty extent.",[188,1868,1869,1872,1873,1875],{},[191,1870,1871],{},"Geometry assertions fail by a tiny amount."," Compare with ",[18,1874,991],{}," and a relative tolerance, never exactly.",[188,1877,1878,1881],{},[191,1879,1880],{},"Tests pass locally and fail in CI."," A different GDAL or PROJ version. Pin the container tag and the environment.",[180,1883,1885],{"id":1884},"conclusion","Conclusion",[14,1887,1888,1889,1891],{},"Configure ",[18,1890,20],{}," with the interface and canvas disabled, register the provider once in a session fixture that yields it, build inputs as memory layers so the expected result is visible in the test, and assert on counts exactly and geometry approximately. Test the failure paths as deliberately as the happy one, and pin the container so CI results stay comparable.",[180,1893,1895],{"id":1894},"frequently-asked-questions","Frequently Asked Questions",[14,1897,1898,1901,1902,1905],{},[191,1899,1900],{},"Do I need pytest-qgis at all?","\nNo — the initialisation can be written by hand in a ",[18,1903,1904],{},"conftest.py",". The plugin exists because getting the setup and, especially, the teardown right is fiddly and easy to get subtly wrong.",[14,1907,1908,1911,1912,1915],{},[191,1909,1910],{},"Can I test dialogs the same way?","\nYes, with ",[18,1913,1914],{},"qgis_qui_enabled = true"," and pytest-qt for interaction. It is slower and more brittle than testing an algorithm, which is a good argument for keeping logic out of dialogs.",[14,1917,1918,1921,1922,1925],{},[191,1919,1920],{},"How do I test against a real file?","\nPut small fixture files in the test directory and load them with ",[18,1923,1924],{},"QgsVectorLayer(path, name, \"ogr\")",". Keep them tiny; a fixture nobody can read in a text editor makes failures hard to diagnose.",[14,1927,1928,1931,1932,1934],{},[191,1929,1930],{},"Should I test the native algorithms my code calls?","\nNo. Test that your algorithm passes the right parameters and handles the results, not that QGIS's buffer works. Mocking ",[18,1933,318],{}," is occasionally the right way to assert on the parameters.",[180,1936,1938],{"id":1937},"related","Related",[185,1940,1941,1946,1952,1957,1963],{},[188,1942,1943,1945],{},[26,1944,29],{"href":28}," — the guide this recipe belongs to",[188,1947,1948],{},[26,1949,1951],{"href":1950},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Funit-test-qgis-plugin-with-pytest\u002F","Unit Test a QGIS Plugin with pytest",[188,1953,1954],{},[26,1955,1956],{"href":1736},"Run QGIS Plugin Tests in GitHub Actions",[188,1958,1959],{},[26,1960,1962],{"href":1961},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Fmock-qgis-interface-in-plugin-tests\u002F","Mock the QGIS Interface in Plugin Tests",[188,1964,1965],{},[26,1966,1967],{"href":205},"Write a Custom Processing Algorithm in PyQGIS",[1969,1970,1971],"style",{},"html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}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 pre.shiki code .sjoCn, html code.shiki .sjoCn{--shiki-default:#9AA79F}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .s4JwU, html code.shiki .s4JwU{--shiki-default:#85E89D}",{"title":229,"searchDepth":271,"depth":271,"links":1973},[1974,1975,1976,1977,1978,1979,1980,1981,1982,1983,1984,1985,1986],{"id":182,"depth":271,"text":183},{"id":218,"depth":271,"text":219},{"id":311,"depth":271,"text":312},{"id":471,"depth":271,"text":472},{"id":675,"depth":271,"text":676},{"id":1088,"depth":271,"text":1089},{"id":1354,"depth":271,"text":1355},{"id":1611,"depth":271,"text":1612},{"id":1741,"depth":271,"text":1742},{"id":1831,"depth":271,"text":1832},{"id":1884,"depth":271,"text":1885},{"id":1894,"depth":271,"text":1895},{"id":1937,"depth":271,"text":1938},"Write fast, isolated tests for custom QGIS Processing algorithms — the pytest-qgis fixtures, building memory layers as input, asserting on outputs, and running headlessly in CI.","md",{"slug":12,"type":1990,"breadcrumb":1991,"datePublished":1992,"dateModified":1992},"article","Testing Processing Algorithms","2026-08-27","\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Ftest-processing-algorithms-with-pytest-qgis",{"title":5,"description":1987},"qgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Ftest-processing-algorithms-with-pytest-qgis\u002Findex","kXMo2uq8vI7EZTr1v4PXizpZW4fWqucS3j_0pboTJFA",1787823363254]