[{"data":1,"prerenderedAt":1285},["ShallowReactive",2],{"page-\u002Fadvanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002F":3},{"id":4,"title":5,"body":6,"description":1246,"extension":1247,"meta":1248,"navigation":335,"path":1281,"seo":1282,"stem":1283,"__hash__":1284},"content\u002Fadvanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002Findex.md","Controlling Time and Randomness in Tests",{"type":7,"value":8,"toc":1230},"minimark",[9,30,35,71,105,127,131,158,288,292,297,312,413,416,464,477,481,489,572,576,587,650,654,665,718,722,728,805,809,812,911,915,956,960,1094,1098,1110,1131,1148,1161,1165,1219,1226],[10,11,12,13,17,18,21,22,25,26,29],"p",{},"A test that asserts an order expires \"in 24 hours\", or that a shuffled deck matches a golden sequence, or that a generated invoice ID equals a fixed string, is a test coupled to the wall clock and the global random state. It passes on the afternoon you wrote it and fails at 23:59 UTC, on a leap day, in a different timezone CI runner, or simply on the next run once ",[14,15,16],"code",{},"pytest-randomly"," reseeds the generator. The symptom is the most corrosive kind of flake: green locally, intermittently red in CI, with no code change to blame. The cure is determinism — freeze time to a known instant, seed every random source, fix UUID generation, and, where you control the code, inject a clock so the dependency is explicit rather than ambient. This guide covers ",[14,19,20],{},"freezegun"," and ",[14,23,24],{},"time-machine",", stdlib and numpy seeding, deterministic ",[14,27,28],{},"uuid",", and the injectable-clock seam that makes most freezing unnecessary.",[31,32,34],"h2",{"id":33},"prerequisites","Prerequisites",[36,37,38,45,50,60],"ul",{},[39,40,41,44],"li",{},[14,42,43],{},"python >= 3.9",".",[39,46,47,44],{},[14,48,49],{},"pytest >= 8.0",[39,51,52,53,56,57,44],{},"Time control: ",[14,54,55],{},"freezegun >= 1.5"," and\u002For ",[14,58,59],{},"time-machine >= 2.14",[39,61,62,63,66,67,70],{},"Optional numerical stack: ",[14,64,65],{},"numpy >= 1.26"," (for ",[14,68,69],{},"numpy.random.Generator",").",[72,73,78],"pre",{"className":74,"code":75,"language":76,"meta":77,"style":77},"language-bash shiki shiki-themes github-light github-dark","pip install \"pytest>=8.0\" \"freezegun>=1.5\" \"time-machine>=2.14\" \"numpy>=1.26\"\n","bash","",[14,79,80],{"__ignoreMap":77},[81,82,85,89,93,96,99,102],"span",{"class":83,"line":84},"line",1,[81,86,88],{"class":87},"sScJk","pip",[81,90,92],{"class":91},"sZZnC"," install",[81,94,95],{"class":91}," \"pytest>=8.0\"",[81,97,98],{"class":91}," \"freezegun>=1.5\"",[81,100,101],{"class":91}," \"time-machine>=2.14\"",[81,103,104],{"class":91}," \"numpy>=1.26\"\n",[10,106,107,108,117,118,122,123,126],{},"This guide leans on the ",[109,110,112,113,116],"a",{"href":111},"\u002Fadvanced-pytest-architecture-configuration\u002Fmastering-pytest-fixtures\u002F","pytest ",[14,114,115],{},"monkeypatch"," fixture"," for the lightweight patching cases and on the namespace rules in ",[109,119,121],{"href":120},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fpatching-strategies-for-complex-codebases\u002F","Patching Strategies for Complex Codebases"," — getting the patch target right matters as much for ",[14,124,125],{},"datetime"," as for any other dependency.",[31,128,130],{"id":129},"core-concept","Core concept",[10,132,133,134,138,139,141,142,145,146,149,150,153,154,157],{},"There are two strategies, and they sit at opposite ends of a design spectrum. The first is ",[135,136,137],"strong",{},"interception",": a test-time library or ",[14,140,115],{}," swaps ",[14,143,144],{},"datetime.now",", ",[14,147,148],{},"time.time",", or ",[14,151,152],{},"random.random"," for a controlled version, leaving production code untouched. The second is ",[135,155,156],{},"injection",": production code is written to receive its clock and random source as dependencies, so a test supplies fixed ones with no patching at all. Injection produces the cleanest tests and the most honest code, but interception is what you reach for when you cannot change the code under test.",[159,160,163,284],"figure",{"className":161},[162],"diagram",[164,165,172,173,172,177,172,181,172,191,172,198,172,207,172,211,172,215,172,221,172,225,172,229,172,232,172,236,172,241,172,244,172,247,172,250,172,252,172,255,172,258,172,260,172,263,172,268,172,272,172,276,172,280],"svg",{"viewBox":166,"role":167,"ariaLabelledBy":168,"xmlns":171},"0 0 820 380","img",[169,170],"clockseam-t","clockseam-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[174,175,176],"title",{"id":169},"The injectable-clock seam",[178,179,180],"desc",{"id":170},"Code that calls datetime.now directly must be intercepted by a freezing library, whereas code that accepts a clock dependency takes a fixed clock straight from the test.",[182,183,190],"text",{"x":184,"y":185,"textAnchor":186,"fontSize":187,"fontWeight":188,"fill":189},"410","34","middle","19","700","#3d405b","Two ways to control time",[182,192,197],{"x":193,"y":194,"textAnchor":186,"fontSize":195,"fontWeight":188,"fill":196},"220","68","13","#e07a5f","Ambient clock",[199,200],"rect",{"x":201,"y":202,"width":203,"height":201,"rx":204,"fill":205,"stroke":196,"strokeWidth":206},"60","84","320","12","#fffdf8","2",[182,208,210],{"x":193,"y":209,"textAnchor":186,"fontSize":204,"fill":189},"110","code calls datetime.now() directly",[182,212,214],{"x":193,"y":213,"textAnchor":186,"fontSize":204,"fill":189},"130","test must freeze \u002F monkeypatch",[199,216],{"x":201,"y":217,"width":203,"height":218,"rx":204,"fill":219,"stroke":189,"strokeWidth":220},"160","56","#f4f1de","1.5",[182,222,224],{"x":193,"y":223,"textAnchor":186,"fontSize":204,"fill":189},"184","freezegun \u002F time-machine",[182,226,228],{"x":193,"y":227,"textAnchor":186,"fontSize":204,"fill":189},"202","intercepts the global clock",[83,230],{"x1":193,"y1":231,"x2":193,"y2":217,"stroke":196,"strokeWidth":206},"144",[233,234],"polygon",{"points":235,"fill":196},"220,160 215,150 225,150",[182,237,240],{"x":238,"y":194,"textAnchor":186,"fontSize":195,"fontWeight":188,"fill":239},"600","#81b29a","Injected clock",[199,242],{"x":243,"y":202,"width":203,"height":201,"rx":204,"fill":205,"stroke":239,"strokeWidth":206},"440",[182,245,246],{"x":238,"y":209,"textAnchor":186,"fontSize":204,"fill":189},"code takes now=... as a parameter",[182,248,249],{"x":238,"y":213,"textAnchor":186,"fontSize":204,"fill":189},"dependency is explicit",[199,251],{"x":243,"y":217,"width":203,"height":218,"rx":204,"fill":219,"stroke":189,"strokeWidth":220},[182,253,254],{"x":238,"y":223,"textAnchor":186,"fontSize":204,"fill":189},"test passes a fixed clock",[182,256,257],{"x":238,"y":227,"textAnchor":186,"fontSize":204,"fill":189},"no patching library needed",[83,259],{"x1":238,"y1":231,"x2":238,"y2":217,"stroke":239,"strokeWidth":206},[233,261],{"points":262,"fill":239},"600,160 595,150 605,150",[199,264],{"x":201,"y":265,"width":188,"height":266,"rx":267,"fill":219,"stroke":189,"strokeWidth":220},"250","106","14",[182,269,271],{"x":184,"y":270,"textAnchor":186,"fontSize":195,"fontWeight":188,"fill":189},"278","Same idea for randomness",[182,273,275],{"x":184,"y":274,"textAnchor":186,"fontSize":204,"fill":189},"304","ambient: seed the global random \u002F numpy state",[182,277,279],{"x":184,"y":278,"textAnchor":186,"fontSize":204,"fill":189},"326","injected: pass a seeded Random or Generator instance",[182,281,283],{"x":184,"y":282,"textAnchor":186,"fontSize":204,"fill":189},"346","injection removes hidden global state entirely",[285,286,287],"figcaption",{},"Code that reads an ambient clock or global RNG must be intercepted at test time; code that accepts a clock and a random source as dependencies takes fixed ones straight from the test, removing hidden global state.",[31,289,291],{"id":290},"step-by-step-implementation","Step-by-step implementation",[293,294,296],"h3",{"id":295},"_1-freeze-time-with-freezegun","1. Freeze time with freezegun",[10,298,299,301,302,145,305,308,309,311],{},[14,300,20],{}," patches ",[14,303,304],{},"datetime.datetime",[14,306,307],{},"datetime.date",", and ",[14,310,148],{}," across all modules so any code reading the clock during the frozen block sees the same instant.",[72,313,317],{"className":314,"code":315,"language":316,"meta":77,"style":77},"language-python shiki shiki-themes github-light github-dark","from datetime import datetime, timezone, timedelta\nfrom freezegun import freeze_time\n\ndef order_expiry(created: datetime) -> datetime:\n    return created + timedelta(hours=24)\n\ndef is_expired() -> bool:\n    # Reads the wall clock directly — the dependency is ambient.\n    return datetime.now(timezone.utc) > datetime(2026, 1, 1, tzinfo=timezone.utc)\n\n@freeze_time(\"2026-06-18T12:00:00Z\")\ndef test_expiry_is_deterministic():\n    now = datetime.now(timezone.utc)\n    assert now == datetime(2026, 6, 18, 12, 0, tzinfo=timezone.utc)\n    assert order_expiry(now) == datetime(2026, 6, 19, 12, 0, tzinfo=timezone.utc)\n    assert is_expired() is True\n","python",[14,318,319,324,330,337,343,349,354,360,366,372,377,383,389,395,401,407],{"__ignoreMap":77},[81,320,321],{"class":83,"line":84},[81,322,323],{},"from datetime import datetime, timezone, timedelta\n",[81,325,327],{"class":83,"line":326},2,[81,328,329],{},"from freezegun import freeze_time\n",[81,331,333],{"class":83,"line":332},3,[81,334,336],{"emptyLinePlaceholder":335},true,"\n",[81,338,340],{"class":83,"line":339},4,[81,341,342],{},"def order_expiry(created: datetime) -> datetime:\n",[81,344,346],{"class":83,"line":345},5,[81,347,348],{},"    return created + timedelta(hours=24)\n",[81,350,352],{"class":83,"line":351},6,[81,353,336],{"emptyLinePlaceholder":335},[81,355,357],{"class":83,"line":356},7,[81,358,359],{},"def is_expired() -> bool:\n",[81,361,363],{"class":83,"line":362},8,[81,364,365],{},"    # Reads the wall clock directly — the dependency is ambient.\n",[81,367,369],{"class":83,"line":368},9,[81,370,371],{},"    return datetime.now(timezone.utc) > datetime(2026, 1, 1, tzinfo=timezone.utc)\n",[81,373,375],{"class":83,"line":374},10,[81,376,336],{"emptyLinePlaceholder":335},[81,378,380],{"class":83,"line":379},11,[81,381,382],{},"@freeze_time(\"2026-06-18T12:00:00Z\")\n",[81,384,386],{"class":83,"line":385},12,[81,387,388],{},"def test_expiry_is_deterministic():\n",[81,390,392],{"class":83,"line":391},13,[81,393,394],{},"    now = datetime.now(timezone.utc)\n",[81,396,398],{"class":83,"line":397},14,[81,399,400],{},"    assert now == datetime(2026, 6, 18, 12, 0, tzinfo=timezone.utc)\n",[81,402,404],{"class":83,"line":403},15,[81,405,406],{},"    assert order_expiry(now) == datetime(2026, 6, 19, 12, 0, tzinfo=timezone.utc)\n",[81,408,410],{"class":83,"line":409},16,[81,411,412],{},"    assert is_expired() is True\n",[10,414,415],{},"Use the context-manager form when you need the clock to advance mid-test:",[72,417,419],{"className":314,"code":418,"language":316,"meta":77,"style":77},"from freezegun import freeze_time\nfrom datetime import datetime\n\ndef test_clock_can_tick():\n    with freeze_time(\"2026-06-18T12:00:00Z\") as frozen:\n        t0 = datetime.now()\n        frozen.tick()                       # advance 1 second by default\n        frozen.tick(delta=60)               # advance 60 seconds\n        assert (datetime.now() - t0).total_seconds() == 61\n",[14,420,421,425,430,434,439,444,449,454,459],{"__ignoreMap":77},[81,422,423],{"class":83,"line":84},[81,424,329],{},[81,426,427],{"class":83,"line":326},[81,428,429],{},"from datetime import datetime\n",[81,431,432],{"class":83,"line":332},[81,433,336],{"emptyLinePlaceholder":335},[81,435,436],{"class":83,"line":339},[81,437,438],{},"def test_clock_can_tick():\n",[81,440,441],{"class":83,"line":345},[81,442,443],{},"    with freeze_time(\"2026-06-18T12:00:00Z\") as frozen:\n",[81,445,446],{"class":83,"line":351},[81,447,448],{},"        t0 = datetime.now()\n",[81,450,451],{"class":83,"line":356},[81,452,453],{},"        frozen.tick()                       # advance 1 second by default\n",[81,455,456],{"class":83,"line":362},[81,457,458],{},"        frozen.tick(delta=60)               # advance 60 seconds\n",[81,460,461],{"class":83,"line":368},[81,462,463],{},"        assert (datetime.now() - t0).total_seconds() == 61\n",[10,465,466,467,469,470,472,473,44],{},"The trade-offs and breakage points of ",[14,468,20],{}," versus a raw ",[14,471,115],{}," are dissected in ",[109,474,476],{"href":475},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002Ffreezing-time-with-freezegun-vs-monkeypatch\u002F","Freezing Time: freezegun vs monkeypatch",[293,478,480],{"id":479},"_2-reach-for-time-machine-when-speed-or-c-extensions-matter","2. Reach for time-machine when speed or C extensions matter",[10,482,483,485,486,488],{},[14,484,24],{}," patches at the CPython level (it hooks the datetime type and the underlying clock), making it dramatically faster than ",[14,487,20],{}," and able to fool C-extension code that calls the libc clock.",[72,490,492],{"className":314,"code":491,"language":316,"meta":77,"style":77},"import time\nimport datetime as dt\nimport time_machine\n\n@time_machine.travel(\"2026-06-18 12:00 +0000\")\ndef test_time_machine_freezes_everything():\n    assert dt.datetime.now(dt.timezone.utc).year == 2026\n    # time.time() is frozen too, including for C code that reads it.\n    assert int(time.time()) == 1781870400\n\ndef test_time_machine_tick():\n    # tick=False freezes; call .shift() to advance deliberately.\n    with time_machine.travel(\"2026-06-18 12:00 +0000\", tick=False) as traveller:\n        t0 = time.time()\n        traveller.shift(delta=30)\n        assert time.time() - t0 == 30\n",[14,493,494,499,504,509,513,518,523,528,533,538,542,547,552,557,562,567],{"__ignoreMap":77},[81,495,496],{"class":83,"line":84},[81,497,498],{},"import time\n",[81,500,501],{"class":83,"line":326},[81,502,503],{},"import datetime as dt\n",[81,505,506],{"class":83,"line":332},[81,507,508],{},"import time_machine\n",[81,510,511],{"class":83,"line":339},[81,512,336],{"emptyLinePlaceholder":335},[81,514,515],{"class":83,"line":345},[81,516,517],{},"@time_machine.travel(\"2026-06-18 12:00 +0000\")\n",[81,519,520],{"class":83,"line":351},[81,521,522],{},"def test_time_machine_freezes_everything():\n",[81,524,525],{"class":83,"line":356},[81,526,527],{},"    assert dt.datetime.now(dt.timezone.utc).year == 2026\n",[81,529,530],{"class":83,"line":362},[81,531,532],{},"    # time.time() is frozen too, including for C code that reads it.\n",[81,534,535],{"class":83,"line":368},[81,536,537],{},"    assert int(time.time()) == 1781870400\n",[81,539,540],{"class":83,"line":374},[81,541,336],{"emptyLinePlaceholder":335},[81,543,544],{"class":83,"line":379},[81,545,546],{},"def test_time_machine_tick():\n",[81,548,549],{"class":83,"line":385},[81,550,551],{},"    # tick=False freezes; call .shift() to advance deliberately.\n",[81,553,554],{"class":83,"line":391},[81,555,556],{},"    with time_machine.travel(\"2026-06-18 12:00 +0000\", tick=False) as traveller:\n",[81,558,559],{"class":83,"line":397},[81,560,561],{},"        t0 = time.time()\n",[81,563,564],{"class":83,"line":403},[81,565,566],{},"        traveller.shift(delta=30)\n",[81,568,569],{"class":83,"line":409},[81,570,571],{},"        assert time.time() - t0 == 30\n",[293,573,575],{"id":574},"_3-seed-the-standard-library-rng","3. Seed the standard-library RNG",[10,577,578,579,582,583,586],{},"The stdlib ",[14,580,581],{},"random"," module uses a global Mersenne Twister. Seed it for reproducibility, but prefer an explicit ",[14,584,585],{},"random.Random"," instance so one test cannot poison another's global state.",[72,588,590],{"className":314,"code":589,"language":316,"meta":77,"style":77},"import random\n\ndef shuffle_deck(cards: list[int], rng: random.Random) -> list[int]:\n    out = list(cards)\n    rng.shuffle(out)                        # uses the INJECTED generator\n    return out\n\ndef test_shuffle_is_reproducible():\n    rng = random.Random(0)                  # local, seeded generator\n    assert shuffle_deck([1, 2, 3, 4, 5], rng) == [4, 2, 3, 5, 1]\n    # Re-seeding reproduces the exact sequence.\n    assert shuffle_deck([1, 2, 3, 4, 5], random.Random(0)) == [4, 2, 3, 5, 1]\n",[14,591,592,597,601,606,611,616,621,625,630,635,640,645],{"__ignoreMap":77},[81,593,594],{"class":83,"line":84},[81,595,596],{},"import random\n",[81,598,599],{"class":83,"line":326},[81,600,336],{"emptyLinePlaceholder":335},[81,602,603],{"class":83,"line":332},[81,604,605],{},"def shuffle_deck(cards: list[int], rng: random.Random) -> list[int]:\n",[81,607,608],{"class":83,"line":339},[81,609,610],{},"    out = list(cards)\n",[81,612,613],{"class":83,"line":345},[81,614,615],{},"    rng.shuffle(out)                        # uses the INJECTED generator\n",[81,617,618],{"class":83,"line":351},[81,619,620],{},"    return out\n",[81,622,623],{"class":83,"line":356},[81,624,336],{"emptyLinePlaceholder":335},[81,626,627],{"class":83,"line":362},[81,628,629],{},"def test_shuffle_is_reproducible():\n",[81,631,632],{"class":83,"line":368},[81,633,634],{},"    rng = random.Random(0)                  # local, seeded generator\n",[81,636,637],{"class":83,"line":374},[81,638,639],{},"    assert shuffle_deck([1, 2, 3, 4, 5], rng) == [4, 2, 3, 5, 1]\n",[81,641,642],{"class":83,"line":379},[81,643,644],{},"    # Re-seeding reproduces the exact sequence.\n",[81,646,647],{"class":83,"line":385},[81,648,649],{},"    assert shuffle_deck([1, 2, 3, 4, 5], random.Random(0)) == [4, 2, 3, 5, 1]\n",[293,651,653],{"id":652},"_4-seed-numpy-with-a-generator-not-the-legacy-global","4. Seed numpy with a Generator, not the legacy global",[10,655,656,657,660,661,664],{},"Modern numpy seeding uses ",[14,658,659],{},"default_rng",". Avoid ",[14,662,663],{},"numpy.random.seed",", which mutates a process-global legacy state shared across the suite.",[72,666,668],{"className":314,"code":667,"language":316,"meta":77,"style":77},"import numpy as np\n\ndef sample_weights(n: int, rng: np.random.Generator) -> np.ndarray:\n    return rng.normal(size=n)               # injected Generator\n\ndef test_numpy_is_deterministic():\n    rng = np.random.default_rng(42)         # modern, isolated bit generator\n    first = sample_weights(3, rng)\n    second = sample_weights(3, np.random.default_rng(42))\n    np.testing.assert_array_equal(first, second)\n",[14,669,670,675,679,684,689,693,698,703,708,713],{"__ignoreMap":77},[81,671,672],{"class":83,"line":84},[81,673,674],{},"import numpy as np\n",[81,676,677],{"class":83,"line":326},[81,678,336],{"emptyLinePlaceholder":335},[81,680,681],{"class":83,"line":332},[81,682,683],{},"def sample_weights(n: int, rng: np.random.Generator) -> np.ndarray:\n",[81,685,686],{"class":83,"line":339},[81,687,688],{},"    return rng.normal(size=n)               # injected Generator\n",[81,690,691],{"class":83,"line":345},[81,692,336],{"emptyLinePlaceholder":335},[81,694,695],{"class":83,"line":351},[81,696,697],{},"def test_numpy_is_deterministic():\n",[81,699,700],{"class":83,"line":356},[81,701,702],{},"    rng = np.random.default_rng(42)         # modern, isolated bit generator\n",[81,704,705],{"class":83,"line":362},[81,706,707],{},"    first = sample_weights(3, rng)\n",[81,709,710],{"class":83,"line":368},[81,711,712],{},"    second = sample_weights(3, np.random.default_rng(42))\n",[81,714,715],{"class":83,"line":374},[81,716,717],{},"    np.testing.assert_array_equal(first, second)\n",[293,719,721],{"id":720},"_5-make-uuid-deterministic","5. Make uuid deterministic",[10,723,724,727],{},[14,725,726],{},"uuid.uuid4()"," is random, so generated identifiers break golden-file assertions. Patch it with a counted factory, or — better — inject an id generator.",[72,729,731],{"className":314,"code":730,"language":316,"meta":77,"style":77},"import uuid\nfrom itertools import count\n\ndef make_id_factory():\n    counter = count(1)\n    # Deterministic, sortable, reproducible identifiers.\n    return lambda: uuid.UUID(int=next(counter))\n\ndef create_record(name: str, id_gen=uuid.uuid4) -> dict:\n    return {\"id\": str(id_gen()), \"name\": name}   # id source is injectable\n\ndef test_uuid_is_deterministic():\n    id_gen = make_id_factory()\n    assert create_record(\"a\", id_gen)[\"id\"] == \"00000000-0000-0000-0000-000000000001\"\n    assert create_record(\"b\", id_gen)[\"id\"] == \"00000000-0000-0000-0000-000000000002\"\n",[14,732,733,738,743,747,752,757,762,767,771,776,781,785,790,795,800],{"__ignoreMap":77},[81,734,735],{"class":83,"line":84},[81,736,737],{},"import uuid\n",[81,739,740],{"class":83,"line":326},[81,741,742],{},"from itertools import count\n",[81,744,745],{"class":83,"line":332},[81,746,336],{"emptyLinePlaceholder":335},[81,748,749],{"class":83,"line":339},[81,750,751],{},"def make_id_factory():\n",[81,753,754],{"class":83,"line":345},[81,755,756],{},"    counter = count(1)\n",[81,758,759],{"class":83,"line":351},[81,760,761],{},"    # Deterministic, sortable, reproducible identifiers.\n",[81,763,764],{"class":83,"line":356},[81,765,766],{},"    return lambda: uuid.UUID(int=next(counter))\n",[81,768,769],{"class":83,"line":362},[81,770,336],{"emptyLinePlaceholder":335},[81,772,773],{"class":83,"line":368},[81,774,775],{},"def create_record(name: str, id_gen=uuid.uuid4) -> dict:\n",[81,777,778],{"class":83,"line":374},[81,779,780],{},"    return {\"id\": str(id_gen()), \"name\": name}   # id source is injectable\n",[81,782,783],{"class":83,"line":379},[81,784,336],{"emptyLinePlaceholder":335},[81,786,787],{"class":83,"line":385},[81,788,789],{},"def test_uuid_is_deterministic():\n",[81,791,792],{"class":83,"line":391},[81,793,794],{},"    id_gen = make_id_factory()\n",[81,796,797],{"class":83,"line":397},[81,798,799],{},"    assert create_record(\"a\", id_gen)[\"id\"] == \"00000000-0000-0000-0000-000000000001\"\n",[81,801,802],{"class":83,"line":403},[81,803,804],{},"    assert create_record(\"b\", id_gen)[\"id\"] == \"00000000-0000-0000-0000-000000000002\"\n",[293,806,808],{"id":807},"_6-prefer-the-injectable-clock-seam","6. Prefer the injectable-clock seam",[10,810,811],{},"The cleanest fix needs no library. Have production code accept its clock as a dependency; the test passes a fixed one.",[72,813,815],{"className":314,"code":814,"language":316,"meta":77,"style":77},"from datetime import datetime, timezone, timedelta\nfrom collections.abc import Callable\n\nclass Session:\n    # now is injected; defaults to the real clock in production.\n    def __init__(self, now: Callable[[], datetime] = lambda: datetime.now(timezone.utc)):\n        self._now = now\n        self.created = self._now()\n\n    def expired(self, ttl=timedelta(minutes=30)) -> bool:\n        return self._now() > self.created + ttl\n\ndef test_session_expiry_with_injected_clock():\n    clock = iter([\n        datetime(2026, 6, 18, 12, 0, tzinfo=timezone.utc),   # creation\n        datetime(2026, 6, 18, 12, 31, tzinfo=timezone.utc),  # check, 31 min later\n    ])\n    session = Session(now=lambda: next(clock))\n    assert session.expired() is True        # no freezing library needed\n",[14,816,817,821,826,830,835,840,845,850,855,859,864,869,873,878,883,888,893,899,905],{"__ignoreMap":77},[81,818,819],{"class":83,"line":84},[81,820,323],{},[81,822,823],{"class":83,"line":326},[81,824,825],{},"from collections.abc import Callable\n",[81,827,828],{"class":83,"line":332},[81,829,336],{"emptyLinePlaceholder":335},[81,831,832],{"class":83,"line":339},[81,833,834],{},"class Session:\n",[81,836,837],{"class":83,"line":345},[81,838,839],{},"    # now is injected; defaults to the real clock in production.\n",[81,841,842],{"class":83,"line":351},[81,843,844],{},"    def __init__(self, now: Callable[[], datetime] = lambda: datetime.now(timezone.utc)):\n",[81,846,847],{"class":83,"line":356},[81,848,849],{},"        self._now = now\n",[81,851,852],{"class":83,"line":362},[81,853,854],{},"        self.created = self._now()\n",[81,856,857],{"class":83,"line":368},[81,858,336],{"emptyLinePlaceholder":335},[81,860,861],{"class":83,"line":374},[81,862,863],{},"    def expired(self, ttl=timedelta(minutes=30)) -> bool:\n",[81,865,866],{"class":83,"line":379},[81,867,868],{},"        return self._now() > self.created + ttl\n",[81,870,871],{"class":83,"line":385},[81,872,336],{"emptyLinePlaceholder":335},[81,874,875],{"class":83,"line":391},[81,876,877],{},"def test_session_expiry_with_injected_clock():\n",[81,879,880],{"class":83,"line":397},[81,881,882],{},"    clock = iter([\n",[81,884,885],{"class":83,"line":403},[81,886,887],{},"        datetime(2026, 6, 18, 12, 0, tzinfo=timezone.utc),   # creation\n",[81,889,890],{"class":83,"line":409},[81,891,892],{},"        datetime(2026, 6, 18, 12, 31, tzinfo=timezone.utc),  # check, 31 min later\n",[81,894,896],{"class":83,"line":895},17,[81,897,898],{},"    ])\n",[81,900,902],{"class":83,"line":901},18,[81,903,904],{},"    session = Session(now=lambda: next(clock))\n",[81,906,908],{"class":83,"line":907},19,[81,909,910],{},"    assert session.expired() is True        # no freezing library needed\n",[31,912,914],{"id":913},"verification","Verification",[36,916,917,924,934,941],{},[39,918,919,920,923],{},"Run any time- or random-dependent test twice with ",[14,921,922],{},"pytest -p randomly"," (pytest-randomly) enabled; identical results prove no ambient state leaks in.",[39,925,926,927,21,930,933],{},"Re-run the suite under ",[14,928,929],{},"TZ=Pacific\u002FKiritimati pytest",[14,931,932],{},"TZ=Etc\u002FGMT+12 pytest","; a test that only passes in one timezone is reading the wall clock somewhere you missed.",[39,935,936,937,940],{},"For numpy code, assert with ",[14,938,939],{},"np.testing.assert_array_equal"," against a stored golden array rather than eyeballing floats.",[39,942,943,944,947,948,951,952,955],{},"Confirm ",[14,945,946],{},"freeze_time","\u002F",[14,949,950],{},"travel"," actually covers the call site by asserting ",[14,953,954],{},"datetime.now()"," inside the block equals the frozen instant before asserting business logic.",[31,957,959],{"id":958},"troubleshooting","Troubleshooting",[961,962,963,979],"table",{},[964,965,966],"thead",{},[967,968,969,973,976],"tr",{},[970,971,972],"th",{},"Symptom",[970,974,975],{},"Root cause",[970,977,978],{},"Fix",[980,981,982,1007,1023,1041,1058,1077],"tbody",{},[967,983,984,988,998],{},[985,986,987],"td",{},"Test fails only at certain times of day",[985,989,990,991,993,994,997],{},"Code reads ",[14,992,954],{}," \u002F ",[14,995,996],{},"time.time()"," un-frozen",[985,999,1000,1001,947,1003,1006],{},"Freeze the block with ",[14,1002,946],{},[14,1004,1005],{},"time_machine.travel",", or inject a clock",[967,1008,1009,1014,1017],{},[985,1010,1011,1013],{},[14,1012,946],{}," has no effect on a library call",[985,1015,1016],{},"The library is a C extension reading the libc clock directly",[985,1018,1019,1020,1022],{},"Switch to ",[14,1021,24],{},", which patches at the CPython clock level",[967,1024,1025,1031,1038],{},[985,1026,1027,1028],{},"Random test still flaky after ",[14,1029,1030],{},"random.seed",[985,1032,1033,1034,1037],{},"A second RNG (numpy, secrets, or a fresh ",[14,1035,1036],{},"Random()",") is unseeded",[985,1039,1040],{},"Seed every generator, or inject seeded instances so none is ambient",[967,1042,1043,1046,1051],{},[985,1044,1045],{},"numpy results differ across machines",[985,1047,1048,1050],{},[14,1049,663],{}," legacy global state was mutated elsewhere",[985,1052,1053,1054,1057],{},"Use ",[14,1055,1056],{},"np.random.default_rng(seed)"," and pass the Generator in",[967,1059,1060,1065,1070],{},[985,1061,1062,1063],{},"Golden assertion breaks on ",[14,1064,28],{},[985,1066,1067,1069],{},[14,1068,726],{}," is random per call",[985,1071,1072,1073,1076],{},"Patch ",[14,1074,1075],{},"uuid.uuid4"," or inject a counted id factory",[967,1078,1079,1082,1091],{},[985,1080,1081],{},"Frozen time leaks into the next test",[985,1083,1084,1087,1088],{},[14,1085,1086],{},"freeze_time().start()"," without ",[14,1089,1090],{},"stop()",[985,1092,1093],{},"Use the decorator\u002Fcontext-manager form so teardown is automatic",[31,1095,1097],{"id":1096},"frequently-asked-questions","Frequently Asked Questions",[10,1099,1100,1103,1104,1106,1107,1109],{},[135,1101,1102],{},"Why does my test pass locally but fail at midnight or in CI?","\nThe code reads the wall clock or a random source, so its output changes with the environment. Freeze time with ",[14,1105,20],{}," or ",[14,1108,24],{},", seed the RNG, and inject a clock so the test produces the same result regardless of when or where it runs.",[10,1111,1112,1115,1116,1118,1119,1121,1122,1124,1125,1127,1128,1130],{},[135,1113,1114],{},"Should I use freezegun or time-machine to freeze time?","\nUse ",[14,1117,20],{}," for broad compatibility and a simple API; use ",[14,1120,24],{}," when speed matters or when C-extension code calls the libc clock directly, because ",[14,1123,24],{}," patches at the CPython datetime\u002Fclock level and is far faster. ",[14,1126,20],{}," cannot intercept C code that bypasses Python's ",[14,1129,125],{}," module.",[10,1132,1133,1136,1137,1140,1141,1144,1145,1147],{},[135,1134,1135],{},"How do I make random and numpy produce the same values every run?","\nSeed both generators before the code runs: ",[14,1138,1139],{},"random.seed(0)"," for the stdlib, and a numpy Generator created with ",[14,1142,1143],{},"numpy.random.default_rng(0)"," rather than the legacy global ",[14,1146,663],{},". Prefer passing an explicit seeded Generator into the code so global state cannot leak between tests.",[10,1149,1150,1153,1154,1157,1158,1160],{},[135,1151,1152],{},"What is the testability seam for time?","\nAn injectable clock: the code takes a callable like ",[14,1155,1156],{},"now=datetime.now"," as a parameter or constructor argument instead of calling ",[14,1159,954],{}," directly. Tests pass a fixed clock, so no monkeypatching or freezing library is needed and the dependency is explicit.",[31,1162,1164],{"id":1163},"related-guides","Related guides",[36,1166,1167,1177,1185,1194,1211],{},[39,1168,1169,1170,1172,1173,1176],{},"The focused comparison in ",[109,1171,476],{"href":475}," shows exactly where each approach breaks and how ",[14,1174,1175],{},"tick()"," behaves.",[39,1178,1179,1180,1184],{},"The injectable clock is a special case of ",[109,1181,1183],{"href":1182},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002F","dependency injection for testability",", which generalizes the seam to any external dependency.",[39,1186,1187,1188,1190,1191,44],{},"When you do patch ",[14,1189,144],{}," directly, getting the target namespace right is the same discipline covered in ",[109,1192,1193],{"href":120},"patching strategies for complex codebases",[39,1195,1196,1197,947,1200,1203,1204,1206,1207,44],{},"For the underlying ",[14,1198,1199],{},"MagicMock",[14,1201,1202],{},"patch"," mechanics behind a deterministic ",[14,1205,28],{}," double, see the ",[109,1208,1210],{"href":1209},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdeep-dive-into-unittestmock\u002F","deep dive into unittest.mock",[39,1212,1213,1214,1218],{},"To generate randomized-but-reproducible inputs rather than seed a single sequence by hand, drive tests with ",[109,1215,1217],{"href":1216},"\u002Fproperty-based-fuzz-testing-strategies\u002F","property-based and fuzz testing strategies",", whose Hypothesis layer manages its own deterministic seeding.",[10,1220,1221,1222],{},"← Back to ",[109,1223,1225],{"href":1224},"\u002Fadvanced-mocking-test-doubles-in-python\u002F","Advanced Mocking & Test Doubles in Python",[1227,1228,1229],"style",{},"html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":77,"searchDepth":326,"depth":326,"links":1231},[1232,1233,1234,1242,1243,1244,1245],{"id":33,"depth":326,"text":34},{"id":129,"depth":326,"text":130},{"id":290,"depth":326,"text":291,"children":1235},[1236,1237,1238,1239,1240,1241],{"id":295,"depth":332,"text":296},{"id":479,"depth":332,"text":480},{"id":574,"depth":332,"text":575},{"id":652,"depth":332,"text":653},{"id":720,"depth":332,"text":721},{"id":807,"depth":332,"text":808},{"id":913,"depth":326,"text":914},{"id":958,"depth":326,"text":959},{"id":1096,"depth":326,"text":1097},{"id":1163,"depth":326,"text":1164},"Make time- and random-dependent Python tests deterministic: freezegun, time-machine, seeding random and numpy, deterministic uuid, and the injectable-clock seam.","md",{"slug":1249,"type":1250,"breadcrumb":1251,"datePublished":1252,"dateModified":1252,"faq":1253,"howto":1262},"controlling-time-and-randomness-in-tests","cluster","Time & Randomness","2026-06-18",[1254,1256,1258,1260],{"q":1102,"a":1255},"The code reads the wall clock or a random source, so its output changes with the environment. Freeze time with freezegun or time-machine, seed the RNG, and inject a clock so the test produces the same result regardless of when or where it runs.",{"q":1114,"a":1257},"Use freezegun for broad compatibility and a simple API; use time-machine when speed matters or when C-extension code calls the libc clock directly, because time-machine patches at the CPython datetime\u002Fclock level and is far faster. freezegun cannot intercept C code that bypasses Python's datetime module.",{"q":1135,"a":1259},"Seed both generators before the code runs: random.seed(0) for the stdlib, and a numpy Generator created with numpy.random.default_rng(0) rather than the legacy global numpy.random.seed. Prefer passing an explicit seeded Generator into the code so global state cannot leak between tests.",{"q":1152,"a":1261},"An injectable clock: the code takes a callable like now=datetime.now as a parameter or constructor argument instead of calling datetime.now() directly. Tests pass a fixed clock, so no monkeypatching or freezing library is needed and the dependency is explicit.",{"name":1263,"description":1264,"steps":1265},"How to control time and randomness in tests","Freeze the clock, seed every RNG, fix uuid generation, and prefer an injected clock so time- and random-dependent tests are deterministic.",[1266,1269,1272,1275,1278],{"name":1267,"text":1268},"Freeze the clock","Wrap the test with freeze_time or time_machine.travel so datetime.now, time.time, and date.today return a fixed instant.",{"name":1270,"text":1271},"Seed every random source","Call random.seed and create a seeded numpy Generator with default_rng so both the stdlib and numpy produce identical sequences each run.",{"name":1273,"text":1274},"Pin uuid generation","Patch uuid.uuid4 with a deterministic factory or pass an id generator into the code so generated identifiers are reproducible.",{"name":1276,"text":1277},"Prefer an injected clock","Refactor the code to accept a now callable or clock object so tests pass a fixed clock without any patching library.",{"name":1279,"text":1280},"Verify determinism","Run the test twice with pytest-randomly enabled and confirm identical results to prove no wall-clock or RNG state leaks in.","\u002Fadvanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests",{"title":5,"description":1246},"advanced-mocking-test-doubles-in-python\u002Fcontrolling-time-and-randomness-in-tests\u002Findex","VDnGdPNSr9o6NR0t6lw7j57WmR_7s9sdepKQuUWoqHA",1781793487885]