[{"data":1,"prerenderedAt":3064},["ShallowReactive",2],{"doc:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-win32com-and-xlwings-code-without-excel":3,"surround:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-win32com-and-xlwings-code-without-excel":3029,"ancestors:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-win32com-and-xlwings-code-without-excel":3038,"children:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-win32com-and-xlwings-code-without-excel":3043,"related:\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-win32com-and-xlwings-code-without-excel":3044},{"id":4,"title":5,"body":6,"dateModified":2994,"datePublished":2994,"description":2995,"extension":2996,"faq":2997,"keyword":3006,"kind":3007,"meta":3008,"navigation":100,"path":3017,"seo":3018,"slug":3021,"stem":3022,"topics":3023,"type":3027,"__hash__":3028},"docs\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-win32com-and-xlwings-code-without-excel\u002Findex.md","Test win32com and xlwings Code Without Excel",{"type":7,"value":8,"toc":2983},"minimark",[9,24,29,64,67,71,79,460,491,604,608,611,1184,1207,1211,1681,1847,1854,1947,2098,2102,2109,2238,2683,2713,2843,2857,2868,2872,2911,2915,2918,2922,2928,2934,2940,2946,2950,2979],[10,11,12,13,17,18,23],"p",{},"Code that automates Excel itself — refreshing connections, running macros, printing, reading values Excel has calculated — is the hardest Python Excel code to test. It needs Windows, an installed and licensed Excel, and a desktop session; CI runners, colleagues' Macs and containers have none of these. So it often goes untested, and its bugs show up as a hung ",[14,15,16],"code",{},"EXCEL.EXE"," on a server, a workbook saved over by mistake, or a macro run against the wrong file. Most of the logic in such code, though, is not Excel's: which files to open, which sheet and range to read, what to do with the values, when to save and how to clean up when something fails. That logic can be tested anywhere, if the Excel calls are kept at the edge and replaced in tests with small fakes. This page restructures a win32com refresh job and an xlwings report so they can be tested on Linux, writes the fakes, tests the clean-up path, and keeps real-Excel tests for the machines that have it. It belongs to ",[19,20,22],"a",{"href":21},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002F","Testing and Packaging Excel Automation Scripts",".",[25,26,28],"h2",{"id":27},"prerequisites","Prerequisites",[30,31,36],"pre",{"className":32,"code":33,"language":34,"meta":35,"style":35},"language-bash shiki shiki-themes github-light github-dark-high-contrast","pip install pytest pandas\n# pywin32 and xlwings are only needed where Excel is installed\n","bash","",[14,37,38,57],{"__ignoreMap":35},[39,40,43,47,51,54],"span",{"class":41,"line":42},"line",1,[39,44,46],{"class":45},"sMTad","pip",[39,48,50],{"class":49},"srMev"," install",[39,52,53],{"class":49}," pytest",[39,55,56],{"class":49}," pandas\n",[39,58,60],{"class":41,"line":59},2,[39,61,63],{"class":62},"s-wDw","# pywin32 and xlwings are only needed where Excel is installed\n",[10,65,66],{},"The tests on this page were run on Linux, where neither Excel nor pywin32 exists — exactly the situation of a CI runner. xlwings installs on Linux but cannot start Excel there, so the same approach applies to it.",[25,68,70],{"id":69},"step-1-keep-the-excel-calls-in-one-thin-function","Step 1 — Keep the Excel calls in one thin function",[10,72,73,74,78],{},"The job opens a workbook, refreshes its data connections, waits for the refresh, reads a total from a named cell, saves and closes — and must quit Excel whatever happens. Write it so that the Excel application is ",[75,76,77],"em",{},"passed in"," rather than created inside:",[30,80,84],{"className":81,"code":82,"language":83,"meta":35,"style":35},"language-python shiki shiki-themes github-light github-dark-high-contrast","import logging\n\nlog = logging.getLogger(__name__)\n\ndef refresh_and_read(app, path, total_name=\"GrandTotal\"):\n    \"\"\"Refresh all connections in the workbook at path and return the value of a named cell.\"\"\"\n    wb = app.Workbooks.Open(str(path), UpdateLinks=0, ReadOnly=False)\n    try:\n        wb.RefreshAll()\n        app.CalculateUntilAsyncQueriesDone()\n        total = wb.Names(total_name).RefersToRange.Value\n        if total is None:\n            raise ValueError(f\"{total_name} is empty after refresh\")\n        wb.Save()\n        return float(total)\n    finally:\n        wb.Close(SaveChanges=False)\n\ndef run(paths, make_app):\n    app = make_app()\n    app.DisplayAlerts = False\n    try:\n        return {str(p): refresh_and_read(app, p) for p in paths}\n    finally:\n        app.Quit()\n\nif __name__ == \"__main__\":                                   # the only place that knows about win32com\n    import win32com.client\n    print(run([\"C:\u002Freports\u002Fsales.xlsx\"], lambda: win32com.client.DispatchEx(\"Excel.Application\")))\n","python",[14,85,86,96,102,121,126,147,153,192,201,207,213,224,241,274,280,292,300,315,320,331,342,353,360,385,392,398,403,424,433],{"__ignoreMap":35},[39,87,88,92],{"class":41,"line":42},[39,89,91],{"class":90},"s-kum","import",[39,93,95],{"class":94},"skGVy"," logging\n",[39,97,98],{"class":41,"line":59},[39,99,101],{"emptyLinePlaceholder":100},true,"\n",[39,103,105,108,111,114,118],{"class":41,"line":104},3,[39,106,107],{"class":94},"log ",[39,109,110],{"class":90},"=",[39,112,113],{"class":94}," logging.getLogger(",[39,115,117],{"class":116},"sP0c6","__name__",[39,119,120],{"class":94},")\n",[39,122,124],{"class":41,"line":123},4,[39,125,101],{"emptyLinePlaceholder":100},[39,127,129,132,136,139,141,144],{"class":41,"line":128},5,[39,130,131],{"class":90},"def",[39,133,135],{"class":134},"s_Opv"," refresh_and_read",[39,137,138],{"class":94},"(app, path, total_name",[39,140,110],{"class":90},[39,142,143],{"class":49},"\"GrandTotal\"",[39,145,146],{"class":94},"):\n",[39,148,150],{"class":41,"line":149},6,[39,151,152],{"class":49},"    \"\"\"Refresh all connections in the workbook at path and return the value of a named cell.\"\"\"\n",[39,154,156,159,161,164,167,170,174,176,179,182,185,187,190],{"class":41,"line":155},7,[39,157,158],{"class":94},"    wb ",[39,160,110],{"class":90},[39,162,163],{"class":94}," app.Workbooks.Open(",[39,165,166],{"class":116},"str",[39,168,169],{"class":94},"(path), ",[39,171,173],{"class":172},"sa561","UpdateLinks",[39,175,110],{"class":90},[39,177,178],{"class":116},"0",[39,180,181],{"class":94},", ",[39,183,184],{"class":172},"ReadOnly",[39,186,110],{"class":90},[39,188,189],{"class":116},"False",[39,191,120],{"class":94},[39,193,195,198],{"class":41,"line":194},8,[39,196,197],{"class":90},"    try",[39,199,200],{"class":94},":\n",[39,202,204],{"class":41,"line":203},9,[39,205,206],{"class":94},"        wb.RefreshAll()\n",[39,208,210],{"class":41,"line":209},10,[39,211,212],{"class":94},"        app.CalculateUntilAsyncQueriesDone()\n",[39,214,216,219,221],{"class":41,"line":215},11,[39,217,218],{"class":94},"        total ",[39,220,110],{"class":90},[39,222,223],{"class":94}," wb.Names(total_name).RefersToRange.Value\n",[39,225,227,230,233,236,239],{"class":41,"line":226},12,[39,228,229],{"class":90},"        if",[39,231,232],{"class":94}," total ",[39,234,235],{"class":90},"is",[39,237,238],{"class":116}," None",[39,240,200],{"class":94},[39,242,244,247,250,253,256,259,263,266,269,272],{"class":41,"line":243},13,[39,245,246],{"class":90},"            raise",[39,248,249],{"class":116}," ValueError",[39,251,252],{"class":94},"(",[39,254,255],{"class":90},"f",[39,257,258],{"class":49},"\"",[39,260,262],{"class":261},"sSjpA","{",[39,264,265],{"class":94},"total_name",[39,267,268],{"class":261},"}",[39,270,271],{"class":49}," is empty after refresh\"",[39,273,120],{"class":94},[39,275,277],{"class":41,"line":276},14,[39,278,279],{"class":94},"        wb.Save()\n",[39,281,283,286,289],{"class":41,"line":282},15,[39,284,285],{"class":90},"        return",[39,287,288],{"class":116}," float",[39,290,291],{"class":94},"(total)\n",[39,293,295,298],{"class":41,"line":294},16,[39,296,297],{"class":90},"    finally",[39,299,200],{"class":94},[39,301,303,306,309,311,313],{"class":41,"line":302},17,[39,304,305],{"class":94},"        wb.Close(",[39,307,308],{"class":172},"SaveChanges",[39,310,110],{"class":90},[39,312,189],{"class":116},[39,314,120],{"class":94},[39,316,318],{"class":41,"line":317},18,[39,319,101],{"emptyLinePlaceholder":100},[39,321,323,325,328],{"class":41,"line":322},19,[39,324,131],{"class":90},[39,326,327],{"class":134}," run",[39,329,330],{"class":94},"(paths, make_app):\n",[39,332,334,337,339],{"class":41,"line":333},20,[39,335,336],{"class":94},"    app ",[39,338,110],{"class":90},[39,340,341],{"class":94}," make_app()\n",[39,343,345,348,350],{"class":41,"line":344},21,[39,346,347],{"class":94},"    app.DisplayAlerts ",[39,349,110],{"class":90},[39,351,352],{"class":116}," False\n",[39,354,356,358],{"class":41,"line":355},22,[39,357,197],{"class":90},[39,359,200],{"class":94},[39,361,363,365,368,370,373,376,379,382],{"class":41,"line":362},23,[39,364,285],{"class":90},[39,366,367],{"class":94}," {",[39,369,166],{"class":116},[39,371,372],{"class":94},"(p): refresh_and_read(app, p) ",[39,374,375],{"class":90},"for",[39,377,378],{"class":94}," p ",[39,380,381],{"class":90},"in",[39,383,384],{"class":94}," paths}\n",[39,386,388,390],{"class":41,"line":387},24,[39,389,297],{"class":90},[39,391,200],{"class":94},[39,393,395],{"class":41,"line":394},25,[39,396,397],{"class":94},"        app.Quit()\n",[39,399,401],{"class":41,"line":400},26,[39,402,101],{"emptyLinePlaceholder":100},[39,404,406,409,412,415,418,421],{"class":41,"line":405},27,[39,407,408],{"class":90},"if",[39,410,411],{"class":116}," __name__",[39,413,414],{"class":90}," ==",[39,416,417],{"class":49}," \"__main__\"",[39,419,420],{"class":94},":                                   ",[39,422,423],{"class":62},"# the only place that knows about win32com\n",[39,425,427,430],{"class":41,"line":426},28,[39,428,429],{"class":90},"    import",[39,431,432],{"class":94}," win32com.client\n",[39,434,436,439,442,445,448,451,454,457],{"class":41,"line":435},29,[39,437,438],{"class":116},"    print",[39,440,441],{"class":94},"(run([",[39,443,444],{"class":49},"\"C:\u002Freports\u002Fsales.xlsx\"",[39,446,447],{"class":94},"], ",[39,449,450],{"class":90},"lambda",[39,452,453],{"class":94},": win32com.client.DispatchEx(",[39,455,456],{"class":49},"\"Excel.Application\"",[39,458,459],{"class":94},")))\n",[10,461,462,465,466,469,470,473,474,477,478,481,482,486,487,23],{},[14,463,464],{},"run"," receives a ",[14,467,468],{},"make_app"," function instead of calling ",[14,471,472],{},"win32com.client.DispatchEx"," itself; only the ",[14,475,476],{},"__main__"," block, which tests never execute, imports win32com. The ",[14,479,480],{},"try … finally"," blocks are the important part: whatever goes wrong — a missing name, a failed refresh — every opened workbook is closed without saving over it, and Excel is told to quit. Those are exactly the behaviours worth testing, because getting them wrong leaves invisible Excel processes running on a server until someone kills them. The COM calls themselves are covered in ",[19,483,485],{"href":484},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Frefresh-excel-data-connections-and-recalculate-with-win32com\u002F","Refresh Excel Data Connections and Recalculate with win32com",", and what happens when clean-up is skipped in ",[19,488,490],{"href":489},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fclose-excel-cleanly-and-avoid-orphan-com-processes\u002F","Close Excel Cleanly and Avoid Orphan COM Processes",[492,493,501,502,501,506,501,510,501,517,501,527,501,534,501,539,501,544,501,549,501,554,501,559,501,563,501,567,501,570,501,574,501,579,501,583,501,587,501,590,501,595,501,600],"svg",{"viewBox":494,"role":495,"ariaLabelledBy":496,"xmlns":499,"style":500},"84 0 592 376","img",[497,498],"wx-layers-t","wx-layers-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","width:100%;max-width:592px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif","\n  ",[503,504,505],"title",{"id":497},"Where the Excel calls live",[507,508,509],"desc",{"id":498},"The entry point is the only place that creates the real Excel application. The job logic receives an application object and is tested with fakes. Real-Excel integration tests run only on Windows machines with Excel installed.",[511,512],"rect",{"x":513,"y":178,"width":514,"height":515,"fill":516},"84","592","376","#ffffff",[511,518],{"x":519,"y":520,"width":521,"height":522,"rx":523,"fill":524,"stroke":525,"style":526},"100.0","16.0","560.0","59.0","12","#e7ebef","var(--line,#cdd5e6)","stroke-width:2px",[528,529,533],"text",{"x":530,"y":531,"style":532},"380.0","41.5","font-size:13.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","Entry point (__main__)",[528,535,538],{"x":530,"y":536,"style":537},"61.0","font-size:11.5px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","creates Excel via win32com",[41,540],{"x1":530,"y1":541,"x2":530,"y2":542,"stroke":543,"style":526},"80.0","102.0","var(--brand,#5b5cf0)",[545,546],"polygon",{"points":547,"fill":548},"380.0,106.0 375.0,97.0 385.0,97.0","#5b5cf0",[511,550],{"x":519,"y":551,"width":521,"height":522,"rx":523,"fill":552,"stroke":553,"style":526},"111.0","#d9f4f1","var(--teal,#0f9488)",[528,555,558],{"x":530,"y":556,"style":557},"136.5","font-size:13.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Job logic (run, refresh_and_read)",[528,560,562],{"x":530,"y":561,"style":537},"156.0","receives app, tested with fakes",[41,564],{"x1":530,"y1":565,"x2":530,"y2":566,"stroke":543,"style":526},"175.0","197.0",[545,568],{"points":569,"fill":548},"380.0,201.0 375.0,192.0 385.0,192.0",[511,571],{"x":519,"y":572,"width":521,"height":522,"rx":523,"fill":573,"stroke":543,"style":526},"206.0","#f0f4ff",[528,575,578],{"x":530,"y":576,"style":577},"231.5","font-size:13.5px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Fakes",[528,580,582],{"x":530,"y":581,"style":537},"251.0","record calls, simulate failures",[41,584],{"x1":530,"y1":585,"x2":530,"y2":586,"stroke":543,"style":526},"270.0","292.0",[545,588],{"points":589,"fill":548},"380.0,296.0 375.0,287.0 385.0,287.0",[511,591],{"x":519,"y":592,"width":521,"height":522,"rx":523,"fill":593,"stroke":594,"style":526},"301.0","#fdefd8","var(--gold,#b4740a)",[528,596,599],{"x":530,"y":597,"style":598},"326.5","font-size:13.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","Integration tests",[528,601,603],{"x":530,"y":602,"style":537},"346.0","Windows with Excel only",[25,605,607],{"id":606},"step-2-fakes-that-record-what-happened","Step 2 — Fakes that record what happened",[10,609,610],{},"A fake is a small class with the same attribute names as the COM objects the code uses, which records calls instead of performing them. It needs only the parts the code touches:",[30,612,614],{"className":81,"code":613,"language":83,"meta":35,"style":35},"class FakeRange:\n    def __init__(self, value):\n        self.Value = value\n\nclass FakeName:\n    def __init__(self, value):\n        self.RefersToRange = FakeRange(value)\n\nclass FakeWorkbook:\n    def __init__(self, app, path, names, fail_refresh=False):\n        self.app, self.path, self._names, self.fail_refresh = app, path, names, fail_refresh\n        self.saved = self.closed = False\n        self.close_args = None\n\n    def RefreshAll(self):\n        self.app.calls.append((\"RefreshAll\", self.path))\n        if self.fail_refresh:\n            raise RuntimeError(\"(-2147352567, 'Exception occurred.', 'The connection failed')\")\n\n    def Names(self, name):\n        if name not in self._names:\n            raise KeyError(name)\n        return FakeName(self._names[name])\n\n    def Save(self):\n        self.saved = True\n\n    def Close(self, SaveChanges):\n        self.closed, self.close_args = True, {\"SaveChanges\": SaveChanges}\n\nclass FakeWorkbooks:\n    def __init__(self, app):\n        self.app = app\n\n    def Open(self, path, **kwargs):\n        wb = FakeWorkbook(self.app, path, **self.app.books[path])\n        self.app.opened.append(wb)\n        return wb\n\nclass FakeExcel:\n    def __init__(self, books):\n        self.books, self.opened, self.calls = books, [], []\n        self.Workbooks = FakeWorkbooks(self)\n        self.DisplayAlerts = True\n        self.quit_called = False\n\n    def CalculateUntilAsyncQueriesDone(self):\n        self.calls.append((\"CalculateUntilAsyncQueriesDone\",))\n\n    def Quit(self):\n        self.quit_called = True\n",[14,615,616,626,637,650,654,663,671,683,687,696,711,739,758,770,774,784,801,810,824,828,838,856,866,878,882,891,902,906,916,941,946,956,966,979,984,1001,1024,1032,1040,1045,1055,1065,1088,1105,1117,1129,1134,1144,1158,1163,1173],{"__ignoreMap":35},[39,617,618,621,624],{"class":41,"line":42},[39,619,620],{"class":90},"class",[39,622,623],{"class":45}," FakeRange",[39,625,200],{"class":94},[39,627,628,631,634],{"class":41,"line":59},[39,629,630],{"class":90},"    def",[39,632,633],{"class":116}," __init__",[39,635,636],{"class":94},"(self, value):\n",[39,638,639,642,645,647],{"class":41,"line":104},[39,640,641],{"class":116},"        self",[39,643,644],{"class":94},".Value ",[39,646,110],{"class":90},[39,648,649],{"class":94}," value\n",[39,651,652],{"class":41,"line":123},[39,653,101],{"emptyLinePlaceholder":100},[39,655,656,658,661],{"class":41,"line":128},[39,657,620],{"class":90},[39,659,660],{"class":45}," FakeName",[39,662,200],{"class":94},[39,664,665,667,669],{"class":41,"line":149},[39,666,630],{"class":90},[39,668,633],{"class":116},[39,670,636],{"class":94},[39,672,673,675,678,680],{"class":41,"line":155},[39,674,641],{"class":116},[39,676,677],{"class":94},".RefersToRange ",[39,679,110],{"class":90},[39,681,682],{"class":94}," FakeRange(value)\n",[39,684,685],{"class":41,"line":194},[39,686,101],{"emptyLinePlaceholder":100},[39,688,689,691,694],{"class":41,"line":203},[39,690,620],{"class":90},[39,692,693],{"class":45}," FakeWorkbook",[39,695,200],{"class":94},[39,697,698,700,702,705,707,709],{"class":41,"line":209},[39,699,630],{"class":90},[39,701,633],{"class":116},[39,703,704],{"class":94},"(self, app, path, names, fail_refresh",[39,706,110],{"class":90},[39,708,189],{"class":116},[39,710,146],{"class":94},[39,712,713,715,718,721,724,726,729,731,734,736],{"class":41,"line":215},[39,714,641],{"class":116},[39,716,717],{"class":94},".app, ",[39,719,720],{"class":116},"self",[39,722,723],{"class":94},".path, ",[39,725,720],{"class":116},[39,727,728],{"class":94},"._names, ",[39,730,720],{"class":116},[39,732,733],{"class":94},".fail_refresh ",[39,735,110],{"class":90},[39,737,738],{"class":94}," app, path, names, fail_refresh\n",[39,740,741,743,746,748,751,754,756],{"class":41,"line":226},[39,742,641],{"class":116},[39,744,745],{"class":94},".saved ",[39,747,110],{"class":90},[39,749,750],{"class":116}," self",[39,752,753],{"class":94},".closed ",[39,755,110],{"class":90},[39,757,352],{"class":116},[39,759,760,762,765,767],{"class":41,"line":243},[39,761,641],{"class":116},[39,763,764],{"class":94},".close_args ",[39,766,110],{"class":90},[39,768,769],{"class":116}," None\n",[39,771,772],{"class":41,"line":276},[39,773,101],{"emptyLinePlaceholder":100},[39,775,776,778,781],{"class":41,"line":282},[39,777,630],{"class":90},[39,779,780],{"class":134}," RefreshAll",[39,782,783],{"class":94},"(self):\n",[39,785,786,788,791,794,796,798],{"class":41,"line":294},[39,787,641],{"class":116},[39,789,790],{"class":94},".app.calls.append((",[39,792,793],{"class":49},"\"RefreshAll\"",[39,795,181],{"class":94},[39,797,720],{"class":116},[39,799,800],{"class":94},".path))\n",[39,802,803,805,807],{"class":41,"line":302},[39,804,229],{"class":90},[39,806,750],{"class":116},[39,808,809],{"class":94},".fail_refresh:\n",[39,811,812,814,817,819,822],{"class":41,"line":317},[39,813,246],{"class":90},[39,815,816],{"class":116}," RuntimeError",[39,818,252],{"class":94},[39,820,821],{"class":49},"\"(-2147352567, 'Exception occurred.', 'The connection failed')\"",[39,823,120],{"class":94},[39,825,826],{"class":41,"line":322},[39,827,101],{"emptyLinePlaceholder":100},[39,829,830,832,835],{"class":41,"line":333},[39,831,630],{"class":90},[39,833,834],{"class":134}," Names",[39,836,837],{"class":94},"(self, name):\n",[39,839,840,842,845,848,851,853],{"class":41,"line":344},[39,841,229],{"class":90},[39,843,844],{"class":94}," name ",[39,846,847],{"class":90},"not",[39,849,850],{"class":90}," in",[39,852,750],{"class":116},[39,854,855],{"class":94},"._names:\n",[39,857,858,860,863],{"class":41,"line":355},[39,859,246],{"class":90},[39,861,862],{"class":116}," KeyError",[39,864,865],{"class":94},"(name)\n",[39,867,868,870,873,875],{"class":41,"line":362},[39,869,285],{"class":90},[39,871,872],{"class":94}," FakeName(",[39,874,720],{"class":116},[39,876,877],{"class":94},"._names[name])\n",[39,879,880],{"class":41,"line":387},[39,881,101],{"emptyLinePlaceholder":100},[39,883,884,886,889],{"class":41,"line":394},[39,885,630],{"class":90},[39,887,888],{"class":134}," Save",[39,890,783],{"class":94},[39,892,893,895,897,899],{"class":41,"line":400},[39,894,641],{"class":116},[39,896,745],{"class":94},[39,898,110],{"class":90},[39,900,901],{"class":116}," True\n",[39,903,904],{"class":41,"line":405},[39,905,101],{"emptyLinePlaceholder":100},[39,907,908,910,913],{"class":41,"line":426},[39,909,630],{"class":90},[39,911,912],{"class":134}," Close",[39,914,915],{"class":94},"(self, SaveChanges):\n",[39,917,918,920,923,925,927,929,932,935,938],{"class":41,"line":435},[39,919,641],{"class":116},[39,921,922],{"class":94},".closed, ",[39,924,720],{"class":116},[39,926,764],{"class":94},[39,928,110],{"class":90},[39,930,931],{"class":116}," True",[39,933,934],{"class":94},", {",[39,936,937],{"class":49},"\"SaveChanges\"",[39,939,940],{"class":94},": SaveChanges}\n",[39,942,944],{"class":41,"line":943},30,[39,945,101],{"emptyLinePlaceholder":100},[39,947,949,951,954],{"class":41,"line":948},31,[39,950,620],{"class":90},[39,952,953],{"class":45}," FakeWorkbooks",[39,955,200],{"class":94},[39,957,959,961,963],{"class":41,"line":958},32,[39,960,630],{"class":90},[39,962,633],{"class":116},[39,964,965],{"class":94},"(self, app):\n",[39,967,969,971,974,976],{"class":41,"line":968},33,[39,970,641],{"class":116},[39,972,973],{"class":94},".app ",[39,975,110],{"class":90},[39,977,978],{"class":94}," app\n",[39,980,982],{"class":41,"line":981},34,[39,983,101],{"emptyLinePlaceholder":100},[39,985,987,989,992,995,998],{"class":41,"line":986},35,[39,988,630],{"class":90},[39,990,991],{"class":134}," Open",[39,993,994],{"class":94},"(self, path, ",[39,996,997],{"class":90},"**",[39,999,1000],{"class":94},"kwargs):\n",[39,1002,1004,1007,1009,1012,1014,1017,1019,1021],{"class":41,"line":1003},36,[39,1005,1006],{"class":94},"        wb ",[39,1008,110],{"class":90},[39,1010,1011],{"class":94}," FakeWorkbook(",[39,1013,720],{"class":116},[39,1015,1016],{"class":94},".app, path, ",[39,1018,997],{"class":90},[39,1020,720],{"class":116},[39,1022,1023],{"class":94},".app.books[path])\n",[39,1025,1027,1029],{"class":41,"line":1026},37,[39,1028,641],{"class":116},[39,1030,1031],{"class":94},".app.opened.append(wb)\n",[39,1033,1035,1037],{"class":41,"line":1034},38,[39,1036,285],{"class":90},[39,1038,1039],{"class":94}," wb\n",[39,1041,1043],{"class":41,"line":1042},39,[39,1044,101],{"emptyLinePlaceholder":100},[39,1046,1048,1050,1053],{"class":41,"line":1047},40,[39,1049,620],{"class":90},[39,1051,1052],{"class":45}," FakeExcel",[39,1054,200],{"class":94},[39,1056,1058,1060,1062],{"class":41,"line":1057},41,[39,1059,630],{"class":90},[39,1061,633],{"class":116},[39,1063,1064],{"class":94},"(self, books):\n",[39,1066,1068,1070,1073,1075,1078,1080,1083,1085],{"class":41,"line":1067},42,[39,1069,641],{"class":116},[39,1071,1072],{"class":94},".books, ",[39,1074,720],{"class":116},[39,1076,1077],{"class":94},".opened, ",[39,1079,720],{"class":116},[39,1081,1082],{"class":94},".calls ",[39,1084,110],{"class":90},[39,1086,1087],{"class":94}," books, [], []\n",[39,1089,1091,1093,1096,1098,1101,1103],{"class":41,"line":1090},43,[39,1092,641],{"class":116},[39,1094,1095],{"class":94},".Workbooks ",[39,1097,110],{"class":90},[39,1099,1100],{"class":94}," FakeWorkbooks(",[39,1102,720],{"class":116},[39,1104,120],{"class":94},[39,1106,1108,1110,1113,1115],{"class":41,"line":1107},44,[39,1109,641],{"class":116},[39,1111,1112],{"class":94},".DisplayAlerts ",[39,1114,110],{"class":90},[39,1116,901],{"class":116},[39,1118,1120,1122,1125,1127],{"class":41,"line":1119},45,[39,1121,641],{"class":116},[39,1123,1124],{"class":94},".quit_called ",[39,1126,110],{"class":90},[39,1128,352],{"class":116},[39,1130,1132],{"class":41,"line":1131},46,[39,1133,101],{"emptyLinePlaceholder":100},[39,1135,1137,1139,1142],{"class":41,"line":1136},47,[39,1138,630],{"class":90},[39,1140,1141],{"class":134}," CalculateUntilAsyncQueriesDone",[39,1143,783],{"class":94},[39,1145,1147,1149,1152,1155],{"class":41,"line":1146},48,[39,1148,641],{"class":116},[39,1150,1151],{"class":94},".calls.append((",[39,1153,1154],{"class":49},"\"CalculateUntilAsyncQueriesDone\"",[39,1156,1157],{"class":94},",))\n",[39,1159,1161],{"class":41,"line":1160},49,[39,1162,101],{"emptyLinePlaceholder":100},[39,1164,1166,1168,1171],{"class":41,"line":1165},50,[39,1167,630],{"class":90},[39,1169,1170],{"class":134}," Quit",[39,1172,783],{"class":94},[39,1174,1176,1178,1180,1182],{"class":41,"line":1175},51,[39,1177,641],{"class":116},[39,1179,1124],{"class":94},[39,1181,110],{"class":90},[39,1183,901],{"class":116},[10,1185,1186,1187,1190,1191,1194,1195,1198,1199,1202,1203,1206],{},"Plain classes are clearer than ",[14,1188,1189],{},"unittest.mock.MagicMock"," here. A ",[14,1192,1193],{},"MagicMock"," accepts ",[75,1196,1197],{},"any"," attribute and call, so a typo such as ",[14,1200,1201],{},"wb.RefreshALL()"," passes silently; a fake raises ",[14,1204,1205],{},"AttributeError",", just as the real object would fail. The fake's books are configured per test: which named values exist and whether refreshing should fail with a COM-style error.",[25,1208,1210],{"id":1209},"step-3-test-the-happy-path-and-the-clean-up-path","Step 3 — Test the happy path and the clean-up path",[30,1212,1214],{"className":81,"code":1213,"language":83,"meta":35,"style":35},"import pytest\nfrom jobs.refresh import run\nfrom tests.fakes import FakeExcel\n\ndef test_refreshes_saves_closes_and_quits():\n    excel = FakeExcel({\"a.xlsx\": {\"names\": {\"GrandTotal\": 1250.5}}, \"b.xlsx\": {\"names\": {\"GrandTotal\": 80}}})\n    result = run([\"a.xlsx\", \"b.xlsx\"], make_app=lambda: excel)\n    assert result == {\"a.xlsx\": 1250.5, \"b.xlsx\": 80.0}\n    assert [c[0] for c in excel.calls] == [\"RefreshAll\", \"CalculateUntilAsyncQueriesDone\"] * 2\n    assert all(wb.saved and wb.closed for wb in excel.opened)\n    assert excel.DisplayAlerts is False and excel.quit_called\n\ndef test_failed_refresh_closes_without_saving_and_quits():\n    excel = FakeExcel({\"a.xlsx\": {\"names\": {\"GrandTotal\": 1}, \"fail_refresh\": True}})\n    with pytest.raises(RuntimeError, match=\"connection failed\"):\n        run([\"a.xlsx\"], make_app=lambda: excel)\n    wb = excel.opened[0]\n    assert wb.closed and wb.close_args == {\"SaveChanges\": False} and not wb.saved\n    assert excel.quit_called\n\ndef test_empty_total_is_an_error_not_a_zero():\n    excel = FakeExcel({\"a.xlsx\": {\"names\": {\"GrandTotal\": None}}})\n    with pytest.raises(ValueError, match=\"GrandTotal is empty\"):\n        run([\"a.xlsx\"], make_app=lambda: excel)\n    assert excel.opened[0].closed and excel.quit_called\n",[14,1215,1216,1223,1236,1248,1252,1262,1313,1339,1369,1410,1436,1454,1458,1467,1504,1527,1542,1556,1588,1594,1598,1607,1632,1652,1666],{"__ignoreMap":35},[39,1217,1218,1220],{"class":41,"line":42},[39,1219,91],{"class":90},[39,1221,1222],{"class":94}," pytest\n",[39,1224,1225,1228,1231,1233],{"class":41,"line":59},[39,1226,1227],{"class":90},"from",[39,1229,1230],{"class":94}," jobs.refresh ",[39,1232,91],{"class":90},[39,1234,1235],{"class":94}," run\n",[39,1237,1238,1240,1243,1245],{"class":41,"line":104},[39,1239,1227],{"class":90},[39,1241,1242],{"class":94}," tests.fakes ",[39,1244,91],{"class":90},[39,1246,1247],{"class":94}," FakeExcel\n",[39,1249,1250],{"class":41,"line":123},[39,1251,101],{"emptyLinePlaceholder":100},[39,1253,1254,1256,1259],{"class":41,"line":128},[39,1255,131],{"class":90},[39,1257,1258],{"class":134}," test_refreshes_saves_closes_and_quits",[39,1260,1261],{"class":94},"():\n",[39,1263,1264,1267,1269,1272,1275,1278,1281,1283,1285,1288,1291,1294,1297,1299,1301,1303,1305,1307,1310],{"class":41,"line":149},[39,1265,1266],{"class":94},"    excel ",[39,1268,110],{"class":90},[39,1270,1271],{"class":94}," FakeExcel({",[39,1273,1274],{"class":49},"\"a.xlsx\"",[39,1276,1277],{"class":94},": {",[39,1279,1280],{"class":49},"\"names\"",[39,1282,1277],{"class":94},[39,1284,143],{"class":49},[39,1286,1287],{"class":94},": ",[39,1289,1290],{"class":116},"1250.5",[39,1292,1293],{"class":94},"}}, ",[39,1295,1296],{"class":49},"\"b.xlsx\"",[39,1298,1277],{"class":94},[39,1300,1280],{"class":49},[39,1302,1277],{"class":94},[39,1304,143],{"class":49},[39,1306,1287],{"class":94},[39,1308,1309],{"class":116},"80",[39,1311,1312],{"class":94},"}}})\n",[39,1314,1315,1318,1320,1323,1325,1327,1329,1331,1333,1336],{"class":41,"line":155},[39,1316,1317],{"class":94},"    result ",[39,1319,110],{"class":90},[39,1321,1322],{"class":94}," run([",[39,1324,1274],{"class":49},[39,1326,181],{"class":94},[39,1328,1296],{"class":49},[39,1330,447],{"class":94},[39,1332,468],{"class":172},[39,1334,1335],{"class":90},"=lambda",[39,1337,1338],{"class":94},": excel)\n",[39,1340,1341,1344,1347,1350,1352,1354,1356,1358,1360,1362,1364,1366],{"class":41,"line":194},[39,1342,1343],{"class":90},"    assert",[39,1345,1346],{"class":94}," result ",[39,1348,1349],{"class":90},"==",[39,1351,367],{"class":94},[39,1353,1274],{"class":49},[39,1355,1287],{"class":94},[39,1357,1290],{"class":116},[39,1359,181],{"class":94},[39,1361,1296],{"class":49},[39,1363,1287],{"class":94},[39,1365,541],{"class":116},[39,1367,1368],{"class":94},"}\n",[39,1370,1371,1373,1376,1378,1381,1383,1386,1388,1391,1393,1396,1398,1400,1402,1404,1407],{"class":41,"line":203},[39,1372,1343],{"class":90},[39,1374,1375],{"class":94}," [c[",[39,1377,178],{"class":116},[39,1379,1380],{"class":94},"] ",[39,1382,375],{"class":90},[39,1384,1385],{"class":94}," c ",[39,1387,381],{"class":90},[39,1389,1390],{"class":94}," excel.calls] ",[39,1392,1349],{"class":90},[39,1394,1395],{"class":94}," [",[39,1397,793],{"class":49},[39,1399,181],{"class":94},[39,1401,1154],{"class":49},[39,1403,1380],{"class":94},[39,1405,1406],{"class":90},"*",[39,1408,1409],{"class":116}," 2\n",[39,1411,1412,1414,1417,1420,1423,1426,1428,1431,1433],{"class":41,"line":209},[39,1413,1343],{"class":90},[39,1415,1416],{"class":116}," all",[39,1418,1419],{"class":94},"(wb.saved ",[39,1421,1422],{"class":90},"and",[39,1424,1425],{"class":94}," wb.closed ",[39,1427,375],{"class":90},[39,1429,1430],{"class":94}," wb ",[39,1432,381],{"class":90},[39,1434,1435],{"class":94}," excel.opened)\n",[39,1437,1438,1440,1443,1445,1448,1451],{"class":41,"line":215},[39,1439,1343],{"class":90},[39,1441,1442],{"class":94}," excel.DisplayAlerts ",[39,1444,235],{"class":90},[39,1446,1447],{"class":116}," False",[39,1449,1450],{"class":90}," and",[39,1452,1453],{"class":94}," excel.quit_called\n",[39,1455,1456],{"class":41,"line":226},[39,1457,101],{"emptyLinePlaceholder":100},[39,1459,1460,1462,1465],{"class":41,"line":243},[39,1461,131],{"class":90},[39,1463,1464],{"class":134}," test_failed_refresh_closes_without_saving_and_quits",[39,1466,1261],{"class":94},[39,1468,1469,1471,1473,1475,1477,1479,1481,1483,1485,1487,1490,1493,1496,1498,1501],{"class":41,"line":276},[39,1470,1266],{"class":94},[39,1472,110],{"class":90},[39,1474,1271],{"class":94},[39,1476,1274],{"class":49},[39,1478,1277],{"class":94},[39,1480,1280],{"class":49},[39,1482,1277],{"class":94},[39,1484,143],{"class":49},[39,1486,1287],{"class":94},[39,1488,1489],{"class":116},"1",[39,1491,1492],{"class":94},"}, ",[39,1494,1495],{"class":49},"\"fail_refresh\"",[39,1497,1287],{"class":94},[39,1499,1500],{"class":116},"True",[39,1502,1503],{"class":94},"}})\n",[39,1505,1506,1509,1512,1515,1517,1520,1522,1525],{"class":41,"line":282},[39,1507,1508],{"class":90},"    with",[39,1510,1511],{"class":94}," pytest.raises(",[39,1513,1514],{"class":116},"RuntimeError",[39,1516,181],{"class":94},[39,1518,1519],{"class":172},"match",[39,1521,110],{"class":90},[39,1523,1524],{"class":49},"\"connection failed\"",[39,1526,146],{"class":94},[39,1528,1529,1532,1534,1536,1538,1540],{"class":41,"line":294},[39,1530,1531],{"class":94},"        run([",[39,1533,1274],{"class":49},[39,1535,447],{"class":94},[39,1537,468],{"class":172},[39,1539,1335],{"class":90},[39,1541,1338],{"class":94},[39,1543,1544,1546,1548,1551,1553],{"class":41,"line":302},[39,1545,158],{"class":94},[39,1547,110],{"class":90},[39,1549,1550],{"class":94}," excel.opened[",[39,1552,178],{"class":116},[39,1554,1555],{"class":94},"]\n",[39,1557,1558,1560,1562,1564,1567,1569,1571,1573,1575,1577,1580,1582,1585],{"class":41,"line":317},[39,1559,1343],{"class":90},[39,1561,1425],{"class":94},[39,1563,1422],{"class":90},[39,1565,1566],{"class":94}," wb.close_args ",[39,1568,1349],{"class":90},[39,1570,367],{"class":94},[39,1572,937],{"class":49},[39,1574,1287],{"class":94},[39,1576,189],{"class":116},[39,1578,1579],{"class":94},"} ",[39,1581,1422],{"class":90},[39,1583,1584],{"class":90}," not",[39,1586,1587],{"class":94}," wb.saved\n",[39,1589,1590,1592],{"class":41,"line":322},[39,1591,1343],{"class":90},[39,1593,1453],{"class":94},[39,1595,1596],{"class":41,"line":333},[39,1597,101],{"emptyLinePlaceholder":100},[39,1599,1600,1602,1605],{"class":41,"line":344},[39,1601,131],{"class":90},[39,1603,1604],{"class":134}," test_empty_total_is_an_error_not_a_zero",[39,1606,1261],{"class":94},[39,1608,1609,1611,1613,1615,1617,1619,1621,1623,1625,1627,1630],{"class":41,"line":355},[39,1610,1266],{"class":94},[39,1612,110],{"class":90},[39,1614,1271],{"class":94},[39,1616,1274],{"class":49},[39,1618,1277],{"class":94},[39,1620,1280],{"class":49},[39,1622,1277],{"class":94},[39,1624,143],{"class":49},[39,1626,1287],{"class":94},[39,1628,1629],{"class":116},"None",[39,1631,1312],{"class":94},[39,1633,1634,1636,1638,1641,1643,1645,1647,1650],{"class":41,"line":362},[39,1635,1508],{"class":90},[39,1637,1511],{"class":94},[39,1639,1640],{"class":116},"ValueError",[39,1642,181],{"class":94},[39,1644,1519],{"class":172},[39,1646,110],{"class":90},[39,1648,1649],{"class":49},"\"GrandTotal is empty\"",[39,1651,146],{"class":94},[39,1653,1654,1656,1658,1660,1662,1664],{"class":41,"line":387},[39,1655,1531],{"class":94},[39,1657,1274],{"class":49},[39,1659,447],{"class":94},[39,1661,468],{"class":172},[39,1663,1335],{"class":90},[39,1665,1338],{"class":94},[39,1667,1668,1670,1672,1674,1677,1679],{"class":41,"line":394},[39,1669,1343],{"class":90},[39,1671,1550],{"class":94},[39,1673,178],{"class":116},[39,1675,1676],{"class":94},"].closed ",[39,1678,1422],{"class":90},[39,1680,1453],{"class":94},[30,1682,1684],{"className":81,"code":1683,"language":83,"meta":35,"style":35},"import subprocess, sys\n\ndef pytest_run(*args):\n    out = subprocess.run([sys.executable, \"-m\", \"pytest\", \"-q\", \"-p\", \"no:cacheprovider\", *args], capture_output=True, text=True).stdout\n    print(\"\\n\".join(l for l in out.splitlines() if \"passed\" in l or \"failed\" in l or \"skipped\" in l or l.startswith(\"FAILED\")))\n\npytest_run(\"tests\u002Ftest_refresh.py\")\n",[14,1685,1686,1693,1697,1711,1769,1833,1837],{"__ignoreMap":35},[39,1687,1688,1690],{"class":41,"line":42},[39,1689,91],{"class":90},[39,1691,1692],{"class":94}," subprocess, sys\n",[39,1694,1695],{"class":41,"line":59},[39,1696,101],{"emptyLinePlaceholder":100},[39,1698,1699,1701,1704,1706,1708],{"class":41,"line":104},[39,1700,131],{"class":90},[39,1702,1703],{"class":134}," pytest_run",[39,1705,252],{"class":94},[39,1707,1406],{"class":90},[39,1709,1710],{"class":94},"args):\n",[39,1712,1713,1716,1718,1721,1724,1726,1729,1731,1734,1736,1739,1741,1744,1746,1748,1751,1754,1756,1758,1760,1762,1764,1766],{"class":41,"line":123},[39,1714,1715],{"class":94},"    out ",[39,1717,110],{"class":90},[39,1719,1720],{"class":94}," subprocess.run([sys.executable, ",[39,1722,1723],{"class":49},"\"-m\"",[39,1725,181],{"class":94},[39,1727,1728],{"class":49},"\"pytest\"",[39,1730,181],{"class":94},[39,1732,1733],{"class":49},"\"-q\"",[39,1735,181],{"class":94},[39,1737,1738],{"class":49},"\"-p\"",[39,1740,181],{"class":94},[39,1742,1743],{"class":49},"\"no:cacheprovider\"",[39,1745,181],{"class":94},[39,1747,1406],{"class":90},[39,1749,1750],{"class":94},"args], ",[39,1752,1753],{"class":172},"capture_output",[39,1755,110],{"class":90},[39,1757,1500],{"class":116},[39,1759,181],{"class":94},[39,1761,528],{"class":172},[39,1763,110],{"class":90},[39,1765,1500],{"class":116},[39,1767,1768],{"class":94},").stdout\n",[39,1770,1771,1773,1775,1777,1780,1782,1785,1787,1790,1792,1795,1797,1800,1802,1804,1807,1810,1812,1814,1816,1819,1821,1823,1825,1828,1831],{"class":41,"line":128},[39,1772,438],{"class":116},[39,1774,252],{"class":94},[39,1776,258],{"class":49},[39,1778,1779],{"class":261},"\\n",[39,1781,258],{"class":49},[39,1783,1784],{"class":94},".join(l ",[39,1786,375],{"class":90},[39,1788,1789],{"class":94}," l ",[39,1791,381],{"class":90},[39,1793,1794],{"class":94}," out.splitlines() ",[39,1796,408],{"class":90},[39,1798,1799],{"class":49}," \"passed\"",[39,1801,850],{"class":90},[39,1803,1789],{"class":94},[39,1805,1806],{"class":90},"or",[39,1808,1809],{"class":49}," \"failed\"",[39,1811,850],{"class":90},[39,1813,1789],{"class":94},[39,1815,1806],{"class":90},[39,1817,1818],{"class":49}," \"skipped\"",[39,1820,850],{"class":90},[39,1822,1789],{"class":94},[39,1824,1806],{"class":90},[39,1826,1827],{"class":94}," l.startswith(",[39,1829,1830],{"class":49},"\"FAILED\"",[39,1832,459],{"class":94},[39,1834,1835],{"class":41,"line":149},[39,1836,101],{"emptyLinePlaceholder":100},[39,1838,1839,1842,1845],{"class":41,"line":155},[39,1840,1841],{"class":94},"pytest_run(",[39,1843,1844],{"class":49},"\"tests\u002Ftest_refresh.py\"",[39,1846,120],{"class":94},[10,1848,1849,1850,1853],{},"All three tests pass in well under a second, on a machine without Excel. The second test is the one that pays for the effort: a refresh failure — simulated with the kind of message a COM error carries — still closes the workbook ",[75,1851,1852],{},"without saving"," and still quits Excel. The third makes sure an empty named cell is reported as an error rather than quietly becoming a total of zero. These are the behaviours that, untested, produce hung processes and wrong numbers in production.",[492,1855,501,1861,501,1864,501,1867,501,1872,501,1878,501,1889,501,1895,501,1901,501,1905,501,1909,501,1913,501,1917,501,1920,501,1927,501,1931,501,1935,501,1938,501,1941,501,1944],{"viewBox":1856,"role":495,"ariaLabelledBy":1857,"xmlns":499,"style":1860},"4 0 752 233",[1858,1859],"wx-fake-t","wx-fake-d","width:100%;max-width:752px;height:auto;display:block;margin:1.5rem auto;font-family:Inter,ui-sans-serif,system-ui,sans-serif",[503,1862,1863],{"id":1858},"A recording fake or a MagicMock",[507,1865,1866],{"id":1859},"A small fake class defines only the attributes the code uses, so typos fail like they would against Excel, and it records calls for assertions. A MagicMock accepts any attribute or call, so misspelled method names pass silently.",[511,1868],{"x":1869,"y":178,"width":1870,"height":1871,"fill":516},"4","752","233",[511,1873],{"x":1874,"y":520,"width":1875,"height":1876,"rx":1877,"fill":552,"stroke":553,"style":526},"20.0","351.0","201.0","14",[1879,1880,1882],"g",{"transform":1881},"translate(182.5,29.0) scale(1.083)",[1883,1884],"path",{"d":1885,"fill":1886,"stroke":1887,"style":1888},"M5 12.5l4.5 4.5L19 7","none","var(--teal-ink,#0b6157)","stroke-width:1.66px;stroke-linecap:round;stroke-linejoin:round",[528,1890,1894],{"x":1891,"y":1892,"style":1893},"195.5","78.0","font-size:14px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","Recording fake",[41,1896],{"x1":1897,"y1":1898,"x2":1899,"y2":1898,"stroke":553,"style":1900},"36.0","91.0","355.0","stroke-width:1px",[528,1902,1904],{"x":1891,"y":551,"style":1903},"font-size:12px;font-weight:400;fill:var(--text,#172033);text-anchor:middle","only real attribute names",[528,1906,1908],{"x":1891,"y":1907,"style":1903},"137.0","typos raise AttributeError",[528,1910,1912],{"x":1891,"y":1911,"style":1903},"163.0","simulates COM errors",[528,1914,1916],{"x":1891,"y":1915,"style":1903},"189.0","records calls and saves",[511,1918],{"x":1919,"y":520,"width":1875,"height":1876,"rx":1877,"fill":593,"stroke":594,"style":526},"389.0",[1879,1921,1923],{"transform":1922},"translate(551.5,29.0) scale(1.083)",[1883,1924],{"d":1925,"fill":1886,"stroke":1926,"style":1888},"M12 3.5l9.5 17h-19z M12 10v5 M12 17.8v.4","var(--gold-ink,#7a4e06)",[528,1928,1193],{"x":1929,"y":1892,"style":1930},"564.5","font-size:14px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle",[41,1932],{"x1":1933,"y1":1898,"x2":1934,"y2":1898,"stroke":594,"style":1900},"405.0","724.0",[528,1936,1937],{"x":1929,"y":551,"style":1903},"accepts any attribute",[528,1939,1940],{"x":1929,"y":1907,"style":1903},"wb.RefreshALL() passes",[528,1942,1943],{"x":1929,"y":1911,"style":1903},"quick to write",[528,1945,1946],{"x":1929,"y":1915,"style":1903},"assertions on call lists",[492,1948,501,1953,501,1956,501,1959,501,1962,501,1966,501,1972,501,1975,501,1979,501,1981,501,1985,501,1990,501,1992,501,1994,501,1999,501,2002,501,2009,501,2015,501,2020,501,2023,501,2028,501,2033,501,2036,501,2039,501,2044,501,2048,501,2052,501,2055,501,2060,501,2065,501,2068,501,2071,501,2076,501,2081,501,2086,501,2089,501,2094],{"viewBox":1949,"role":495,"ariaLabelledBy":1950,"xmlns":499,"style":1860},"4 0 752 388",[1951,1952],"wx-fail-t","wx-fail-d",[503,1954,1955],{"id":1951},"What the failed-refresh test checks",[507,1957,1958],{"id":1952},"The test passes a fake Excel to run. The job opens the workbook and calls RefreshAll, which the fake makes fail. The job then closes the workbook without saving and quits Excel before the error reaches the test, which asserts all three.",[511,1960],{"x":1869,"y":178,"width":1870,"height":1961,"fill":516},"388",[511,1963],{"x":1874,"y":520,"width":1964,"height":1965,"rx":523,"fill":573,"stroke":543,"style":526},"180.0","48.0",[528,1967,1971],{"x":1968,"y":1969,"style":1970},"110.0","44.5","font-size:13px;font-weight:700;fill:var(--brand-strong,#4338ca);text-anchor:middle","Test",[511,1973],{"x":1974,"y":520,"width":1964,"height":1965,"rx":523,"fill":524,"stroke":525,"style":526},"290.0",[528,1976,1978],{"x":530,"y":1969,"style":1977},"font-size:13px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","run()",[511,1980],{"x":521,"y":520,"width":1964,"height":1965,"rx":523,"fill":524,"stroke":525,"style":526},[528,1982,1984],{"x":1983,"y":1969,"style":1977},"650.0","FakeExcel",[41,1986],{"x1":1968,"y1":1987,"x2":1968,"y2":1988,"stroke":525,"style":1989},"64.0","372.0","stroke-width:1.5px;stroke-dasharray:5 4",[41,1991],{"x1":530,"y1":1987,"x2":530,"y2":1988,"stroke":525,"style":1989},[41,1993],{"x1":1983,"y1":1987,"x2":1983,"y2":1988,"stroke":525,"style":1989},[41,1995],{"x1":1996,"y1":1997,"x2":1998,"y2":1997,"stroke":543,"style":526},"115.0","104.0","371.0",[545,2000],{"points":2001,"fill":548},"375.0,104.0 366.0,109.0 366.0,99.0",[511,2003],{"x":2004,"y":2005,"width":565,"height":2006,"rx":2007,"fill":516,"stroke":525,"style":2008},"157.5","75.0","21.0","10","stroke-width:1.2px",[528,2010,2014],{"x":2011,"y":2012,"style":2013},"245.0","89.5","font-size:11.5px;font-weight:700;fill:var(--text,#172033);text-anchor:middle","1. make_app=lambda: fake",[41,2016],{"x1":2017,"y1":2018,"x2":2019,"y2":2018,"stroke":543,"style":526},"385.0","152.0","641.0",[545,2021],{"points":2022,"fill":548},"645.0,152.0 636.0,157.0 636.0,147.0",[511,2024],{"x":2025,"y":2026,"width":2027,"height":2006,"rx":2007,"fill":516,"stroke":525,"style":2008},"424.8","123.0","180.4",[528,2029,2032],{"x":2030,"y":2031,"style":2013},"515.0","137.5","2. Workbooks.Open(a.xlsx)",[41,2034],{"x1":2017,"y1":2035,"x2":2019,"y2":2035,"stroke":543,"style":526},"200.0",[545,2037],{"points":2038,"fill":548},"645.0,200.0 636.0,205.0 636.0,195.0",[511,2040],{"x":2041,"y":2042,"width":2043,"height":2006,"rx":2007,"fill":516,"stroke":525,"style":2008},"467.7","171.0","94.6",[528,2045,2047],{"x":2030,"y":2046,"style":2013},"185.5","3. RefreshAll",[41,2049],{"x1":2050,"y1":2051,"x2":1919,"y2":2051,"stroke":543,"style":526},"645.0","248.0",[545,2053],{"points":2054,"fill":548},"385.0,248.0 394.0,243.0 394.0,253.0",[511,2056],{"x":2057,"y":2058,"width":2059,"height":2006,"rx":2007,"fill":593,"stroke":594,"style":2008},"450.3","219.0","129.3",[528,2061,2064],{"x":2030,"y":2062,"style":2063},"233.5","font-size:11.5px;font-weight:700;fill:var(--gold-ink,#7a4e06);text-anchor:middle","4. COM-style error",[41,2066],{"x1":2017,"y1":2067,"x2":2019,"y2":2067,"stroke":543,"style":526},"296.0",[545,2069],{"points":2070,"fill":548},"645.0,296.0 636.0,301.0 636.0,291.0",[511,2072],{"x":2073,"y":2074,"width":2075,"height":2006,"rx":2007,"fill":552,"stroke":553,"style":2008},"401.0","267.0","228.1",[528,2077,2080],{"x":2030,"y":2078,"style":2079},"281.5","font-size:11.5px;font-weight:700;fill:var(--teal-ink,#0b6157);text-anchor:middle","5. Close(SaveChanges=False), Quit",[41,2082],{"x1":2083,"y1":2084,"x2":2085,"y2":2084,"stroke":543,"style":526},"375.0","344.0","119.0",[545,2087],{"points":2088,"fill":548},"115.0,344.0 124.0,339.0 124.0,349.0",[511,2090],{"x":2091,"y":2092,"width":2093,"height":2006,"rx":2007,"fill":516,"stroke":525,"style":2008},"185.3","315.0","119.5",[528,2095,2097],{"x":2011,"y":2096,"style":2013},"329.5","6. error re-raised",[25,2099,2101],{"id":2100},"step-4-xlwings-code-and-older-code-that-imports-win32com","Step 4 — xlwings code and older code that imports win32com",[10,2103,2104,2105,2108],{},"xlwings code is tested the same way. Pass the book in, and fake the parts of the xlwings API the code uses — here ",[14,2106,2107],{},"sheet.range(...).options(...).value",":",[30,2110,2112],{"className":81,"code":2111,"language":83,"meta":35,"style":35},"import pandas as pd\n\ndef write_summary(book, df):\n    sheet = book.sheets[\"Summary\"]\n    sheet.range(\"A1\").options(index=False).value = df.groupby(\"Region\", as_index=False)[\"Amount\"].sum()\n    sheet.range(\"E1\").value = \"Total\"\n    sheet.range(\"F1\").value = float(df[\"Amount\"].sum())\n",[14,2113,2114,2127,2131,2141,2156,2203,2217],{"__ignoreMap":35},[39,2115,2116,2118,2121,2124],{"class":41,"line":42},[39,2117,91],{"class":90},[39,2119,2120],{"class":94}," pandas ",[39,2122,2123],{"class":90},"as",[39,2125,2126],{"class":94}," pd\n",[39,2128,2129],{"class":41,"line":59},[39,2130,101],{"emptyLinePlaceholder":100},[39,2132,2133,2135,2138],{"class":41,"line":104},[39,2134,131],{"class":90},[39,2136,2137],{"class":134}," write_summary",[39,2139,2140],{"class":94},"(book, df):\n",[39,2142,2143,2146,2148,2151,2154],{"class":41,"line":123},[39,2144,2145],{"class":94},"    sheet ",[39,2147,110],{"class":90},[39,2149,2150],{"class":94}," book.sheets[",[39,2152,2153],{"class":49},"\"Summary\"",[39,2155,1555],{"class":94},[39,2157,2158,2161,2164,2167,2170,2172,2174,2177,2179,2182,2185,2187,2190,2192,2194,2197,2200],{"class":41,"line":128},[39,2159,2160],{"class":94},"    sheet.range(",[39,2162,2163],{"class":49},"\"A1\"",[39,2165,2166],{"class":94},").options(",[39,2168,2169],{"class":172},"index",[39,2171,110],{"class":90},[39,2173,189],{"class":116},[39,2175,2176],{"class":94},").value ",[39,2178,110],{"class":90},[39,2180,2181],{"class":94}," df.groupby(",[39,2183,2184],{"class":49},"\"Region\"",[39,2186,181],{"class":94},[39,2188,2189],{"class":172},"as_index",[39,2191,110],{"class":90},[39,2193,189],{"class":116},[39,2195,2196],{"class":94},")[",[39,2198,2199],{"class":49},"\"Amount\"",[39,2201,2202],{"class":94},"].sum()\n",[39,2204,2205,2207,2210,2212,2214],{"class":41,"line":149},[39,2206,2160],{"class":94},[39,2208,2209],{"class":49},"\"E1\"",[39,2211,2176],{"class":94},[39,2213,110],{"class":90},[39,2215,2216],{"class":49}," \"Total\"\n",[39,2218,2219,2221,2224,2226,2228,2230,2233,2235],{"class":41,"line":155},[39,2220,2160],{"class":94},[39,2222,2223],{"class":49},"\"F1\"",[39,2225,2176],{"class":94},[39,2227,110],{"class":90},[39,2229,288],{"class":116},[39,2231,2232],{"class":94},"(df[",[39,2234,2199],{"class":49},[39,2236,2237],{"class":94},"].sum())\n",[30,2239,2241],{"className":81,"code":2240,"language":83,"meta":35,"style":35},"import pandas as pd\nfrom jobs.xw_report import write_summary\n\nclass FakeXwRange:\n    def __init__(self, sheet, address):\n        self.sheet, self.address, self.opts = sheet, address, {}\n    def options(self, **opts):\n        self.opts = opts\n        return self\n    @property\n    def value(self):\n        return self.sheet.cells.get(self.address)\n    @value.setter\n    def value(self, v):\n        self.sheet.cells[self.address] = (v, self.opts)\n\nclass FakeXwSheet:\n    def __init__(self):\n        self.cells = {}\n    def range(self, address):\n        return FakeXwRange(self, address)\n\nclass FakeBook:\n    def __init__(self):\n        self.sheets = {\"Summary\": FakeXwSheet()}\n\ndef test_summary_written_without_index():\n    book = FakeBook()\n    df = pd.DataFrame({\"Region\": [\"N\", \"S\", \"N\"], \"Amount\": [10.0, 5.0, 2.5]})\n    write_summary(book, df)\n    table, opts = book.sheets[\"Summary\"].cells[\"A1\"]\n    assert opts == {\"index\": False}\n    assert table.to_dict(\"list\") == {\"Region\": [\"N\", \"S\"], \"Amount\": [12.5, 5.0]}\n    assert book.sheets[\"Summary\"].cells[\"F1\"][0] == 17.5\n",[14,2242,2243,2253,2265,2269,2278,2287,2309,2324,2335,2342,2350,2359,2373,2378,2387,2409,2413,2422,2430,2442,2452,2464,2468,2477,2485,2501,2505,2514,2524,2573,2578,2596,2616,2659],{"__ignoreMap":35},[39,2244,2245,2247,2249,2251],{"class":41,"line":42},[39,2246,91],{"class":90},[39,2248,2120],{"class":94},[39,2250,2123],{"class":90},[39,2252,2126],{"class":94},[39,2254,2255,2257,2260,2262],{"class":41,"line":59},[39,2256,1227],{"class":90},[39,2258,2259],{"class":94}," jobs.xw_report ",[39,2261,91],{"class":90},[39,2263,2264],{"class":94}," write_summary\n",[39,2266,2267],{"class":41,"line":104},[39,2268,101],{"emptyLinePlaceholder":100},[39,2270,2271,2273,2276],{"class":41,"line":123},[39,2272,620],{"class":90},[39,2274,2275],{"class":45}," FakeXwRange",[39,2277,200],{"class":94},[39,2279,2280,2282,2284],{"class":41,"line":128},[39,2281,630],{"class":90},[39,2283,633],{"class":116},[39,2285,2286],{"class":94},"(self, sheet, address):\n",[39,2288,2289,2291,2294,2296,2299,2301,2304,2306],{"class":41,"line":149},[39,2290,641],{"class":116},[39,2292,2293],{"class":94},".sheet, ",[39,2295,720],{"class":116},[39,2297,2298],{"class":94},".address, ",[39,2300,720],{"class":116},[39,2302,2303],{"class":94},".opts ",[39,2305,110],{"class":90},[39,2307,2308],{"class":94}," sheet, address, {}\n",[39,2310,2311,2313,2316,2319,2321],{"class":41,"line":155},[39,2312,630],{"class":90},[39,2314,2315],{"class":134}," options",[39,2317,2318],{"class":94},"(self, ",[39,2320,997],{"class":90},[39,2322,2323],{"class":94},"opts):\n",[39,2325,2326,2328,2330,2332],{"class":41,"line":194},[39,2327,641],{"class":116},[39,2329,2303],{"class":94},[39,2331,110],{"class":90},[39,2333,2334],{"class":94}," opts\n",[39,2336,2337,2339],{"class":41,"line":203},[39,2338,285],{"class":90},[39,2340,2341],{"class":116}," self\n",[39,2343,2344,2347],{"class":41,"line":209},[39,2345,2346],{"class":134},"    @",[39,2348,2349],{"class":116},"property\n",[39,2351,2352,2354,2357],{"class":41,"line":215},[39,2353,630],{"class":90},[39,2355,2356],{"class":134}," value",[39,2358,783],{"class":94},[39,2360,2361,2363,2365,2368,2370],{"class":41,"line":226},[39,2362,285],{"class":90},[39,2364,750],{"class":116},[39,2366,2367],{"class":94},".sheet.cells.get(",[39,2369,720],{"class":116},[39,2371,2372],{"class":94},".address)\n",[39,2374,2375],{"class":41,"line":243},[39,2376,2377],{"class":134},"    @value.setter\n",[39,2379,2380,2382,2384],{"class":41,"line":276},[39,2381,630],{"class":90},[39,2383,2356],{"class":134},[39,2385,2386],{"class":94},"(self, v):\n",[39,2388,2389,2391,2394,2396,2399,2401,2404,2406],{"class":41,"line":282},[39,2390,641],{"class":116},[39,2392,2393],{"class":94},".sheet.cells[",[39,2395,720],{"class":116},[39,2397,2398],{"class":94},".address] ",[39,2400,110],{"class":90},[39,2402,2403],{"class":94}," (v, ",[39,2405,720],{"class":116},[39,2407,2408],{"class":94},".opts)\n",[39,2410,2411],{"class":41,"line":294},[39,2412,101],{"emptyLinePlaceholder":100},[39,2414,2415,2417,2420],{"class":41,"line":302},[39,2416,620],{"class":90},[39,2418,2419],{"class":45}," FakeXwSheet",[39,2421,200],{"class":94},[39,2423,2424,2426,2428],{"class":41,"line":317},[39,2425,630],{"class":90},[39,2427,633],{"class":116},[39,2429,783],{"class":94},[39,2431,2432,2434,2437,2439],{"class":41,"line":322},[39,2433,641],{"class":116},[39,2435,2436],{"class":94},".cells ",[39,2438,110],{"class":90},[39,2440,2441],{"class":94}," {}\n",[39,2443,2444,2446,2449],{"class":41,"line":333},[39,2445,630],{"class":90},[39,2447,2448],{"class":116}," range",[39,2450,2451],{"class":94},"(self, address):\n",[39,2453,2454,2456,2459,2461],{"class":41,"line":344},[39,2455,285],{"class":90},[39,2457,2458],{"class":94}," FakeXwRange(",[39,2460,720],{"class":116},[39,2462,2463],{"class":94},", address)\n",[39,2465,2466],{"class":41,"line":355},[39,2467,101],{"emptyLinePlaceholder":100},[39,2469,2470,2472,2475],{"class":41,"line":362},[39,2471,620],{"class":90},[39,2473,2474],{"class":45}," FakeBook",[39,2476,200],{"class":94},[39,2478,2479,2481,2483],{"class":41,"line":387},[39,2480,630],{"class":90},[39,2482,633],{"class":116},[39,2484,783],{"class":94},[39,2486,2487,2489,2492,2494,2496,2498],{"class":41,"line":394},[39,2488,641],{"class":116},[39,2490,2491],{"class":94},".sheets ",[39,2493,110],{"class":90},[39,2495,367],{"class":94},[39,2497,2153],{"class":49},[39,2499,2500],{"class":94},": FakeXwSheet()}\n",[39,2502,2503],{"class":41,"line":400},[39,2504,101],{"emptyLinePlaceholder":100},[39,2506,2507,2509,2512],{"class":41,"line":405},[39,2508,131],{"class":90},[39,2510,2511],{"class":134}," test_summary_written_without_index",[39,2513,1261],{"class":94},[39,2515,2516,2519,2521],{"class":41,"line":426},[39,2517,2518],{"class":94},"    book ",[39,2520,110],{"class":90},[39,2522,2523],{"class":94}," FakeBook()\n",[39,2525,2526,2529,2531,2534,2536,2539,2542,2544,2547,2549,2551,2553,2555,2557,2560,2562,2565,2567,2570],{"class":41,"line":435},[39,2527,2528],{"class":94},"    df ",[39,2530,110],{"class":90},[39,2532,2533],{"class":94}," pd.DataFrame({",[39,2535,2184],{"class":49},[39,2537,2538],{"class":94},": [",[39,2540,2541],{"class":49},"\"N\"",[39,2543,181],{"class":94},[39,2545,2546],{"class":49},"\"S\"",[39,2548,181],{"class":94},[39,2550,2541],{"class":49},[39,2552,447],{"class":94},[39,2554,2199],{"class":49},[39,2556,2538],{"class":94},[39,2558,2559],{"class":116},"10.0",[39,2561,181],{"class":94},[39,2563,2564],{"class":116},"5.0",[39,2566,181],{"class":94},[39,2568,2569],{"class":116},"2.5",[39,2571,2572],{"class":94},"]})\n",[39,2574,2575],{"class":41,"line":943},[39,2576,2577],{"class":94},"    write_summary(book, df)\n",[39,2579,2580,2583,2585,2587,2589,2592,2594],{"class":41,"line":948},[39,2581,2582],{"class":94},"    table, opts ",[39,2584,110],{"class":90},[39,2586,2150],{"class":94},[39,2588,2153],{"class":49},[39,2590,2591],{"class":94},"].cells[",[39,2593,2163],{"class":49},[39,2595,1555],{"class":94},[39,2597,2598,2600,2603,2605,2607,2610,2612,2614],{"class":41,"line":958},[39,2599,1343],{"class":90},[39,2601,2602],{"class":94}," opts ",[39,2604,1349],{"class":90},[39,2606,367],{"class":94},[39,2608,2609],{"class":49},"\"index\"",[39,2611,1287],{"class":94},[39,2613,189],{"class":116},[39,2615,1368],{"class":94},[39,2617,2618,2620,2623,2626,2629,2631,2633,2635,2637,2639,2641,2643,2645,2647,2649,2652,2654,2656],{"class":41,"line":968},[39,2619,1343],{"class":90},[39,2621,2622],{"class":94}," table.to_dict(",[39,2624,2625],{"class":49},"\"list\"",[39,2627,2628],{"class":94},") ",[39,2630,1349],{"class":90},[39,2632,367],{"class":94},[39,2634,2184],{"class":49},[39,2636,2538],{"class":94},[39,2638,2541],{"class":49},[39,2640,181],{"class":94},[39,2642,2546],{"class":49},[39,2644,447],{"class":94},[39,2646,2199],{"class":49},[39,2648,2538],{"class":94},[39,2650,2651],{"class":116},"12.5",[39,2653,181],{"class":94},[39,2655,2564],{"class":116},[39,2657,2658],{"class":94},"]}\n",[39,2660,2661,2663,2665,2667,2669,2671,2674,2676,2678,2680],{"class":41,"line":981},[39,2662,1343],{"class":90},[39,2664,2150],{"class":94},[39,2666,2153],{"class":49},[39,2668,2591],{"class":94},[39,2670,2223],{"class":49},[39,2672,2673],{"class":94},"][",[39,2675,178],{"class":116},[39,2677,1380],{"class":94},[39,2679,1349],{"class":90},[39,2681,2682],{"class":116}," 17.5\n",[10,2684,2685,2686,2689,2690,1287,2693,2696,2697,2700,2701,2704,2705,2708,2709,2712],{},"For older code that does ",[14,2687,2688],{},"import win32com.client"," at the top of the module, inject a stand-in module before importing it, in a fixture or ",[14,2691,2692],{},"conftest.py",[14,2694,2695],{},"sys.modules[\"win32com\"] = types.ModuleType(\"win32com\")"," plus a ",[14,2698,2699],{},"win32com.client"," module whose ",[14,2702,2703],{},"Dispatch"," returns your fake. It works, but it is a sign the code would benefit from Step 1's restructuring. Tests that really need Excel — checking that a macro still exists, or that a refresh against a test database succeeds — go in a separate file guarded with ",[14,2706,2707],{},"pytest.importorskip(\"win32com.client\")"," or a ",[14,2710,2711],{},"skipif(sys.platform != \"win32\")"," marker, and run on a Windows machine with Excel:",[30,2714,2716],{"className":81,"code":2715,"language":83,"meta":35,"style":35},"import sys\nimport pytest\n\npytestmark = pytest.mark.skipif(sys.platform != \"win32\", reason=\"needs Windows with Excel installed\")\n\ndef test_real_refresh(tmp_path):\n    win32 = pytest.importorskip(\"win32com.client\")\n    from jobs.refresh import run\n    result = run([\"C:\u002Fci\u002Ffixtures\u002Fsales.xlsx\"], lambda: win32.DispatchEx(\"Excel.Application\"))\n    assert result[\"C:\u002Fci\u002Ffixtures\u002Fsales.xlsx\"] > 0\n",[14,2717,2718,2725,2731,2735,2763,2767,2777,2792,2803,2826],{"__ignoreMap":35},[39,2719,2720,2722],{"class":41,"line":42},[39,2721,91],{"class":90},[39,2723,2724],{"class":94}," sys\n",[39,2726,2727,2729],{"class":41,"line":59},[39,2728,91],{"class":90},[39,2730,1222],{"class":94},[39,2732,2733],{"class":41,"line":104},[39,2734,101],{"emptyLinePlaceholder":100},[39,2736,2737,2740,2742,2745,2748,2751,2753,2756,2758,2761],{"class":41,"line":123},[39,2738,2739],{"class":94},"pytestmark ",[39,2741,110],{"class":90},[39,2743,2744],{"class":94}," pytest.mark.skipif(sys.platform ",[39,2746,2747],{"class":90},"!=",[39,2749,2750],{"class":49}," \"win32\"",[39,2752,181],{"class":94},[39,2754,2755],{"class":172},"reason",[39,2757,110],{"class":90},[39,2759,2760],{"class":49},"\"needs Windows with Excel installed\"",[39,2762,120],{"class":94},[39,2764,2765],{"class":41,"line":128},[39,2766,101],{"emptyLinePlaceholder":100},[39,2768,2769,2771,2774],{"class":41,"line":149},[39,2770,131],{"class":90},[39,2772,2773],{"class":134}," test_real_refresh",[39,2775,2776],{"class":94},"(tmp_path):\n",[39,2778,2779,2782,2784,2787,2790],{"class":41,"line":155},[39,2780,2781],{"class":94},"    win32 ",[39,2783,110],{"class":90},[39,2785,2786],{"class":94}," pytest.importorskip(",[39,2788,2789],{"class":49},"\"win32com.client\"",[39,2791,120],{"class":94},[39,2793,2794,2797,2799,2801],{"class":41,"line":194},[39,2795,2796],{"class":90},"    from",[39,2798,1230],{"class":94},[39,2800,91],{"class":90},[39,2802,1235],{"class":94},[39,2804,2805,2807,2809,2811,2814,2816,2818,2821,2823],{"class":41,"line":203},[39,2806,1317],{"class":94},[39,2808,110],{"class":90},[39,2810,1322],{"class":94},[39,2812,2813],{"class":49},"\"C:\u002Fci\u002Ffixtures\u002Fsales.xlsx\"",[39,2815,447],{"class":94},[39,2817,450],{"class":90},[39,2819,2820],{"class":94},": win32.DispatchEx(",[39,2822,456],{"class":49},[39,2824,2825],{"class":94},"))\n",[39,2827,2828,2830,2833,2835,2837,2840],{"class":41,"line":209},[39,2829,1343],{"class":90},[39,2831,2832],{"class":94}," result[",[39,2834,2813],{"class":49},[39,2836,1380],{"class":94},[39,2838,2839],{"class":90},">",[39,2841,2842],{"class":116}," 0\n",[30,2844,2846],{"className":81,"code":2845,"language":83,"meta":35,"style":35},"pytest_run(\"tests\")\n",[14,2847,2848],{"__ignoreMap":35},[39,2849,2850,2852,2855],{"class":41,"line":42},[39,2851,1841],{"class":94},[39,2853,2854],{"class":49},"\"tests\"",[39,2856,120],{"class":94},[10,2858,2859,2860,2863,2864,23],{},"Four tests pass and the real-Excel test is skipped on Linux (run with ",[14,2861,2862],{},"-rs"," to see the skip reason) — so the suite runs everywhere, and the Windows machine that has Excel runs the one extra test. Running Excel jobs on a server in the first place has its own pitfalls, covered in ",[19,2865,2867],{"href":2866},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Frun-excel-com-automation-from-task-scheduler\u002F","Run Excel COM Automation from Task Scheduler Without a Logged-In User",[25,2869,2871],{"id":2870},"common-pitfalls","Common pitfalls",[2873,2874,2875,2883,2889,2895,2905],"ul",{},[2876,2877,2878,2882],"li",{},[2879,2880,2881],"strong",{},"Creating Excel inside the logic."," Pass the application in so tests can substitute it.",[2876,2884,2885,2888],{},[2879,2886,2887],{},"MagicMock everywhere."," It hides typos; small fakes fail like the real thing.",[2876,2890,2891,2894],{},[2879,2892,2893],{},"Only testing success."," Test that failures still close workbooks and quit Excel.",[2876,2896,2897,2900,2901,2904],{},[2879,2898,2899],{},"Imports that need Windows."," Keep ",[14,2902,2903],{},"win32com"," imports at the entry point.",[2876,2906,2907,2910],{},[2879,2908,2909],{},"Real-Excel tests that fail on CI."," Skip them by platform and run them where Excel exists.",[25,2912,2914],{"id":2913},"conclusion","Conclusion",[10,2916,2917],{},"To test Excel-driving code without Excel, keep the COM or xlwings calls in thin functions that receive the application or book as an argument, and import win32com only at the entry point. Replace the real objects in tests with small recording fakes that define only what the code uses, simulate failures, and assert that workbooks are closed without saving and Excel is quit even when something goes wrong. Keep the few tests that need real Excel in a separate file skipped on other platforms.",[25,2919,2921],{"id":2920},"frequently-asked-questions","Frequently asked questions",[10,2923,2924,2927],{},[2879,2925,2926],{},"How do I unit test win32com code on Linux?","\nPass the Excel application into your functions instead of creating it with Dispatch, and in tests pass a fake object with the same methods and properties that records what was called.",[10,2929,2930,2933],{},[2879,2931,2932],{},"Should I use MagicMock for COM objects?","\nSmall hand-written fakes are safer; MagicMock accepts any attribute, so misspelled COM method names pass silently.",[10,2935,2936,2939],{},[2879,2937,2938],{},"How do I test that Excel is closed after an error?","\nMake the fake raise an error during a step such as RefreshAll, then assert that the workbook's Close was called with SaveChanges False and that Quit was called.",[10,2941,2942,2945],{},[2879,2943,2944],{},"How do I run real Excel tests only on Windows?","\nPut them in a separate module marked with skipif for non-Windows platforms, and use pytest.importorskip for win32com.client.",[25,2947,2949],{"id":2948},"related","Related",[2873,2951,2952,2958,2965,2972],{},[2876,2953,2954,2955,2957],{},"Up one level: ",[19,2956,22],{"href":21}," — reliable, shippable code.",[2876,2959,2960,2964],{},[19,2961,2963],{"href":2962},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-output-with-pytest\u002F","Test Excel Output with pytest"," — testing generated files.",[2876,2966,2967,2971],{},[19,2968,2970],{"href":2969},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Frun-an-excel-macro-from-python-with-win32com\u002F","Run an Excel Macro from Python with win32com"," — code worth testing this way.",[2876,2973,2974,2978],{},[19,2975,2977],{"href":2976},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-excel-code-against-several-library-versions-with-nox\u002F","Test Excel Code Against Several Library Versions with nox"," — running the suite in many environments.",[2980,2981,2982],"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 pre.shiki code .s-wDw, html code.shiki .s-wDw{--shiki-default:#6A737D;--shiki-dark:#BDC4CC}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-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":35,"searchDepth":59,"depth":59,"links":2984},[2985,2986,2987,2988,2989,2990,2991,2992,2993],{"id":27,"depth":59,"text":28},{"id":69,"depth":59,"text":70},{"id":606,"depth":59,"text":607},{"id":1209,"depth":59,"text":1210},{"id":2100,"depth":59,"text":2101},{"id":2870,"depth":59,"text":2871},{"id":2913,"depth":59,"text":2914},{"id":2920,"depth":59,"text":2921},{"id":2948,"depth":59,"text":2949},"2026-10-02","Unit-test Python code that drives Excel through win32com or xlwings on machines without Excel — Linux CI runners, Macs, containers: keep the Excel calls in a thin layer, pass the application in, replace it with small fakes that record what was done, check that workbooks are closed and Excel quit even after errors, stub the win32com import for older code, and keep real-Excel tests for Windows only.","md",[2998,3000,3002,3004],{"q":2926,"a":2999},"Pass the Excel application into your functions instead of creating it with Dispatch, and in tests pass a fake object with the same methods and properties that records what was called.",{"q":2932,"a":3001},"Small hand-written fakes are safer; MagicMock accepts any attribute, so misspelled COM method names pass silently.",{"q":2938,"a":3003},"Make the fake raise an error during a step such as RefreshAll, then assert that the workbook's Close was called with SaveChanges False and that Quit was called.",{"q":2944,"a":3005},"Put them in a separate module marked with skipif for non-Windows platforms, and use pytest.importorskip for win32com.client.","test win32com and xlwings code without excel",null,{"breadcrumb":3009},[3010,3013,3016],{"name":3011,"item":3012},"Home","\u002F",{"name":3014,"item":3015},"Automating Reporting Workflows with Python and Excel","\u002Fautomating-reporting-workflows\u002F",{"name":22,"item":21},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-win32com-and-xlwings-code-without-excel",{"title":3019,"description":3020},"Test win32com and xlwings Code Without Excel in pytest","pytest for Excel COM automation without Excel: thin adapters, injected application objects, recording fakes for win32com and xlwings ranges, cleanup-on-error tests, sys.modules stubs and Windows-only integration tests.","test-win32com-and-xlwings-code-without-excel","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-win32com-and-xlwings-code-without-excel\u002Findex",[3024,3025,3026],"com-automation","xlwings","reliable-jobs","how-to","nS_BM8Al_Q3MGFY3WFEK63ZDOfha90hYQmvHU9A91I4",[3030,3034],{"title":3031,"path":3032,"stem":3033,"children":-1},"Test That Excel Charts Were Written Correctly","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-that-excel-charts-were-written-correctly","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftest-that-excel-charts-were-written-correctly\u002Findex",{"title":3035,"path":3036,"stem":3037,"children":-1},"Lint and Type-Check an Excel Automation Project","\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftype-check-an-excel-automation-project","automating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts\u002Ftype-check-an-excel-automation-project\u002Findex",[3039,3041],{"path":3040,"title":3014},"\u002Fautomating-reporting-workflows",{"path":3042,"title":22},"\u002Fautomating-reporting-workflows\u002Ftesting-and-packaging-excel-automation-scripts",[],[3045,3049,3052,3054,3058,3061],{"path":3046,"title":3047,"kind":3048},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32","Automating Excel with COM and pywin32","guide",{"path":3050,"title":3051,"kind":3027},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fautomate-excel-on-macos-with-python","Automate Excel on macOS with Python",{"path":3053,"title":490,"kind":3027},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fclose-excel-cleanly-and-avoid-orphan-com-processes",{"path":3055,"title":3056,"kind":3057},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fhandle-com-errors-and-excel-dialog-prompts-in-python","Handle COM Errors and Excel Dialog Prompts in Python","fix",{"path":3059,"title":3060,"kind":3027},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fprint-an-excel-workbook-from-python-with-win32com","Print an Excel Workbook from Python with win32com",{"path":3062,"title":3063,"kind":3027},"\u002Fgetting-started-with-python-excel-automation\u002Fautomating-excel-with-com-and-pywin32\u002Fread-and-write-cell-ranges-with-win32com","Read and Write Cell Ranges with win32com",1790972142977]