[{"data":1,"prerenderedAt":2843},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory":3,"surround:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory":2809,"ancestors:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory":2817,"children:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory":2822,"related:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory":2823},{"id":4,"title":5,"body":6,"dateModified":2776,"datePublished":2776,"description":2777,"extension":2778,"faq":2779,"keyword":2788,"kind":2789,"meta":2790,"navigation":334,"path":2798,"seo":2799,"slug":2802,"stem":2803,"topics":2804,"type":2807,"__hash__":2808},"docs\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory\u002Findex.md","Create Excel Test Fixtures in Memory for pytest",{"type":7,"value":8,"toc":2763},"minimark",[9,31,248,253,284,288,291,1268,1274,1278,1289,1576,1587,1591,1594,1737,1882,1898,1902,1912,2298,2339,2355,2465,2469,2476,2613,2616,2620,2630,2634,2686,2690,2700,2704,2710,2716,2722,2728,2732,2759],[10,11,12,13,17,18,17,21,24,25,30],"p",{},"Tests for Excel code tend to accumulate a folder of sample workbooks: ",[14,15,16],"code",{},"test_input.xlsx",", ",[14,19,20],{},"test_input2.xlsx",[14,22,23],{},"broken_one_from_finance.xlsx",". Nobody remembers what each contains, a diff cannot show what changed, and adding a case means opening Excel. Generating fixtures in code fixes all three — and makes it cheap to test the ugly layouts that real exports produce. This guide, part of ",[26,27,29],"a",{"href":28},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002F","Testing and Packaging Excel Automation Scripts",", builds a small fixture library, a loader that must cope with every variant, and a parametrized test suite that ran ten tests in a third of a second.",[32,33,41,42,41,46,41,50,41,58,41,68,41,79,41,86,41,91,41,99,41,106,41,112,41,117,41,120,41,125,41,128,41,134,41,138,41,142,41,145,41,148,41,153,41,160,41,165,41,169,41,172,41,175,41,180,41,187,41,192,41,196,41,199,41,202,41,205,41,211,41,215,41,219,41,222,41,225,41,228,41,234,41,238,41,242,41,245],"svg",{"viewBox":34,"role":35,"ariaLabelledBy":36,"xmlns":39,"style":40},"4 0 752 644","img",[37,38],"fx-fan-t","fx-fan-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[43,44,45],"title",{"id":37},"One helper, many awkward workbooks",[47,48,49],"desc",{"id":38},"A make_xlsx helper builds a workbook in memory from rows and optional extras. Small builder functions use it to produce the layouts real exports have — a clean table, title rows above the header, dates and numbers stored as text, error cells, formulas without stored results, styled empty rows and a hidden sheet in front — each returned as bytes.",[51,52],"rect",{"x":53,"y":54,"width":55,"height":56,"fill":57},"4","0","752","644","#ffffff",[51,59],{"x":60,"y":61,"width":62,"height":63,"rx":64,"fill":65,"stroke":66,"style":67},"20.0","275.5","220.0","93.0","12","#f0f4ff","var(--brand,#5b5cf0)","stroke-width:2px",[69,70,72],"g",{"transform":71},"translate(118.0,288.5) scale(1.000)",[73,74],"path",{"d":75,"fill":76,"stroke":77,"style":78},"M12 3c-4 0-4 1.6-4 3v2h4.2v1H6c-2 0-3 1.4-3 4s1 4 3 4h2v-2.6c0-1.6 1.3-2.9 2.9-2.9h4.2c1.3 0 2.4-1.1 2.4-2.4V6c0-1.6-1.8-3-5.5-3z M10 5.5h.01 M12 21c4 0 4-1.6 4-3v-2h-4.2v-1H18c2 0 3-1.4 3-4s-1-4-3-4h-2v2.6c0 1.6-1.3 2.9-2.9 2.9H8.9c-1.3 0-2.4 1.1-2.4 2.4V18c0 1.6 1.8 3 5.5 3z M14 18.5h.01","none","var(--brand-strong,#4338ca)","stroke-width:1.80px;stroke-linecap:round;stroke-linejoin:round",[80,81,85],"text",{"x":82,"y":83,"style":84},"130.0","335.0","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","make_xlsx(rows, …)",[80,87,90],{"x":82,"y":88,"style":89},"354.5","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","returns bytes",[51,92],{"x":93,"y":94,"width":95,"height":96,"rx":64,"fill":97,"stroke":98,"style":67},"490.0","16.0","250.0","92.0","#d9f4f1","var(--teal,#0f9488)",[69,100,102],{"transform":101},"translate(603.0,29.0) scale(1.000)",[73,103],{"d":104,"fill":76,"stroke":105,"style":78},"M5 12.5l4.5 4.5L19 7","var(--teal-ink,#0b6157)",[80,107,111],{"x":108,"y":109,"style":110},"615.0","75.0","font-size:13px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","clean",[80,113,116],{"x":108,"y":114,"style":115},"94.0","font-size:11px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","the happy path",[73,118],{"d":119,"fill":76,"stroke":66,"style":67},"M244.0,322.0 C364.0,322.0 364.0,62.0 480.0,62.0",[121,122],"polygon",{"points":123,"fill":124},"484.0,62.0 475.0,67.0 475.0,57.0","#5b5cf0",[51,126],{"x":93,"y":127,"width":95,"height":96,"rx":64,"fill":97,"stroke":98,"style":67},"120.0",[69,129,131],{"transform":130},"translate(603.0,133.0) scale(1.000)",[73,132],{"d":133,"fill":76,"stroke":105,"style":78},"M12 3l9 5-9 5-9-5z M3 13l9 5 9-5 M3 17l9 5 9-5",[80,135,137],{"x":108,"y":136,"style":110},"179.0","title_rows",[80,139,141],{"x":108,"y":140,"style":115},"198.0","header on row 3",[73,143],{"d":144,"fill":76,"stroke":66,"style":67},"M244.0,322.0 C364.0,322.0 364.0,166.0 480.0,166.0",[121,146],{"points":147,"fill":124},"484.0,166.0 475.0,171.0 475.0,161.0",[51,149],{"x":93,"y":150,"width":95,"height":96,"rx":64,"fill":151,"stroke":152,"style":67},"224.0","#fdefd8","var(--gold,#b4740a)",[69,154,156],{"transform":155},"translate(603.0,237.0) scale(1.000)",[73,157],{"d":158,"fill":76,"stroke":159,"style":78},"M4 4h16v16H4z M4 9h16 M4 14h16 M10 4v16","var(--gold-ink,#7a4e06)",[80,161,164],{"x":108,"y":162,"style":163},"283.0","font-size:13px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","dates \u002F numbers as text",[80,166,168],{"x":108,"y":167,"style":115},"302.0","\"2026-09-01\", \"120.50\"",[73,170],{"d":171,"fill":76,"stroke":66,"style":67},"M244.0,322.0 C364.0,322.0 364.0,270.0 480.0,270.0",[121,173],{"points":174,"fill":124},"484.0,270.0 475.0,275.0 475.0,265.0",[51,176],{"x":93,"y":177,"width":95,"height":96,"rx":64,"fill":178,"stroke":179,"style":67},"328.0","#fce9e9","var(--danger,#dc2626)",[69,181,183],{"transform":182},"translate(603.0,341.0) scale(1.000)",[73,184],{"d":185,"fill":76,"stroke":186,"style":78},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--accent-ink,#be185d)",[80,188,191],{"x":108,"y":189,"style":190},"387.0","font-size:13px;font-weight:700;fill:var(--accent-ink,#be185d);text-anchor:middle","error_cells",[80,193,195],{"x":108,"y":194,"style":115},"406.0","#N\u002FA in Amount",[73,197],{"d":198,"fill":76,"stroke":66,"style":67},"M244.0,322.0 C364.0,322.0 364.0,374.0 480.0,374.0",[121,200],{"points":201,"fill":124},"484.0,374.0 475.0,379.0 475.0,369.0",[51,203],{"x":93,"y":204,"width":95,"height":96,"rx":64,"fill":178,"stroke":179,"style":67},"432.0",[69,206,208],{"transform":207},"translate(603.0,445.0) scale(1.000)",[73,209],{"d":210,"fill":76,"stroke":186,"style":78},"M9 8l-4 4 4 4 M15 8l4 4-4 4",[80,212,214],{"x":108,"y":213,"style":190},"491.0","formula_amounts",[80,216,218],{"x":108,"y":217,"style":115},"510.0","no cached results",[73,220],{"d":221,"fill":76,"stroke":66,"style":67},"M244.0,322.0 C364.0,322.0 364.0,478.0 480.0,478.0",[121,223],{"points":224,"fill":124},"484.0,478.0 475.0,483.0 475.0,473.0",[51,226],{"x":93,"y":227,"width":95,"height":96,"rx":64,"fill":97,"stroke":98,"style":67},"536.0",[69,229,231],{"transform":230},"translate(603.0,549.0) scale(1.000)",[73,232],{"d":233,"fill":76,"stroke":105,"style":78},"M2 12s4-7 10-7 10 7 10 7-4 7-10 7S2 12 2 12z M12 9a3 3 0 1 0 .01 0z",[80,235,237],{"x":108,"y":236,"style":110},"595.0","blank rows, hidden sheet",[80,239,241],{"x":108,"y":240,"style":115},"614.0","styled empties, Lookup first",[73,243],{"d":244,"fill":76,"stroke":66,"style":67},"M244.0,322.0 C364.0,322.0 364.0,582.0 480.0,582.0",[121,246],{"points":247,"fill":124},"484.0,582.0 475.0,587.0 475.0,577.0",[249,250,252],"h2",{"id":251},"prerequisites","Prerequisites",[254,255,260],"pre",{"className":256,"code":257,"language":258,"meta":259,"style":259},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pandas openpyxl pytest\n","bash","",[14,261,262],{"__ignoreMap":259},[263,264,267,271,275,278,281],"span",{"class":265,"line":266},"line",1,[263,268,270],{"class":269},"sMTad","pip",[263,272,274],{"class":273},"srMev"," install",[263,276,277],{"class":273}," pandas",[263,279,280],{"class":273}," openpyxl",[263,282,283],{"class":273}," pytest\n",[249,285,287],{"id":286},"step-1-write-the-helper-and-the-variants","Step 1 — Write the helper and the variants",[10,289,290],{},"One helper builds a workbook from rows and returns its bytes. Each variant is a small function named after what makes it awkward, so a failing test says \"title_rows\" rather than \"test_input2.xlsx\":",[254,292,296],{"className":293,"code":294,"language":295,"meta":259,"style":259},"language-python shiki shiki-themes github-light github-dark-high-contrast","# xlsx_fixtures.py — workbooks for tests, built in memory\nimport datetime as dt\nimport io\n\nfrom openpyxl import Workbook\nfrom openpyxl.styles import Font, PatternFill\n\nHEADER = [\"Order ID\", \"Region\", \"Order date\", \"Amount\"]\nROWS = [\n    [1001, \"North\", dt.date(2026, 9, 1), 120.5],\n    [1002, \"South\", dt.date(2026, 9, 2), 40.0],\n    [1003, \"East\", dt.date(2026, 9, 2), 80.0],\n]\n\ndef make_xlsx(rows, sheet=\"Orders\", title_rows=(), extra=None):\n    \"\"\"Build a workbook in memory and return its bytes.\"\"\"\n    wb = Workbook()\n    ws = wb.active\n    ws.title = sheet\n    for line in title_rows:\n        ws.append([line])\n    for row in rows:\n        ws.append(row)\n    if extra:\n        extra(wb, ws)\n    buffer = io.BytesIO()\n    wb.save(buffer)\n    return buffer.getvalue()\n\ndef clean():\n    return make_xlsx([HEADER, *ROWS])\n\ndef title_rows():\n    return make_xlsx([HEADER, *ROWS], title_rows=[\"Orders export\", \"Generated 2026-09-23\"])\n\ndef dates_as_text():\n    return make_xlsx([HEADER, *[[r[0], r[1], r[2].isoformat(), r[3]] for r in ROWS]])\n\ndef numbers_as_text():\n    return make_xlsx([HEADER, *[[r[0], r[1], r[2], f\"{r[3]:.2f}\"] for r in ROWS]])\n\ndef error_cells():\n    return make_xlsx([HEADER, *ROWS[:2], [1003, \"East\", dt.date(2026, 9, 2), \"#N\u002FA\"]])\n\ndef formula_amounts():\n    rows = [[r[0], r[1], r[2], f\"={r[3]}*1\"] for r in ROWS]\n    return make_xlsx([HEADER, *rows])\n\ndef formatted_blank_rows():\n    def style_empty_rows(wb, ws):\n        for row in range(5, 40):\n            ws.cell(row, 1).fill = PatternFill(\"solid\", fgColor=\"FFFFF2CC\")\n    return make_xlsx([HEADER, *ROWS], extra=style_empty_rows)\n\ndef hidden_sheet_first():\n    def add_hidden(wb, ws):\n        lookup = wb.create_sheet(\"Lookup\", 0)\n        lookup.append([\"code\", \"name\"])\n        lookup.sheet_state = \"hidden\"\n    return make_xlsx([HEADER, *ROWS], extra=add_hidden)\n","python",[14,297,298,304,321,329,336,350,363,368,402,413,452,485,517,522,527,562,568,579,590,601,616,622,635,641,650,656,667,673,682,687,698,718,723,733,769,774,784,834,839,849,915,920,930,978,983,993,1046,1062,1067,1077,1089,1115,1147,1172,1177,1187,1197,1217,1233,1244],{"__ignoreMap":259},[263,299,300],{"class":265,"line":266},[263,301,303],{"class":302},"s-wDw","# xlsx_fixtures.py — workbooks for tests, built in memory\n",[263,305,307,311,315,318],{"class":265,"line":306},2,[263,308,310],{"class":309},"s-kum","import",[263,312,314],{"class":313},"skGVy"," datetime ",[263,316,317],{"class":309},"as",[263,319,320],{"class":313}," dt\n",[263,322,324,326],{"class":265,"line":323},3,[263,325,310],{"class":309},[263,327,328],{"class":313}," io\n",[263,330,332],{"class":265,"line":331},4,[263,333,335],{"emptyLinePlaceholder":334},true,"\n",[263,337,339,342,345,347],{"class":265,"line":338},5,[263,340,341],{"class":309},"from",[263,343,344],{"class":313}," openpyxl ",[263,346,310],{"class":309},[263,348,349],{"class":313}," Workbook\n",[263,351,353,355,358,360],{"class":265,"line":352},6,[263,354,341],{"class":309},[263,356,357],{"class":313}," openpyxl.styles ",[263,359,310],{"class":309},[263,361,362],{"class":313}," Font, PatternFill\n",[263,364,366],{"class":265,"line":365},7,[263,367,335],{"emptyLinePlaceholder":334},[263,369,371,375,378,381,384,386,389,391,394,396,399],{"class":265,"line":370},8,[263,372,374],{"class":373},"sP0c6","HEADER",[263,376,377],{"class":309}," =",[263,379,380],{"class":313}," [",[263,382,383],{"class":273},"\"Order ID\"",[263,385,17],{"class":313},[263,387,388],{"class":273},"\"Region\"",[263,390,17],{"class":313},[263,392,393],{"class":273},"\"Order date\"",[263,395,17],{"class":313},[263,397,398],{"class":273},"\"Amount\"",[263,400,401],{"class":313},"]\n",[263,403,405,408,410],{"class":265,"line":404},9,[263,406,407],{"class":373},"ROWS",[263,409,377],{"class":309},[263,411,412],{"class":313}," [\n",[263,414,416,419,422,424,427,430,433,435,438,440,443,446,449],{"class":265,"line":415},10,[263,417,418],{"class":313},"    [",[263,420,421],{"class":373},"1001",[263,423,17],{"class":313},[263,425,426],{"class":273},"\"North\"",[263,428,429],{"class":313},", dt.date(",[263,431,432],{"class":373},"2026",[263,434,17],{"class":313},[263,436,437],{"class":373},"9",[263,439,17],{"class":313},[263,441,442],{"class":373},"1",[263,444,445],{"class":313},"), ",[263,447,448],{"class":373},"120.5",[263,450,451],{"class":313},"],\n",[263,453,455,457,460,462,465,467,469,471,473,475,478,480,483],{"class":265,"line":454},11,[263,456,418],{"class":313},[263,458,459],{"class":373},"1002",[263,461,17],{"class":313},[263,463,464],{"class":273},"\"South\"",[263,466,429],{"class":313},[263,468,432],{"class":373},[263,470,17],{"class":313},[263,472,437],{"class":373},[263,474,17],{"class":313},[263,476,477],{"class":373},"2",[263,479,445],{"class":313},[263,481,482],{"class":373},"40.0",[263,484,451],{"class":313},[263,486,488,490,493,495,498,500,502,504,506,508,510,512,515],{"class":265,"line":487},12,[263,489,418],{"class":313},[263,491,492],{"class":373},"1003",[263,494,17],{"class":313},[263,496,497],{"class":273},"\"East\"",[263,499,429],{"class":313},[263,501,432],{"class":373},[263,503,17],{"class":313},[263,505,437],{"class":373},[263,507,17],{"class":313},[263,509,477],{"class":373},[263,511,445],{"class":313},[263,513,514],{"class":373},"80.0",[263,516,451],{"class":313},[263,518,520],{"class":265,"line":519},13,[263,521,401],{"class":313},[263,523,525],{"class":265,"line":524},14,[263,526,335],{"emptyLinePlaceholder":334},[263,528,530,533,537,540,543,546,549,551,554,556,559],{"class":265,"line":529},15,[263,531,532],{"class":309},"def",[263,534,536],{"class":535},"s_Opv"," make_xlsx",[263,538,539],{"class":313},"(rows, sheet",[263,541,542],{"class":309},"=",[263,544,545],{"class":273},"\"Orders\"",[263,547,548],{"class":313},", title_rows",[263,550,542],{"class":309},[263,552,553],{"class":313},"(), extra",[263,555,542],{"class":309},[263,557,558],{"class":373},"None",[263,560,561],{"class":313},"):\n",[263,563,565],{"class":265,"line":564},16,[263,566,567],{"class":273},"    \"\"\"Build a workbook in memory and return its bytes.\"\"\"\n",[263,569,571,574,576],{"class":265,"line":570},17,[263,572,573],{"class":313},"    wb ",[263,575,542],{"class":309},[263,577,578],{"class":313}," Workbook()\n",[263,580,582,585,587],{"class":265,"line":581},18,[263,583,584],{"class":313},"    ws ",[263,586,542],{"class":309},[263,588,589],{"class":313}," wb.active\n",[263,591,593,596,598],{"class":265,"line":592},19,[263,594,595],{"class":313},"    ws.title ",[263,597,542],{"class":309},[263,599,600],{"class":313}," sheet\n",[263,602,604,607,610,613],{"class":265,"line":603},20,[263,605,606],{"class":309},"    for",[263,608,609],{"class":313}," line ",[263,611,612],{"class":309},"in",[263,614,615],{"class":313}," title_rows:\n",[263,617,619],{"class":265,"line":618},21,[263,620,621],{"class":313},"        ws.append([line])\n",[263,623,625,627,630,632],{"class":265,"line":624},22,[263,626,606],{"class":309},[263,628,629],{"class":313}," row ",[263,631,612],{"class":309},[263,633,634],{"class":313}," rows:\n",[263,636,638],{"class":265,"line":637},23,[263,639,640],{"class":313},"        ws.append(row)\n",[263,642,644,647],{"class":265,"line":643},24,[263,645,646],{"class":309},"    if",[263,648,649],{"class":313}," extra:\n",[263,651,653],{"class":265,"line":652},25,[263,654,655],{"class":313},"        extra(wb, ws)\n",[263,657,659,662,664],{"class":265,"line":658},26,[263,660,661],{"class":313},"    buffer ",[263,663,542],{"class":309},[263,665,666],{"class":313}," io.BytesIO()\n",[263,668,670],{"class":265,"line":669},27,[263,671,672],{"class":313},"    wb.save(buffer)\n",[263,674,676,679],{"class":265,"line":675},28,[263,677,678],{"class":309},"    return",[263,680,681],{"class":313}," buffer.getvalue()\n",[263,683,685],{"class":265,"line":684},29,[263,686,335],{"emptyLinePlaceholder":334},[263,688,690,692,695],{"class":265,"line":689},30,[263,691,532],{"class":309},[263,693,694],{"class":535}," clean",[263,696,697],{"class":313},"():\n",[263,699,701,703,706,708,710,713,715],{"class":265,"line":700},31,[263,702,678],{"class":309},[263,704,705],{"class":313}," make_xlsx([",[263,707,374],{"class":373},[263,709,17],{"class":313},[263,711,712],{"class":309},"*",[263,714,407],{"class":373},[263,716,717],{"class":313},"])\n",[263,719,721],{"class":265,"line":720},32,[263,722,335],{"emptyLinePlaceholder":334},[263,724,726,728,731],{"class":265,"line":725},33,[263,727,532],{"class":309},[263,729,730],{"class":535}," title_rows",[263,732,697],{"class":313},[263,734,736,738,740,742,744,746,748,751,754,756,759,762,764,767],{"class":265,"line":735},34,[263,737,678],{"class":309},[263,739,705],{"class":313},[263,741,374],{"class":373},[263,743,17],{"class":313},[263,745,712],{"class":309},[263,747,407],{"class":373},[263,749,750],{"class":313},"], ",[263,752,137],{"class":753},"sa561",[263,755,542],{"class":309},[263,757,758],{"class":313},"[",[263,760,761],{"class":273},"\"Orders export\"",[263,763,17],{"class":313},[263,765,766],{"class":273},"\"Generated 2026-09-23\"",[263,768,717],{"class":313},[263,770,772],{"class":265,"line":771},35,[263,773,335],{"emptyLinePlaceholder":334},[263,775,777,779,782],{"class":265,"line":776},36,[263,778,532],{"class":309},[263,780,781],{"class":535}," dates_as_text",[263,783,697],{"class":313},[263,785,787,789,791,793,795,797,800,802,805,807,809,811,814,817,820,823,826,828,831],{"class":265,"line":786},37,[263,788,678],{"class":309},[263,790,705],{"class":313},[263,792,374],{"class":373},[263,794,17],{"class":313},[263,796,712],{"class":309},[263,798,799],{"class":313},"[[r[",[263,801,54],{"class":373},[263,803,804],{"class":313},"], r[",[263,806,442],{"class":373},[263,808,804],{"class":313},[263,810,477],{"class":373},[263,812,813],{"class":313},"].isoformat(), r[",[263,815,816],{"class":373},"3",[263,818,819],{"class":313},"]] ",[263,821,822],{"class":309},"for",[263,824,825],{"class":313}," r ",[263,827,612],{"class":309},[263,829,830],{"class":373}," ROWS",[263,832,833],{"class":313},"]])\n",[263,835,837],{"class":265,"line":836},38,[263,838,335],{"emptyLinePlaceholder":334},[263,840,842,844,847],{"class":265,"line":841},39,[263,843,532],{"class":309},[263,845,846],{"class":535}," numbers_as_text",[263,848,697],{"class":313},[263,850,852,854,856,858,860,862,864,866,868,870,872,874,876,879,882,886,889,891,894,897,900,902,905,907,909,911,913],{"class":265,"line":851},40,[263,853,678],{"class":309},[263,855,705],{"class":313},[263,857,374],{"class":373},[263,859,17],{"class":313},[263,861,712],{"class":309},[263,863,799],{"class":313},[263,865,54],{"class":373},[263,867,804],{"class":313},[263,869,442],{"class":373},[263,871,804],{"class":313},[263,873,477],{"class":373},[263,875,750],{"class":313},[263,877,878],{"class":309},"f",[263,880,881],{"class":273},"\"",[263,883,885],{"class":884},"sSjpA","{",[263,887,888],{"class":313},"r[",[263,890,816],{"class":373},[263,892,893],{"class":313},"]",[263,895,896],{"class":309},":.2f",[263,898,899],{"class":884},"}",[263,901,881],{"class":273},[263,903,904],{"class":313},"] ",[263,906,822],{"class":309},[263,908,825],{"class":313},[263,910,612],{"class":309},[263,912,830],{"class":373},[263,914,833],{"class":313},[263,916,918],{"class":265,"line":917},41,[263,919,335],{"emptyLinePlaceholder":334},[263,921,923,925,928],{"class":265,"line":922},42,[263,924,532],{"class":309},[263,926,927],{"class":535}," error_cells",[263,929,697],{"class":313},[263,931,933,935,937,939,941,943,945,948,950,953,955,957,959,961,963,965,967,969,971,973,976],{"class":265,"line":932},43,[263,934,678],{"class":309},[263,936,705],{"class":313},[263,938,374],{"class":373},[263,940,17],{"class":313},[263,942,712],{"class":309},[263,944,407],{"class":373},[263,946,947],{"class":313},"[:",[263,949,477],{"class":373},[263,951,952],{"class":313},"], [",[263,954,492],{"class":373},[263,956,17],{"class":313},[263,958,497],{"class":273},[263,960,429],{"class":313},[263,962,432],{"class":373},[263,964,17],{"class":313},[263,966,437],{"class":373},[263,968,17],{"class":313},[263,970,477],{"class":373},[263,972,445],{"class":313},[263,974,975],{"class":273},"\"#N\u002FA\"",[263,977,833],{"class":313},[263,979,981],{"class":265,"line":980},44,[263,982,335],{"emptyLinePlaceholder":334},[263,984,986,988,991],{"class":265,"line":985},45,[263,987,532],{"class":309},[263,989,990],{"class":535}," formula_amounts",[263,992,697],{"class":313},[263,994,996,999,1001,1004,1006,1008,1010,1012,1014,1016,1018,1021,1023,1025,1027,1029,1031,1034,1036,1038,1040,1042,1044],{"class":265,"line":995},46,[263,997,998],{"class":313},"    rows ",[263,1000,542],{"class":309},[263,1002,1003],{"class":313}," [[r[",[263,1005,54],{"class":373},[263,1007,804],{"class":313},[263,1009,442],{"class":373},[263,1011,804],{"class":313},[263,1013,477],{"class":373},[263,1015,750],{"class":313},[263,1017,878],{"class":309},[263,1019,1020],{"class":273},"\"=",[263,1022,885],{"class":884},[263,1024,888],{"class":313},[263,1026,816],{"class":373},[263,1028,893],{"class":313},[263,1030,899],{"class":884},[263,1032,1033],{"class":273},"*1\"",[263,1035,904],{"class":313},[263,1037,822],{"class":309},[263,1039,825],{"class":313},[263,1041,612],{"class":309},[263,1043,830],{"class":373},[263,1045,401],{"class":313},[263,1047,1049,1051,1053,1055,1057,1059],{"class":265,"line":1048},47,[263,1050,678],{"class":309},[263,1052,705],{"class":313},[263,1054,374],{"class":373},[263,1056,17],{"class":313},[263,1058,712],{"class":309},[263,1060,1061],{"class":313},"rows])\n",[263,1063,1065],{"class":265,"line":1064},48,[263,1066,335],{"emptyLinePlaceholder":334},[263,1068,1070,1072,1075],{"class":265,"line":1069},49,[263,1071,532],{"class":309},[263,1073,1074],{"class":535}," formatted_blank_rows",[263,1076,697],{"class":313},[263,1078,1080,1083,1086],{"class":265,"line":1079},50,[263,1081,1082],{"class":309},"    def",[263,1084,1085],{"class":535}," style_empty_rows",[263,1087,1088],{"class":313},"(wb, ws):\n",[263,1090,1092,1095,1097,1099,1102,1105,1108,1110,1113],{"class":265,"line":1091},51,[263,1093,1094],{"class":309},"        for",[263,1096,629],{"class":313},[263,1098,612],{"class":309},[263,1100,1101],{"class":373}," range",[263,1103,1104],{"class":313},"(",[263,1106,1107],{"class":373},"5",[263,1109,17],{"class":313},[263,1111,1112],{"class":373},"40",[263,1114,561],{"class":313},[263,1116,1118,1121,1123,1126,1128,1131,1134,1136,1139,1141,1144],{"class":265,"line":1117},52,[263,1119,1120],{"class":313},"            ws.cell(row, ",[263,1122,442],{"class":373},[263,1124,1125],{"class":313},").fill ",[263,1127,542],{"class":309},[263,1129,1130],{"class":313}," PatternFill(",[263,1132,1133],{"class":273},"\"solid\"",[263,1135,17],{"class":313},[263,1137,1138],{"class":753},"fgColor",[263,1140,542],{"class":309},[263,1142,1143],{"class":273},"\"FFFFF2CC\"",[263,1145,1146],{"class":313},")\n",[263,1148,1150,1152,1154,1156,1158,1160,1162,1164,1167,1169],{"class":265,"line":1149},53,[263,1151,678],{"class":309},[263,1153,705],{"class":313},[263,1155,374],{"class":373},[263,1157,17],{"class":313},[263,1159,712],{"class":309},[263,1161,407],{"class":373},[263,1163,750],{"class":313},[263,1165,1166],{"class":753},"extra",[263,1168,542],{"class":309},[263,1170,1171],{"class":313},"style_empty_rows)\n",[263,1173,1175],{"class":265,"line":1174},54,[263,1176,335],{"emptyLinePlaceholder":334},[263,1178,1180,1182,1185],{"class":265,"line":1179},55,[263,1181,532],{"class":309},[263,1183,1184],{"class":535}," hidden_sheet_first",[263,1186,697],{"class":313},[263,1188,1190,1192,1195],{"class":265,"line":1189},56,[263,1191,1082],{"class":309},[263,1193,1194],{"class":535}," add_hidden",[263,1196,1088],{"class":313},[263,1198,1200,1203,1205,1208,1211,1213,1215],{"class":265,"line":1199},57,[263,1201,1202],{"class":313},"        lookup ",[263,1204,542],{"class":309},[263,1206,1207],{"class":313}," wb.create_sheet(",[263,1209,1210],{"class":273},"\"Lookup\"",[263,1212,17],{"class":313},[263,1214,54],{"class":373},[263,1216,1146],{"class":313},[263,1218,1220,1223,1226,1228,1231],{"class":265,"line":1219},58,[263,1221,1222],{"class":313},"        lookup.append([",[263,1224,1225],{"class":273},"\"code\"",[263,1227,17],{"class":313},[263,1229,1230],{"class":273},"\"name\"",[263,1232,717],{"class":313},[263,1234,1236,1239,1241],{"class":265,"line":1235},59,[263,1237,1238],{"class":313},"        lookup.sheet_state ",[263,1240,542],{"class":309},[263,1242,1243],{"class":273}," \"hidden\"\n",[263,1245,1247,1249,1251,1253,1255,1257,1259,1261,1263,1265],{"class":265,"line":1246},60,[263,1248,678],{"class":309},[263,1250,705],{"class":313},[263,1252,374],{"class":373},[263,1254,17],{"class":313},[263,1256,712],{"class":309},[263,1258,407],{"class":373},[263,1260,750],{"class":313},[263,1262,1166],{"class":753},[263,1264,542],{"class":309},[263,1266,1267],{"class":313},"add_hidden)\n",[10,1269,1270,1271,1273],{},"Building a workbook this small took about 1.5 ms, so a suite can create hundreds without noticing. ",[14,1272,1166],{}," lets a variant change anything the helper does not cover — styles, extra sheets, sheet state — without the helper growing a parameter for every quirk.",[249,1275,1277],{"id":1276},"step-2-make-the-code-under-test-accept-file-objects","Step 2 — Make the code under test accept file objects",[10,1279,1280,1281,1284,1285,1288],{},"The loader below finds the header row wherever it is, drops empty rows and converts types. It reads with ",[14,1282,1283],{},"pd.read_excel(source, …)",", which accepts a path or a file object, so tests can pass ",[14,1286,1287],{},"io.BytesIO(data)"," and production code can pass a path:",[254,1290,1292],{"className":293,"code":1291,"language":295,"meta":259,"style":259},"# loader.py — the code under test\nimport pandas as pd\n\ndef load_orders(source, sheet=\"Orders\"):\n    raw = pd.read_excel(source, sheet_name=sheet, header=None, dtype=object)\n    is_header = raw.apply(lambda row: \"Order ID\" in row.values, axis=1)\n    if not is_header.any():\n        raise ValueError(\"no header row with 'Order ID' found\")\n    start = is_header.idxmax()\n    frame = raw.iloc[start + 1:].set_axis(raw.iloc[start].tolist(), axis=1)\n    frame = frame.dropna(how=\"all\").reset_index(drop=True)\n    frame[\"Order ID\"] = frame[\"Order ID\"].astype(int)\n    frame[\"Order date\"] = pd.to_datetime(frame[\"Order date\"], errors=\"coerce\")\n    frame[\"Amount\"] = pd.to_numeric(frame[\"Amount\"], errors=\"coerce\")\n    return frame\n",[14,1293,1294,1299,1311,1315,1331,1368,1401,1411,1426,1436,1463,1493,1517,1544,1569],{"__ignoreMap":259},[263,1295,1296],{"class":265,"line":266},[263,1297,1298],{"class":302},"# loader.py — the code under test\n",[263,1300,1301,1303,1306,1308],{"class":265,"line":306},[263,1302,310],{"class":309},[263,1304,1305],{"class":313}," pandas ",[263,1307,317],{"class":309},[263,1309,1310],{"class":313}," pd\n",[263,1312,1313],{"class":265,"line":323},[263,1314,335],{"emptyLinePlaceholder":334},[263,1316,1317,1319,1322,1325,1327,1329],{"class":265,"line":331},[263,1318,532],{"class":309},[263,1320,1321],{"class":535}," load_orders",[263,1323,1324],{"class":313},"(source, sheet",[263,1326,542],{"class":309},[263,1328,545],{"class":273},[263,1330,561],{"class":313},[263,1332,1333,1336,1338,1341,1344,1346,1349,1352,1354,1356,1358,1361,1363,1366],{"class":265,"line":338},[263,1334,1335],{"class":313},"    raw ",[263,1337,542],{"class":309},[263,1339,1340],{"class":313}," pd.read_excel(source, ",[263,1342,1343],{"class":753},"sheet_name",[263,1345,542],{"class":309},[263,1347,1348],{"class":313},"sheet, ",[263,1350,1351],{"class":753},"header",[263,1353,542],{"class":309},[263,1355,558],{"class":373},[263,1357,17],{"class":313},[263,1359,1360],{"class":753},"dtype",[263,1362,542],{"class":309},[263,1364,1365],{"class":373},"object",[263,1367,1146],{"class":313},[263,1369,1370,1373,1375,1378,1381,1384,1386,1389,1392,1395,1397,1399],{"class":265,"line":352},[263,1371,1372],{"class":313},"    is_header ",[263,1374,542],{"class":309},[263,1376,1377],{"class":313}," raw.apply(",[263,1379,1380],{"class":309},"lambda",[263,1382,1383],{"class":313}," row: ",[263,1385,383],{"class":273},[263,1387,1388],{"class":309}," in",[263,1390,1391],{"class":313}," row.values, ",[263,1393,1394],{"class":753},"axis",[263,1396,542],{"class":309},[263,1398,442],{"class":373},[263,1400,1146],{"class":313},[263,1402,1403,1405,1408],{"class":265,"line":365},[263,1404,646],{"class":309},[263,1406,1407],{"class":309}," not",[263,1409,1410],{"class":313}," is_header.any():\n",[263,1412,1413,1416,1419,1421,1424],{"class":265,"line":370},[263,1414,1415],{"class":309},"        raise",[263,1417,1418],{"class":373}," ValueError",[263,1420,1104],{"class":313},[263,1422,1423],{"class":273},"\"no header row with 'Order ID' found\"",[263,1425,1146],{"class":313},[263,1427,1428,1431,1433],{"class":265,"line":404},[263,1429,1430],{"class":313},"    start ",[263,1432,542],{"class":309},[263,1434,1435],{"class":313}," is_header.idxmax()\n",[263,1437,1438,1441,1443,1446,1449,1452,1455,1457,1459,1461],{"class":265,"line":415},[263,1439,1440],{"class":313},"    frame ",[263,1442,542],{"class":309},[263,1444,1445],{"class":313}," raw.iloc[start ",[263,1447,1448],{"class":309},"+",[263,1450,1451],{"class":373}," 1",[263,1453,1454],{"class":313},":].set_axis(raw.iloc[start].tolist(), ",[263,1456,1394],{"class":753},[263,1458,542],{"class":309},[263,1460,442],{"class":373},[263,1462,1146],{"class":313},[263,1464,1465,1467,1469,1472,1475,1477,1480,1483,1486,1488,1491],{"class":265,"line":454},[263,1466,1440],{"class":313},[263,1468,542],{"class":309},[263,1470,1471],{"class":313}," frame.dropna(",[263,1473,1474],{"class":753},"how",[263,1476,542],{"class":309},[263,1478,1479],{"class":273},"\"all\"",[263,1481,1482],{"class":313},").reset_index(",[263,1484,1485],{"class":753},"drop",[263,1487,542],{"class":309},[263,1489,1490],{"class":373},"True",[263,1492,1146],{"class":313},[263,1494,1495,1498,1500,1502,1504,1507,1509,1512,1515],{"class":265,"line":487},[263,1496,1497],{"class":313},"    frame[",[263,1499,383],{"class":273},[263,1501,904],{"class":313},[263,1503,542],{"class":309},[263,1505,1506],{"class":313}," frame[",[263,1508,383],{"class":273},[263,1510,1511],{"class":313},"].astype(",[263,1513,1514],{"class":373},"int",[263,1516,1146],{"class":313},[263,1518,1519,1521,1523,1525,1527,1530,1532,1534,1537,1539,1542],{"class":265,"line":519},[263,1520,1497],{"class":313},[263,1522,393],{"class":273},[263,1524,904],{"class":313},[263,1526,542],{"class":309},[263,1528,1529],{"class":313}," pd.to_datetime(frame[",[263,1531,393],{"class":273},[263,1533,750],{"class":313},[263,1535,1536],{"class":753},"errors",[263,1538,542],{"class":309},[263,1540,1541],{"class":273},"\"coerce\"",[263,1543,1146],{"class":313},[263,1545,1546,1548,1550,1552,1554,1557,1559,1561,1563,1565,1567],{"class":265,"line":524},[263,1547,1497],{"class":313},[263,1549,398],{"class":273},[263,1551,904],{"class":313},[263,1553,542],{"class":309},[263,1555,1556],{"class":313}," pd.to_numeric(frame[",[263,1558,398],{"class":273},[263,1560,750],{"class":313},[263,1562,1536],{"class":753},[263,1564,542],{"class":309},[263,1566,1541],{"class":273},[263,1568,1146],{"class":313},[263,1570,1571,1573],{"class":265,"line":529},[263,1572,678],{"class":309},[263,1574,1575],{"class":313}," frame\n",[10,1577,1578,1579,1582,1583,1586],{},"Code that insists on a path — it calls ",[14,1580,1581],{},"os.path.getsize"," or opens the file itself — is harder to test. Accepting \"anything ",[14,1584,1585],{},"read_excel"," accepts\" is usually a one-line change.",[249,1588,1590],{"id":1589},"step-3-look-at-what-each-variant-does-to-pandas","Step 3 — Look at what each variant does to pandas",[10,1592,1593],{},"Before writing assertions, load every variant once and look. Several behave in ways worth pinning down in a test:",[254,1595,1597],{"className":293,"code":1596,"language":295,"meta":259,"style":259},"import io\nimport pandas as pd\n\nvariants = [clean, title_rows, dates_as_text, numbers_as_text, error_cells,\n            formula_amounts, formatted_blank_rows, hidden_sheet_first]\nfor build in variants:\n    frame = load_orders(io.BytesIO(build()))\n    print(f\"{build.__name__:22} rows={len(frame)} amount_sum={frame['Amount'].sum():.1f} \"\n          f\"missing_amounts={frame['Amount'].isna().sum()}\")\n",[14,1598,1599,1605,1615,1619,1629,1634,1646,1655,1714],{"__ignoreMap":259},[263,1600,1601,1603],{"class":265,"line":266},[263,1602,310],{"class":309},[263,1604,328],{"class":313},[263,1606,1607,1609,1611,1613],{"class":265,"line":306},[263,1608,310],{"class":309},[263,1610,1305],{"class":313},[263,1612,317],{"class":309},[263,1614,1310],{"class":313},[263,1616,1617],{"class":265,"line":323},[263,1618,335],{"emptyLinePlaceholder":334},[263,1620,1621,1624,1626],{"class":265,"line":331},[263,1622,1623],{"class":313},"variants ",[263,1625,542],{"class":309},[263,1627,1628],{"class":313}," [clean, title_rows, dates_as_text, numbers_as_text, error_cells,\n",[263,1630,1631],{"class":265,"line":338},[263,1632,1633],{"class":313},"            formula_amounts, formatted_blank_rows, hidden_sheet_first]\n",[263,1635,1636,1638,1641,1643],{"class":265,"line":352},[263,1637,822],{"class":309},[263,1639,1640],{"class":313}," build ",[263,1642,612],{"class":309},[263,1644,1645],{"class":313}," variants:\n",[263,1647,1648,1650,1652],{"class":265,"line":365},[263,1649,1440],{"class":313},[263,1651,542],{"class":309},[263,1653,1654],{"class":313}," load_orders(io.BytesIO(build()))\n",[263,1656,1657,1660,1662,1664,1666,1668,1671,1674,1677,1679,1682,1684,1687,1690,1692,1695,1697,1700,1703,1706,1709,1711],{"class":265,"line":370},[263,1658,1659],{"class":373},"    print",[263,1661,1104],{"class":313},[263,1663,878],{"class":309},[263,1665,881],{"class":273},[263,1667,885],{"class":884},[263,1669,1670],{"class":313},"build.",[263,1672,1673],{"class":373},"__name__",[263,1675,1676],{"class":309},":22",[263,1678,899],{"class":884},[263,1680,1681],{"class":273}," rows=",[263,1683,885],{"class":884},[263,1685,1686],{"class":373},"len",[263,1688,1689],{"class":313},"(frame)",[263,1691,899],{"class":884},[263,1693,1694],{"class":273}," amount_sum=",[263,1696,885],{"class":884},[263,1698,1699],{"class":313},"frame[",[263,1701,1702],{"class":273},"'Amount'",[263,1704,1705],{"class":313},"].sum()",[263,1707,1708],{"class":309},":.1f",[263,1710,899],{"class":884},[263,1712,1713],{"class":273}," \"\n",[263,1715,1716,1719,1722,1724,1726,1728,1731,1733,1735],{"class":265,"line":404},[263,1717,1718],{"class":309},"          f",[263,1720,1721],{"class":273},"\"missing_amounts=",[263,1723,885],{"class":884},[263,1725,1699],{"class":313},[263,1727,1702],{"class":273},[263,1729,1730],{"class":313},"].isna().sum()",[263,1732,899],{"class":884},[263,1734,881],{"class":273},[263,1736,1146],{"class":313},[32,1738,41,1743,41,1746,41,1749,41,1752,41,1757,41,1762,41,1768,41,1771,41,1775,41,1780,41,1785,41,1787,41,1791,41,1793,41,1796,41,1799,41,1802,41,1804,41,1808,41,1810,41,1813,41,1816,41,1819,41,1821,41,1825,41,1827,41,1830,41,1833,41,1836,41,1838,41,1841,41,1843,41,1846,41,1849,41,1853,41,1855,41,1859,41,1861,41,1864,41,1867,41,1871,41,1873,41,1877,41,1879],{"viewBox":1739,"role":35,"ariaLabelledBy":1740,"xmlns":39,"style":40},"4 0 752 276",[1741,1742],"fx-variants-t","fx-variants-d",[43,1744,1745],{"id":1741},"What each variant simulates, and what the loader has to do",[47,1747,1748],{"id":1742},"Title rows simulate report headers above the table; the loader searches for the header row. Dates and numbers as text simulate CSV-like exports; the loader converts them. Error cells such as N\u002FA read as missing values. Formulas written by openpyxl have no stored results and read as missing. Styled empty rows and a hidden first sheet simulate formatting leftovers and lookup tabs; the loader drops empty rows and reads the sheet by name.",[51,1750],{"x":53,"y":54,"width":55,"height":1751,"fill":57},"276",[51,1753],{"x":60,"y":94,"width":1754,"height":482,"rx":54,"fill":57,"stroke":1755,"style":1756},"178.5","var(--line,#cdd5e6)","stroke-width:1px",[51,1758],{"x":1759,"y":94,"width":1760,"height":482,"rx":54,"fill":65,"stroke":66,"style":1761},"198.5","270.7","stroke-width:1.2px",[80,1763,1767],{"x":1764,"y":1765,"style":1766},"333.9","41.0","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Simulates",[51,1769],{"x":1770,"y":94,"width":1760,"height":482,"rx":54,"fill":65,"stroke":66,"style":1761},"469.3",[80,1772,1774],{"x":1773,"y":1765,"style":1766},"604.6","Loader must",[51,1776],{"x":60,"y":1777,"width":1754,"height":1778,"rx":54,"fill":1779,"stroke":1755,"style":1756},"56.0","34.0","#e7ebef",[80,1781,137],{"x":1782,"y":1783,"style":1784},"32.0","78.0","font-size:12.5px;font-weight:700;fill:var(--text,#172033);text-anchor:start",[51,1786],{"x":1759,"y":1777,"width":1760,"height":1778,"rx":54,"fill":57,"stroke":1755,"style":1756},[80,1788,1790],{"x":1764,"y":1789,"style":89},"77.0","report title above the table",[51,1792],{"x":1770,"y":1777,"width":1760,"height":1778,"rx":54,"fill":57,"stroke":1755,"style":1756},[80,1794,1795],{"x":1773,"y":1789,"style":89},"find the header row",[51,1797],{"x":60,"y":1798,"width":1754,"height":1778,"rx":54,"fill":1779,"stroke":1755,"style":1756},"90.0",[80,1800,164],{"x":1782,"y":1801,"style":1784},"112.0",[51,1803],{"x":1759,"y":1798,"width":1760,"height":1778,"rx":54,"fill":57,"stroke":1755,"style":1756},[80,1805,1807],{"x":1764,"y":1806,"style":89},"111.0","text exports",[51,1809],{"x":1770,"y":1798,"width":1760,"height":1778,"rx":54,"fill":57,"stroke":1755,"style":1756},[80,1811,1812],{"x":1773,"y":1806,"style":89},"convert types",[51,1814],{"x":60,"y":1815,"width":1754,"height":1778,"rx":54,"fill":1779,"stroke":1755,"style":1756},"124.0",[80,1817,191],{"x":1782,"y":1818,"style":1784},"146.0",[51,1820],{"x":1759,"y":1815,"width":1760,"height":1778,"rx":54,"fill":57,"stroke":1755,"style":1756},[80,1822,1824],{"x":1764,"y":1823,"style":89},"145.0","#N\u002FA from a failed lookup",[51,1826],{"x":1770,"y":1815,"width":1760,"height":1778,"rx":54,"fill":57,"stroke":1755,"style":1756},[80,1828,1829],{"x":1773,"y":1823,"style":89},"treat as missing",[51,1831],{"x":60,"y":1832,"width":1754,"height":1778,"rx":54,"fill":1779,"stroke":1755,"style":1756},"158.0",[80,1834,214],{"x":1782,"y":1835,"style":1784},"180.0",[51,1837],{"x":1759,"y":1832,"width":1760,"height":1778,"rx":54,"fill":57,"stroke":1755,"style":1756},[80,1839,1840],{"x":1764,"y":136,"style":89},"formulas without results",[51,1842],{"x":1770,"y":1832,"width":1760,"height":1778,"rx":54,"fill":57,"stroke":1755,"style":1756},[80,1844,1845],{"x":1773,"y":136,"style":89},"cannot compute: NaN",[51,1847],{"x":60,"y":1848,"width":1754,"height":1778,"rx":54,"fill":1779,"stroke":1755,"style":1756},"192.0",[80,1850,1852],{"x":1782,"y":1851,"style":1784},"214.0","formatted_blank_rows",[51,1854],{"x":1759,"y":1848,"width":1760,"height":1778,"rx":54,"fill":57,"stroke":1755,"style":1756},[80,1856,1858],{"x":1764,"y":1857,"style":89},"213.0","styled empty rows",[51,1860],{"x":1770,"y":1848,"width":1760,"height":1778,"rx":54,"fill":57,"stroke":1755,"style":1756},[80,1862,1863],{"x":1773,"y":1857,"style":89},"drop empty rows",[51,1865],{"x":60,"y":1866,"width":1754,"height":1778,"rx":54,"fill":1779,"stroke":1755,"style":1756},"226.0",[80,1868,1870],{"x":1782,"y":1869,"style":1784},"248.0","hidden_sheet_first",[51,1872],{"x":1759,"y":1866,"width":1760,"height":1778,"rx":54,"fill":57,"stroke":1755,"style":1756},[80,1874,1876],{"x":1764,"y":1875,"style":89},"247.0","lookup tab before data",[51,1878],{"x":1770,"y":1866,"width":1760,"height":1778,"rx":54,"fill":57,"stroke":1755,"style":1756},[80,1880,1881],{"x":1773,"y":1875,"style":89},"read the sheet by name",[10,1883,1884,1885,1888,1889,1892,1893,1897],{},"Two results surprise people. Error cells such as ",[14,1886,1887],{},"#N\u002FA"," come back as missing values, not as the text \"#N\u002FA\". And the formula variant loads with every amount missing: openpyxl writes ",[14,1890,1891],{},"=120.5*1"," into the cell but no result, and pandas reads the stored result. A test built on generated formulas therefore tests nothing — write values, or recalculate the file first as described in ",[26,1894,1896],{"href":1895},"\u002Fgetting-started-with-python-excel-automation\u002Fworking-with-excel-formulas-in-python\u002Frecalculate-excel-formulas-without-excel-in-python\u002F","Recalculate Excel Formulas Without Excel in Python",".",[249,1899,1901],{"id":1900},"step-4-run-one-test-across-every-layout","Step 4 — Run one test across every layout",[10,1903,1904,1907,1908,1911],{},[14,1905,1906],{},"pytest.mark.parametrize"," runs the same assertions against each builder; ",[14,1909,1910],{},"ids"," uses the function names, so the report reads like a list of layouts:",[254,1913,1915],{"className":293,"code":1914,"language":295,"meta":259,"style":259},"# test_loader.py\nimport io\n\nimport pandas as pd\nimport pytest\n\nimport xlsx_fixtures as fx\nfrom loader import load_orders\n\n@pytest.mark.parametrize(\"build\", [fx.clean, fx.title_rows, fx.dates_as_text,\n                                   fx.numbers_as_text, fx.formatted_blank_rows,\n                                   fx.hidden_sheet_first], ids=lambda f: f.__name__)\ndef test_awkward_layouts_load_the_same(build):\n    frame = load_orders(io.BytesIO(build()))\n    assert frame[\"Order ID\"].tolist() == [1001, 1002, 1003]\n    assert frame[\"Amount\"].sum() == pytest.approx(240.5)\n    assert frame[\"Order date\"].dt.day.tolist() == [1, 2, 2]\n\ndef test_error_cells_become_missing():\n    frame = load_orders(io.BytesIO(fx.error_cells()))\n    assert frame[\"Amount\"].isna().tolist() == [False, False, True]\n\ndef test_formulas_written_by_openpyxl_have_no_values():\n    frame = load_orders(io.BytesIO(fx.formula_amounts()))\n    assert frame[\"Amount\"].isna().all()           # no cached results in the file\n\ndef test_missing_header_is_reported():\n    with pytest.raises(ValueError, match=\"no header row\"):\n        load_orders(io.BytesIO(fx.make_xlsx([[\"just\", \"text\"]])))\n\ndef test_loader_accepts_a_path(tmp_path):\n    path = tmp_path \u002F \"orders.xlsx\"\n    path.write_bytes(fx.clean())\n    assert len(load_orders(path)) == 3\n",[14,1916,1917,1922,1928,1932,1942,1948,1952,1964,1976,1980,1993,1998,2015,2025,2033,2062,2083,2110,2114,2123,2132,2160,2164,2173,2182,2196,2200,2209,2232,2248,2252,2262,2278,2283],{"__ignoreMap":259},[263,1918,1919],{"class":265,"line":266},[263,1920,1921],{"class":302},"# test_loader.py\n",[263,1923,1924,1926],{"class":265,"line":306},[263,1925,310],{"class":309},[263,1927,328],{"class":313},[263,1929,1930],{"class":265,"line":323},[263,1931,335],{"emptyLinePlaceholder":334},[263,1933,1934,1936,1938,1940],{"class":265,"line":331},[263,1935,310],{"class":309},[263,1937,1305],{"class":313},[263,1939,317],{"class":309},[263,1941,1310],{"class":313},[263,1943,1944,1946],{"class":265,"line":338},[263,1945,310],{"class":309},[263,1947,283],{"class":313},[263,1949,1950],{"class":265,"line":352},[263,1951,335],{"emptyLinePlaceholder":334},[263,1953,1954,1956,1959,1961],{"class":265,"line":365},[263,1955,310],{"class":309},[263,1957,1958],{"class":313}," xlsx_fixtures ",[263,1960,317],{"class":309},[263,1962,1963],{"class":313}," fx\n",[263,1965,1966,1968,1971,1973],{"class":265,"line":370},[263,1967,341],{"class":309},[263,1969,1970],{"class":313}," loader ",[263,1972,310],{"class":309},[263,1974,1975],{"class":313}," load_orders\n",[263,1977,1978],{"class":265,"line":404},[263,1979,335],{"emptyLinePlaceholder":334},[263,1981,1982,1985,1987,1990],{"class":265,"line":415},[263,1983,1984],{"class":535},"@pytest.mark.parametrize",[263,1986,1104],{"class":313},[263,1988,1989],{"class":273},"\"build\"",[263,1991,1992],{"class":313},", [fx.clean, fx.title_rows, fx.dates_as_text,\n",[263,1994,1995],{"class":265,"line":454},[263,1996,1997],{"class":313},"                                   fx.numbers_as_text, fx.formatted_blank_rows,\n",[263,1999,2000,2003,2005,2008,2011,2013],{"class":265,"line":487},[263,2001,2002],{"class":313},"                                   fx.hidden_sheet_first], ",[263,2004,1910],{"class":753},[263,2006,2007],{"class":309},"=lambda",[263,2009,2010],{"class":313}," f: f.",[263,2012,1673],{"class":373},[263,2014,1146],{"class":313},[263,2016,2017,2019,2022],{"class":265,"line":519},[263,2018,532],{"class":309},[263,2020,2021],{"class":535}," test_awkward_layouts_load_the_same",[263,2023,2024],{"class":313},"(build):\n",[263,2026,2027,2029,2031],{"class":265,"line":524},[263,2028,1440],{"class":313},[263,2030,542],{"class":309},[263,2032,1654],{"class":313},[263,2034,2035,2038,2040,2042,2045,2048,2050,2052,2054,2056,2058,2060],{"class":265,"line":529},[263,2036,2037],{"class":309},"    assert",[263,2039,1506],{"class":313},[263,2041,383],{"class":273},[263,2043,2044],{"class":313},"].tolist() ",[263,2046,2047],{"class":309},"==",[263,2049,380],{"class":313},[263,2051,421],{"class":373},[263,2053,17],{"class":313},[263,2055,459],{"class":373},[263,2057,17],{"class":313},[263,2059,492],{"class":373},[263,2061,401],{"class":313},[263,2063,2064,2066,2068,2070,2073,2075,2078,2081],{"class":265,"line":564},[263,2065,2037],{"class":309},[263,2067,1506],{"class":313},[263,2069,398],{"class":273},[263,2071,2072],{"class":313},"].sum() ",[263,2074,2047],{"class":309},[263,2076,2077],{"class":313}," pytest.approx(",[263,2079,2080],{"class":373},"240.5",[263,2082,1146],{"class":313},[263,2084,2085,2087,2089,2091,2094,2096,2098,2100,2102,2104,2106,2108],{"class":265,"line":570},[263,2086,2037],{"class":309},[263,2088,1506],{"class":313},[263,2090,393],{"class":273},[263,2092,2093],{"class":313},"].dt.day.tolist() ",[263,2095,2047],{"class":309},[263,2097,380],{"class":313},[263,2099,442],{"class":373},[263,2101,17],{"class":313},[263,2103,477],{"class":373},[263,2105,17],{"class":313},[263,2107,477],{"class":373},[263,2109,401],{"class":313},[263,2111,2112],{"class":265,"line":581},[263,2113,335],{"emptyLinePlaceholder":334},[263,2115,2116,2118,2121],{"class":265,"line":592},[263,2117,532],{"class":309},[263,2119,2120],{"class":535}," test_error_cells_become_missing",[263,2122,697],{"class":313},[263,2124,2125,2127,2129],{"class":265,"line":603},[263,2126,1440],{"class":313},[263,2128,542],{"class":309},[263,2130,2131],{"class":313}," load_orders(io.BytesIO(fx.error_cells()))\n",[263,2133,2134,2136,2138,2140,2143,2145,2147,2150,2152,2154,2156,2158],{"class":265,"line":618},[263,2135,2037],{"class":309},[263,2137,1506],{"class":313},[263,2139,398],{"class":273},[263,2141,2142],{"class":313},"].isna().tolist() ",[263,2144,2047],{"class":309},[263,2146,380],{"class":313},[263,2148,2149],{"class":373},"False",[263,2151,17],{"class":313},[263,2153,2149],{"class":373},[263,2155,17],{"class":313},[263,2157,1490],{"class":373},[263,2159,401],{"class":313},[263,2161,2162],{"class":265,"line":624},[263,2163,335],{"emptyLinePlaceholder":334},[263,2165,2166,2168,2171],{"class":265,"line":637},[263,2167,532],{"class":309},[263,2169,2170],{"class":535}," test_formulas_written_by_openpyxl_have_no_values",[263,2172,697],{"class":313},[263,2174,2175,2177,2179],{"class":265,"line":643},[263,2176,1440],{"class":313},[263,2178,542],{"class":309},[263,2180,2181],{"class":313}," load_orders(io.BytesIO(fx.formula_amounts()))\n",[263,2183,2184,2186,2188,2190,2193],{"class":265,"line":652},[263,2185,2037],{"class":309},[263,2187,1506],{"class":313},[263,2189,398],{"class":273},[263,2191,2192],{"class":313},"].isna().all()           ",[263,2194,2195],{"class":302},"# no cached results in the file\n",[263,2197,2198],{"class":265,"line":658},[263,2199,335],{"emptyLinePlaceholder":334},[263,2201,2202,2204,2207],{"class":265,"line":669},[263,2203,532],{"class":309},[263,2205,2206],{"class":535}," test_missing_header_is_reported",[263,2208,697],{"class":313},[263,2210,2211,2214,2217,2220,2222,2225,2227,2230],{"class":265,"line":675},[263,2212,2213],{"class":309},"    with",[263,2215,2216],{"class":313}," pytest.raises(",[263,2218,2219],{"class":373},"ValueError",[263,2221,17],{"class":313},[263,2223,2224],{"class":753},"match",[263,2226,542],{"class":309},[263,2228,2229],{"class":273},"\"no header row\"",[263,2231,561],{"class":313},[263,2233,2234,2237,2240,2242,2245],{"class":265,"line":684},[263,2235,2236],{"class":313},"        load_orders(io.BytesIO(fx.make_xlsx([[",[263,2238,2239],{"class":273},"\"just\"",[263,2241,17],{"class":313},[263,2243,2244],{"class":273},"\"text\"",[263,2246,2247],{"class":313},"]])))\n",[263,2249,2250],{"class":265,"line":689},[263,2251,335],{"emptyLinePlaceholder":334},[263,2253,2254,2256,2259],{"class":265,"line":700},[263,2255,532],{"class":309},[263,2257,2258],{"class":535}," test_loader_accepts_a_path",[263,2260,2261],{"class":313},"(tmp_path):\n",[263,2263,2264,2267,2269,2272,2275],{"class":265,"line":720},[263,2265,2266],{"class":313},"    path ",[263,2268,542],{"class":309},[263,2270,2271],{"class":313}," tmp_path ",[263,2273,2274],{"class":309},"\u002F",[263,2276,2277],{"class":273}," \"orders.xlsx\"\n",[263,2279,2280],{"class":265,"line":725},[263,2281,2282],{"class":313},"    path.write_bytes(fx.clean())\n",[263,2284,2285,2287,2290,2293,2295],{"class":265,"line":735},[263,2286,2037],{"class":309},[263,2288,2289],{"class":373}," len",[263,2291,2292],{"class":313},"(load_orders(path)) ",[263,2294,2047],{"class":309},[263,2296,2297],{"class":373}," 3\n",[254,2299,2301],{"className":256,"code":2300,"language":258,"meta":259,"style":259},"pytest -v test_loader.py\n# test_loader.py::test_awkward_layouts_load_the_same[clean] PASSED\n# test_loader.py::test_awkward_layouts_load_the_same[title_rows] PASSED\n# test_loader.py::test_awkward_layouts_load_the_same[dates_as_text] PASSED\n# …\n# 10 passed in 0.31s\n",[14,2302,2303,2314,2319,2324,2329,2334],{"__ignoreMap":259},[263,2304,2305,2308,2311],{"class":265,"line":266},[263,2306,2307],{"class":269},"pytest",[263,2309,2310],{"class":373}," -v",[263,2312,2313],{"class":273}," test_loader.py\n",[263,2315,2316],{"class":265,"line":306},[263,2317,2318],{"class":302},"# test_loader.py::test_awkward_layouts_load_the_same[clean] PASSED\n",[263,2320,2321],{"class":265,"line":323},[263,2322,2323],{"class":302},"# test_loader.py::test_awkward_layouts_load_the_same[title_rows] PASSED\n",[263,2325,2326],{"class":265,"line":331},[263,2327,2328],{"class":302},"# test_loader.py::test_awkward_layouts_load_the_same[dates_as_text] PASSED\n",[263,2330,2331],{"class":265,"line":338},[263,2332,2333],{"class":302},"# …\n",[263,2335,2336],{"class":265,"line":352},[263,2337,2338],{"class":302},"# 10 passed in 0.31s\n",[10,2340,2341,2342,2345,2346,2350,2351,1897],{},"The formula test is deliberately a test of a limitation: it documents that generated formulas have no values, so nobody later \"fixes\" the loader to handle a case the fixture cannot produce. The last test writes the bytes to ",[14,2343,2344],{},"tmp_path"," for the one path-based entry point — generate in memory, touch the disk only where the code requires it. Assertions on the ",[2347,2348,2349],"em",{},"output"," workbook are covered in ",[26,2352,2354],{"href":2353},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","Test Excel Output with pytest",[32,2356,41,2361,41,2364,41,2367,41,2370,41,2375,41,2382,41,2387,41,2392,41,2397,41,2403,41,2407,41,2411,41,2415,41,2418,41,2423,41,2428,41,2432,41,2436,41,2441,41,2444,41,2449,41,2452,41,2457,41,2460],{"viewBox":2357,"role":35,"ariaLabelledBy":2358,"xmlns":39,"style":40},"4 0 752 233",[2359,2360],"fx-compare-t","fx-compare-d",[43,2362,2363],{"id":2359},"Sample files in the repository versus fixtures built in code",[47,2365,2366],{"id":2360},"Committed sample workbooks are real and quick to add once, but their contents are invisible in code review, they cannot be diffed and variants multiply as copies. Fixtures built in code show their data in the test, diff cleanly, turn a new variant into one function and took 4.5 ms against 4.8 ms through a file, so speed is not the trade-off. Real exports still catch quirks nobody thought to generate.",[51,2368],{"x":53,"y":54,"width":55,"height":2369,"fill":57},"233",[51,2371],{"x":60,"y":94,"width":2372,"height":2373,"rx":2374,"fill":151,"stroke":152,"style":67},"351.0","201.0","14",[69,2376,2378],{"transform":2377},"translate(182.5,29.0) scale(1.083)",[73,2379],{"d":2380,"fill":76,"stroke":159,"style":2381},"M6 3h9l4 4v14H6z M15 3v4h4","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[80,2383,2386],{"x":2384,"y":1783,"style":2385},"195.5","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Committed .xlsx samples",[265,2388],{"x1":2389,"y1":2390,"x2":2391,"y2":2390,"stroke":152,"style":1756},"36.0","91.0","355.0",[80,2393,2396],{"x":2394,"y":1806,"style":2395},"60.0","font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:start","real quirks from the source system",[69,2398,2400],{"transform":2399},"translate(38.5,99.5) scale(0.625)",[73,2401],{"d":104,"fill":76,"stroke":98,"style":2402},"stroke-width:2.88px;stroke-linecap:round;stroke-linejoin:round",[80,2404,2406],{"x":2394,"y":2405,"style":2395},"137.0","− contents invisible in review",[80,2408,2410],{"x":2394,"y":2409,"style":2395},"163.0","− binary diffs",[80,2412,2414],{"x":2394,"y":2413,"style":2395},"189.0","− variants multiply as copies",[51,2416],{"x":2417,"y":94,"width":2372,"height":2373,"rx":2374,"fill":97,"stroke":98,"style":67},"389.0",[69,2419,2421],{"transform":2420},"translate(551.5,29.0) scale(1.083)",[73,2422],{"d":210,"fill":76,"stroke":105,"style":2381},[80,2424,2427],{"x":2425,"y":1783,"style":2426},"564.5","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Built in memory",[265,2429],{"x1":2430,"y1":2390,"x2":2431,"y2":2390,"stroke":98,"style":1756},"405.0","724.0",[80,2433,2435],{"x":2434,"y":1806,"style":2395},"429.0","data visible in the test",[69,2437,2439],{"transform":2438},"translate(407.5,99.5) scale(0.625)",[73,2440],{"d":104,"fill":76,"stroke":98,"style":2402},[80,2442,2443],{"x":2434,"y":2405,"style":2395},"one function per variant",[69,2445,2447],{"transform":2446},"translate(407.5,125.5) scale(0.625)",[73,2448],{"d":104,"fill":76,"stroke":98,"style":2402},[80,2450,2451],{"x":2434,"y":2409,"style":2395},"clean diffs",[69,2453,2455],{"transform":2454},"translate(407.5,151.5) scale(0.625)",[73,2456],{"d":104,"fill":76,"stroke":98,"style":2402},[80,2458,2459],{"x":2434,"y":2413,"style":2395},"4.5 ms vs 4.8 ms via a file",[69,2461,2463],{"transform":2462},"translate(407.5,177.5) scale(0.625)",[73,2464],{"d":104,"fill":76,"stroke":98,"style":2402},[249,2466,2468],{"id":2467},"step-5-share-the-builders-across-test-files","Step 5 — Share the builders across test files",[10,2470,2471,2472,2475],{},"Once several test modules need workbooks, expose the helper as a pytest fixture in ",[14,2473,2474],{},"conftest.py",". A fixture that returns a function — the \"factory as fixture\" pattern — lets each test build exactly the rows it needs, while pytest takes care of finding it:",[254,2477,2479],{"className":293,"code":2478,"language":295,"meta":259,"style":259},"# conftest.py\nimport io\nimport pytest\nimport xlsx_fixtures\n\n@pytest.fixture\ndef orders_upload():\n    def build(rows=None, **options):\n        data = xlsx_fixtures.make_xlsx(rows or [xlsx_fixtures.HEADER, *xlsx_fixtures.ROWS], **options)\n        return io.BytesIO(data)\n    return build\n\n# in any test module:\n# def test_title_rows(orders_upload):\n#     frame = load_orders(orders_upload(title_rows=[\"Orders export\"]))\n",[14,2480,2481,2486,2492,2498,2505,2509,2514,2523,2545,2579,2587,2594,2598,2603,2608],{"__ignoreMap":259},[263,2482,2483],{"class":265,"line":266},[263,2484,2485],{"class":302},"# conftest.py\n",[263,2487,2488,2490],{"class":265,"line":306},[263,2489,310],{"class":309},[263,2491,328],{"class":313},[263,2493,2494,2496],{"class":265,"line":323},[263,2495,310],{"class":309},[263,2497,283],{"class":313},[263,2499,2500,2502],{"class":265,"line":331},[263,2501,310],{"class":309},[263,2503,2504],{"class":313}," xlsx_fixtures\n",[263,2506,2507],{"class":265,"line":338},[263,2508,335],{"emptyLinePlaceholder":334},[263,2510,2511],{"class":265,"line":352},[263,2512,2513],{"class":535},"@pytest.fixture\n",[263,2515,2516,2518,2521],{"class":265,"line":365},[263,2517,532],{"class":309},[263,2519,2520],{"class":535}," orders_upload",[263,2522,697],{"class":313},[263,2524,2525,2527,2530,2533,2535,2537,2539,2542],{"class":265,"line":370},[263,2526,1082],{"class":309},[263,2528,2529],{"class":535}," build",[263,2531,2532],{"class":313},"(rows",[263,2534,542],{"class":309},[263,2536,558],{"class":373},[263,2538,17],{"class":313},[263,2540,2541],{"class":309},"**",[263,2543,2544],{"class":313},"options):\n",[263,2546,2547,2550,2552,2555,2558,2561,2563,2565,2567,2570,2572,2574,2576],{"class":265,"line":404},[263,2548,2549],{"class":313},"        data ",[263,2551,542],{"class":309},[263,2553,2554],{"class":313}," xlsx_fixtures.make_xlsx(rows ",[263,2556,2557],{"class":309},"or",[263,2559,2560],{"class":313}," [xlsx_fixtures.",[263,2562,374],{"class":373},[263,2564,17],{"class":313},[263,2566,712],{"class":309},[263,2568,2569],{"class":313},"xlsx_fixtures.",[263,2571,407],{"class":373},[263,2573,750],{"class":313},[263,2575,2541],{"class":309},[263,2577,2578],{"class":313},"options)\n",[263,2580,2581,2584],{"class":265,"line":415},[263,2582,2583],{"class":309},"        return",[263,2585,2586],{"class":313}," io.BytesIO(data)\n",[263,2588,2589,2591],{"class":265,"line":454},[263,2590,678],{"class":309},[263,2592,2593],{"class":313}," build\n",[263,2595,2596],{"class":265,"line":487},[263,2597,335],{"emptyLinePlaceholder":334},[263,2599,2600],{"class":265,"line":519},[263,2601,2602],{"class":302},"# in any test module:\n",[263,2604,2605],{"class":265,"line":524},[263,2606,2607],{"class":302},"# def test_title_rows(orders_upload):\n",[263,2609,2610],{"class":265,"line":529},[263,2611,2612],{"class":302},"#     frame = load_orders(orders_upload(title_rows=[\"Orders export\"]))\n",[10,2614,2615],{},"The test states only what differs from the default — here, one title row — which keeps each test short and makes its intent obvious in review.",[249,2617,2619],{"id":2618},"step-6-keep-a-few-real-files-and-name-them-well","Step 6 — Keep a few real files, and name them well",[10,2621,2622,2623,17,2626,2629],{},"Generated fixtures only contain the quirks you thought of. Keep a handful of genuine exports from each source system — trimmed to a few rows, with personal data removed — and name them after what makes them special: ",[14,2624,2625],{},"erp_export_merged_header.xlsx",[14,2627,2628],{},"bank_statement_with_totals_row.xlsx",". When a new bug comes from a real file, reproduce it with a builder function first; keep the real file only if the builder cannot express the quirk.",[249,2631,2633],{"id":2632},"common-pitfalls","Common pitfalls",[2635,2636,2637,2664,2674,2680],"ul",{},[2638,2639,2640,2644,2645,2648,2649,2652,2653,2656,2657,2660,2661,1897],"li",{},[2641,2642,2643],"strong",{},"Dates written as datetimes compare as timestamps."," openpyxl stores ",[14,2646,2647],{},"date"," values as datetimes; pandas reads them as ",[14,2650,2651],{},"Timestamp",". Compare with ",[14,2654,2655],{},".dt.date"," or ",[14,2658,2659],{},"pd.Timestamp",", not with ",[14,2662,2663],{},"datetime.date",[2638,2665,2666,2669,2670,2673],{},[2641,2667,2668],{},"Fixtures with shared state."," Build a fresh workbook in each call rather than caching one ",[14,2671,2672],{},"Workbook"," object that a test might modify.",[2638,2675,2676,2679],{},[2641,2677,2678],{},"Comparing bytes."," openpyxl writes the save time into every file, so two builds of the same fixture are never byte-identical. Compare content.",[2638,2681,2682,2685],{},[2641,2683,2684],{},"Huge generated fixtures."," A test that needs 100,000 rows is a performance test; mark it and keep it out of the fast suite.",[249,2687,2689],{"id":2688},"conclusion","Conclusion",[10,2691,2692,2693,2696,2697,2699],{},"Generate test workbooks with a small ",[14,2694,2695],{},"make_xlsx"," helper that returns bytes, name each awkward variant after its quirk, let the code under test accept file objects, and run the same assertions over every layout with ",[14,2698,1906],{},". Pin down the surprising behaviours — error cells and generated formulas both read as missing — and keep a few trimmed real exports for the quirks nobody predicted.",[249,2701,2703],{"id":2702},"frequently-asked-questions","Frequently asked questions",[10,2705,2706,2709],{},[2641,2707,2708],{},"How do I create an Excel file for a pytest test without saving it to disk?","\nBuild the workbook with openpyxl, save it to an io.BytesIO buffer and return buffer.getvalue(). pandas.read_excel and openpyxl.load_workbook both accept io.BytesIO(data), so the code under test can read it exactly as it would read a file.",[10,2711,2712,2715],{},[2641,2713,2714],{},"Why do formulas in my generated test workbook read as NaN in pandas?","\nopenpyxl writes formulas but not their results, and pandas reads the stored result. A test workbook with =A2*2 in a cell therefore reads as NaN. Write the value instead, or recalculate the file with LibreOffice when the test really needs formula results.",[10,2717,2718,2721],{},[2641,2719,2720],{},"Are in-memory Excel fixtures faster than files?","\nNot noticeably. In a benchmark, building and reading a small workbook took 4.5 ms in memory and 4.8 ms through a temporary file. The benefits are elsewhere: no binary files in the repository, each test's data visible in the code, and variants that are one function call away.",[10,2723,2724,2727],{},[2641,2725,2726],{},"Should I still keep some real Excel files in the tests?","\nYes, a few. Files exported by the actual source system catch quirks you would not think to generate. Keep them small, name them after what makes them special, and build everything else in memory.",[249,2729,2731],{"id":2730},"related","Related",[2635,2733,2734,2740,2745,2752],{},[2638,2735,2736,2737,2739],{},"Up one level: ",[26,2738,29],{"href":28}," — making report code dependable.",[2638,2741,2742,2744],{},[26,2743,2354],{"href":2353}," — assertions on the workbooks your code writes.",[2638,2746,2747,2751],{},[26,2748,2750],{"href":2749},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcompare-excel-output-to-a-golden-file-in-tests\u002F","Compare Excel Output to a Golden File in Tests"," — checking a whole report at once.",[2638,2753,2754,2758],{},[26,2755,2757],{"href":2756},"\u002Fgetting-started-with-python-excel-automation\u002Freading-excel-files-with-pandas\u002Fskip-rows-and-set-header-when-reading-excel-with-pandas\u002F","Skip Rows and Set the Header When Reading Excel with pandas"," — the header problem the loader solves.",[2760,2761,2762],"style",{},"html pre.shiki code .sMTad, html code.shiki .sMTad{--shiki-default:#6F42C1;--shiki-dark:#FFB757}html pre.shiki code .srMev, html code.shiki .srMev{--shiki-default:#032F62;--shiki-dark:#ADDCFF}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);}html pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}html pre.shiki code .s-kum, html code.shiki .s-kum{--shiki-default:#D73A49;--shiki-dark:#FF9492}html pre.shiki code .skGVy, html code.shiki .skGVy{--shiki-default:#24292E;--shiki-dark:#F0F3F6}html pre.shiki code .sP0c6, html code.shiki .sP0c6{--shiki-default:#005CC5;--shiki-dark:#91CBFF}html pre.shiki code .s_Opv, html code.shiki .s_Opv{--shiki-default:#6F42C1;--shiki-dark:#DBB7FF}html pre.shiki code .sa561, html code.shiki .sa561{--shiki-default:#E36209;--shiki-dark:#FFB757}html pre.shiki code .sSjpA, html code.shiki .sSjpA{--shiki-default:#005CC5;--shiki-dark:#FF9492}",{"title":259,"searchDepth":306,"depth":306,"links":2764},[2765,2766,2767,2768,2769,2770,2771,2772,2773,2774,2775],{"id":251,"depth":306,"text":252},{"id":286,"depth":306,"text":287},{"id":1276,"depth":306,"text":1277},{"id":1589,"depth":306,"text":1590},{"id":1900,"depth":306,"text":1901},{"id":2467,"depth":306,"text":2468},{"id":2618,"depth":306,"text":2619},{"id":2632,"depth":306,"text":2633},{"id":2688,"depth":306,"text":2689},{"id":2702,"depth":306,"text":2703},{"id":2730,"depth":306,"text":2731},"2026-09-23","Stop committing sample spreadsheets to the test folder. Build each test workbook in memory with openpyxl — including the awkward shapes real exports have, such as title rows above the header, dates and numbers stored as text, error cells, formulas without results and formatted empty rows — and run one test across all of them with pytest.mark.parametrize.","md",[2780,2782,2784,2786],{"q":2708,"a":2781},"Build the workbook with openpyxl, save it to an io.BytesIO buffer and return buffer.getvalue(). pandas.read_excel and openpyxl.load_workbook both accept io.BytesIO(data), so the code under test can read it exactly as it would read a file.",{"q":2714,"a":2783},"openpyxl writes formulas but not their results, and pandas reads the stored result. A test workbook with =A2*2 in a cell therefore reads as NaN. Write the value instead, or recalculate the file with LibreOffice when the test really needs formula results.",{"q":2720,"a":2785},"Not noticeably. In a benchmark, building and reading a small workbook took 4.5 ms in memory and 4.8 ms through a temporary file. The benefits are elsewhere: no binary files in the repository, each test's data visible in the code, and variants that are one function call away.",{"q":2726,"a":2787},"Yes, a few. Files exported by the actual source system catch quirks you would not think to generate. Keep them small, name them after what makes them special, and build everything else in memory.","create excel test fixtures in memory for pytest",null,{"breadcrumb":2791},[2792,2794,2797],{"name":2793,"item":2274},"Home",{"name":2795,"item":2796},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":29,"item":28},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory",{"title":2800,"description":2801},"Create Excel Test Fixtures in Memory for pytest (openpyxl, BytesIO)","Generate xlsx test fixtures in memory for pytest — a make_xlsx helper returning bytes, variant builders for title rows, text dates, text numbers, #N\u002FA cells, openpyxl formulas that read as NaN, styled blank rows and hidden sheets, parametrized tests with readable ids, and when a real file path is still needed.","create-excel-test-fixtures-in-memory","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcreate-excel-test-fixtures-in-memory\u002Findex",[2805,2806],"reliable-jobs","openpyxl","how-to","InPB66DbpzKXKzti7HbhStdX9B-Hed7yQw2rPxn8GW8",[2810,2813],{"title":2750,"path":2811,"stem":2812,"children":-1},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcompare-excel-output-to-a-golden-file-in-tests","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fcompare-excel-output-to-a-golden-file-in-tests\u002Findex",{"title":2814,"path":2815,"stem":2816,"children":-1},"Diff Excel Files in Git with a textconv Driver","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fdiff-excel-files-in-git-with-a-textconv-driver","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Fdiff-excel-files-in-git-with-a-textconv-driver\u002Findex",[2818,2820],{"path":2819,"title":2795},"\u002Fautomating-reporting-workflows",{"path":2821,"title":29},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts",[],[2824,2828,2831,2834,2837,2840],{"path":2825,"title":2826,"kind":2827},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation","Error Handling and Logging in Excel Automation","guide",{"path":2829,"title":2830,"kind":2807},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fdetect-a-scheduled-report-that-did-not-run","Detect a Scheduled Report That Did Not Run",{"path":2832,"title":2833,"kind":2807},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fignore-excel-lock-files-when-processing-a-folder","Ignore Excel Lock Files When Processing a Folder",{"path":2835,"title":2836,"kind":2807},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Flog-python-excel-script-output-to-a-file","Log Python Excel Script Output to a File",{"path":2838,"title":2839,"kind":2807},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fmake-an-excel-report-job-safe-to-rerun","Make an Excel Report Job Safe to Rerun",{"path":2841,"title":2842,"kind":2807},"\u002Fautomating-reporting-workflows\u002Ferror-handling-and-logging-in-excel-automation\u002Fprevent-formula-injection-in-excel-exports","Prevent Formula Injection in Excel Exports from Python",1790972140674]