[{"data":1,"prerenderedAt":1197},["ShallowReactive",2],{"page-\u002Fintegration-database-and-service-testing\u002Ftest-data-factories-and-builders\u002Ffactory-boy-versus-plain-fixture-builders\u002F":3},{"id":4,"title":5,"body":6,"description":1160,"extension":1161,"meta":1162,"navigation":83,"path":1193,"seo":1194,"stem":1195,"__hash__":1196},"content\u002Fintegration-database-and-service-testing\u002Ftest-data-factories-and-builders\u002Ffactory-boy-versus-plain-fixture-builders\u002Findex.md","factory_boy versus Plain Fixture Builders",{"type":7,"value":8,"toc":1149},"minimark",[9,18,23,48,52,55,182,325,474,478,489,496,500,563,567,570,731,737,741,744,765,771,786,885,959,962,965,969,972,985,988,994,1069,1076,1080,1089,1101,1107,1111,1140,1145],[10,11,12,13,17],"p",{},"Every suite eventually has to decide how tests create their data, and the two serious options look similar enough that the choice is often made by whoever writes the first one. ",[14,15,16],"code",{},"factory_boy"," brings sequences, related-object creation, traits and ORM session handling; plain builder functions bring zero dependencies and nothing to learn. They are good at different things, and most mature suites use both.",[19,20,22],"h2",{"id":21},"prerequisites","Prerequisites",[24,25,26,33,45],"ul",{},[27,28,29,32],"li",{},[14,30,31],{},"factory_boy >= 3.3"," if it is under consideration; nothing extra for builders.",[27,34,35,38,39,44],{},[14,36,37],{},"pytest >= 8.0"," and, for persisted data, the transactional session from ",[40,41,43],"a",{"href":42},"\u002Fintegration-database-and-service-testing\u002Fdatabase-fixtures-and-transactional-tests\u002F","database fixtures and transactional tests",".",[27,46,47],{},"A clear view of which test data is persisted and which is merely constructed, because that distinction decides most of this.",[19,49,51],{"id":50},"solution","Solution",[10,53,54],{},"Use a builder where the data is a value, and a factory where it is a persisted graph.",[56,57,62],"pre",{"className":58,"code":59,"language":60,"meta":61,"style":61},"language-python shiki shiki-themes github-light github-dark","# Builders: values, payloads, configuration. No dependency, no session.\nfrom dataclasses import dataclass, replace\n\n\n@dataclass(frozen=True)\nclass ChargeRequest:\n    customer_id: str = \"cus_test\"\n    amount_minor: int = 1000\n    currency: str = \"GBP\"\n    idempotency_key: str = \"key-1\"\n\n\ndef a_charge(**overrides) -> ChargeRequest:\n    # replace() gives an immutable override; a typo is a TypeError here.\n    return replace(ChargeRequest(), **overrides)\n\n\ndef test_zero_amount_is_rejected(api):\n    response = api.post(\"\u002Fcharges\", json=a_charge(amount_minor=0).__dict__)\n    assert response.status_code == 422\n","python","",[14,63,64,72,78,85,90,96,102,108,114,120,126,131,136,142,148,154,159,164,170,176],{"__ignoreMap":61},[65,66,69],"span",{"class":67,"line":68},"line",1,[65,70,71],{},"# Builders: values, payloads, configuration. No dependency, no session.\n",[65,73,75],{"class":67,"line":74},2,[65,76,77],{},"from dataclasses import dataclass, replace\n",[65,79,81],{"class":67,"line":80},3,[65,82,84],{"emptyLinePlaceholder":83},true,"\n",[65,86,88],{"class":67,"line":87},4,[65,89,84],{"emptyLinePlaceholder":83},[65,91,93],{"class":67,"line":92},5,[65,94,95],{},"@dataclass(frozen=True)\n",[65,97,99],{"class":67,"line":98},6,[65,100,101],{},"class ChargeRequest:\n",[65,103,105],{"class":67,"line":104},7,[65,106,107],{},"    customer_id: str = \"cus_test\"\n",[65,109,111],{"class":67,"line":110},8,[65,112,113],{},"    amount_minor: int = 1000\n",[65,115,117],{"class":67,"line":116},9,[65,118,119],{},"    currency: str = \"GBP\"\n",[65,121,123],{"class":67,"line":122},10,[65,124,125],{},"    idempotency_key: str = \"key-1\"\n",[65,127,129],{"class":67,"line":128},11,[65,130,84],{"emptyLinePlaceholder":83},[65,132,134],{"class":67,"line":133},12,[65,135,84],{"emptyLinePlaceholder":83},[65,137,139],{"class":67,"line":138},13,[65,140,141],{},"def a_charge(**overrides) -> ChargeRequest:\n",[65,143,145],{"class":67,"line":144},14,[65,146,147],{},"    # replace() gives an immutable override; a typo is a TypeError here.\n",[65,149,151],{"class":67,"line":150},15,[65,152,153],{},"    return replace(ChargeRequest(), **overrides)\n",[65,155,157],{"class":67,"line":156},16,[65,158,84],{"emptyLinePlaceholder":83},[65,160,162],{"class":67,"line":161},17,[65,163,84],{"emptyLinePlaceholder":83},[65,165,167],{"class":67,"line":166},18,[65,168,169],{},"def test_zero_amount_is_rejected(api):\n",[65,171,173],{"class":67,"line":172},19,[65,174,175],{},"    response = api.post(\"\u002Fcharges\", json=a_charge(amount_minor=0).__dict__)\n",[65,177,179],{"class":67,"line":178},20,[65,180,181],{},"    assert response.status_code == 422\n",[56,183,185],{"className":58,"code":184,"language":60,"meta":61,"style":61},"# factory_boy: persisted models with relations, bound to the test session.\nimport factory\n\nfrom myapp.models import Customer, Order\n\n\nclass CustomerFactory(factory.alchemy.SQLAlchemyModelFactory):\n    class Meta:\n        model = Customer\n        sqlalchemy_session_persistence = \"flush\"   # never commit\n\n    email = factory.Sequence(lambda n: f\"customer-{n}@example.test\")\n    country = \"GB\"\n\n\nclass OrderFactory(factory.alchemy.SQLAlchemyModelFactory):\n    class Meta:\n        model = Order\n        sqlalchemy_session_persistence = \"flush\"\n\n    customer = factory.SubFactory(CustomerFactory)   # created and linked for you\n    status = \"open\"\n\n\ndef test_open_orders_are_listed(db_session, bind_factories):\n    OrderFactory.create_batch(3)\n    OrderFactory(status=\"cancelled\")\n    assert len(list_open_orders(db_session)) == 3\n",[14,186,187,192,197,201,206,210,214,219,224,229,234,238,243,248,252,256,261,265,270,275,279,285,291,296,301,307,313,319],{"__ignoreMap":61},[65,188,189],{"class":67,"line":68},[65,190,191],{},"# factory_boy: persisted models with relations, bound to the test session.\n",[65,193,194],{"class":67,"line":74},[65,195,196],{},"import factory\n",[65,198,199],{"class":67,"line":80},[65,200,84],{"emptyLinePlaceholder":83},[65,202,203],{"class":67,"line":87},[65,204,205],{},"from myapp.models import Customer, Order\n",[65,207,208],{"class":67,"line":92},[65,209,84],{"emptyLinePlaceholder":83},[65,211,212],{"class":67,"line":98},[65,213,84],{"emptyLinePlaceholder":83},[65,215,216],{"class":67,"line":104},[65,217,218],{},"class CustomerFactory(factory.alchemy.SQLAlchemyModelFactory):\n",[65,220,221],{"class":67,"line":110},[65,222,223],{},"    class Meta:\n",[65,225,226],{"class":67,"line":116},[65,227,228],{},"        model = Customer\n",[65,230,231],{"class":67,"line":122},[65,232,233],{},"        sqlalchemy_session_persistence = \"flush\"   # never commit\n",[65,235,236],{"class":67,"line":128},[65,237,84],{"emptyLinePlaceholder":83},[65,239,240],{"class":67,"line":133},[65,241,242],{},"    email = factory.Sequence(lambda n: f\"customer-{n}@example.test\")\n",[65,244,245],{"class":67,"line":138},[65,246,247],{},"    country = \"GB\"\n",[65,249,250],{"class":67,"line":144},[65,251,84],{"emptyLinePlaceholder":83},[65,253,254],{"class":67,"line":150},[65,255,84],{"emptyLinePlaceholder":83},[65,257,258],{"class":67,"line":156},[65,259,260],{},"class OrderFactory(factory.alchemy.SQLAlchemyModelFactory):\n",[65,262,263],{"class":67,"line":161},[65,264,223],{},[65,266,267],{"class":67,"line":166},[65,268,269],{},"        model = Order\n",[65,271,272],{"class":67,"line":172},[65,273,274],{},"        sqlalchemy_session_persistence = \"flush\"\n",[65,276,277],{"class":67,"line":178},[65,278,84],{"emptyLinePlaceholder":83},[65,280,282],{"class":67,"line":281},21,[65,283,284],{},"    customer = factory.SubFactory(CustomerFactory)   # created and linked for you\n",[65,286,288],{"class":67,"line":287},22,[65,289,290],{},"    status = \"open\"\n",[65,292,294],{"class":67,"line":293},23,[65,295,84],{"emptyLinePlaceholder":83},[65,297,299],{"class":67,"line":298},24,[65,300,84],{"emptyLinePlaceholder":83},[65,302,304],{"class":67,"line":303},25,[65,305,306],{},"def test_open_orders_are_listed(db_session, bind_factories):\n",[65,308,310],{"class":67,"line":309},26,[65,311,312],{},"    OrderFactory.create_batch(3)\n",[65,314,316],{"class":67,"line":315},27,[65,317,318],{},"    OrderFactory(status=\"cancelled\")\n",[65,320,322],{"class":67,"line":321},28,[65,323,324],{},"    assert len(list_open_orders(db_session)) == 3\n",[326,327,330,470],"figure",{"className":328},[329],"diagram",[331,332,339,340,339,344,339,348,339,356,339,366,339,375,339,381,339,388,339,391,339,397,339,401,339,407,339,411,339,414,339,418,339,421,339,424,339,428,339,431,339,435,339,439,339,442,339,445,339,449,339,452,339,455,339,461,339,464,339,467],"svg",{"viewBox":333,"role":334,"ariaLabelledBy":335,"xmlns":338},"0 0 820 268","img",[336,337],"fvb-t","fvb-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[341,342,343],"title",{"id":336},"What each approach handles for you",[345,346,347],"desc",{"id":337},"A comparison across five capabilities. Unique sequences, related-object creation, ORM session persistence and named traits are built into factory_boy and must be hand-written in a builder. Zero dependencies, immutability and plain-function readability are properties of builders that factory_boy does not offer.",[349,350],"rect",{"x":351,"y":351,"width":352,"height":353,"rx":354,"fill":355},"0","820","268","14","#fffdf8",[357,358,365],"text",{"x":359,"y":360,"textAnchor":361,"fontSize":362,"fontWeight":363,"fill":364},"410","28","middle","16","700","#3d405b","Different strengths, not a better and a worse",[349,367],{"x":368,"y":369,"width":370,"height":371,"rx":372,"fill":373,"stroke":364,"strokeWidth":374},"26","48","260","36","9","#f4f1de","1.5",[357,376,380],{"x":377,"y":378,"textAnchor":361,"fontSize":379,"fontWeight":363,"fill":364},"156","71","12","capability",[349,382],{"x":383,"y":369,"width":384,"height":371,"rx":372,"fill":385,"stroke":386,"strokeWidth":387},"296","240","#e6f0ea","#81b29a","2",[357,389,16],{"x":390,"y":378,"textAnchor":361,"fontSize":379,"fontWeight":363,"fill":364},"416",[349,392],{"x":393,"y":369,"width":394,"height":371,"rx":372,"fill":395,"stroke":396,"strokeWidth":387},"546","248","#f7f0da","#f2cc8f",[357,398,400],{"x":399,"y":378,"textAnchor":361,"fontSize":379,"fontWeight":363,"fill":364},"670","plain builder",[357,402,406],{"x":403,"y":404,"fontSize":405,"fill":364},"42","112","11.5","unique sequences",[357,408,410],{"x":390,"y":404,"textAnchor":361,"fontSize":405,"fill":409},"#2a5f49","built in",[357,412,413],{"x":399,"y":404,"textAnchor":361,"fontSize":405,"fill":364},"itertools.count",[357,415,417],{"x":403,"y":416,"fontSize":405,"fill":364},"142","related objects",[357,419,420],{"x":390,"y":416,"textAnchor":361,"fontSize":405,"fill":409},"SubFactory",[357,422,423],{"x":399,"y":416,"textAnchor":361,"fontSize":405,"fill":364},"call another builder",[357,425,427],{"x":403,"y":426,"fontSize":405,"fill":364},"172","ORM persistence",[357,429,430],{"x":390,"y":426,"textAnchor":361,"fontSize":405,"fill":409},"session + flush",[357,432,434],{"x":399,"y":426,"textAnchor":361,"fontSize":405,"fill":433},"#8a5a00","by hand",[357,436,438],{"x":403,"y":437,"fontSize":405,"fill":364},"202","immutability",[357,440,441],{"x":390,"y":437,"textAnchor":361,"fontSize":405,"fill":433},"not the model",[357,443,444],{"x":399,"y":437,"textAnchor":361,"fontSize":405,"fill":409},"frozen dataclass",[357,446,448],{"x":403,"y":447,"fontSize":405,"fill":364},"232","dependencies",[357,450,451],{"x":390,"y":447,"textAnchor":361,"fontSize":405,"fill":364},"one package",[357,453,454],{"x":399,"y":447,"textAnchor":361,"fontSize":405,"fill":409},"none",[67,456],{"x1":368,"y1":457,"x2":458,"y2":457,"stroke":459,"strokeWidth":460},"124","794","rgba(61,64,91,0.14)","1.4",[67,462],{"x1":368,"y1":463,"x2":458,"y2":463,"stroke":459,"strokeWidth":460},"154",[67,465],{"x1":368,"y1":466,"x2":458,"y2":466,"stroke":459,"strokeWidth":460},"184",[67,468],{"x1":368,"y1":469,"x2":458,"y2":469,"stroke":459,"strokeWidth":460},"214",[471,472,473],"figcaption",{},"The top three rows are where factory_boy saves real work; the bottom two are where builders are simply better. Persistence is the dividing line.",[19,475,477],{"id":476},"why-this-works","Why this works",[10,479,480,481,484,485,488],{},"A builder is the smallest thing that separates defaults from overrides. ",[14,482,483],{},"dataclasses.replace"," copies a frozen instance with a few fields changed, which is exactly the \"valid object, plus what this test is about\" shape. Because the dataclass is frozen, a test cannot mutate a shared default by accident, and because ",[14,486,487],{},"replace"," validates field names, a misspelt override fails at the call site.",[10,490,491,493,494,44],{},[14,492,16],{}," does the same for models and adds the parts that are genuinely tedious by hand: generating unique values per call, creating and linking related objects, and pushing everything through the ORM session so primary keys exist and relationships resolve. Reimplementing those in builders is possible and usually ends up as a smaller, less tested copy of ",[14,495,16],{},[19,497,499],{"id":498},"edge-cases-and-failure-modes","Edge cases and failure modes",[24,501,502,521,530,543,553],{},[27,503,504,508,509,512,513,516,517,44],{},[505,506,507],"strong",{},"Factories that commit."," ",[14,510,511],{},"sqlalchemy_session_persistence = \"commit\""," ends the test's transaction. Use ",[14,514,515],{},"flush",", as described in ",[40,518,520],{"href":519},"\u002Fintegration-database-and-service-testing\u002Fdatabase-fixtures-and-transactional-tests\u002Frolling-back-every-test-with-nested-transactions\u002F","rolling back every test with nested transactions",[27,522,523,526,527,529],{},[505,524,525],{},"SubFactory chains."," Each ",[14,528,420],{}," creates a row, so a three-level chain inserts several rows per call. Keep optional relations opt-in.",[27,531,532,535,536,539,540,44],{},[505,533,534],{},"Builders that grow a parameter per test."," A builder with fifteen keyword arguments is a god factory in disguise. Split it into named builders — ",[14,537,538],{},"an_overdue_invoice()",", ",[14,541,542],{},"a_refunded_charge()",[27,544,545,548,549,552],{},[505,546,547],{},"Mutable defaults in builders."," A default list or dict shared between calls leaks state between tests. Use ",[14,550,551],{},"field(default_factory=list)"," in the dataclass.",[27,554,555,558,559,44],{},[505,556,557],{},"Random defaults."," Anything asserted on must be deterministic; see ",[40,560,562],{"href":561},"\u002Fintegration-database-and-service-testing\u002Ftest-data-factories-and-builders\u002Fgenerating-reproducible-fake-data-with-faker\u002F","generating reproducible fake data with Faker",[19,564,566],{"id":565},"writing-a-builder-that-scales","Writing a builder that scales",[10,568,569],{},"The naive builder — a function with keyword defaults returning a dict — works until the object has nested parts. A small amount of structure keeps it readable as the domain grows.",[56,571,573],{"className":58,"code":572,"language":60,"meta":61,"style":61},"from dataclasses import dataclass, field, replace\nfrom itertools import count\n\n_ids = count(1)\n\n\n@dataclass(frozen=True)\nclass LineItem:\n    sku: str = \"SKU-1\"\n    quantity: int = 1\n    unit_minor: int = 500\n\n\n@dataclass(frozen=True)\nclass OrderPayload:\n    order_id: str = field(default_factory=lambda: f\"ord-{next(_ids)}\")  # unique\n    currency: str = \"GBP\"\n    lines: tuple[LineItem, ...] = (LineItem(),)                         # immutable\n\n\ndef an_order(*, lines=None, **overrides) -> OrderPayload:\n    if lines is not None:\n        overrides[\"lines\"] = tuple(lines)\n    return replace(OrderPayload(), **overrides)\n\n\ndef a_line(**overrides) -> LineItem:\n    return replace(LineItem(), **overrides)\n\n\ndef test_total_across_lines(pricing):\n    order = an_order(lines=[a_line(quantity=2), a_line(unit_minor=250)])\n    assert pricing.total(order) == 1250\n",[14,574,575,580,585,589,594,598,602,606,611,616,621,626,630,634,638,643,648,652,657,661,665,670,675,680,685,689,693,698,703,708,713,719,725],{"__ignoreMap":61},[65,576,577],{"class":67,"line":68},[65,578,579],{},"from dataclasses import dataclass, field, replace\n",[65,581,582],{"class":67,"line":74},[65,583,584],{},"from itertools import count\n",[65,586,587],{"class":67,"line":80},[65,588,84],{"emptyLinePlaceholder":83},[65,590,591],{"class":67,"line":87},[65,592,593],{},"_ids = count(1)\n",[65,595,596],{"class":67,"line":92},[65,597,84],{"emptyLinePlaceholder":83},[65,599,600],{"class":67,"line":98},[65,601,84],{"emptyLinePlaceholder":83},[65,603,604],{"class":67,"line":104},[65,605,95],{},[65,607,608],{"class":67,"line":110},[65,609,610],{},"class LineItem:\n",[65,612,613],{"class":67,"line":116},[65,614,615],{},"    sku: str = \"SKU-1\"\n",[65,617,618],{"class":67,"line":122},[65,619,620],{},"    quantity: int = 1\n",[65,622,623],{"class":67,"line":128},[65,624,625],{},"    unit_minor: int = 500\n",[65,627,628],{"class":67,"line":133},[65,629,84],{"emptyLinePlaceholder":83},[65,631,632],{"class":67,"line":138},[65,633,84],{"emptyLinePlaceholder":83},[65,635,636],{"class":67,"line":144},[65,637,95],{},[65,639,640],{"class":67,"line":150},[65,641,642],{},"class OrderPayload:\n",[65,644,645],{"class":67,"line":156},[65,646,647],{},"    order_id: str = field(default_factory=lambda: f\"ord-{next(_ids)}\")  # unique\n",[65,649,650],{"class":67,"line":161},[65,651,119],{},[65,653,654],{"class":67,"line":166},[65,655,656],{},"    lines: tuple[LineItem, ...] = (LineItem(),)                         # immutable\n",[65,658,659],{"class":67,"line":172},[65,660,84],{"emptyLinePlaceholder":83},[65,662,663],{"class":67,"line":178},[65,664,84],{"emptyLinePlaceholder":83},[65,666,667],{"class":67,"line":281},[65,668,669],{},"def an_order(*, lines=None, **overrides) -> OrderPayload:\n",[65,671,672],{"class":67,"line":287},[65,673,674],{},"    if lines is not None:\n",[65,676,677],{"class":67,"line":293},[65,678,679],{},"        overrides[\"lines\"] = tuple(lines)\n",[65,681,682],{"class":67,"line":298},[65,683,684],{},"    return replace(OrderPayload(), **overrides)\n",[65,686,687],{"class":67,"line":303},[65,688,84],{"emptyLinePlaceholder":83},[65,690,691],{"class":67,"line":309},[65,692,84],{"emptyLinePlaceholder":83},[65,694,695],{"class":67,"line":315},[65,696,697],{},"def a_line(**overrides) -> LineItem:\n",[65,699,700],{"class":67,"line":321},[65,701,702],{},"    return replace(LineItem(), **overrides)\n",[65,704,706],{"class":67,"line":705},29,[65,707,84],{"emptyLinePlaceholder":83},[65,709,711],{"class":67,"line":710},30,[65,712,84],{"emptyLinePlaceholder":83},[65,714,716],{"class":67,"line":715},31,[65,717,718],{},"def test_total_across_lines(pricing):\n",[65,720,722],{"class":67,"line":721},32,[65,723,724],{},"    order = an_order(lines=[a_line(quantity=2), a_line(unit_minor=250)])\n",[65,726,728],{"class":67,"line":727},33,[65,729,730],{},"    assert pricing.total(order) == 1250\n",[10,732,733,734,736],{},"Tuples rather than lists for nested collections keep the whole structure immutable, and ",[14,735,413],{}," gives the one feature of factory sequences that builders most often need. Beyond that, a builder module stays a plain Python file anyone can read in a minute, which is its main advantage over a factory class hierarchy.",[19,738,740],{"id":739},"readability-at-the-call-site","Readability at the call site",[10,742,743],{},"Whichever mechanism is chosen, the property worth optimising is how a test reads, because tests are read far more often than they are written. Three conventions make a large difference and apply equally to factories and builders.",[10,745,746,508,749,539,752,539,754,756,757,760,761,764],{},[505,747,748],{},"Name builders after the domain, with an article.",[14,750,751],{},"an_order()",[14,753,542],{},[14,755,538],{}," read as prose inside a test and make the scenario obvious. ",[14,758,759],{},"make_order()"," and ",[14,762,763],{},"order_factory()"," describe the mechanism rather than the thing.",[10,766,767,770],{},[505,768,769],{},"Pass only what the test is about."," If a test is about VAT exemption, the only override should be the exemption. Every additional keyword is a claim that the test depends on that value, and readers take such claims seriously. A test with six overrides where one matters teaches the reader nothing about which one.",[10,772,773,508,776,778,779,782,783,785],{},[505,774,775],{},"Prefer named variants to flags.",[14,777,538],{}," is clearer than ",[14,780,781],{},"an_invoice(overdue=True)"," once the variant involves more than one field, because the builder can set the due date, the status and the reminder count consistently. With ",[14,784,16],{}," the same idea is a trait; with builders it is a second function that calls the first.",[56,787,789],{"className":58,"code":788,"language":60,"meta":61,"style":61},"from datetime import date, timedelta\n\n\ndef an_invoice(**overrides) -> Invoice:\n    return replace(Invoice(), **overrides)\n\n\ndef an_overdue_invoice(**overrides) -> Invoice:\n    # One place that knows what \"overdue\" means: status, date and reminders agree.\n    defaults = dict(\n        status=\"open\",\n        due_on=date(2026, 1, 1) - timedelta(days=30),\n        reminders_sent=2,\n    )\n    return an_invoice(**{**defaults, **overrides})\n\n\ndef test_overdue_invoices_are_escalated(escalation):\n    assert escalation.should_escalate(an_overdue_invoice())\n    assert not escalation.should_escalate(an_invoice())\n",[14,790,791,796,800,804,809,814,818,822,827,832,837,842,847,852,857,862,866,870,875,880],{"__ignoreMap":61},[65,792,793],{"class":67,"line":68},[65,794,795],{},"from datetime import date, timedelta\n",[65,797,798],{"class":67,"line":74},[65,799,84],{"emptyLinePlaceholder":83},[65,801,802],{"class":67,"line":80},[65,803,84],{"emptyLinePlaceholder":83},[65,805,806],{"class":67,"line":87},[65,807,808],{},"def an_invoice(**overrides) -> Invoice:\n",[65,810,811],{"class":67,"line":92},[65,812,813],{},"    return replace(Invoice(), **overrides)\n",[65,815,816],{"class":67,"line":98},[65,817,84],{"emptyLinePlaceholder":83},[65,819,820],{"class":67,"line":104},[65,821,84],{"emptyLinePlaceholder":83},[65,823,824],{"class":67,"line":110},[65,825,826],{},"def an_overdue_invoice(**overrides) -> Invoice:\n",[65,828,829],{"class":67,"line":116},[65,830,831],{},"    # One place that knows what \"overdue\" means: status, date and reminders agree.\n",[65,833,834],{"class":67,"line":122},[65,835,836],{},"    defaults = dict(\n",[65,838,839],{"class":67,"line":128},[65,840,841],{},"        status=\"open\",\n",[65,843,844],{"class":67,"line":133},[65,845,846],{},"        due_on=date(2026, 1, 1) - timedelta(days=30),\n",[65,848,849],{"class":67,"line":138},[65,850,851],{},"        reminders_sent=2,\n",[65,853,854],{"class":67,"line":144},[65,855,856],{},"    )\n",[65,858,859],{"class":67,"line":150},[65,860,861],{},"    return an_invoice(**{**defaults, **overrides})\n",[65,863,864],{"class":67,"line":156},[65,865,84],{"emptyLinePlaceholder":83},[65,867,868],{"class":67,"line":161},[65,869,84],{"emptyLinePlaceholder":83},[65,871,872],{"class":67,"line":166},[65,873,874],{},"def test_overdue_invoices_are_escalated(escalation):\n",[65,876,877],{"class":67,"line":172},[65,878,879],{},"    assert escalation.should_escalate(an_overdue_invoice())\n",[65,881,882],{"class":67,"line":178},[65,883,884],{},"    assert not escalation.should_escalate(an_invoice())\n",[326,886,888,956],{"className":887},[329],[331,889,339,894,339,897,339,900,339,904,339,909,339,914,339,919,339,927,339,931,339,935,339,938,339,941,339,945,339,948,339,950,339,953],{"viewBox":890,"role":334,"ariaLabelledBy":891,"xmlns":338},"0 0 800 226",[892,893],"rd-t","rd-d",[341,895,896],{"id":892},"A flag-driven builder versus a named variant",[345,898,899],{"id":893},"Two call sites for the same scenario. The flag form passes overdue equals true and leaves the reader to trust that the builder sets the related fields consistently. The named variant an_overdue_invoice states the scenario in the domain's own words and owns the consistency between status, due date and reminder count.",[349,901],{"x":351,"y":351,"width":902,"height":903,"rx":354,"fill":355},"800","226",[357,905,908],{"x":906,"y":360,"textAnchor":361,"fontSize":907,"fontWeight":363,"fill":364},"400","15.5","The call site is the documentation",[349,910],{"x":368,"y":911,"width":912,"height":913,"rx":379,"fill":395,"stroke":396,"strokeWidth":387},"50","360","152",[357,915,918],{"x":916,"y":917,"textAnchor":361,"fontSize":379,"fontWeight":363,"fill":364},"206","76","flag",[349,920],{"x":921,"y":922,"width":923,"height":924,"rx":925,"fill":355,"stroke":926,"strokeWidth":460},"46","90","320","34","8","rgba(61,64,91,0.35)",[357,928,930],{"x":916,"y":404,"textAnchor":361,"fontSize":929,"fill":364},"11","an_invoice(overdue=True, reminders=2)",[357,932,934],{"x":921,"y":933,"fontSize":929,"fill":433},"150","reader must trust the fields agree",[357,936,937],{"x":921,"y":426,"fontSize":929,"fill":364},"and the flag list keeps growing",[349,939],{"x":940,"y":911,"width":912,"height":913,"rx":379,"fill":385,"stroke":386,"strokeWidth":387},"414",[357,942,944],{"x":943,"y":917,"textAnchor":361,"fontSize":379,"fontWeight":363,"fill":364},"594","named variant",[349,946],{"x":947,"y":922,"width":923,"height":924,"rx":925,"fill":355,"stroke":926,"strokeWidth":460},"434",[357,949,538],{"x":943,"y":404,"textAnchor":361,"fontSize":929,"fill":364},[357,951,952],{"x":947,"y":933,"fontSize":929,"fill":409},"one place defines \"overdue\"",[357,954,955],{"x":947,"y":426,"fontSize":929,"fill":364},"the test reads as the scenario",[471,957,958],{},"When the definition of \"overdue\" changes, one function changes, and every test using it follows without edits.",[10,960,961],{},"These conventions matter more than the choice of library. A suite of well-named builders reads better than one of carelessly used factories, and the reverse is equally true.",[10,963,964],{},"A useful review check follows from all of this. When a pull request adds a test, read only the arrangement lines and ask what scenario they describe. If the answer is obvious from the builder and factory names alone — \"an overdue invoice for an exempt customer\" — the data layer is doing its job. If the answer requires reading the builder's implementation or counting keyword arguments, the test is about to become one of the ones nobody wants to touch, and a new named variant is cheaper to add now than to extract later. Applied consistently, that single question keeps both factories and builders small, named and honest, which is most of what separates a data layer that helps from one that has to be worked around.",[19,966,968],{"id":967},"deciding-for-a-real-suite","Deciding for a real suite",[10,970,971],{},"The choice is rarely all-or-nothing, and a short inventory settles it quickly. List every kind of object tests currently construct, and mark two things for each: whether it is persisted through the ORM, and how many related objects a typical test needs alongside it.",[10,973,974,975,977,978,980,981,984],{},"Persisted objects with relations — orders with customers and lines, subscriptions with plans and invoices — are where ",[14,976,16],{}," pays back immediately. ",[14,979,420],{},", session binding and ",[14,982,983],{},"create_batch"," remove exactly the code that makes hand-written setup long and error-prone.",[10,986,987],{},"Unpersisted values — request payloads, event messages, configuration, domain value objects — gain nothing from a factory class. A builder is shorter, has no dependency, and is immutable by construction.",[10,989,990,991,993],{},"Persisted objects without relations sit in the middle, and either works. The tiebreaker is consistency: if the suite already uses ",[14,992,16],{}," for the related models, using it for the flat ones too keeps one vocabulary for \"things in the database\".",[326,995,997,1066],{"className":996},[329],[331,998,339,1003,339,1006,339,1009,339,1011,339,1014,339,1019,339,1023,339,1027,339,1031,339,1038,339,1043,339,1046,339,1049,339,1051,339,1054,339,1058,339,1063],{"viewBox":999,"role":334,"ariaLabelledBy":1000,"xmlns":338},"0 0 800 240",[1001,1002],"inv-t","inv-d",[341,1004,1005],{"id":1001},"Choosing per kind of test data",[345,1007,1008],{"id":1002},"A two-by-two grid by persistence and relation depth. Persisted and related data suits factory_boy. Unpersisted values suit plain builders regardless of depth. Persisted flat data can use either, with consistency with the rest of the suite as the tiebreaker.",[349,1010],{"x":351,"y":351,"width":902,"height":384,"rx":354,"fill":355},[357,1012,1013],{"x":906,"y":360,"textAnchor":361,"fontSize":907,"fontWeight":363,"fill":364},"Persistence decides; depth confirms",[357,1015,1018],{"x":1016,"y":1017,"textAnchor":361,"fontSize":405,"fontWeight":363,"fill":364},"300","62","flat",[357,1020,1022],{"x":1021,"y":1017,"textAnchor":361,"fontSize":405,"fontWeight":363,"fill":364},"590","with relations",[357,1024,1026],{"x":1025,"y":404,"textAnchor":361,"fontSize":405,"fontWeight":363,"fill":364},"96","persisted",[357,1028,1030],{"x":1025,"y":1029,"textAnchor":361,"fontSize":405,"fontWeight":363,"fill":364},"186","value only",[349,1032],{"x":1033,"y":1034,"width":1035,"height":1036,"rx":1037,"fill":395,"stroke":396,"strokeWidth":387},"176","74","250","70","10",[357,1039,1042],{"x":1040,"y":1041,"textAnchor":361,"fontSize":405,"fontWeight":363,"fill":364},"301","104","either",[357,1044,1045],{"x":1040,"y":457,"textAnchor":361,"fontSize":929,"fill":364},"match the rest of the suite",[349,1047],{"x":1048,"y":1034,"width":1016,"height":1036,"rx":1037,"fill":385,"stroke":386,"strokeWidth":387},"440",[357,1050,16],{"x":1021,"y":1041,"textAnchor":361,"fontSize":405,"fontWeight":363,"fill":364},[357,1052,1053],{"x":1021,"y":457,"textAnchor":361,"fontSize":929,"fill":409},"SubFactory and session binding pay back",[349,1055],{"x":1033,"y":913,"width":1056,"height":1057,"rx":1037,"fill":385,"stroke":386,"strokeWidth":387},"564","68",[357,1059,1062],{"x":1060,"y":1061,"textAnchor":361,"fontSize":405,"fontWeight":363,"fill":364},"458","182","plain builders",[357,1064,1065],{"x":1060,"y":437,"textAnchor":361,"fontSize":929,"fill":409},"frozen dataclasses with replace(), no dependency",[471,1067,1068],{},"The bottom row is the one teams most often get wrong in the other direction, writing factory classes for payloads that would be clearer as a function.",[10,1070,1071,1072,1075],{},"Once decided, put both in one test-support module per domain area — ",[14,1073,1074],{},"tests\u002Fsupport\u002Fbilling.py"," holding the billing factories and builders together — so a reader looking for \"how do I make an invoice for a test\" finds one place regardless of which mechanism is behind it.",[19,1077,1079],{"id":1078},"frequently-asked-questions","Frequently Asked Questions",[10,1081,1082,1085,1086,1088],{},[505,1083,1084],{},"Is factory_boy worth the dependency for a small project?","\nUsually not until the project has several related models that tests create together. A dozen builder functions with keyword defaults cover a small domain perfectly well. ",[14,1087,16],{}," earns its place when related-object creation, sequences and traits would otherwise be reimplemented by hand in each builder.",[10,1090,1091,1094,1095,1097,1098,1100],{},[505,1092,1093],{},"Can factories and builders coexist in one suite?","\nYes, and they often should: ",[14,1096,16],{}," for persisted ORM models, plain builders for value objects, request payloads and configuration. The split follows persistence — anything that must be flushed through a session benefits from ",[14,1099,16],{},"'s session handling.",[10,1102,1103,1106],{},[505,1104,1105],{},"Do builders need to be pytest fixtures?","\nNo. A builder is an ordinary function a test calls with its own arguments. Making it a fixture removes the ability to pass per-test overrides without a factory-as-fixture indirection, which is extra ceremony for no benefit.",[19,1108,1110],{"id":1109},"related","Related",[24,1112,1113,1120,1126,1133],{},[27,1114,1115,1119],{},[40,1116,1118],{"href":1117},"\u002Fintegration-database-and-service-testing\u002Ftest-data-factories-and-builders\u002F","Test Data Factories & Builders"," — traits, sequences and the decay modes of both approaches.",[27,1121,1122,1125],{},[40,1123,1124],{"href":561},"Generating Reproducible Fake Data with Faker"," — keeping generated values deterministic.",[27,1127,1128,1132],{},[40,1129,1131],{"href":1130},"\u002Fadvanced-mocking-test-doubles-in-python\u002Fdependency-injection-for-testability\u002Fwiring-test-doubles-through-a-factory-function\u002F","Wiring Test Doubles Through a Factory Function"," — the same defaults-plus-overrides idea for collaborators.",[27,1134,1135,1139],{},[40,1136,1138],{"href":1137},"\u002Fproperty-based-fuzz-testing-strategies\u002Fdesigning-strategies-for-domain-data\u002F","Designing Strategies for Domain Data"," — when the goal is exploring inputs rather than filling fields.",[10,1141,1142,1143],{},"← Back to ",[40,1144,1118],{"href":1117},[1146,1147,1148],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":61,"searchDepth":74,"depth":74,"links":1150},[1151,1152,1153,1154,1155,1156,1157,1158,1159],{"id":21,"depth":74,"text":22},{"id":50,"depth":74,"text":51},{"id":476,"depth":74,"text":477},{"id":498,"depth":74,"text":499},{"id":565,"depth":74,"text":566},{"id":739,"depth":74,"text":740},{"id":967,"depth":74,"text":968},{"id":1078,"depth":74,"text":1079},{"id":1109,"depth":74,"text":1110},"Choose between factory_boy and hand-written builder functions for test data: persistence handling, related objects, traits, readability, and when each one wins.","md",{"slug":1163,"type":1164,"breadcrumb":1165,"datePublished":1166,"dateModified":1166,"faq":1167,"howto":1174},"factory-boy-versus-plain-fixture-builders","article","factory_boy vs Builders","2026-09-18",[1168,1170,1172],{"q":1084,"a":1169},"Usually not until the project has several related models that tests create together. A dozen builder functions with keyword defaults cover a small domain perfectly well. factory_boy earns its place when related-object creation, sequences and traits would otherwise be reimplemented by hand in each builder.",{"q":1093,"a":1171},"Yes, and they often should: factory_boy for persisted ORM models, plain builders for value objects, request payloads and configuration. The split follows persistence — anything that must be flushed through a session benefits from factory_boy's session handling.",{"q":1105,"a":1173},"No. A builder is an ordinary function a test calls with its own arguments. Making it a fixture removes the ability to pass per-test overrides without a factory-as-fixture indirection, which is extra ceremony for no benefit.",{"name":1175,"description":1176,"steps":1177},"How to choose between factory_boy and plain builders","Decide by persistence and object-graph depth, then keep defaults valid and overrides minimal in whichever you pick.",[1178,1181,1184,1187,1190],{"name":1179,"text":1180},"Classify the data","Separate persisted, related ORM models from plain values such as payloads, config and value objects.",{"name":1182,"text":1183},"Use builders for values","Write functions returning a frozen dataclass with defaults, using dataclasses.replace for overrides.",{"name":1185,"text":1186},"Use factory_boy for persisted graphs","Define SQLAlchemy or Django factories bound to the test session with flush persistence.",{"name":1188,"text":1189},"Keep defaults valid and deterministic","Every default produces a valid object and nothing asserted on is random.",{"name":1191,"text":1192},"Review usage periodically","Delete unused factories and split any that have grown parameters for every scenario.","\u002Fintegration-database-and-service-testing\u002Ftest-data-factories-and-builders\u002Ffactory-boy-versus-plain-fixture-builders",{"title":5,"description":1160},"integration-database-and-service-testing\u002Ftest-data-factories-and-builders\u002Ffactory-boy-versus-plain-fixture-builders\u002Findex","VFQwqsl_KWlgCeL8aktj73Ng_ZAiyia7vZozIJryDVE",1789718769565]