[{"data":1,"prerenderedAt":2921},["ShallowReactive",2],{"blog-post-integration-tests-that-tell-stories":3,"blog-post-comments-/blog/integration-tests-that-tell-stories":2913},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"tags":10,"created":14,"excerpt":15,"body":35,"_type":2907,"_id":2908,"_source":2909,"_file":2910,"_stem":2911,"_extension":2912},"/blog/integration-tests-that-tell-stories","blog",false,"","Integration Tests That Tell Stories","Back in 2018, I wrote about my preferred integration testing strategy. The core idea was simple: run your services in Docker containers, test them as black boxes, and keep tests close to production reality. Seven years later, I still believe in that philosophy, but my approach has matured significantly. Here I want to share what I've learned about writing integration tests that are not just effective, but actually pleasant to maintain.",[11,12,13],"testing","java","spring-boot","2026-02-04 19:10:00 +0100",{"type":16,"children":17},"root",[18],{"type":19,"tag":20,"props":21,"children":22},"element","p",{},[23,26,33],{"type":24,"value":25},"text","Back in 2018, I wrote about ",{"type":19,"tag":27,"props":28,"children":30},"a",{"href":29},"/blog/my-preferred-integration-test-strategy",[31],{"type":24,"value":32},"my preferred integration testing strategy",{"type":24,"value":34},". The core idea was simple: run your services in Docker containers, test them as black boxes, and keep tests close to production reality. Seven years later, I still believe in that philosophy, but my approach has matured significantly. Here I want to share what I've learned about writing integration tests that are not just effective, but actually pleasant to maintain.",{"type":16,"children":36,"toc":2895},[37,46,53,58,141,146,152,157,162,211,216,222,227,236,241,246,258,1081,1086,1092,1097,1404,1409,1414,1734,1740,1745,1750,1762,1767,1969,1974,1980,1991,1996,2491,2496,2502,2507,2512,2520,2525,2531,2536,2730,2743,2749,2754,2764,2790,2824,2830,2835,2879,2884,2889],{"type":19,"tag":20,"props":38,"children":39},{},[40,41,45],{"type":24,"value":25},{"type":19,"tag":27,"props":42,"children":43},{"href":29},[44],{"type":24,"value":32},{"type":24,"value":34},{"type":19,"tag":47,"props":48,"children":50},"h2",{"id":49},"the-stack",[51],{"type":24,"value":52},"The Stack",{"type":19,"tag":20,"props":54,"children":55},{},[56],{"type":24,"value":57},"Before diving in, let me be upfront about the technology stack. This approach is built around Spring Boot and its ecosystem:",{"type":19,"tag":59,"props":60,"children":61},"ul",{},[62,74,84,94,104,121,131],{"type":19,"tag":63,"props":64,"children":65},"li",{},[66,72],{"type":19,"tag":67,"props":68,"children":69},"strong",{},[70],{"type":24,"value":71},"Spring Boot Test",{"type":24,"value":73}," - bootstraps the application context",{"type":19,"tag":63,"props":75,"children":76},{},[77,82],{"type":19,"tag":67,"props":78,"children":79},{},[80],{"type":24,"value":81},"TestContainers",{"type":24,"value":83}," - spins up real databases (MySQL, PostgreSQL, Redis) in Docker",{"type":19,"tag":63,"props":85,"children":86},{},[87,92],{"type":19,"tag":67,"props":88,"children":89},{},[90],{"type":24,"value":91},"WireMock",{"type":24,"value":93}," - mocks external HTTP services",{"type":19,"tag":63,"props":95,"children":96},{},[97,102],{"type":19,"tag":67,"props":98,"children":99},{},[100],{"type":24,"value":101},"REST Assured",{"type":24,"value":103}," - makes HTTP calls to your API with a fluent syntax",{"type":19,"tag":63,"props":105,"children":106},{},[107,112,114],{"type":19,"tag":67,"props":108,"children":109},{},[110],{"type":24,"value":111},"JsonUnit",{"type":24,"value":113}," - powerful JSON assertions with ",{"type":19,"tag":115,"props":116,"children":118},"code",{"className":117},[],[119],{"type":24,"value":120},"assertThatJson()",{"type":19,"tag":63,"props":122,"children":123},{},[124,129],{"type":19,"tag":67,"props":125,"children":126},{},[127],{"type":24,"value":128},"Awaitility",{"type":24,"value":130}," - handles async operations with timeouts",{"type":19,"tag":63,"props":132,"children":133},{},[134,139],{"type":19,"tag":67,"props":135,"children":136},{},[137],{"type":24,"value":138},"JUnit 5",{"type":24,"value":140}," - test framework with extensions",{"type":19,"tag":20,"props":142,"children":143},{},[144],{"type":24,"value":145},"The downside of this approach compared to my 2018 pure-Docker setup? It's bound to a specific tech stack. If you're in an early-stage startup where the tech stack isn't finalized, or you're planning a migration from Spring Boot to something else, the container-based approach offers better isolation and portability. But if Spring Boot is your home, this setup has much less friction for local development and gives you nicer control over things like mocks and assertions.",{"type":19,"tag":47,"props":147,"children":149},{"id":148},"the-problem-with-most-integration-tests",[150],{"type":24,"value":151},"The Problem with Most Integration Tests",{"type":19,"tag":20,"props":153,"children":154},{},[155],{"type":24,"value":156},"Let me start with what typically goes wrong. You've seen this pattern: a test class with dozens of test methods, each one setting up mocks, making calls, and asserting on results. Six months later, someone refactors the service layer, and suddenly half your tests break - not because the behavior changed, but because the internal implementation did.",{"type":19,"tag":20,"props":158,"children":159},{},[160],{"type":24,"value":161},"This happens because most integration tests are written at the wrong level of abstraction. What do I mean by \"wrong level\"? Things like:",{"type":19,"tag":59,"props":163,"children":164},{},[165,183,201],{"type":19,"tag":63,"props":166,"children":167},{},[168,173,175,181],{"type":19,"tag":67,"props":169,"children":170},{},[171],{"type":24,"value":172},"Mocking dependency classes instead of APIs.",{"type":24,"value":174}," You inject a mocked ",{"type":19,"tag":115,"props":176,"children":178},{"className":177},[],[179],{"type":24,"value":180},"PaymentService",{"type":24,"value":182}," instead of letting it run and mocking the actual Stripe API it calls.",{"type":19,"tag":63,"props":184,"children":185},{},[186,191,193,199],{"type":19,"tag":67,"props":187,"children":188},{},[189],{"type":24,"value":190},"Injecting mocked beans.",{"type":24,"value":192}," Spring makes it easy to ",{"type":19,"tag":115,"props":194,"children":196},{"className":195},[],[197],{"type":24,"value":198},"@MockBean",{"type":24,"value":200}," your way through tests, but now your test knows about internal wiring.",{"type":19,"tag":63,"props":202,"children":203},{},[204,209],{"type":19,"tag":67,"props":205,"children":206},{},[207],{"type":24,"value":208},"Removing parts of the code from the test flow.",{"type":24,"value":210}," You disable the email sender, skip the audit logging, bypass the cache - and now you're not testing what actually runs in production.",{"type":19,"tag":20,"props":212,"children":213},{},[214],{"type":24,"value":215},"These tests are brittle, hard to read, and nobody trusts them.",{"type":19,"tag":47,"props":217,"children":219},{"id":218},"tests-as-stories",[220],{"type":24,"value":221},"Tests as Stories",{"type":19,"tag":20,"props":223,"children":224},{},[225],{"type":24,"value":226},"The insight that changed everything for me was thinking about tests as stories. A well-written test should read like a user story:",{"type":19,"tag":228,"props":229,"children":230},"blockquote",{},[231],{"type":19,"tag":20,"props":232,"children":233},{},[234],{"type":24,"value":235},"\"Given a customer with items in their cart, when they proceed to checkout, then an order is created and payment is processed.\"",{"type":19,"tag":20,"props":237,"children":238},{},[239],{"type":24,"value":240},"When you write tests this way, something interesting happens: the test becomes documentation. New team members can read your test suite and understand what the system does without diving into the implementation.",{"type":19,"tag":20,"props":242,"children":243},{},[244],{"type":24,"value":245},"We've taken this further: our frontend developers, who don't know Java, use these tests to understand backend functionality. They read the test names, see the HTTP requests being made, observe the expected responses - and suddenly they have the technical documentation that's usually missing. The JSON request and response bodies are right there in the test. No Swagger page that's three versions behind, no Confluence document that nobody updates.",{"type":19,"tag":20,"props":247,"children":248},{},[249,251,256],{"type":24,"value":250},"But here's the key insight: ",{"type":19,"tag":67,"props":252,"children":253},{},[254],{"type":24,"value":255},"knowing what to show explicitly versus what to hide",{"type":24,"value":257},". The focus of each test - the \"story\" - should be written out explicitly in the test body. The supporting code - setup, common mocks, utilities - should be abstracted away. These are the boring parts that distract from the story.",{"type":19,"tag":259,"props":260,"children":263},"pre",{"code":261,"language":12,"meta":7,"className":262,"style":7},"@Test\nvoid checkout_shall_createOrderAndProcessPayment() {\n    // given: customer with items in cart (use fixture - not the focus)\n    var customerId = ApiFixtures.createCustomer(authToken);\n    ApiFixtures.addToCart(customerId, \"product-123\", 2, authToken);\n\n    // when: checkout (THIS is the focus - show explicitly)\n    var response = given()\n        .accept(ContentType.JSON)\n        .contentType(ContentType.JSON)\n        .header(\"Authorization\", \"Bearer \" + authToken)\n        .body(\"\"\"\n            {\n                \"paymentMethod\": \"card\",\n                \"shippingAddress\": {\n                    \"street\": \"123 Main St\",\n                    \"city\": \"Boston\"\n                }\n            }\n            \"\"\")\n        .when()\n        .post(\"/v1/customers/{customerId}/checkout\", customerId)\n        .then()\n        .statusCode(201)\n        .extract()\n        .response();\n\n    // then: verify order created with all expected fields\n    assertThatJson(response.asString())\n        .isObject()\n        .containsOnlyKeys(\"orderId\", \"status\", \"total\", \"createdAt\")\n        .containsEntry(\"status\", \"PAYMENT_PENDING\")\n        .hasEntrySatisfying(\"orderId\", isValidUuid())\n        .hasEntrySatisfying(\"total\", isPositiveNumber());\n\n    // and: verify payment was initiated\n    stripeApiMock.verify(\n        postRequestedFor(urlPathEqualTo(\"/v1/payment_intents\"))\n            .withRequestBody(matchingJsonPath(\"$.customer\", equalTo(customerId)))\n    );\n}\n","language-java shiki shiki-themes github-light github-dark monokai",[264],{"type":19,"tag":115,"props":265,"children":266},{"__ignoreMap":7},[267,285,305,315,350,391,401,410,437,456,473,515,537,546,555,564,573,582,591,600,614,631,658,675,701,718,736,744,753,777,794,847,881,915,949,957,966,985,1017,1063,1072],{"type":19,"tag":268,"props":269,"children":272},"span",{"class":270,"line":271},"line",1,[273,279],{"type":19,"tag":268,"props":274,"children":276},{"style":275},"--shiki-default:#24292E;--shiki-dark:#E1E4E8;--shiki-sepia:#F8F8F2",[277],{"type":24,"value":278},"@",{"type":19,"tag":268,"props":280,"children":282},{"style":281},"--shiki-default:#D73A49;--shiki-default-font-style:inherit;--shiki-dark:#F97583;--shiki-dark-font-style:inherit;--shiki-sepia:#66D9EF;--shiki-sepia-font-style:italic",[283],{"type":24,"value":284},"Test\n",{"type":19,"tag":268,"props":286,"children":288},{"class":270,"line":287},2,[289,294,300],{"type":19,"tag":268,"props":290,"children":291},{"style":281},[292],{"type":24,"value":293},"void",{"type":19,"tag":268,"props":295,"children":297},{"style":296},"--shiki-default:#6F42C1;--shiki-dark:#B392F0;--shiki-sepia:#A6E22E",[298],{"type":24,"value":299}," checkout_shall_createOrderAndProcessPayment",{"type":19,"tag":268,"props":301,"children":302},{"style":275},[303],{"type":24,"value":304},"() {\n",{"type":19,"tag":268,"props":306,"children":308},{"class":270,"line":307},3,[309],{"type":19,"tag":268,"props":310,"children":312},{"style":311},"--shiki-default:#6A737D;--shiki-dark:#6A737D;--shiki-sepia:#88846F",[313],{"type":24,"value":314},"    // given: customer with items in cart (use fixture - not the focus)\n",{"type":19,"tag":268,"props":316,"children":318},{"class":270,"line":317},4,[319,324,329,335,340,345],{"type":19,"tag":268,"props":320,"children":321},{"style":281},[322],{"type":24,"value":323},"    var",{"type":19,"tag":268,"props":325,"children":326},{"style":275},[327],{"type":24,"value":328}," customerId ",{"type":19,"tag":268,"props":330,"children":332},{"style":331},"--shiki-default:#D73A49;--shiki-dark:#F97583;--shiki-sepia:#F92672",[333],{"type":24,"value":334},"=",{"type":19,"tag":268,"props":336,"children":337},{"style":275},[338],{"type":24,"value":339}," ApiFixtures.",{"type":19,"tag":268,"props":341,"children":342},{"style":296},[343],{"type":24,"value":344},"createCustomer",{"type":19,"tag":268,"props":346,"children":347},{"style":275},[348],{"type":24,"value":349},"(authToken);\n",{"type":19,"tag":268,"props":351,"children":353},{"class":270,"line":352},5,[354,359,364,369,375,380,386],{"type":19,"tag":268,"props":355,"children":356},{"style":275},[357],{"type":24,"value":358},"    ApiFixtures.",{"type":19,"tag":268,"props":360,"children":361},{"style":296},[362],{"type":24,"value":363},"addToCart",{"type":19,"tag":268,"props":365,"children":366},{"style":275},[367],{"type":24,"value":368},"(customerId, ",{"type":19,"tag":268,"props":370,"children":372},{"style":371},"--shiki-default:#032F62;--shiki-dark:#9ECBFF;--shiki-sepia:#E6DB74",[373],{"type":24,"value":374},"\"product-123\"",{"type":19,"tag":268,"props":376,"children":377},{"style":275},[378],{"type":24,"value":379},", ",{"type":19,"tag":268,"props":381,"children":383},{"style":382},"--shiki-default:#005CC5;--shiki-dark:#79B8FF;--shiki-sepia:#AE81FF",[384],{"type":24,"value":385},"2",{"type":19,"tag":268,"props":387,"children":388},{"style":275},[389],{"type":24,"value":390},", authToken);\n",{"type":19,"tag":268,"props":392,"children":394},{"class":270,"line":393},6,[395],{"type":19,"tag":268,"props":396,"children":398},{"emptyLinePlaceholder":397},true,[399],{"type":24,"value":400},"\n",{"type":19,"tag":268,"props":402,"children":404},{"class":270,"line":403},7,[405],{"type":19,"tag":268,"props":406,"children":407},{"style":311},[408],{"type":24,"value":409},"    // when: checkout (THIS is the focus - show explicitly)\n",{"type":19,"tag":268,"props":411,"children":413},{"class":270,"line":412},8,[414,418,423,427,432],{"type":19,"tag":268,"props":415,"children":416},{"style":281},[417],{"type":24,"value":323},{"type":19,"tag":268,"props":419,"children":420},{"style":275},[421],{"type":24,"value":422}," response ",{"type":19,"tag":268,"props":424,"children":425},{"style":331},[426],{"type":24,"value":334},{"type":19,"tag":268,"props":428,"children":429},{"style":296},[430],{"type":24,"value":431}," given",{"type":19,"tag":268,"props":433,"children":434},{"style":275},[435],{"type":24,"value":436},"()\n",{"type":19,"tag":268,"props":438,"children":440},{"class":270,"line":439},9,[441,446,451],{"type":19,"tag":268,"props":442,"children":443},{"style":275},[444],{"type":24,"value":445},"        .",{"type":19,"tag":268,"props":447,"children":448},{"style":296},[449],{"type":24,"value":450},"accept",{"type":19,"tag":268,"props":452,"children":453},{"style":275},[454],{"type":24,"value":455},"(ContentType.JSON)\n",{"type":19,"tag":268,"props":457,"children":459},{"class":270,"line":458},10,[460,464,469],{"type":19,"tag":268,"props":461,"children":462},{"style":275},[463],{"type":24,"value":445},{"type":19,"tag":268,"props":465,"children":466},{"style":296},[467],{"type":24,"value":468},"contentType",{"type":19,"tag":268,"props":470,"children":471},{"style":275},[472],{"type":24,"value":455},{"type":19,"tag":268,"props":474,"children":476},{"class":270,"line":475},11,[477,481,486,491,496,500,505,510],{"type":19,"tag":268,"props":478,"children":479},{"style":275},[480],{"type":24,"value":445},{"type":19,"tag":268,"props":482,"children":483},{"style":296},[484],{"type":24,"value":485},"header",{"type":19,"tag":268,"props":487,"children":488},{"style":275},[489],{"type":24,"value":490},"(",{"type":19,"tag":268,"props":492,"children":493},{"style":371},[494],{"type":24,"value":495},"\"Authorization\"",{"type":19,"tag":268,"props":497,"children":498},{"style":275},[499],{"type":24,"value":379},{"type":19,"tag":268,"props":501,"children":502},{"style":371},[503],{"type":24,"value":504},"\"Bearer \"",{"type":19,"tag":268,"props":506,"children":507},{"style":331},[508],{"type":24,"value":509}," +",{"type":19,"tag":268,"props":511,"children":512},{"style":275},[513],{"type":24,"value":514}," authToken)\n",{"type":19,"tag":268,"props":516,"children":518},{"class":270,"line":517},12,[519,523,528,532],{"type":19,"tag":268,"props":520,"children":521},{"style":275},[522],{"type":24,"value":445},{"type":19,"tag":268,"props":524,"children":525},{"style":296},[526],{"type":24,"value":527},"body",{"type":19,"tag":268,"props":529,"children":530},{"style":275},[531],{"type":24,"value":490},{"type":19,"tag":268,"props":533,"children":534},{"style":371},[535],{"type":24,"value":536},"\"\"\"\n",{"type":19,"tag":268,"props":538,"children":540},{"class":270,"line":539},13,[541],{"type":19,"tag":268,"props":542,"children":543},{"style":371},[544],{"type":24,"value":545},"            {\n",{"type":19,"tag":268,"props":547,"children":549},{"class":270,"line":548},14,[550],{"type":19,"tag":268,"props":551,"children":552},{"style":371},[553],{"type":24,"value":554},"                \"paymentMethod\": \"card\",\n",{"type":19,"tag":268,"props":556,"children":558},{"class":270,"line":557},15,[559],{"type":19,"tag":268,"props":560,"children":561},{"style":371},[562],{"type":24,"value":563},"                \"shippingAddress\": {\n",{"type":19,"tag":268,"props":565,"children":567},{"class":270,"line":566},16,[568],{"type":19,"tag":268,"props":569,"children":570},{"style":371},[571],{"type":24,"value":572},"                    \"street\": \"123 Main St\",\n",{"type":19,"tag":268,"props":574,"children":576},{"class":270,"line":575},17,[577],{"type":19,"tag":268,"props":578,"children":579},{"style":371},[580],{"type":24,"value":581},"                    \"city\": \"Boston\"\n",{"type":19,"tag":268,"props":583,"children":585},{"class":270,"line":584},18,[586],{"type":19,"tag":268,"props":587,"children":588},{"style":371},[589],{"type":24,"value":590},"                }\n",{"type":19,"tag":268,"props":592,"children":594},{"class":270,"line":593},19,[595],{"type":19,"tag":268,"props":596,"children":597},{"style":371},[598],{"type":24,"value":599},"            }\n",{"type":19,"tag":268,"props":601,"children":603},{"class":270,"line":602},20,[604,609],{"type":19,"tag":268,"props":605,"children":606},{"style":371},[607],{"type":24,"value":608},"            \"\"\"",{"type":19,"tag":268,"props":610,"children":611},{"style":275},[612],{"type":24,"value":613},")\n",{"type":19,"tag":268,"props":615,"children":617},{"class":270,"line":616},21,[618,622,627],{"type":19,"tag":268,"props":619,"children":620},{"style":275},[621],{"type":24,"value":445},{"type":19,"tag":268,"props":623,"children":624},{"style":296},[625],{"type":24,"value":626},"when",{"type":19,"tag":268,"props":628,"children":629},{"style":275},[630],{"type":24,"value":436},{"type":19,"tag":268,"props":632,"children":634},{"class":270,"line":633},22,[635,639,644,648,653],{"type":19,"tag":268,"props":636,"children":637},{"style":275},[638],{"type":24,"value":445},{"type":19,"tag":268,"props":640,"children":641},{"style":296},[642],{"type":24,"value":643},"post",{"type":19,"tag":268,"props":645,"children":646},{"style":275},[647],{"type":24,"value":490},{"type":19,"tag":268,"props":649,"children":650},{"style":371},[651],{"type":24,"value":652},"\"/v1/customers/{customerId}/checkout\"",{"type":19,"tag":268,"props":654,"children":655},{"style":275},[656],{"type":24,"value":657},", customerId)\n",{"type":19,"tag":268,"props":659,"children":661},{"class":270,"line":660},23,[662,666,671],{"type":19,"tag":268,"props":663,"children":664},{"style":275},[665],{"type":24,"value":445},{"type":19,"tag":268,"props":667,"children":668},{"style":296},[669],{"type":24,"value":670},"then",{"type":19,"tag":268,"props":672,"children":673},{"style":275},[674],{"type":24,"value":436},{"type":19,"tag":268,"props":676,"children":678},{"class":270,"line":677},24,[679,683,688,692,697],{"type":19,"tag":268,"props":680,"children":681},{"style":275},[682],{"type":24,"value":445},{"type":19,"tag":268,"props":684,"children":685},{"style":296},[686],{"type":24,"value":687},"statusCode",{"type":19,"tag":268,"props":689,"children":690},{"style":275},[691],{"type":24,"value":490},{"type":19,"tag":268,"props":693,"children":694},{"style":382},[695],{"type":24,"value":696},"201",{"type":19,"tag":268,"props":698,"children":699},{"style":275},[700],{"type":24,"value":613},{"type":19,"tag":268,"props":702,"children":704},{"class":270,"line":703},25,[705,709,714],{"type":19,"tag":268,"props":706,"children":707},{"style":275},[708],{"type":24,"value":445},{"type":19,"tag":268,"props":710,"children":711},{"style":296},[712],{"type":24,"value":713},"extract",{"type":19,"tag":268,"props":715,"children":716},{"style":275},[717],{"type":24,"value":436},{"type":19,"tag":268,"props":719,"children":721},{"class":270,"line":720},26,[722,726,731],{"type":19,"tag":268,"props":723,"children":724},{"style":275},[725],{"type":24,"value":445},{"type":19,"tag":268,"props":727,"children":728},{"style":296},[729],{"type":24,"value":730},"response",{"type":19,"tag":268,"props":732,"children":733},{"style":275},[734],{"type":24,"value":735},"();\n",{"type":19,"tag":268,"props":737,"children":739},{"class":270,"line":738},27,[740],{"type":19,"tag":268,"props":741,"children":742},{"emptyLinePlaceholder":397},[743],{"type":24,"value":400},{"type":19,"tag":268,"props":745,"children":747},{"class":270,"line":746},28,[748],{"type":19,"tag":268,"props":749,"children":750},{"style":311},[751],{"type":24,"value":752},"    // then: verify order created with all expected fields\n",{"type":19,"tag":268,"props":754,"children":756},{"class":270,"line":755},29,[757,762,767,772],{"type":19,"tag":268,"props":758,"children":759},{"style":296},[760],{"type":24,"value":761},"    assertThatJson",{"type":19,"tag":268,"props":763,"children":764},{"style":275},[765],{"type":24,"value":766},"(response.",{"type":19,"tag":268,"props":768,"children":769},{"style":296},[770],{"type":24,"value":771},"asString",{"type":19,"tag":268,"props":773,"children":774},{"style":275},[775],{"type":24,"value":776},"())\n",{"type":19,"tag":268,"props":778,"children":780},{"class":270,"line":779},30,[781,785,790],{"type":19,"tag":268,"props":782,"children":783},{"style":275},[784],{"type":24,"value":445},{"type":19,"tag":268,"props":786,"children":787},{"style":296},[788],{"type":24,"value":789},"isObject",{"type":19,"tag":268,"props":791,"children":792},{"style":275},[793],{"type":24,"value":436},{"type":19,"tag":268,"props":795,"children":797},{"class":270,"line":796},31,[798,802,807,811,816,820,825,829,834,838,843],{"type":19,"tag":268,"props":799,"children":800},{"style":275},[801],{"type":24,"value":445},{"type":19,"tag":268,"props":803,"children":804},{"style":296},[805],{"type":24,"value":806},"containsOnlyKeys",{"type":19,"tag":268,"props":808,"children":809},{"style":275},[810],{"type":24,"value":490},{"type":19,"tag":268,"props":812,"children":813},{"style":371},[814],{"type":24,"value":815},"\"orderId\"",{"type":19,"tag":268,"props":817,"children":818},{"style":275},[819],{"type":24,"value":379},{"type":19,"tag":268,"props":821,"children":822},{"style":371},[823],{"type":24,"value":824},"\"status\"",{"type":19,"tag":268,"props":826,"children":827},{"style":275},[828],{"type":24,"value":379},{"type":19,"tag":268,"props":830,"children":831},{"style":371},[832],{"type":24,"value":833},"\"total\"",{"type":19,"tag":268,"props":835,"children":836},{"style":275},[837],{"type":24,"value":379},{"type":19,"tag":268,"props":839,"children":840},{"style":371},[841],{"type":24,"value":842},"\"createdAt\"",{"type":19,"tag":268,"props":844,"children":845},{"style":275},[846],{"type":24,"value":613},{"type":19,"tag":268,"props":848,"children":850},{"class":270,"line":849},32,[851,855,860,864,868,872,877],{"type":19,"tag":268,"props":852,"children":853},{"style":275},[854],{"type":24,"value":445},{"type":19,"tag":268,"props":856,"children":857},{"style":296},[858],{"type":24,"value":859},"containsEntry",{"type":19,"tag":268,"props":861,"children":862},{"style":275},[863],{"type":24,"value":490},{"type":19,"tag":268,"props":865,"children":866},{"style":371},[867],{"type":24,"value":824},{"type":19,"tag":268,"props":869,"children":870},{"style":275},[871],{"type":24,"value":379},{"type":19,"tag":268,"props":873,"children":874},{"style":371},[875],{"type":24,"value":876},"\"PAYMENT_PENDING\"",{"type":19,"tag":268,"props":878,"children":879},{"style":275},[880],{"type":24,"value":613},{"type":19,"tag":268,"props":882,"children":884},{"class":270,"line":883},33,[885,889,894,898,902,906,911],{"type":19,"tag":268,"props":886,"children":887},{"style":275},[888],{"type":24,"value":445},{"type":19,"tag":268,"props":890,"children":891},{"style":296},[892],{"type":24,"value":893},"hasEntrySatisfying",{"type":19,"tag":268,"props":895,"children":896},{"style":275},[897],{"type":24,"value":490},{"type":19,"tag":268,"props":899,"children":900},{"style":371},[901],{"type":24,"value":815},{"type":19,"tag":268,"props":903,"children":904},{"style":275},[905],{"type":24,"value":379},{"type":19,"tag":268,"props":907,"children":908},{"style":296},[909],{"type":24,"value":910},"isValidUuid",{"type":19,"tag":268,"props":912,"children":913},{"style":275},[914],{"type":24,"value":776},{"type":19,"tag":268,"props":916,"children":918},{"class":270,"line":917},34,[919,923,927,931,935,939,944],{"type":19,"tag":268,"props":920,"children":921},{"style":275},[922],{"type":24,"value":445},{"type":19,"tag":268,"props":924,"children":925},{"style":296},[926],{"type":24,"value":893},{"type":19,"tag":268,"props":928,"children":929},{"style":275},[930],{"type":24,"value":490},{"type":19,"tag":268,"props":932,"children":933},{"style":371},[934],{"type":24,"value":833},{"type":19,"tag":268,"props":936,"children":937},{"style":275},[938],{"type":24,"value":379},{"type":19,"tag":268,"props":940,"children":941},{"style":296},[942],{"type":24,"value":943},"isPositiveNumber",{"type":19,"tag":268,"props":945,"children":946},{"style":275},[947],{"type":24,"value":948},"());\n",{"type":19,"tag":268,"props":950,"children":952},{"class":270,"line":951},35,[953],{"type":19,"tag":268,"props":954,"children":955},{"emptyLinePlaceholder":397},[956],{"type":24,"value":400},{"type":19,"tag":268,"props":958,"children":960},{"class":270,"line":959},36,[961],{"type":19,"tag":268,"props":962,"children":963},{"style":311},[964],{"type":24,"value":965},"    // and: verify payment was initiated\n",{"type":19,"tag":268,"props":967,"children":969},{"class":270,"line":968},37,[970,975,980],{"type":19,"tag":268,"props":971,"children":972},{"style":275},[973],{"type":24,"value":974},"    stripeApiMock.",{"type":19,"tag":268,"props":976,"children":977},{"style":296},[978],{"type":24,"value":979},"verify",{"type":19,"tag":268,"props":981,"children":982},{"style":275},[983],{"type":24,"value":984},"(\n",{"type":19,"tag":268,"props":986,"children":988},{"class":270,"line":987},38,[989,994,998,1003,1007,1012],{"type":19,"tag":268,"props":990,"children":991},{"style":296},[992],{"type":24,"value":993},"        postRequestedFor",{"type":19,"tag":268,"props":995,"children":996},{"style":275},[997],{"type":24,"value":490},{"type":19,"tag":268,"props":999,"children":1000},{"style":296},[1001],{"type":24,"value":1002},"urlPathEqualTo",{"type":19,"tag":268,"props":1004,"children":1005},{"style":275},[1006],{"type":24,"value":490},{"type":19,"tag":268,"props":1008,"children":1009},{"style":371},[1010],{"type":24,"value":1011},"\"/v1/payment_intents\"",{"type":19,"tag":268,"props":1013,"children":1014},{"style":275},[1015],{"type":24,"value":1016},"))\n",{"type":19,"tag":268,"props":1018,"children":1020},{"class":270,"line":1019},39,[1021,1026,1031,1035,1040,1044,1049,1053,1058],{"type":19,"tag":268,"props":1022,"children":1023},{"style":275},[1024],{"type":24,"value":1025},"            .",{"type":19,"tag":268,"props":1027,"children":1028},{"style":296},[1029],{"type":24,"value":1030},"withRequestBody",{"type":19,"tag":268,"props":1032,"children":1033},{"style":275},[1034],{"type":24,"value":490},{"type":19,"tag":268,"props":1036,"children":1037},{"style":296},[1038],{"type":24,"value":1039},"matchingJsonPath",{"type":19,"tag":268,"props":1041,"children":1042},{"style":275},[1043],{"type":24,"value":490},{"type":19,"tag":268,"props":1045,"children":1046},{"style":371},[1047],{"type":24,"value":1048},"\"$.customer\"",{"type":19,"tag":268,"props":1050,"children":1051},{"style":275},[1052],{"type":24,"value":379},{"type":19,"tag":268,"props":1054,"children":1055},{"style":296},[1056],{"type":24,"value":1057},"equalTo",{"type":19,"tag":268,"props":1059,"children":1060},{"style":275},[1061],{"type":24,"value":1062},"(customerId)))\n",{"type":19,"tag":268,"props":1064,"children":1066},{"class":270,"line":1065},40,[1067],{"type":19,"tag":268,"props":1068,"children":1069},{"style":275},[1070],{"type":24,"value":1071},"    );\n",{"type":19,"tag":268,"props":1073,"children":1075},{"class":270,"line":1074},41,[1076],{"type":19,"tag":268,"props":1077,"children":1078},{"style":275},[1079],{"type":24,"value":1080},"}\n",{"type":19,"tag":20,"props":1082,"children":1083},{},[1084],{"type":24,"value":1085},"Notice how the checkout request body is shown explicitly - that's what we're testing. But customer creation and cart setup are hidden in fixtures. The reader immediately knows: this test is about checkout behavior, not about cart management.",{"type":19,"tag":47,"props":1087,"children":1089},{"id":1088},"contract-tests-vs-behavior-tests",[1090],{"type":24,"value":1091},"Contract Tests vs Behavior Tests",{"type":19,"tag":20,"props":1093,"children":1094},{},[1095],{"type":24,"value":1096},"For each API endpoint, I write at least one \"contract test\" that verifies the complete response structure:",{"type":19,"tag":259,"props":1098,"children":1100},{"code":1099,"language":12,"meta":7,"className":262,"style":7},"@Test\nvoid getOrder_shall_returnCompleteOrderStructure() {\n    var orderId = ApiFixtures.createOrder(authToken, orderData);\n\n    var response = ApiFixtures.getOrder(orderId, authToken);\n\n    // Contract test: verify EVERY field\n    assertThatJson(response.asString())\n        .isObject()\n        .containsOnlyKeys(\"id\", \"customerId\", \"items\", \"status\",\n                          \"total\", \"createdAt\", \"updatedAt\")\n        .hasEntrySatisfying(\"id\", isValidUuid())\n        .hasEntrySatisfying(\"createdAt\", isValidTimestamp())\n        // ... check every single field\n}\n",[1101],{"type":19,"tag":115,"props":1102,"children":1103},{"__ignoreMap":7},[1104,1115,1131,1161,1168,1197,1204,1212,1231,1246,1297,1326,1357,1389,1397],{"type":19,"tag":268,"props":1105,"children":1106},{"class":270,"line":271},[1107,1111],{"type":19,"tag":268,"props":1108,"children":1109},{"style":275},[1110],{"type":24,"value":278},{"type":19,"tag":268,"props":1112,"children":1113},{"style":281},[1114],{"type":24,"value":284},{"type":19,"tag":268,"props":1116,"children":1117},{"class":270,"line":287},[1118,1122,1127],{"type":19,"tag":268,"props":1119,"children":1120},{"style":281},[1121],{"type":24,"value":293},{"type":19,"tag":268,"props":1123,"children":1124},{"style":296},[1125],{"type":24,"value":1126}," getOrder_shall_returnCompleteOrderStructure",{"type":19,"tag":268,"props":1128,"children":1129},{"style":275},[1130],{"type":24,"value":304},{"type":19,"tag":268,"props":1132,"children":1133},{"class":270,"line":307},[1134,1138,1143,1147,1151,1156],{"type":19,"tag":268,"props":1135,"children":1136},{"style":281},[1137],{"type":24,"value":323},{"type":19,"tag":268,"props":1139,"children":1140},{"style":275},[1141],{"type":24,"value":1142}," orderId ",{"type":19,"tag":268,"props":1144,"children":1145},{"style":331},[1146],{"type":24,"value":334},{"type":19,"tag":268,"props":1148,"children":1149},{"style":275},[1150],{"type":24,"value":339},{"type":19,"tag":268,"props":1152,"children":1153},{"style":296},[1154],{"type":24,"value":1155},"createOrder",{"type":19,"tag":268,"props":1157,"children":1158},{"style":275},[1159],{"type":24,"value":1160},"(authToken, orderData);\n",{"type":19,"tag":268,"props":1162,"children":1163},{"class":270,"line":317},[1164],{"type":19,"tag":268,"props":1165,"children":1166},{"emptyLinePlaceholder":397},[1167],{"type":24,"value":400},{"type":19,"tag":268,"props":1169,"children":1170},{"class":270,"line":352},[1171,1175,1179,1183,1187,1192],{"type":19,"tag":268,"props":1172,"children":1173},{"style":281},[1174],{"type":24,"value":323},{"type":19,"tag":268,"props":1176,"children":1177},{"style":275},[1178],{"type":24,"value":422},{"type":19,"tag":268,"props":1180,"children":1181},{"style":331},[1182],{"type":24,"value":334},{"type":19,"tag":268,"props":1184,"children":1185},{"style":275},[1186],{"type":24,"value":339},{"type":19,"tag":268,"props":1188,"children":1189},{"style":296},[1190],{"type":24,"value":1191},"getOrder",{"type":19,"tag":268,"props":1193,"children":1194},{"style":275},[1195],{"type":24,"value":1196},"(orderId, authToken);\n",{"type":19,"tag":268,"props":1198,"children":1199},{"class":270,"line":393},[1200],{"type":19,"tag":268,"props":1201,"children":1202},{"emptyLinePlaceholder":397},[1203],{"type":24,"value":400},{"type":19,"tag":268,"props":1205,"children":1206},{"class":270,"line":403},[1207],{"type":19,"tag":268,"props":1208,"children":1209},{"style":311},[1210],{"type":24,"value":1211},"    // Contract test: verify EVERY field\n",{"type":19,"tag":268,"props":1213,"children":1214},{"class":270,"line":412},[1215,1219,1223,1227],{"type":19,"tag":268,"props":1216,"children":1217},{"style":296},[1218],{"type":24,"value":761},{"type":19,"tag":268,"props":1220,"children":1221},{"style":275},[1222],{"type":24,"value":766},{"type":19,"tag":268,"props":1224,"children":1225},{"style":296},[1226],{"type":24,"value":771},{"type":19,"tag":268,"props":1228,"children":1229},{"style":275},[1230],{"type":24,"value":776},{"type":19,"tag":268,"props":1232,"children":1233},{"class":270,"line":439},[1234,1238,1242],{"type":19,"tag":268,"props":1235,"children":1236},{"style":275},[1237],{"type":24,"value":445},{"type":19,"tag":268,"props":1239,"children":1240},{"style":296},[1241],{"type":24,"value":789},{"type":19,"tag":268,"props":1243,"children":1244},{"style":275},[1245],{"type":24,"value":436},{"type":19,"tag":268,"props":1247,"children":1248},{"class":270,"line":458},[1249,1253,1257,1261,1266,1270,1275,1279,1284,1288,1292],{"type":19,"tag":268,"props":1250,"children":1251},{"style":275},[1252],{"type":24,"value":445},{"type":19,"tag":268,"props":1254,"children":1255},{"style":296},[1256],{"type":24,"value":806},{"type":19,"tag":268,"props":1258,"children":1259},{"style":275},[1260],{"type":24,"value":490},{"type":19,"tag":268,"props":1262,"children":1263},{"style":371},[1264],{"type":24,"value":1265},"\"id\"",{"type":19,"tag":268,"props":1267,"children":1268},{"style":275},[1269],{"type":24,"value":379},{"type":19,"tag":268,"props":1271,"children":1272},{"style":371},[1273],{"type":24,"value":1274},"\"customerId\"",{"type":19,"tag":268,"props":1276,"children":1277},{"style":275},[1278],{"type":24,"value":379},{"type":19,"tag":268,"props":1280,"children":1281},{"style":371},[1282],{"type":24,"value":1283},"\"items\"",{"type":19,"tag":268,"props":1285,"children":1286},{"style":275},[1287],{"type":24,"value":379},{"type":19,"tag":268,"props":1289,"children":1290},{"style":371},[1291],{"type":24,"value":824},{"type":19,"tag":268,"props":1293,"children":1294},{"style":275},[1295],{"type":24,"value":1296},",\n",{"type":19,"tag":268,"props":1298,"children":1299},{"class":270,"line":475},[1300,1305,1309,1313,1317,1322],{"type":19,"tag":268,"props":1301,"children":1302},{"style":371},[1303],{"type":24,"value":1304},"                          \"total\"",{"type":19,"tag":268,"props":1306,"children":1307},{"style":275},[1308],{"type":24,"value":379},{"type":19,"tag":268,"props":1310,"children":1311},{"style":371},[1312],{"type":24,"value":842},{"type":19,"tag":268,"props":1314,"children":1315},{"style":275},[1316],{"type":24,"value":379},{"type":19,"tag":268,"props":1318,"children":1319},{"style":371},[1320],{"type":24,"value":1321},"\"updatedAt\"",{"type":19,"tag":268,"props":1323,"children":1324},{"style":275},[1325],{"type":24,"value":613},{"type":19,"tag":268,"props":1327,"children":1328},{"class":270,"line":517},[1329,1333,1337,1341,1345,1349,1353],{"type":19,"tag":268,"props":1330,"children":1331},{"style":275},[1332],{"type":24,"value":445},{"type":19,"tag":268,"props":1334,"children":1335},{"style":296},[1336],{"type":24,"value":893},{"type":19,"tag":268,"props":1338,"children":1339},{"style":275},[1340],{"type":24,"value":490},{"type":19,"tag":268,"props":1342,"children":1343},{"style":371},[1344],{"type":24,"value":1265},{"type":19,"tag":268,"props":1346,"children":1347},{"style":275},[1348],{"type":24,"value":379},{"type":19,"tag":268,"props":1350,"children":1351},{"style":296},[1352],{"type":24,"value":910},{"type":19,"tag":268,"props":1354,"children":1355},{"style":275},[1356],{"type":24,"value":776},{"type":19,"tag":268,"props":1358,"children":1359},{"class":270,"line":539},[1360,1364,1368,1372,1376,1380,1385],{"type":19,"tag":268,"props":1361,"children":1362},{"style":275},[1363],{"type":24,"value":445},{"type":19,"tag":268,"props":1365,"children":1366},{"style":296},[1367],{"type":24,"value":893},{"type":19,"tag":268,"props":1369,"children":1370},{"style":275},[1371],{"type":24,"value":490},{"type":19,"tag":268,"props":1373,"children":1374},{"style":371},[1375],{"type":24,"value":842},{"type":19,"tag":268,"props":1377,"children":1378},{"style":275},[1379],{"type":24,"value":379},{"type":19,"tag":268,"props":1381,"children":1382},{"style":296},[1383],{"type":24,"value":1384},"isValidTimestamp",{"type":19,"tag":268,"props":1386,"children":1387},{"style":275},[1388],{"type":24,"value":776},{"type":19,"tag":268,"props":1390,"children":1391},{"class":270,"line":548},[1392],{"type":19,"tag":268,"props":1393,"children":1394},{"style":311},[1395],{"type":24,"value":1396},"        // ... check every single field\n",{"type":19,"tag":268,"props":1398,"children":1399},{"class":270,"line":557},[1400],{"type":19,"tag":268,"props":1401,"children":1402},{"style":275},[1403],{"type":24,"value":1080},{"type":19,"tag":20,"props":1405,"children":1406},{},[1407],{"type":24,"value":1408},"This serves as living documentation of your API contract. If someone adds a field, this test fails. If someone removes a field, this test fails. It's your safety net.",{"type":19,"tag":20,"props":1410,"children":1411},{},[1412],{"type":24,"value":1413},"Other tests for the same endpoint can be \"behavior tests\" with selective assertions:",{"type":19,"tag":259,"props":1415,"children":1417},{"code":1416,"language":12,"meta":7,"className":262,"style":7},"@Test\nvoid cancelOrder_shall_updateStatusToCancelled() {\n    var orderId = ApiFixtures.createOrder(authToken, orderData);\n\n    given()\n        .header(\"Authorization\", \"Bearer \" + authToken)\n        .when()\n        .post(\"/v1/orders/{orderId}/cancel\", orderId)\n        .then()\n        .statusCode(200);\n\n    var response = ApiFixtures.getOrder(orderId, authToken);\n\n    // Behavior test: only check what matters for this test\n    assertThatJson(response.asString())\n        .inPath(\"$.status\")\n        .isEqualTo(\"CANCELLED\");\n}\n",[1418],{"type":19,"tag":115,"props":1419,"children":1420},{"__ignoreMap":7},[1421,1432,1448,1475,1482,1494,1529,1544,1569,1584,1609,1616,1643,1650,1658,1677,1702,1727],{"type":19,"tag":268,"props":1422,"children":1423},{"class":270,"line":271},[1424,1428],{"type":19,"tag":268,"props":1425,"children":1426},{"style":275},[1427],{"type":24,"value":278},{"type":19,"tag":268,"props":1429,"children":1430},{"style":281},[1431],{"type":24,"value":284},{"type":19,"tag":268,"props":1433,"children":1434},{"class":270,"line":287},[1435,1439,1444],{"type":19,"tag":268,"props":1436,"children":1437},{"style":281},[1438],{"type":24,"value":293},{"type":19,"tag":268,"props":1440,"children":1441},{"style":296},[1442],{"type":24,"value":1443}," cancelOrder_shall_updateStatusToCancelled",{"type":19,"tag":268,"props":1445,"children":1446},{"style":275},[1447],{"type":24,"value":304},{"type":19,"tag":268,"props":1449,"children":1450},{"class":270,"line":307},[1451,1455,1459,1463,1467,1471],{"type":19,"tag":268,"props":1452,"children":1453},{"style":281},[1454],{"type":24,"value":323},{"type":19,"tag":268,"props":1456,"children":1457},{"style":275},[1458],{"type":24,"value":1142},{"type":19,"tag":268,"props":1460,"children":1461},{"style":331},[1462],{"type":24,"value":334},{"type":19,"tag":268,"props":1464,"children":1465},{"style":275},[1466],{"type":24,"value":339},{"type":19,"tag":268,"props":1468,"children":1469},{"style":296},[1470],{"type":24,"value":1155},{"type":19,"tag":268,"props":1472,"children":1473},{"style":275},[1474],{"type":24,"value":1160},{"type":19,"tag":268,"props":1476,"children":1477},{"class":270,"line":317},[1478],{"type":19,"tag":268,"props":1479,"children":1480},{"emptyLinePlaceholder":397},[1481],{"type":24,"value":400},{"type":19,"tag":268,"props":1483,"children":1484},{"class":270,"line":352},[1485,1490],{"type":19,"tag":268,"props":1486,"children":1487},{"style":296},[1488],{"type":24,"value":1489},"    given",{"type":19,"tag":268,"props":1491,"children":1492},{"style":275},[1493],{"type":24,"value":436},{"type":19,"tag":268,"props":1495,"children":1496},{"class":270,"line":393},[1497,1501,1505,1509,1513,1517,1521,1525],{"type":19,"tag":268,"props":1498,"children":1499},{"style":275},[1500],{"type":24,"value":445},{"type":19,"tag":268,"props":1502,"children":1503},{"style":296},[1504],{"type":24,"value":485},{"type":19,"tag":268,"props":1506,"children":1507},{"style":275},[1508],{"type":24,"value":490},{"type":19,"tag":268,"props":1510,"children":1511},{"style":371},[1512],{"type":24,"value":495},{"type":19,"tag":268,"props":1514,"children":1515},{"style":275},[1516],{"type":24,"value":379},{"type":19,"tag":268,"props":1518,"children":1519},{"style":371},[1520],{"type":24,"value":504},{"type":19,"tag":268,"props":1522,"children":1523},{"style":331},[1524],{"type":24,"value":509},{"type":19,"tag":268,"props":1526,"children":1527},{"style":275},[1528],{"type":24,"value":514},{"type":19,"tag":268,"props":1530,"children":1531},{"class":270,"line":403},[1532,1536,1540],{"type":19,"tag":268,"props":1533,"children":1534},{"style":275},[1535],{"type":24,"value":445},{"type":19,"tag":268,"props":1537,"children":1538},{"style":296},[1539],{"type":24,"value":626},{"type":19,"tag":268,"props":1541,"children":1542},{"style":275},[1543],{"type":24,"value":436},{"type":19,"tag":268,"props":1545,"children":1546},{"class":270,"line":412},[1547,1551,1555,1559,1564],{"type":19,"tag":268,"props":1548,"children":1549},{"style":275},[1550],{"type":24,"value":445},{"type":19,"tag":268,"props":1552,"children":1553},{"style":296},[1554],{"type":24,"value":643},{"type":19,"tag":268,"props":1556,"children":1557},{"style":275},[1558],{"type":24,"value":490},{"type":19,"tag":268,"props":1560,"children":1561},{"style":371},[1562],{"type":24,"value":1563},"\"/v1/orders/{orderId}/cancel\"",{"type":19,"tag":268,"props":1565,"children":1566},{"style":275},[1567],{"type":24,"value":1568},", orderId)\n",{"type":19,"tag":268,"props":1570,"children":1571},{"class":270,"line":439},[1572,1576,1580],{"type":19,"tag":268,"props":1573,"children":1574},{"style":275},[1575],{"type":24,"value":445},{"type":19,"tag":268,"props":1577,"children":1578},{"style":296},[1579],{"type":24,"value":670},{"type":19,"tag":268,"props":1581,"children":1582},{"style":275},[1583],{"type":24,"value":436},{"type":19,"tag":268,"props":1585,"children":1586},{"class":270,"line":458},[1587,1591,1595,1599,1604],{"type":19,"tag":268,"props":1588,"children":1589},{"style":275},[1590],{"type":24,"value":445},{"type":19,"tag":268,"props":1592,"children":1593},{"style":296},[1594],{"type":24,"value":687},{"type":19,"tag":268,"props":1596,"children":1597},{"style":275},[1598],{"type":24,"value":490},{"type":19,"tag":268,"props":1600,"children":1601},{"style":382},[1602],{"type":24,"value":1603},"200",{"type":19,"tag":268,"props":1605,"children":1606},{"style":275},[1607],{"type":24,"value":1608},");\n",{"type":19,"tag":268,"props":1610,"children":1611},{"class":270,"line":475},[1612],{"type":19,"tag":268,"props":1613,"children":1614},{"emptyLinePlaceholder":397},[1615],{"type":24,"value":400},{"type":19,"tag":268,"props":1617,"children":1618},{"class":270,"line":517},[1619,1623,1627,1631,1635,1639],{"type":19,"tag":268,"props":1620,"children":1621},{"style":281},[1622],{"type":24,"value":323},{"type":19,"tag":268,"props":1624,"children":1625},{"style":275},[1626],{"type":24,"value":422},{"type":19,"tag":268,"props":1628,"children":1629},{"style":331},[1630],{"type":24,"value":334},{"type":19,"tag":268,"props":1632,"children":1633},{"style":275},[1634],{"type":24,"value":339},{"type":19,"tag":268,"props":1636,"children":1637},{"style":296},[1638],{"type":24,"value":1191},{"type":19,"tag":268,"props":1640,"children":1641},{"style":275},[1642],{"type":24,"value":1196},{"type":19,"tag":268,"props":1644,"children":1645},{"class":270,"line":539},[1646],{"type":19,"tag":268,"props":1647,"children":1648},{"emptyLinePlaceholder":397},[1649],{"type":24,"value":400},{"type":19,"tag":268,"props":1651,"children":1652},{"class":270,"line":548},[1653],{"type":19,"tag":268,"props":1654,"children":1655},{"style":311},[1656],{"type":24,"value":1657},"    // Behavior test: only check what matters for this test\n",{"type":19,"tag":268,"props":1659,"children":1660},{"class":270,"line":557},[1661,1665,1669,1673],{"type":19,"tag":268,"props":1662,"children":1663},{"style":296},[1664],{"type":24,"value":761},{"type":19,"tag":268,"props":1666,"children":1667},{"style":275},[1668],{"type":24,"value":766},{"type":19,"tag":268,"props":1670,"children":1671},{"style":296},[1672],{"type":24,"value":771},{"type":19,"tag":268,"props":1674,"children":1675},{"style":275},[1676],{"type":24,"value":776},{"type":19,"tag":268,"props":1678,"children":1679},{"class":270,"line":566},[1680,1684,1689,1693,1698],{"type":19,"tag":268,"props":1681,"children":1682},{"style":275},[1683],{"type":24,"value":445},{"type":19,"tag":268,"props":1685,"children":1686},{"style":296},[1687],{"type":24,"value":1688},"inPath",{"type":19,"tag":268,"props":1690,"children":1691},{"style":275},[1692],{"type":24,"value":490},{"type":19,"tag":268,"props":1694,"children":1695},{"style":371},[1696],{"type":24,"value":1697},"\"$.status\"",{"type":19,"tag":268,"props":1699,"children":1700},{"style":275},[1701],{"type":24,"value":613},{"type":19,"tag":268,"props":1703,"children":1704},{"class":270,"line":575},[1705,1709,1714,1718,1723],{"type":19,"tag":268,"props":1706,"children":1707},{"style":275},[1708],{"type":24,"value":445},{"type":19,"tag":268,"props":1710,"children":1711},{"style":296},[1712],{"type":24,"value":1713},"isEqualTo",{"type":19,"tag":268,"props":1715,"children":1716},{"style":275},[1717],{"type":24,"value":490},{"type":19,"tag":268,"props":1719,"children":1720},{"style":371},[1721],{"type":24,"value":1722},"\"CANCELLED\"",{"type":19,"tag":268,"props":1724,"children":1725},{"style":275},[1726],{"type":24,"value":1608},{"type":19,"tag":268,"props":1728,"children":1729},{"class":270,"line":584},[1730],{"type":19,"tag":268,"props":1731,"children":1732},{"style":275},[1733],{"type":24,"value":1080},{"type":19,"tag":47,"props":1735,"children":1737},{"id":1736},"real-infrastructure-mocked-external-services",[1738],{"type":24,"value":1739},"Real Infrastructure, Mocked External Services",{"type":19,"tag":20,"props":1741,"children":1742},{},[1743],{"type":24,"value":1744},"My 2018 approach used Docker containers for everything, orchestrated manually or via docker-compose. That approach still has merit - it provides perfect isolation and works regardless of your tech stack. If you're running a polyglot microservices architecture or you're unsure whether you'll stick with Spring Boot, the pure container approach is more portable.",{"type":19,"tag":20,"props":1746,"children":1747},{},[1748],{"type":24,"value":1749},"But for teams committed to Spring Boot, TestContainers has made things much more elegant. It handles container lifecycle automatically, integrates with JUnit, and lets you write everything in Java. The friction for local development is minimal - run your test, containers spin up, tests execute, containers stop.",{"type":19,"tag":20,"props":1751,"children":1752},{},[1753,1755,1760],{"type":24,"value":1754},"The key principle remains the same: ",{"type":19,"tag":67,"props":1756,"children":1757},{},[1758],{"type":24,"value":1759},"use real infrastructure for things you control, mock things you don't",{"type":24,"value":1761},".",{"type":19,"tag":20,"props":1763,"children":1764},{},[1765],{"type":24,"value":1766},"Your database? Run a real MySQL/PostgreSQL in a container. Your Redis cache? Real container. But that third-party payment API? Mock it with WireMock. You don't control Stripe's API, and you don't want your tests failing because their sandbox is down.",{"type":19,"tag":259,"props":1768,"children":1770},{"code":1769,"language":12,"meta":7,"className":262,"style":7},"@Container\nprivate static final MySQLContainer\u003C?> mysql = new MySQLContainer\u003C>(\"mysql:8.0\");\n\n@RegisterExtension\nprotected static WireMockExtension stripeApi = WireMockExtension\n    .newInstance()\n    .options(options().dynamicPort())\n    .build();\n",[1771],{"type":19,"tag":115,"props":1772,"children":1773},{"__ignoreMap":7},[1774,1786,1852,1859,1871,1902,1919,1953],{"type":19,"tag":268,"props":1775,"children":1776},{"class":270,"line":271},[1777,1781],{"type":19,"tag":268,"props":1778,"children":1779},{"style":275},[1780],{"type":24,"value":278},{"type":19,"tag":268,"props":1782,"children":1783},{"style":281},[1784],{"type":24,"value":1785},"Container\n",{"type":19,"tag":268,"props":1787,"children":1788},{"class":270,"line":287},[1789,1794,1799,1804,1810,1815,1820,1825,1829,1834,1838,1843,1848],{"type":19,"tag":268,"props":1790,"children":1791},{"style":331},[1792],{"type":24,"value":1793},"private",{"type":19,"tag":268,"props":1795,"children":1796},{"style":331},[1797],{"type":24,"value":1798}," static",{"type":19,"tag":268,"props":1800,"children":1801},{"style":331},[1802],{"type":24,"value":1803}," final",{"type":19,"tag":268,"props":1805,"children":1807},{"style":1806},"--shiki-default:#24292E;--shiki-default-font-style:inherit;--shiki-dark:#E1E4E8;--shiki-dark-font-style:inherit;--shiki-sepia:#66D9EF;--shiki-sepia-font-style:italic",[1808],{"type":24,"value":1809}," MySQLContainer",{"type":19,"tag":268,"props":1811,"children":1812},{"style":275},[1813],{"type":24,"value":1814},"\u003C",{"type":19,"tag":268,"props":1816,"children":1817},{"style":281},[1818],{"type":24,"value":1819},"?",{"type":19,"tag":268,"props":1821,"children":1822},{"style":275},[1823],{"type":24,"value":1824},"> mysql ",{"type":19,"tag":268,"props":1826,"children":1827},{"style":331},[1828],{"type":24,"value":334},{"type":19,"tag":268,"props":1830,"children":1831},{"style":331},[1832],{"type":24,"value":1833}," new",{"type":19,"tag":268,"props":1835,"children":1836},{"style":1806},[1837],{"type":24,"value":1809},{"type":19,"tag":268,"props":1839,"children":1840},{"style":275},[1841],{"type":24,"value":1842},"\u003C>(",{"type":19,"tag":268,"props":1844,"children":1845},{"style":371},[1846],{"type":24,"value":1847},"\"mysql:8.0\"",{"type":19,"tag":268,"props":1849,"children":1850},{"style":275},[1851],{"type":24,"value":1608},{"type":19,"tag":268,"props":1853,"children":1854},{"class":270,"line":307},[1855],{"type":19,"tag":268,"props":1856,"children":1857},{"emptyLinePlaceholder":397},[1858],{"type":24,"value":400},{"type":19,"tag":268,"props":1860,"children":1861},{"class":270,"line":317},[1862,1866],{"type":19,"tag":268,"props":1863,"children":1864},{"style":275},[1865],{"type":24,"value":278},{"type":19,"tag":268,"props":1867,"children":1868},{"style":281},[1869],{"type":24,"value":1870},"RegisterExtension\n",{"type":19,"tag":268,"props":1872,"children":1873},{"class":270,"line":352},[1874,1879,1883,1888,1893,1897],{"type":19,"tag":268,"props":1875,"children":1876},{"style":331},[1877],{"type":24,"value":1878},"protected",{"type":19,"tag":268,"props":1880,"children":1881},{"style":331},[1882],{"type":24,"value":1798},{"type":19,"tag":268,"props":1884,"children":1885},{"style":1806},[1886],{"type":24,"value":1887}," WireMockExtension",{"type":19,"tag":268,"props":1889,"children":1890},{"style":275},[1891],{"type":24,"value":1892}," stripeApi ",{"type":19,"tag":268,"props":1894,"children":1895},{"style":331},[1896],{"type":24,"value":334},{"type":19,"tag":268,"props":1898,"children":1899},{"style":1806},[1900],{"type":24,"value":1901}," WireMockExtension\n",{"type":19,"tag":268,"props":1903,"children":1904},{"class":270,"line":393},[1905,1910,1915],{"type":19,"tag":268,"props":1906,"children":1907},{"style":275},[1908],{"type":24,"value":1909},"    .",{"type":19,"tag":268,"props":1911,"children":1912},{"style":296},[1913],{"type":24,"value":1914},"newInstance",{"type":19,"tag":268,"props":1916,"children":1917},{"style":275},[1918],{"type":24,"value":436},{"type":19,"tag":268,"props":1920,"children":1921},{"class":270,"line":403},[1922,1926,1931,1935,1939,1944,1949],{"type":19,"tag":268,"props":1923,"children":1924},{"style":275},[1925],{"type":24,"value":1909},{"type":19,"tag":268,"props":1927,"children":1928},{"style":296},[1929],{"type":24,"value":1930},"options",{"type":19,"tag":268,"props":1932,"children":1933},{"style":275},[1934],{"type":24,"value":490},{"type":19,"tag":268,"props":1936,"children":1937},{"style":296},[1938],{"type":24,"value":1930},{"type":19,"tag":268,"props":1940,"children":1941},{"style":275},[1942],{"type":24,"value":1943},"().",{"type":19,"tag":268,"props":1945,"children":1946},{"style":296},[1947],{"type":24,"value":1948},"dynamicPort",{"type":19,"tag":268,"props":1950,"children":1951},{"style":275},[1952],{"type":24,"value":776},{"type":19,"tag":268,"props":1954,"children":1955},{"class":270,"line":412},[1956,1960,1965],{"type":19,"tag":268,"props":1957,"children":1958},{"style":275},[1959],{"type":24,"value":1909},{"type":19,"tag":268,"props":1961,"children":1962},{"style":296},[1963],{"type":24,"value":1964},"build",{"type":19,"tag":268,"props":1966,"children":1967},{"style":275},[1968],{"type":24,"value":735},{"type":19,"tag":20,"props":1970,"children":1971},{},[1972],{"type":24,"value":1973},"This gives you confidence that your database queries work correctly, combined with fast, predictable tests that don't depend on external service availability. WireMock also gives you fine-grained control over mock responses - you can simulate errors, timeouts, and edge cases that would be hard to trigger with a real external service.",{"type":19,"tag":47,"props":1975,"children":1977},{"id":1976},"the-stub-vs-verify-strategy",[1978],{"type":24,"value":1979},"The Stub vs Verify Strategy",{"type":19,"tag":20,"props":1981,"children":1982},{},[1983,1985,1990],{"type":24,"value":1984},"Here's a pattern that took me years to figure out: ",{"type":19,"tag":67,"props":1986,"children":1987},{},[1988],{"type":24,"value":1989},"where you put your assertions affects your error messages",{"type":24,"value":1761},{"type":19,"tag":20,"props":1992,"children":1993},{},[1994],{"type":24,"value":1995},"When testing what data you send to an external API, use a minimal stub and detailed verification:",{"type":19,"tag":259,"props":1997,"children":1999},{"code":1998,"language":12,"meta":7,"className":262,"style":7},"// BAD: Strict stub matching\nstripeApi.stubFor(\n    post(urlPathEqualTo(\"/v1/charges\"))\n        .withRequestBody(matchingJsonPath(\"$.amount\", equalTo(\"5000\")))\n        .withRequestBody(matchingJsonPath(\"$.currency\", equalTo(\"usd\")))\n        .willReturn(ok().withBody(\"\"\"{\"id\": \"ch_123\"}\"\"\"))\n);\n// If amount is wrong: \"No matching stub found\" - unhelpful!\n\n// GOOD: Minimal stub + detailed verify\nstripeApi.stubFor(\n    post(urlPathEqualTo(\"/v1/charges\"))\n        .willReturn(ok().withBody(\"\"\"{\"id\": \"ch_123\"}\"\"\"))\n);\n\n// ... trigger the action ...\n\n// verify() shows exactly what was received vs expected\nstripeApi.verify(\n    postRequestedFor(urlPathEqualTo(\"/v1/charges\"))\n        .withRequestBody(matchingJsonPath(\"$.amount\", equalTo(\"5000\")))\n        .withRequestBody(matchingJsonPath(\"$.currency\", equalTo(\"usd\")))\n);\n",[2000],{"type":19,"tag":115,"props":2001,"children":2002},{"__ignoreMap":7},[2003,2011,2028,2057,2107,2156,2199,2206,2214,2221,2229,2244,2271,2310,2317,2324,2332,2339,2347,2362,2390,2437,2484],{"type":19,"tag":268,"props":2004,"children":2005},{"class":270,"line":271},[2006],{"type":19,"tag":268,"props":2007,"children":2008},{"style":311},[2009],{"type":24,"value":2010},"// BAD: Strict stub matching\n",{"type":19,"tag":268,"props":2012,"children":2013},{"class":270,"line":287},[2014,2019,2024],{"type":19,"tag":268,"props":2015,"children":2016},{"style":275},[2017],{"type":24,"value":2018},"stripeApi.",{"type":19,"tag":268,"props":2020,"children":2021},{"style":296},[2022],{"type":24,"value":2023},"stubFor",{"type":19,"tag":268,"props":2025,"children":2026},{"style":275},[2027],{"type":24,"value":984},{"type":19,"tag":268,"props":2029,"children":2030},{"class":270,"line":307},[2031,2036,2040,2044,2048,2053],{"type":19,"tag":268,"props":2032,"children":2033},{"style":296},[2034],{"type":24,"value":2035},"    post",{"type":19,"tag":268,"props":2037,"children":2038},{"style":275},[2039],{"type":24,"value":490},{"type":19,"tag":268,"props":2041,"children":2042},{"style":296},[2043],{"type":24,"value":1002},{"type":19,"tag":268,"props":2045,"children":2046},{"style":275},[2047],{"type":24,"value":490},{"type":19,"tag":268,"props":2049,"children":2050},{"style":371},[2051],{"type":24,"value":2052},"\"/v1/charges\"",{"type":19,"tag":268,"props":2054,"children":2055},{"style":275},[2056],{"type":24,"value":1016},{"type":19,"tag":268,"props":2058,"children":2059},{"class":270,"line":317},[2060,2064,2068,2072,2076,2080,2085,2089,2093,2097,2102],{"type":19,"tag":268,"props":2061,"children":2062},{"style":275},[2063],{"type":24,"value":445},{"type":19,"tag":268,"props":2065,"children":2066},{"style":296},[2067],{"type":24,"value":1030},{"type":19,"tag":268,"props":2069,"children":2070},{"style":275},[2071],{"type":24,"value":490},{"type":19,"tag":268,"props":2073,"children":2074},{"style":296},[2075],{"type":24,"value":1039},{"type":19,"tag":268,"props":2077,"children":2078},{"style":275},[2079],{"type":24,"value":490},{"type":19,"tag":268,"props":2081,"children":2082},{"style":371},[2083],{"type":24,"value":2084},"\"$.amount\"",{"type":19,"tag":268,"props":2086,"children":2087},{"style":275},[2088],{"type":24,"value":379},{"type":19,"tag":268,"props":2090,"children":2091},{"style":296},[2092],{"type":24,"value":1057},{"type":19,"tag":268,"props":2094,"children":2095},{"style":275},[2096],{"type":24,"value":490},{"type":19,"tag":268,"props":2098,"children":2099},{"style":371},[2100],{"type":24,"value":2101},"\"5000\"",{"type":19,"tag":268,"props":2103,"children":2104},{"style":275},[2105],{"type":24,"value":2106},")))\n",{"type":19,"tag":268,"props":2108,"children":2109},{"class":270,"line":352},[2110,2114,2118,2122,2126,2130,2135,2139,2143,2147,2152],{"type":19,"tag":268,"props":2111,"children":2112},{"style":275},[2113],{"type":24,"value":445},{"type":19,"tag":268,"props":2115,"children":2116},{"style":296},[2117],{"type":24,"value":1030},{"type":19,"tag":268,"props":2119,"children":2120},{"style":275},[2121],{"type":24,"value":490},{"type":19,"tag":268,"props":2123,"children":2124},{"style":296},[2125],{"type":24,"value":1039},{"type":19,"tag":268,"props":2127,"children":2128},{"style":275},[2129],{"type":24,"value":490},{"type":19,"tag":268,"props":2131,"children":2132},{"style":371},[2133],{"type":24,"value":2134},"\"$.currency\"",{"type":19,"tag":268,"props":2136,"children":2137},{"style":275},[2138],{"type":24,"value":379},{"type":19,"tag":268,"props":2140,"children":2141},{"style":296},[2142],{"type":24,"value":1057},{"type":19,"tag":268,"props":2144,"children":2145},{"style":275},[2146],{"type":24,"value":490},{"type":19,"tag":268,"props":2148,"children":2149},{"style":371},[2150],{"type":24,"value":2151},"\"usd\"",{"type":19,"tag":268,"props":2153,"children":2154},{"style":275},[2155],{"type":24,"value":2106},{"type":19,"tag":268,"props":2157,"children":2158},{"class":270,"line":393},[2159,2163,2168,2172,2177,2181,2186,2190,2195],{"type":19,"tag":268,"props":2160,"children":2161},{"style":275},[2162],{"type":24,"value":445},{"type":19,"tag":268,"props":2164,"children":2165},{"style":296},[2166],{"type":24,"value":2167},"willReturn",{"type":19,"tag":268,"props":2169,"children":2170},{"style":275},[2171],{"type":24,"value":490},{"type":19,"tag":268,"props":2173,"children":2174},{"style":296},[2175],{"type":24,"value":2176},"ok",{"type":19,"tag":268,"props":2178,"children":2179},{"style":275},[2180],{"type":24,"value":1943},{"type":19,"tag":268,"props":2182,"children":2183},{"style":296},[2184],{"type":24,"value":2185},"withBody",{"type":19,"tag":268,"props":2187,"children":2188},{"style":275},[2189],{"type":24,"value":490},{"type":19,"tag":268,"props":2191,"children":2192},{"style":371},[2193],{"type":24,"value":2194},"\"\"\"{\"id\": \"ch_123\"}\"\"\"",{"type":19,"tag":268,"props":2196,"children":2197},{"style":275},[2198],{"type":24,"value":1016},{"type":19,"tag":268,"props":2200,"children":2201},{"class":270,"line":403},[2202],{"type":19,"tag":268,"props":2203,"children":2204},{"style":275},[2205],{"type":24,"value":1608},{"type":19,"tag":268,"props":2207,"children":2208},{"class":270,"line":412},[2209],{"type":19,"tag":268,"props":2210,"children":2211},{"style":311},[2212],{"type":24,"value":2213},"// If amount is wrong: \"No matching stub found\" - unhelpful!\n",{"type":19,"tag":268,"props":2215,"children":2216},{"class":270,"line":439},[2217],{"type":19,"tag":268,"props":2218,"children":2219},{"emptyLinePlaceholder":397},[2220],{"type":24,"value":400},{"type":19,"tag":268,"props":2222,"children":2223},{"class":270,"line":458},[2224],{"type":19,"tag":268,"props":2225,"children":2226},{"style":311},[2227],{"type":24,"value":2228},"// GOOD: Minimal stub + detailed verify\n",{"type":19,"tag":268,"props":2230,"children":2231},{"class":270,"line":475},[2232,2236,2240],{"type":19,"tag":268,"props":2233,"children":2234},{"style":275},[2235],{"type":24,"value":2018},{"type":19,"tag":268,"props":2237,"children":2238},{"style":296},[2239],{"type":24,"value":2023},{"type":19,"tag":268,"props":2241,"children":2242},{"style":275},[2243],{"type":24,"value":984},{"type":19,"tag":268,"props":2245,"children":2246},{"class":270,"line":517},[2247,2251,2255,2259,2263,2267],{"type":19,"tag":268,"props":2248,"children":2249},{"style":296},[2250],{"type":24,"value":2035},{"type":19,"tag":268,"props":2252,"children":2253},{"style":275},[2254],{"type":24,"value":490},{"type":19,"tag":268,"props":2256,"children":2257},{"style":296},[2258],{"type":24,"value":1002},{"type":19,"tag":268,"props":2260,"children":2261},{"style":275},[2262],{"type":24,"value":490},{"type":19,"tag":268,"props":2264,"children":2265},{"style":371},[2266],{"type":24,"value":2052},{"type":19,"tag":268,"props":2268,"children":2269},{"style":275},[2270],{"type":24,"value":1016},{"type":19,"tag":268,"props":2272,"children":2273},{"class":270,"line":539},[2274,2278,2282,2286,2290,2294,2298,2302,2306],{"type":19,"tag":268,"props":2275,"children":2276},{"style":275},[2277],{"type":24,"value":445},{"type":19,"tag":268,"props":2279,"children":2280},{"style":296},[2281],{"type":24,"value":2167},{"type":19,"tag":268,"props":2283,"children":2284},{"style":275},[2285],{"type":24,"value":490},{"type":19,"tag":268,"props":2287,"children":2288},{"style":296},[2289],{"type":24,"value":2176},{"type":19,"tag":268,"props":2291,"children":2292},{"style":275},[2293],{"type":24,"value":1943},{"type":19,"tag":268,"props":2295,"children":2296},{"style":296},[2297],{"type":24,"value":2185},{"type":19,"tag":268,"props":2299,"children":2300},{"style":275},[2301],{"type":24,"value":490},{"type":19,"tag":268,"props":2303,"children":2304},{"style":371},[2305],{"type":24,"value":2194},{"type":19,"tag":268,"props":2307,"children":2308},{"style":275},[2309],{"type":24,"value":1016},{"type":19,"tag":268,"props":2311,"children":2312},{"class":270,"line":548},[2313],{"type":19,"tag":268,"props":2314,"children":2315},{"style":275},[2316],{"type":24,"value":1608},{"type":19,"tag":268,"props":2318,"children":2319},{"class":270,"line":557},[2320],{"type":19,"tag":268,"props":2321,"children":2322},{"emptyLinePlaceholder":397},[2323],{"type":24,"value":400},{"type":19,"tag":268,"props":2325,"children":2326},{"class":270,"line":566},[2327],{"type":19,"tag":268,"props":2328,"children":2329},{"style":311},[2330],{"type":24,"value":2331},"// ... trigger the action ...\n",{"type":19,"tag":268,"props":2333,"children":2334},{"class":270,"line":575},[2335],{"type":19,"tag":268,"props":2336,"children":2337},{"emptyLinePlaceholder":397},[2338],{"type":24,"value":400},{"type":19,"tag":268,"props":2340,"children":2341},{"class":270,"line":584},[2342],{"type":19,"tag":268,"props":2343,"children":2344},{"style":311},[2345],{"type":24,"value":2346},"// verify() shows exactly what was received vs expected\n",{"type":19,"tag":268,"props":2348,"children":2349},{"class":270,"line":593},[2350,2354,2358],{"type":19,"tag":268,"props":2351,"children":2352},{"style":275},[2353],{"type":24,"value":2018},{"type":19,"tag":268,"props":2355,"children":2356},{"style":296},[2357],{"type":24,"value":979},{"type":19,"tag":268,"props":2359,"children":2360},{"style":275},[2361],{"type":24,"value":984},{"type":19,"tag":268,"props":2363,"children":2364},{"class":270,"line":602},[2365,2370,2374,2378,2382,2386],{"type":19,"tag":268,"props":2366,"children":2367},{"style":296},[2368],{"type":24,"value":2369},"    postRequestedFor",{"type":19,"tag":268,"props":2371,"children":2372},{"style":275},[2373],{"type":24,"value":490},{"type":19,"tag":268,"props":2375,"children":2376},{"style":296},[2377],{"type":24,"value":1002},{"type":19,"tag":268,"props":2379,"children":2380},{"style":275},[2381],{"type":24,"value":490},{"type":19,"tag":268,"props":2383,"children":2384},{"style":371},[2385],{"type":24,"value":2052},{"type":19,"tag":268,"props":2387,"children":2388},{"style":275},[2389],{"type":24,"value":1016},{"type":19,"tag":268,"props":2391,"children":2392},{"class":270,"line":616},[2393,2397,2401,2405,2409,2413,2417,2421,2425,2429,2433],{"type":19,"tag":268,"props":2394,"children":2395},{"style":275},[2396],{"type":24,"value":445},{"type":19,"tag":268,"props":2398,"children":2399},{"style":296},[2400],{"type":24,"value":1030},{"type":19,"tag":268,"props":2402,"children":2403},{"style":275},[2404],{"type":24,"value":490},{"type":19,"tag":268,"props":2406,"children":2407},{"style":296},[2408],{"type":24,"value":1039},{"type":19,"tag":268,"props":2410,"children":2411},{"style":275},[2412],{"type":24,"value":490},{"type":19,"tag":268,"props":2414,"children":2415},{"style":371},[2416],{"type":24,"value":2084},{"type":19,"tag":268,"props":2418,"children":2419},{"style":275},[2420],{"type":24,"value":379},{"type":19,"tag":268,"props":2422,"children":2423},{"style":296},[2424],{"type":24,"value":1057},{"type":19,"tag":268,"props":2426,"children":2427},{"style":275},[2428],{"type":24,"value":490},{"type":19,"tag":268,"props":2430,"children":2431},{"style":371},[2432],{"type":24,"value":2101},{"type":19,"tag":268,"props":2434,"children":2435},{"style":275},[2436],{"type":24,"value":2106},{"type":19,"tag":268,"props":2438,"children":2439},{"class":270,"line":633},[2440,2444,2448,2452,2456,2460,2464,2468,2472,2476,2480],{"type":19,"tag":268,"props":2441,"children":2442},{"style":275},[2443],{"type":24,"value":445},{"type":19,"tag":268,"props":2445,"children":2446},{"style":296},[2447],{"type":24,"value":1030},{"type":19,"tag":268,"props":2449,"children":2450},{"style":275},[2451],{"type":24,"value":490},{"type":19,"tag":268,"props":2453,"children":2454},{"style":296},[2455],{"type":24,"value":1039},{"type":19,"tag":268,"props":2457,"children":2458},{"style":275},[2459],{"type":24,"value":490},{"type":19,"tag":268,"props":2461,"children":2462},{"style":371},[2463],{"type":24,"value":2134},{"type":19,"tag":268,"props":2465,"children":2466},{"style":275},[2467],{"type":24,"value":379},{"type":19,"tag":268,"props":2469,"children":2470},{"style":296},[2471],{"type":24,"value":1057},{"type":19,"tag":268,"props":2473,"children":2474},{"style":275},[2475],{"type":24,"value":490},{"type":19,"tag":268,"props":2477,"children":2478},{"style":371},[2479],{"type":24,"value":2151},{"type":19,"tag":268,"props":2481,"children":2482},{"style":275},[2483],{"type":24,"value":2106},{"type":19,"tag":268,"props":2485,"children":2486},{"class":270,"line":660},[2487],{"type":19,"tag":268,"props":2488,"children":2489},{"style":275},[2490],{"type":24,"value":1608},{"type":19,"tag":20,"props":2492,"children":2493},{},[2494],{"type":24,"value":2495},"When the verify fails, WireMock tells you exactly what was received. When a strict stub fails, you just get \"no matching stub\" with no clue what went wrong.",{"type":19,"tag":47,"props":2497,"children":2499},{"id":2498},"test-class-hierarchy-for-performance",[2500],{"type":24,"value":2501},"Test Class Hierarchy for Performance",{"type":19,"tag":20,"props":2503,"children":2504},{},[2505],{"type":24,"value":2506},"One mistake I see constantly: putting all WireMock servers in a single base test class. Every test starts every mock, even if it doesn't need them.",{"type":19,"tag":20,"props":2508,"children":2509},{},[2510],{"type":24,"value":2511},"Instead, use a hierarchy:",{"type":19,"tag":259,"props":2513,"children":2515},{"code":2514},"IntegrationTestBase                    ← DB, Redis, common auth\n    ├── StripeActionsTestBase          ← Adds Stripe WireMock\n    │       └── PaymentControllerTests\n    ├── TwilioActionsTestBase          ← Adds Twilio WireMock\n    │       └── NotificationTests\n    └── UserControllerTests            ← No extra mocks needed\n",[2516],{"type":19,"tag":115,"props":2517,"children":2518},{"__ignoreMap":7},[2519],{"type":24,"value":2514},{"type":19,"tag":20,"props":2521,"children":2522},{},[2523],{"type":24,"value":2524},"Tests that don't interact with Stripe shouldn't pay the cost of starting its mock server. This becomes significant when you have tests for 10+ external services.",{"type":19,"tag":47,"props":2526,"children":2528},{"id":2527},"custom-assertions-for-readability",[2529],{"type":24,"value":2530},"Custom Assertions for Readability",{"type":19,"tag":20,"props":2532,"children":2533},{},[2534],{"type":24,"value":2535},"Consider these two assertions:",{"type":19,"tag":259,"props":2537,"children":2539},{"code":2538,"language":12,"meta":7,"className":262,"style":7},"// What does this check? ISO date? Phone number? UUID?\n.hasEntrySatisfying(\"createdAt\", o -> assertThat(o)\n    .asString()\n    .matches(\"\\\\d{4}-\\\\d{2}-\\\\d{2}T\\\\d{2}:\\\\d{2}:\\\\d{2}.*\"))\n\n// Immediately clear\n.hasEntrySatisfying(\"createdAt\", isValidTimestamp())\n",[2540],{"type":19,"tag":115,"props":2541,"children":2542},{"__ignoreMap":7},[2543,2551,2590,2605,2684,2691,2699],{"type":19,"tag":268,"props":2544,"children":2545},{"class":270,"line":271},[2546],{"type":19,"tag":268,"props":2547,"children":2548},{"style":311},[2549],{"type":24,"value":2550},"// What does this check? ISO date? Phone number? UUID?\n",{"type":19,"tag":268,"props":2552,"children":2553},{"class":270,"line":287},[2554,2558,2562,2566,2570,2575,2580,2585],{"type":19,"tag":268,"props":2555,"children":2556},{"style":275},[2557],{"type":24,"value":1761},{"type":19,"tag":268,"props":2559,"children":2560},{"style":296},[2561],{"type":24,"value":893},{"type":19,"tag":268,"props":2563,"children":2564},{"style":275},[2565],{"type":24,"value":490},{"type":19,"tag":268,"props":2567,"children":2568},{"style":371},[2569],{"type":24,"value":842},{"type":19,"tag":268,"props":2571,"children":2572},{"style":275},[2573],{"type":24,"value":2574},", o ",{"type":19,"tag":268,"props":2576,"children":2577},{"style":281},[2578],{"type":24,"value":2579},"->",{"type":19,"tag":268,"props":2581,"children":2582},{"style":296},[2583],{"type":24,"value":2584}," assertThat",{"type":19,"tag":268,"props":2586,"children":2587},{"style":275},[2588],{"type":24,"value":2589},"(o)\n",{"type":19,"tag":268,"props":2591,"children":2592},{"class":270,"line":307},[2593,2597,2601],{"type":19,"tag":268,"props":2594,"children":2595},{"style":275},[2596],{"type":24,"value":1909},{"type":19,"tag":268,"props":2598,"children":2599},{"style":296},[2600],{"type":24,"value":771},{"type":19,"tag":268,"props":2602,"children":2603},{"style":275},[2604],{"type":24,"value":436},{"type":19,"tag":268,"props":2606,"children":2607},{"class":270,"line":317},[2608,2612,2617,2621,2626,2631,2636,2640,2645,2649,2654,2658,2663,2667,2671,2675,2680],{"type":19,"tag":268,"props":2609,"children":2610},{"style":275},[2611],{"type":24,"value":1909},{"type":19,"tag":268,"props":2613,"children":2614},{"style":296},[2615],{"type":24,"value":2616},"matches",{"type":19,"tag":268,"props":2618,"children":2619},{"style":275},[2620],{"type":24,"value":490},{"type":19,"tag":268,"props":2622,"children":2623},{"style":371},[2624],{"type":24,"value":2625},"\"",{"type":19,"tag":268,"props":2627,"children":2628},{"style":382},[2629],{"type":24,"value":2630},"\\\\",{"type":19,"tag":268,"props":2632,"children":2633},{"style":371},[2634],{"type":24,"value":2635},"d{4}-",{"type":19,"tag":268,"props":2637,"children":2638},{"style":382},[2639],{"type":24,"value":2630},{"type":19,"tag":268,"props":2641,"children":2642},{"style":371},[2643],{"type":24,"value":2644},"d{2}-",{"type":19,"tag":268,"props":2646,"children":2647},{"style":382},[2648],{"type":24,"value":2630},{"type":19,"tag":268,"props":2650,"children":2651},{"style":371},[2652],{"type":24,"value":2653},"d{2}T",{"type":19,"tag":268,"props":2655,"children":2656},{"style":382},[2657],{"type":24,"value":2630},{"type":19,"tag":268,"props":2659,"children":2660},{"style":371},[2661],{"type":24,"value":2662},"d{2}:",{"type":19,"tag":268,"props":2664,"children":2665},{"style":382},[2666],{"type":24,"value":2630},{"type":19,"tag":268,"props":2668,"children":2669},{"style":371},[2670],{"type":24,"value":2662},{"type":19,"tag":268,"props":2672,"children":2673},{"style":382},[2674],{"type":24,"value":2630},{"type":19,"tag":268,"props":2676,"children":2677},{"style":371},[2678],{"type":24,"value":2679},"d{2}.*\"",{"type":19,"tag":268,"props":2681,"children":2682},{"style":275},[2683],{"type":24,"value":1016},{"type":19,"tag":268,"props":2685,"children":2686},{"class":270,"line":352},[2687],{"type":19,"tag":268,"props":2688,"children":2689},{"emptyLinePlaceholder":397},[2690],{"type":24,"value":400},{"type":19,"tag":268,"props":2692,"children":2693},{"class":270,"line":393},[2694],{"type":19,"tag":268,"props":2695,"children":2696},{"style":311},[2697],{"type":24,"value":2698},"// Immediately clear\n",{"type":19,"tag":268,"props":2700,"children":2701},{"class":270,"line":403},[2702,2706,2710,2714,2718,2722,2726],{"type":19,"tag":268,"props":2703,"children":2704},{"style":275},[2705],{"type":24,"value":1761},{"type":19,"tag":268,"props":2707,"children":2708},{"style":296},[2709],{"type":24,"value":893},{"type":19,"tag":268,"props":2711,"children":2712},{"style":275},[2713],{"type":24,"value":490},{"type":19,"tag":268,"props":2715,"children":2716},{"style":371},[2717],{"type":24,"value":842},{"type":19,"tag":268,"props":2719,"children":2720},{"style":275},[2721],{"type":24,"value":379},{"type":19,"tag":268,"props":2723,"children":2724},{"style":296},[2725],{"type":24,"value":1384},{"type":19,"tag":268,"props":2727,"children":2728},{"style":275},[2729],{"type":24,"value":776},{"type":19,"tag":20,"props":2731,"children":2732},{},[2733,2735,2741],{"type":24,"value":2734},"Create custom assertions for common patterns. When someone reads ",{"type":19,"tag":115,"props":2736,"children":2738},{"className":2737},[],[2739],{"type":24,"value":2740},"isValidTimestamp()",{"type":24,"value":2742},", they instantly know what's being checked. When the test fails, the error message says \"expected valid timestamp\" instead of \"regex didn't match\".",{"type":19,"tag":47,"props":2744,"children":2746},{"id":2745},"what-i-dont-do-anymore",[2747],{"type":24,"value":2748},"What I Don't Do Anymore",{"type":19,"tag":20,"props":2750,"children":2751},{},[2752],{"type":24,"value":2753},"A few things I've deliberately stopped doing:",{"type":19,"tag":20,"props":2755,"children":2756},{},[2757,2762],{"type":19,"tag":67,"props":2758,"children":2759},{},[2760],{"type":24,"value":2761},"Unit tests for every method.",{"type":24,"value":2763}," I only write unit tests for genuinely complex algorithms. Business logic that's just orchestrating calls? Integration tests cover it better and don't break when you refactor.",{"type":19,"tag":20,"props":2765,"children":2766},{},[2767,2772,2774,2780,2782,2788],{"type":19,"tag":67,"props":2768,"children":2769},{},[2770],{"type":24,"value":2771},"Mocking internal services.",{"type":24,"value":2773}," If your ",{"type":19,"tag":115,"props":2775,"children":2777},{"className":2776},[],[2778],{"type":24,"value":2779},"OrderService",{"type":24,"value":2781}," calls your ",{"type":19,"tag":115,"props":2783,"children":2785},{"className":2784},[],[2786],{"type":24,"value":2787},"InventoryService",{"type":24,"value":2789},", don't mock the inventory service. Let them both run. Mock only external boundaries.",{"type":19,"tag":20,"props":2791,"children":2792},{},[2793,2798,2800,2806,2808,2814,2816,2822],{"type":19,"tag":67,"props":2794,"children":2795},{},[2796],{"type":24,"value":2797},"Testing implementation details.",{"type":24,"value":2799}," I don't care that ",{"type":19,"tag":115,"props":2801,"children":2803},{"className":2802},[],[2804],{"type":24,"value":2805},"processOrder()",{"type":24,"value":2807}," calls ",{"type":19,"tag":115,"props":2809,"children":2811},{"className":2810},[],[2812],{"type":24,"value":2813},"validateInventory()",{"type":24,"value":2815}," internally. I care that when I POST to ",{"type":19,"tag":115,"props":2817,"children":2819},{"className":2818},[],[2820],{"type":24,"value":2821},"/orders",{"type":24,"value":2823},", the right things happen.",{"type":19,"tag":47,"props":2825,"children":2827},{"id":2826},"the-result",[2828],{"type":24,"value":2829},"The Result",{"type":19,"tag":20,"props":2831,"children":2832},{},[2833],{"type":24,"value":2834},"Tests written this way have a few nice properties:",{"type":19,"tag":2836,"props":2837,"children":2838},"ol",{},[2839,2849,2859,2869],{"type":19,"tag":63,"props":2840,"children":2841},{},[2842,2847],{"type":19,"tag":67,"props":2843,"children":2844},{},[2845],{"type":24,"value":2846},"They survive refactoring.",{"type":24,"value":2848}," Change the internals all you want - as long as behavior stays the same, tests pass.",{"type":19,"tag":63,"props":2850,"children":2851},{},[2852,2857],{"type":19,"tag":67,"props":2853,"children":2854},{},[2855],{"type":24,"value":2856},"They document requirements.",{"type":24,"value":2858}," Each test is a user story. New developers can read the test suite to understand what the system does.",{"type":19,"tag":63,"props":2860,"children":2861},{},[2862,2867],{"type":19,"tag":67,"props":2863,"children":2864},{},[2865],{"type":24,"value":2866},"They catch real bugs.",{"type":24,"value":2868}," Because they test at API boundaries with real infrastructure, they catch issues that unit tests miss.",{"type":19,"tag":63,"props":2870,"children":2871},{},[2872,2877],{"type":19,"tag":67,"props":2873,"children":2874},{},[2875],{"type":24,"value":2876},"They're maintainable.",{"type":24,"value":2878}," One test per use case means fewer tests overall, and each test has a clear purpose.",{"type":19,"tag":20,"props":2880,"children":2881},{},[2882],{"type":24,"value":2883},"The initial investment is higher than slapping together some unit tests. But six months later, when you're refactoring without fear because your test suite actually gives you confidence? Worth it.",{"type":19,"tag":20,"props":2885,"children":2886},{},[2887],{"type":24,"value":2888},"I've packaged these patterns into a Claude Code skill that helps generate tests following this approach. This is still very experimental and under heavy change but I might soon release it, however the principles apply regardless of what tools you use.",{"type":19,"tag":2890,"props":2891,"children":2892},"style",{},[2893],{"type":24,"value":2894},"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 .sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}html.sepia .shiki span {color: var(--shiki-sepia);background: var(--shiki-sepia-bg);font-style: var(--shiki-sepia-font-style);font-weight: var(--shiki-sepia-font-weight);text-decoration: var(--shiki-sepia-text-decoration);}",{"title":7,"searchDepth":287,"depth":287,"links":2896},[2897,2898,2899,2900,2901,2902,2903,2904,2905,2906],{"id":49,"depth":287,"text":52},{"id":148,"depth":287,"text":151},{"id":218,"depth":287,"text":221},{"id":1088,"depth":287,"text":1091},{"id":1736,"depth":287,"text":1739},{"id":1976,"depth":287,"text":1979},{"id":2498,"depth":287,"text":2501},{"id":2527,"depth":287,"text":2530},{"id":2745,"depth":287,"text":2748},{"id":2826,"depth":287,"text":2829},"markdown","content:blog:20260204.integration-tests-that-tell-stories.md","content","blog/20260204.integration-tests-that-tell-stories.md","blog/20260204.integration-tests-that-tell-stories","md",{"_path":2914,"_dir":7,"_draft":6,"_partial":6,"_locale":7,"body":2915,"_id":2916,"_type":2917,"title":2918,"_source":2909,"_file":2919,"_stem":2920,"_extension":2917},"/comments",[],"content:comments.json","json","Comments","comments.json","comments",1775596069482]