[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"sanity-okU-sb1PsybPzjwa0WiPyhFLhCoEnn7N6s6Li43mw7g":3,"sanity-zvCwQTQDfFCTxQujwKeTOQzHOiH8USdheTfYIN1ZnBI":927},{"data":4,"sourceMap":-1},{"latestPodcast":5,"latestReleases":14,"post":39,"recent":902},[6],{"_id":7,"publishedAt":8,"slug":9,"sponsored":12,"title":13},"f83eb5f0-1237-487f-84d8-f7abf2318c39","2026-06-25T07:40:00.000Z",{"_type":10,"current":11},"slug","code-isnt-causing-your-production-failures",null,"Code isn’t the only thing causing your production failures",[15,21,27,33],{"_id":16,"publishedAt":17,"slug":18,"title":20},"eb5b66eb-9410-4329-83bb-22bbff39402a","2026-04-28T13:00:00.000Z",{"_type":10,"current":19},"turn-scattered-knowledge-into-trusted-intelligence","Turning scattered knowledge into trusted intelligence: Stack Internal 2026.3",{"_id":22,"publishedAt":23,"slug":24,"title":26},"369c2401-b62e-4a37-8ff8-bf603023ecad","2026-03-02T15:03:00.988Z",{"_type":10,"current":25},"what-s-new-at-stack-overflow-march-2026","What’s new at Stack Overflow: March 2026",{"_id":28,"publishedAt":29,"slug":30,"title":32},"5e9053a4-07ea-447c-91ea-29e0b6228537","2026-02-02T15:00:00.000Z",{"_type":10,"current":31},"what-s-new-at-stack-overflow-february-2026","What’s new at Stack Overflow: February 2026",{"_id":34,"publishedAt":35,"slug":36,"title":38},"a1b538eb-a8a6-46d0-80a1-ac70ec9bb935","2026-01-05T10:00:00.000-05:00",{"_type":10,"current":37},"what-s-new-at-stack-overflow-january-2026","What’s new at Stack Overflow: January 2026",{"_createdAt":40,"_id":41,"_rev":42,"_system":43,"_type":46,"_updatedAt":47,"author":48,"body":65,"comments":871,"dateUrl":872,"excerpt":873,"image":874,"legacyBody":877,"product":12,"publishedAt":880,"slug":881,"sponsored":12,"tags":883,"title":901,"visible":871},"2023-05-24T12:51:03Z","wp-post-21091","OuLxq6F1ZwNJHk6WTUPnf2",{"base":44},{"id":41,"rev":45},"LIB4H7UiOBRX1pcyre20CY","blogPost","2026-05-11T18:33:49Z",[49],{"_createdAt":50,"_id":51,"_rev":52,"_type":53,"_updatedAt":54,"avatar":55,"bio":60,"employee":61,"name":62,"slug":63},"2023-05-23T16:27:18Z","wp-author-cap-19328","dgl3SCUzppW3U2LvCoP6c0","blogAuthor","2023-06-20T15:05:12Z",{"_type":56,"asset":57},"image",{"_ref":58,"_type":59},"image-86a0c56b829a0bbe0f28e601dd213fe0e769b7b6-40x40-jpg","reference","","none","Mark Seeman",{"current":64},"mark-seeman",[66,77,90,98,106,115,132,135,163,179,182,201,204,242,250,253,296,304,322,330,345,348,364,372,414,417,432,440,448,456,475,483,486,494,510,526,534,558,566,574,582,590,606,636,651,662,670,678,693,701,717,720,736,739,747,755,763,779,787,790,824,832,840,855,863],{"_key":67,"_type":68,"children":69,"markDefs":75,"style":76},"efcee81458e4","block",[70],{"_key":71,"_type":72,"marks":73,"text":74},"efcee81458e40","span",[],"Assertion Roulette doesn't mean that multiple assertions are bad.",[],"normal",{"_key":78,"_type":68,"children":79,"markDefs":89,"style":76},"0a9f117d2f57",[80,84],{"_key":81,"_type":72,"marks":82,"text":83},"0a9f117d2f570",[],"When I coach teams or individual developers in test-driven development (TDD) or unit testing, I frequently encounter a particular notion: ",{"_key":85,"_type":72,"marks":86,"text":88},"0a9f117d2f571",[87],"em","Multiple assertions are bad. A test must have only one assertion.",[],{"_key":91,"_type":68,"children":92,"markDefs":97,"style":76},"3a7b57fcf3b0",[93],{"_key":94,"_type":72,"marks":95,"text":96},"3a7b57fcf3b00",[],"That idea is rarely helpful.",[],{"_key":99,"_type":68,"children":100,"markDefs":105,"style":76},"5de371ea62f3",[101],{"_key":102,"_type":72,"marks":103,"text":104},"5de371ea62f30",[],"Let's examine a realistic code example and subsequently try to understand the origins of the notion.",[],{"_key":107,"_type":68,"children":108,"markDefs":113,"style":114},"97f7f06597d0",[109],{"_key":110,"_type":72,"marks":111,"text":112},"97f7f06597d00",[],"Outside-in TDD",[],"h2",{"_key":116,"_type":68,"children":117,"markDefs":131,"style":76},"e022d00cf7e1",[118,122,127],{"_key":119,"_type":72,"marks":120,"text":121},"e022d00cf7e10",[],"Consider a REST API that enables you to make and cancel restaurant reservations. First, an HTTP ",{"_key":123,"_type":72,"marks":124,"text":126},"e022d00cf7e11",[125],"code","POST",{"_key":128,"_type":72,"marks":129,"text":130},"e022d00cf7e12",[]," request makes a reservation:",[],{"_key":133,"_type":125,"code":134,"markDefs":12},"c5ae1d410166","POST /restaurants/1/reservations?sig=epi301tdlc57d0HwLCz[...] HTTP/1.1\nContent-Type: application/json\n{\n  \"at\": \"2023-09-22 18:47\",\n  \"name\": \"Teri Bell\",\n  \"email\": \"terrible@example.org\",\n  \"quantity\": 1\n}\n\nHTTP/1.1 201 Created\nContent-Type: application/json; charset=utf-8\nLocation: /restaurants/1/reservations/971167d4c79441b78fe70cc702[...]\n{\n  \"id\": \"971167d4c79441b78fe70cc702d3e1f6\",\n  \"at\": \"2023-09-22T18:47:00.0000000\",\n  \"email\": \"terrible@example.org\",\n  \"name\": \"Teri Bell\",\n  \"quantity\": 1\n}\n",{"_key":136,"_type":68,"children":137,"markDefs":159,"style":76},"3659d0363901",[138,142,147,151,155],{"_key":139,"_type":72,"marks":140,"text":141},"3659d03639010",[],"Notice that in ",{"_key":143,"_type":72,"marks":144,"text":146},"3659d03639011",[145],"e97495b6b1b9","proper REST",{"_key":148,"_type":72,"marks":149,"text":150},"3659d03639012",[]," fashion, the response returns the location of the created reservation in the ",{"_key":152,"_type":72,"marks":153,"text":154},"3659d03639013",[125],"Location",{"_key":156,"_type":72,"marks":157,"text":158},"3659d03639014",[]," header.",[160],{"_key":145,"_type":161,"href":162,"reference":12},"link","https://martinfowler.com/articles/richardsonMaturityModel.html",{"_key":164,"_type":68,"children":165,"markDefs":178,"style":76},"e14a1a4e66f1",[166,170,174],{"_key":167,"_type":72,"marks":168,"text":169},"e14a1a4e66f10",[],"If you change your mind, you can cancel the reservation with a ",{"_key":171,"_type":72,"marks":172,"text":173},"e14a1a4e66f11",[125],"DELETE",{"_key":175,"_type":72,"marks":176,"text":177},"e14a1a4e66f12",[]," request:",[],{"_key":180,"_type":125,"code":181,"markDefs":12},"17355c65f68d","DELETE /restaurants/1/reservations/971167d4c79441b78fe70cc702[...] HTTP/1.1\n\nHTTP/1.1 200 OK\n",{"_key":183,"_type":68,"children":184,"markDefs":198,"style":76},"769bbffdfbbd",[185,189,194],{"_key":186,"_type":72,"marks":187,"text":188},"769bbffdfbbd0",[],"Imagine that this is the desired interaction. Using ",{"_key":190,"_type":72,"marks":191,"text":193},"769bbffdfbbd1",[192],"bbe35b4d81ae","outside-in TDD",{"_key":195,"_type":72,"marks":196,"text":197},"769bbffdfbbd2",[]," you write the following test:",[199],{"_key":192,"_type":161,"href":200,"reference":12},"https://blog.ploeh.dk/outside-in-tdd",{"_key":202,"_type":125,"code":203,"markDefs":12},"baad60de70a8","[Theory]\n[InlineData(884, 18, 47, \"c@example.net\", \"Nick Klimenko\", 2)]\n[InlineData(902, 18, 50, \"emot@example.gov\", \"Emma Otting\", 5)]\npublic async Task DeleteReservation(\n    int days, int hours, int minutes,\n    string email, string name, int quantity)\n{\n    using var api = new LegacyApi();\n    var at = DateTime.Today.AddDays(days).At(hours, minutes)\n        .ToIso8601DateTimeString();\n    var dto = Create.ReservationDto(at, email, name, quantity);\n    var postResp = await api.PostReservation(dto);\n    Uri address = FindReservationAddress(postResp);\n \n    var deleteResp = await api.CreateClient().DeleteAsync(address);\n \n    Assert.True(\n        deleteResp.IsSuccessStatusCode,\n        $\"Actual status code: {deleteResp.StatusCode}.\");\n}\n",{"_key":205,"_type":68,"children":206,"markDefs":237,"style":76},"aea345ccaae8",[207,211,216,220,224,228,233],{"_key":208,"_type":72,"marks":209,"text":210},"aea345ccaae80",[],"This example is in C# using ",{"_key":212,"_type":72,"marks":213,"text":215},"aea345ccaae81",[214],"df50878bee7d","xUnit.net",{"_key":217,"_type":72,"marks":218,"text":219},"aea345ccaae82",[]," because we need ",{"_key":221,"_type":72,"marks":222,"text":223},"aea345ccaae83",[87],"some",{"_key":225,"_type":72,"marks":226,"text":227},"aea345ccaae84",[]," language and framework to show realistic code. The point of the article, however, applies across languages and frameworks. The code examples in this article are based on the sample code base that accompanies my book ",{"_key":229,"_type":72,"marks":230,"text":232},"aea345ccaae85",[231,87],"20ed67cf63f5","Code That Fits in Your Head",{"_key":234,"_type":72,"marks":235,"text":236},"aea345ccaae86",[],".",[238,240],{"_key":214,"_type":161,"href":239,"reference":12},"https://xunit.net/",{"_key":231,"_type":161,"href":241,"reference":12},"https://blog.ploeh.dk/2021/06/14/new-book-code-that-fits-in-your-head",{"_key":243,"_type":68,"children":244,"markDefs":249,"style":76},"60a13ee00144",[245],{"_key":246,"_type":72,"marks":247,"text":248},"60a13ee001440",[],"In order to pass this test, you can implement the server-side code like this:",[],{"_key":251,"_type":125,"code":252,"markDefs":12},"f16ece71ea1e","[HttpDelete(\"restaurants/{restaurantId}/reservations/{id}\")]\npublic void Delete(int restaurantId, string id)\n{\n}\n",{"_key":254,"_type":68,"children":255,"markDefs":293,"style":76},"48b8886bcbbd",[256,260,265,269,273,277,281,285,289],{"_key":257,"_type":72,"marks":258,"text":259},"48b8886bcbbd0",[],"While clearly a ",{"_key":261,"_type":72,"marks":262,"text":264},"48b8886bcbbd1",[263],"a7fbb028cd52","no-op",{"_key":266,"_type":72,"marks":267,"text":268},"48b8886bcbbd2",[],", this implementation passes all tests. The newly-written test asserts that the HTTP response returns a status code in the ",{"_key":270,"_type":72,"marks":271,"text":272},"48b8886bcbbd3",[125],"200",{"_key":274,"_type":72,"marks":275,"text":276},"48b8886bcbbd4",[]," (success) range. This is part of the API's REST protocol, so this response is important. You want to keep this assertion around as a regression test. If the API ever begins to return a status code in the ",{"_key":278,"_type":72,"marks":279,"text":280},"48b8886bcbbd5",[125],"400",{"_key":282,"_type":72,"marks":283,"text":284},"48b8886bcbbd6",[]," or ",{"_key":286,"_type":72,"marks":287,"text":288},"48b8886bcbbd7",[125],"500",{"_key":290,"_type":72,"marks":291,"text":292},"48b8886bcbbd8",[]," range, it would be a breaking change.",[294],{"_key":263,"_type":161,"href":295,"reference":12},"https://en.wikipedia.org/wiki/NOP_(code)",{"_key":297,"_type":68,"children":298,"markDefs":303,"style":76},"df033a3ab1ea",[299],{"_key":300,"_type":72,"marks":301,"text":302},"df033a3ab1ea0",[],"So far, so good. TDD is an incremental process. One test doesn't drive a full feature.",[],{"_key":305,"_type":68,"children":306,"markDefs":319,"style":76},"ad613dc07433",[307,311,316],{"_key":308,"_type":72,"marks":309,"text":310},"ad613dc074330",[],"Since all tests are passing, you can ",{"_key":312,"_type":72,"marks":313,"text":315},"ad613dc074331",[314],"9c0ab49d55f3","commit the changes to source control and proceed to the next iteration",{"_key":317,"_type":72,"marks":318,"text":236},"ad613dc074332",[],[320],{"_key":314,"_type":161,"href":321,"reference":12},"https://stackoverflow.blog/2022/04/06/use-git-tactically/",{"_key":323,"_type":68,"children":324,"markDefs":329,"style":114},"507922c8ca5a",[325],{"_key":326,"_type":72,"marks":327,"text":328},"507922c8ca5a0",[],"Strengthening the postconditions",[],{"_key":331,"_type":68,"children":332,"markDefs":344,"style":76},"1adf85b97b2b",[333,337,341],{"_key":334,"_type":72,"marks":335,"text":336},"1adf85b97b2b0",[],"You should be able to check that the resource is truly gone by making a ",{"_key":338,"_type":72,"marks":339,"text":340},"1adf85b97b2b1",[125],"GET",{"_key":342,"_type":72,"marks":343,"text":177},"1adf85b97b2b2",[],[],{"_key":346,"_type":125,"code":347,"markDefs":12},"629992900f85","GET /restaurants/1/reservations/971167d4c79441b78fe70cc702[...] HTTP/1.1\n\nHTTP/1.1 404 Not Found",{"_key":349,"_type":68,"children":350,"markDefs":363,"style":76},"98148dda9509",[351,355,359],{"_key":352,"_type":72,"marks":353,"text":354},"98148dda95090",[],"This, however, is not the behavior of the current implementation of ",{"_key":356,"_type":72,"marks":357,"text":358},"98148dda95091",[125],"Delete",{"_key":360,"_type":72,"marks":361,"text":362},"98148dda95092",[],", which does nothing. It seems that you're going to need another test.",[],{"_key":365,"_type":68,"children":366,"markDefs":371,"style":76},"dbc03fbd8728",[367],{"_key":368,"_type":72,"marks":369,"text":370},"dbc03fbd87280",[],"Or do you?",[],{"_key":373,"_type":68,"children":374,"markDefs":411,"style":76},"bfa7dbc96384",[375,379,383,387,392,396,399,403,407],{"_key":376,"_type":72,"marks":377,"text":378},"bfa7dbc963840",[],"One option is to ",{"_key":380,"_type":72,"marks":381,"text":382},"bfa7dbc963841",[87],"copy",{"_key":384,"_type":72,"marks":385,"text":386},"bfa7dbc963842",[]," the existing test and change ",{"_key":388,"_type":72,"marks":389,"text":391},"bfa7dbc963843",[390],"f92eab8b807a","the assertion phase",{"_key":393,"_type":72,"marks":394,"text":395},"bfa7dbc963844",[]," to perform the above ",{"_key":397,"_type":72,"marks":398,"text":340},"bfa7dbc963845",[125],{"_key":400,"_type":72,"marks":401,"text":402},"bfa7dbc963846",[]," request to check that the response status is ",{"_key":404,"_type":72,"marks":405,"text":406},"bfa7dbc963847",[125],"404",{"_key":408,"_type":72,"marks":409,"text":410},"bfa7dbc963848",[],":",[412],{"_key":390,"_type":161,"href":413,"reference":12},"https://blog.ploeh.dk/2013/06/24/a-heuristic-for-formatting-code-according-to-the-aaa-pattern",{"_key":415,"_type":125,"code":416,"markDefs":12},"752a8a28c29a","[Theory]\n[InlineData(884, 18, 47, \"c@example.net\", \"Nick Klimenko\", 2)]\n[InlineData(902, 18, 50, \"emot@example.gov\", \"Emma Otting\", 5)]\npublic async Task DeleteReservationActuallyDeletes(\n    int days, int hours, int minutes,\n    string email, string name, int quantity)\n{\n    using var api = new LegacyApi();\n    var at = DateTime.Today.AddDays(days).At(hours, minutes)\n        .ToIso8601DateTimeString();\n    var dto = Create.ReservationDto(at, email, name, quantity);\n    var postResp = await api.PostReservation(dto);\n    Uri address = FindReservationAddress(postResp);\n \n    var deleteResp = await api.CreateClient().DeleteAsync(address);\n \n    var getResp = await api.CreateClient().GetAsync(address);\n    Assert.Equal(HttpStatusCode.NotFound, getResp.StatusCode);\n}\n",{"_key":418,"_type":68,"children":419,"markDefs":431,"style":76},"b983e2bda07c",[420,424,427],{"_key":421,"_type":72,"marks":422,"text":423},"b983e2bda07c0",[],"This does, indeed, prompt you to properly implement the server-side ",{"_key":425,"_type":72,"marks":426,"text":358},"b983e2bda07c1",[125],{"_key":428,"_type":72,"marks":429,"text":430},"b983e2bda07c2",[]," method.",[],{"_key":433,"_type":68,"children":434,"markDefs":439,"style":76},"f7b3cdcf1955",[435],{"_key":436,"_type":72,"marks":437,"text":438},"f7b3cdcf19550",[],"Is this, however, a good idea? Is the test code easy to maintain?",[],{"_key":441,"_type":68,"children":442,"markDefs":447,"style":76},"5b73d9907433",[443],{"_key":444,"_type":72,"marks":445,"text":446},"5b73d99074330",[],"Test code is code too, and you have to maintain it. Copy and paste is problematic in test code for the same reasons that it can be a problem in production code. If you later have to change something, you have to identify all the places that you have to edit. It's easy to miss one, which can lead to bugs. This is true for test code as well.",[],{"_key":449,"_type":68,"children":450,"markDefs":455,"style":114},"b247fdd9ac81",[451],{"_key":452,"_type":72,"marks":453,"text":454},"b247fdd9ac810",[],"One action, more assertions",[],{"_key":457,"_type":68,"children":458,"markDefs":472,"style":76},"f5989a5663d1",[459,463,468],{"_key":460,"_type":72,"marks":461,"text":462},"f5989a5663d10",[],"Instead of copy-and-pasting the first test, why not instead ",{"_key":464,"_type":72,"marks":465,"text":467},"f5989a5663d11",[466],"3fe8f6140439","strengthen the postconditions of the first test case",{"_key":469,"_type":72,"marks":470,"text":471},"f5989a5663d12",[],"?",[473],{"_key":466,"_type":161,"href":474,"reference":12},"https://blog.ploeh.dk/2021/12/13/backwards-compatibility-as-a-profunctor",{"_key":476,"_type":68,"children":477,"markDefs":482,"style":76},"0311c0ada91f",[478],{"_key":479,"_type":72,"marks":480,"text":481},"0311c0ada91f0",[],"Just add the new assertion after the first assertion:",[],{"_key":484,"_type":125,"code":485,"markDefs":12},"c4c268604b70","[Theory]\n[InlineData(884, 18, 47, \"c@example.net\", \"Nick Klimenko\", 2)]\n[InlineData(902, 18, 50, \"emot@example.gov\", \"Emma Otting\", 5)]\npublic async Task DeleteReservation(\n    int days, int hours, int minutes,\n    string email, string name, int quantity)\n{\n    using var api = new LegacyApi();\n    var at = DateTime.Today.AddDays(days).At(hours, minutes)\n        .ToIso8601DateTimeString();\n    var dto = Create.ReservationDto(at, email, name, quantity);\n    var postResp = await api.PostReservation(dto);\n    Uri address = FindReservationAddress(postResp);\n \n    var deleteResp = await api.CreateClient().DeleteAsync(address);\n \n    Assert.True(\n        deleteResp.IsSuccessStatusCode,\n        $\"Actual status code: {deleteResp.StatusCode}.\");\n    var getResp = await api.CreateClient().GetAsync(address);\n    Assert.Equal(HttpStatusCode.NotFound, getResp.StatusCode);\n}\n",{"_key":487,"_type":68,"children":488,"markDefs":493,"style":76},"27a0eefbe3b1",[489],{"_key":490,"_type":72,"marks":491,"text":492},"27a0eefbe3b10",[],"This means that you only have a single test method to maintain instead of two duplicated methods that are almost identical.",[],{"_key":495,"_type":68,"children":496,"markDefs":509,"style":76},"d63056a138ce",[497,501,505],{"_key":498,"_type":72,"marks":499,"text":500},"d63056a138ce0",[87],"But,",{"_key":502,"_type":72,"marks":503,"text":504},"d63056a138ce1",[]," some of the people I've coached might say, ",{"_key":506,"_type":72,"marks":507,"text":508},"d63056a138ce2",[87],"this test has two assertions!",[],{"_key":511,"_type":68,"children":512,"markDefs":525,"style":76},"fb2ee51c5c74",[513,517,521],{"_key":514,"_type":72,"marks":515,"text":516},"fb2ee51c5c740",[],"Indeed. So what? It's one single ",{"_key":518,"_type":72,"marks":519,"text":520},"fb2ee51c5c741",[87],"test case",{"_key":522,"_type":72,"marks":523,"text":524},"fb2ee51c5c742",[],": Cancelling a reservation.",[],{"_key":527,"_type":68,"children":528,"markDefs":533,"style":76},"706047d5185c",[529],{"_key":530,"_type":72,"marks":531,"text":532},"706047d5185c0",[],"While cancelling a reservation is a single action, we care about multiple outcomes:",[],{"_key":535,"_type":68,"children":536,"level":555,"listItem":556,"markDefs":557,"style":76},"2dd04da358e6",[537,541,544,548,551],{"_key":538,"_type":72,"marks":539,"text":540},"2dd04da358e60",[],"The status code after a successful ",{"_key":542,"_type":72,"marks":543,"text":173},"2dd04da358e61",[125],{"_key":545,"_type":72,"marks":546,"text":547},"2dd04da358e62",[]," request should be in the ",{"_key":549,"_type":72,"marks":550,"text":272},"2dd04da358e63",[125],{"_key":552,"_type":72,"marks":553,"text":554},"2dd04da358e64",[]," range.",1,"bullet",[],{"_key":559,"_type":68,"children":560,"level":555,"listItem":556,"markDefs":565,"style":76},"af0bdae645a4",[561],{"_key":562,"_type":72,"marks":563,"text":564},"af0bdae645a40",[],"The reservation resource should be gone.",[],{"_key":567,"_type":68,"children":568,"markDefs":573,"style":76},"73d65aa0813a",[569],{"_key":570,"_type":72,"marks":571,"text":572},"73d65aa0813a0",[],"Developing the system further, we might add more behaviors that we care about. Perhaps the system should also send an email about the cancellation. We should assert that as well. It's still the same test case, though: Successfully cancelling a reservation.",[],{"_key":575,"_type":68,"children":576,"markDefs":581,"style":76},"1008d4a33820",[577],{"_key":578,"_type":72,"marks":579,"text":580},"1008d4a338200",[],"There's nothing wrong with multiple assertions in a single test. The above example illustrates the benefits. A single test case can have multiple outcomes that should all be verified.",[],{"_key":583,"_type":68,"children":584,"markDefs":589,"style":114},"459b27e85d78",[585],{"_key":586,"_type":72,"marks":587,"text":588},"459b27e85d780",[],"Origins of the single assertion notion",[],{"_key":591,"_type":68,"children":592,"markDefs":605,"style":76},"303b2987eb37",[593,597,601],{"_key":594,"_type":72,"marks":595,"text":596},"303b2987eb370",[],"Where does the ",{"_key":598,"_type":72,"marks":599,"text":600},"303b2987eb371",[87],"only one assertion per test",{"_key":602,"_type":72,"marks":603,"text":604},"303b2987eb372",[]," notion come from? I don't know, but I can guess.",[],{"_key":607,"_type":68,"children":608,"markDefs":631,"style":76},"ef1f975eab14",[609,613,618,622,627],{"_key":610,"_type":72,"marks":611,"text":612},"ef1f975eab140",[],"The excellent book ",{"_key":614,"_type":72,"marks":615,"text":617},"ef1f975eab141",[616,87],"4c4e8af03951","xUnit Test Patterns",{"_key":619,"_type":72,"marks":620,"text":621},"ef1f975eab142",[]," describes a test smell named ",{"_key":623,"_type":72,"marks":624,"text":626},"ef1f975eab143",[625],"9d2afb708088","Assertion Roulette",{"_key":628,"_type":72,"marks":629,"text":630},"ef1f975eab144",[],". It describes situations where it may be difficult to determine exactly which assertion caused a test failure.",[632,634],{"_key":616,"_type":161,"href":633,"reference":12},"https://blog.ploeh.dk/ref/xunit-patterns",{"_key":625,"_type":161,"href":635,"reference":12},"http://xunitpatterns.com/Assertion%20Roulette.html",{"_key":637,"_type":68,"children":638,"markDefs":650,"style":76},"8ad5e5116057",[639,643,646],{"_key":640,"_type":72,"marks":641,"text":642},"8ad5e51160570",[],"It looks to me as though the ",{"_key":644,"_type":72,"marks":645,"text":600},"8ad5e51160571",[87],{"_key":647,"_type":72,"marks":648,"text":649},"8ad5e51160572",[]," 'rule' stems from a misreading of the Assertion Roulette description. (I may even have contributed to that myself. I don't remember that I have, but to be honest I've produced so much content about unit testing over the decades that I don't want to assume myself free of guilt.)",[],{"_key":652,"_type":68,"children":653,"markDefs":661,"style":76},"d7363b331b21",[654,657],{"_key":655,"_type":72,"marks":656,"text":617},"d7363b331b210",[87],{"_key":658,"_type":72,"marks":659,"text":660},"d7363b331b211",[]," describes two causes of Assertion Roulette:",[],{"_key":663,"_type":68,"children":664,"level":555,"listItem":556,"markDefs":669,"style":76},"bc5197fc5607",[665],{"_key":666,"_type":72,"marks":667,"text":668},"bc5197fc56070",[],"Eager Test: A single test verifies too much functionality.",[],{"_key":671,"_type":68,"children":672,"level":555,"listItem":556,"markDefs":677,"style":76},"a7c714a51bed",[673],{"_key":674,"_type":72,"marks":675,"text":676},"a7c714a51bed0",[],"Missing Assertion Message",[],{"_key":679,"_type":68,"children":680,"markDefs":692,"style":76},"3215fe4d6765",[681,685,688],{"_key":682,"_type":72,"marks":683,"text":684},"3215fe4d67650",[],"You have an Eager Test when you're trying to exercise more than one ",{"_key":686,"_type":72,"marks":687,"text":520},"3215fe4d67651",[87],{"_key":689,"_type":72,"marks":690,"text":691},"3215fe4d67652",[],". You may be trying to simulate a 'session' where a client performs many steps in order to achieve a goal. As Gerard Meszaros writes regarding the test smell, this is appropriate for manual tests, but rarely for automated tests. It's not the number of assertions that cause problems, but that the test does too much.",[],{"_key":694,"_type":68,"children":695,"markDefs":700,"style":76},"d6d4177123a9",[696],{"_key":697,"_type":72,"marks":698,"text":699},"d6d4177123a90",[],"The other cause occurs when the assertions are sufficiently similar that you can't tell which one failed, and they have no assertion messages.",[],{"_key":702,"_type":68,"children":703,"markDefs":716,"style":76},"5c1d97a96f8c",[704,708,712],{"_key":705,"_type":72,"marks":706,"text":707},"5c1d97a96f8c0",[],"That's not the case with the above example. If the ",{"_key":709,"_type":72,"marks":710,"text":711},"5c1d97a96f8c1",[125],"Assert.True",{"_key":713,"_type":72,"marks":714,"text":715},"5c1d97a96f8c2",[]," assertion fails, the assertion message will tell you:",[],{"_key":718,"_type":125,"code":719,"markDefs":12},"7963813f41fd","Actual status code: NotFound.\nExpected: True\nActual:   False\n",{"_key":721,"_type":68,"children":722,"markDefs":735,"style":76},"3cf03ea03939",[723,727,731],{"_key":724,"_type":72,"marks":725,"text":726},"3cf03ea039390",[],"Likewise, if the ",{"_key":728,"_type":72,"marks":729,"text":730},"3cf03ea039391",[125],"Assert.Equal",{"_key":732,"_type":72,"marks":733,"text":734},"3cf03ea039392",[]," assertion fails, that too will be clear:",[],{"_key":737,"_type":125,"code":738,"markDefs":12},"a84fa6e1b6bc","Assert.Equal() Failure\nExpected: NotFound\nActual:   OK\n",{"_key":740,"_type":68,"children":741,"markDefs":746,"style":76},"f2a39f8d427a",[742],{"_key":743,"_type":72,"marks":744,"text":745},"f2a39f8d427a0",[],"There's no ambiguity.",[],{"_key":748,"_type":68,"children":749,"markDefs":754,"style":114},"5a10eba6775e",[750],{"_key":751,"_type":72,"marks":752,"text":753},"5a10eba6775e0",[],"One assertion per test",[],{"_key":756,"_type":68,"children":757,"markDefs":762,"style":76},"11efd0c996aa",[758],{"_key":759,"_type":72,"marks":760,"text":761},"11efd0c996aa0",[],"Now that you understand that multiple assertions per test are fine, you may be inclined to have a ball adding assertions like there's no tomorrow.",[],{"_key":764,"_type":68,"children":765,"markDefs":778,"style":76},"054ef93c9be0",[766,770,774],{"_key":767,"_type":72,"marks":768,"text":769},"054ef93c9be00",[],"Usually, however, there's a germ of truth in a persistent notion like the ",{"_key":771,"_type":72,"marks":772,"text":773},"054ef93c9be01",[87],"one test, one assertion",{"_key":775,"_type":72,"marks":776,"text":777},"054ef93c9be02",[]," 'rule'. Use good judgement.",[],{"_key":780,"_type":68,"children":781,"markDefs":786,"style":76},"694fc2385dbd",[782],{"_key":783,"_type":72,"marks":784,"text":785},"694fc2385dbd0",[],"If you consider what an automated test is, it's basically a predicate. It's a statement that we expect a particular outcome. We then compare the actual outcome to the expected outcome to see if they are equal. Thus, in essence, the ideal assertion is this:",[],{"_key":788,"_type":125,"code":789,"markDefs":12},"28722ef898f2","Assert.Equal(expected, actual);",{"_key":791,"_type":68,"children":792,"markDefs":821,"style":76},"fd8d4d3b9e53",[793,797,801,805,809,813,818],{"_key":794,"_type":72,"marks":795,"text":796},"fd8d4d3b9e530",[],"I can't always attain that ideal, but whenever I can, I feel deep satisfaction. Sometimes, ",{"_key":798,"_type":72,"marks":799,"text":800},"fd8d4d3b9e531",[125],"expected",{"_key":802,"_type":72,"marks":803,"text":804},"fd8d4d3b9e532",[]," and ",{"_key":806,"_type":72,"marks":807,"text":808},"fd8d4d3b9e533",[125],"actual",{"_key":810,"_type":72,"marks":811,"text":812},"fd8d4d3b9e534",[]," are primitive values like integers or strings, but they might also be complex values that represent the subset of program state that the test cares about. ",{"_key":814,"_type":72,"marks":815,"text":817},"fd8d4d3b9e535",[816],"043de21533cb","As long as the objects have structural equality, such an assertion is meaningful",{"_key":819,"_type":72,"marks":820,"text":236},"fd8d4d3b9e536",[],[822],{"_key":816,"_type":161,"href":823,"reference":12},"https://blog.ploeh.dk/2021/05/03/structural-equality-for-better-tests",{"_key":825,"_type":68,"children":826,"markDefs":831,"style":76},"61a925a2431c",[827],{"_key":828,"_type":72,"marks":829,"text":830},"61a925a2431c0",[],"At other times I can't quite find a way to express the verification step as succinctly as that. If I have to add another assertion or two, I'll do that.",[],{"_key":833,"_type":68,"children":834,"markDefs":839,"style":114},"f18620063a0f",[835],{"_key":836,"_type":72,"marks":837,"text":838},"f18620063a0f0",[],"Conclusion",[],{"_key":841,"_type":68,"children":842,"markDefs":854,"style":76},"0e707aa9ee07",[843,847,850],{"_key":844,"_type":72,"marks":845,"text":846},"0e707aa9ee070",[],"There's this notion that you're only allowed to write one assertion per unit test. It probably originates from real concerns about badly-factored test code, but over the years the nuanced test smell ",{"_key":848,"_type":72,"marks":849,"text":626},"0e707aa9ee071",[87],{"_key":851,"_type":72,"marks":852,"text":853},"0e707aa9ee072",[]," has become garbled into a simpler, but less helpful 'rule'.",[],{"_key":856,"_type":68,"children":857,"markDefs":862,"style":76},"f39bf0ea1b05",[858],{"_key":859,"_type":72,"marks":860,"text":861},"f39bf0ea1b050",[],"That 'rule' often gets in the way of maintainable test code. Programmers following the 'rule' resort to gratuitous copying and pasting instead of adding another assertion to an existing test.",[],{"_key":864,"_type":68,"children":865,"markDefs":870,"style":76},"e077b8e00d9c",[866],{"_key":867,"_type":72,"marks":868,"text":869},"e077b8e00d9c0",[],"If adding a relevant assertion to an existing test is the best way forward, don't let a misunderstood 'rule' stop you.",[],true,"2022/11/03","One test case, not one test assertion. ",{"_type":56,"asset":875},{"_ref":876,"_type":59},"image-0b31f3f5c575642b01c17db8bcb330d68a4fdf37-2560x1344-jpg",{"code":878,"language":879},"\u003C!-- wp:paragraph -->\n\u003Cp>Assertion Roulette doesn't mean that multiple assertions are bad.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>When I coach teams or individual developers in test-driven development (TDD) or unit testing, I frequently encounter a particular notion: \u003Cem>Multiple assertions are bad. A test must have only one assertion.\u003C/em>\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>That idea is rarely helpful.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>Let's examine a realistic code example and subsequently try to understand the origins of the notion.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:heading -->\n\u003Ch2 id=\"h-outside-in-tdd\">Outside-in TDD\u003C/h2>\n\u003C!-- /wp:heading -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>Consider a REST API that enables you to make and cancel restaurant reservations. First, an HTTP \u003Ccode>POST\u003C/code> request makes a reservation:\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:code -->\n\u003Cpre class=\"wp-block-code\">\u003Ccode>POST /restaurants/1/reservations?sig=epi301tdlc57d0HwLCz&#91;...] HTTP/1.1\nContent-Type: application/json\n{\n  \"at\": \"2023-09-22 18:47\",\n  \"name\": \"Teri Bell\",\n  \"email\": \"terrible@example.org\",\n  \"quantity\": 1\n}\n\nHTTP/1.1 201 Created\nContent-Type: application/json; charset=utf-8\nLocation: /restaurants/1/reservations/971167d4c79441b78fe70cc702&#91;...]\n{\n  \"id\": \"971167d4c79441b78fe70cc702d3e1f6\",\n  \"at\": \"2023-09-22T18:47:00.0000000\",\n  \"email\": \"terrible@example.org\",\n  \"name\": \"Teri Bell\",\n  \"quantity\": 1\n}\n\u003C/code>\u003C/pre>\n\u003C!-- /wp:code -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>Notice that in \u003Ca href=\"https://martinfowler.com/articles/richardsonMaturityModel.html\">proper REST\u003C/a> fashion, the response returns the location of the created reservation in the \u003Ccode>Location\u003C/code> header.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>If you change your mind, you can cancel the reservation with a \u003Ccode>DELETE\u003C/code> request:\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:code -->\n\u003Cpre class=\"wp-block-code\">\u003Ccode>DELETE /restaurants/1/reservations/971167d4c79441b78fe70cc702&#91;...] HTTP/1.1\n\nHTTP/1.1 200 OK\n\u003C/code>\u003C/pre>\n\u003C!-- /wp:code -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>Imagine that this is the desired interaction. Using \u003Ca href=\"https://blog.ploeh.dk/outside-in-tdd\">outside-in TDD\u003C/a> you write the following test:\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:code -->\n\u003Cpre class=\"wp-block-code\">\u003Ccode>&#91;Theory]\n&#91;InlineData(884, 18, 47, \"c@example.net\", \"Nick Klimenko\", 2)]\n&#91;InlineData(902, 18, 50, \"emot@example.gov\", \"Emma Otting\", 5)]\npublic async Task DeleteReservation(\n    int days, int hours, int minutes,\n    string email, string name, int quantity)\n{\n    using var api = new LegacyApi();\n    var at = DateTime.Today.AddDays(days).At(hours, minutes)\n        .ToIso8601DateTimeString();\n    var dto = Create.ReservationDto(at, email, name, quantity);\n    var postResp = await api.PostReservation(dto);\n    Uri address = FindReservationAddress(postResp);\n \n    var deleteResp = await api.CreateClient().DeleteAsync(address);\n \n    Assert.True(\n        deleteResp.IsSuccessStatusCode,\n        $\"Actual status code: {deleteResp.StatusCode}.\");\n}\n\u003C/code>\u003C/pre>\n\u003C!-- /wp:code -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>This example is in C# using \u003Ca href=\"https://xunit.net/\">xUnit.net\u003C/a> because we need \u003Cem>some\u003C/em> language and framework to show realistic code. The point of the article, however, applies across languages and frameworks. The code examples in this article are based on the sample code base that accompanies my book \u003Ca href=\"https://blog.ploeh.dk/2021/06/14/new-book-code-that-fits-in-your-head\">\u003Cem>Code That Fits in Your Head\u003C/em>\u003C/a>.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>In order to pass this test, you can implement the server-side code like this:\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:code -->\n\u003Cpre class=\"wp-block-code\">\u003Ccode>&#91;HttpDelete(\"restaurants/{restaurantId}/reservations/{id}\")]\npublic void Delete(int restaurantId, string id)\n{\n}\n\u003C/code>\u003C/pre>\n\u003C!-- /wp:code -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>While clearly a \u003Ca href=\"https://en.wikipedia.org/wiki/NOP_(code)\">no-op\u003C/a>, this implementation passes all tests. The newly-written test asserts that the HTTP response returns a status code in the \u003Ccode>200\u003C/code> (success) range. This is part of the API's REST protocol, so this response is important. You want to keep this assertion around as a regression test. If the API ever begins to return a status code in the \u003Ccode>400\u003C/code> or \u003Ccode>500\u003C/code> range, it would be a breaking change.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>So far, so good. TDD is an incremental process. One test doesn't drive a full feature.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>Since all tests are passing, you can \u003Ca href=\"https://stackoverflow.blog/2022/04/06/use-git-tactically/\">commit the changes to source control and proceed to the next iteration\u003C/a>.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:heading -->\n\u003Ch2 id=\"h-strengthening-the-postconditions\">Strengthening the postconditions\u003C/h2>\n\u003C!-- /wp:heading -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>You should be able to check that the resource is truly gone by making a \u003Ccode>GET\u003C/code> request:\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:code -->\n\u003Cpre class=\"wp-block-code\">\u003Ccode>GET /restaurants/1/reservations/971167d4c79441b78fe70cc702&#91;...] HTTP/1.1\n\nHTTP/1.1 404 Not Found\u003C/code>\u003C/pre>\n\u003C!-- /wp:code -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>This, however, is not the behavior of the current implementation of \u003Ccode>Delete\u003C/code>, which does nothing. It seems that you're going to need another test.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>Or do you?\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>One option is to \u003Cem>copy\u003C/em> the existing test and change \u003Ca href=\"https://blog.ploeh.dk/2013/06/24/a-heuristic-for-formatting-code-according-to-the-aaa-pattern\">the assertion phase\u003C/a> to perform the above \u003Ccode>GET\u003C/code> request to check that the response status is \u003Ccode>404\u003C/code>:\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:code -->\n\u003Cpre class=\"wp-block-code\">\u003Ccode>&#91;Theory]\n&#91;InlineData(884, 18, 47, \"c@example.net\", \"Nick Klimenko\", 2)]\n&#91;InlineData(902, 18, 50, \"emot@example.gov\", \"Emma Otting\", 5)]\npublic async Task DeleteReservationActuallyDeletes(\n    int days, int hours, int minutes,\n    string email, string name, int quantity)\n{\n    using var api = new LegacyApi();\n    var at = DateTime.Today.AddDays(days).At(hours, minutes)\n        .ToIso8601DateTimeString();\n    var dto = Create.ReservationDto(at, email, name, quantity);\n    var postResp = await api.PostReservation(dto);\n    Uri address = FindReservationAddress(postResp);\n \n    var deleteResp = await api.CreateClient().DeleteAsync(address);\n \n    var getResp = await api.CreateClient().GetAsync(address);\n    Assert.Equal(HttpStatusCode.NotFound, getResp.StatusCode);\n}\n\u003C/code>\u003C/pre>\n\u003C!-- /wp:code -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>This does, indeed, prompt you to properly implement the server-side \u003Ccode>Delete\u003C/code> method.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>Is this, however, a good idea? Is the test code easy to maintain?\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>Test code is code too, and you have to maintain it. Copy and paste is problematic in test code for the same reasons that it can be a problem in production code. If you later have to change something, you have to identify all the places that you have to edit. It's easy to miss one, which can lead to bugs. This is true for test code as well.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:heading -->\n\u003Ch2 id=\"h-one-action-more-assertions\">One action, more assertions\u003C/h2>\n\u003C!-- /wp:heading -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>Instead of copy-and-pasting the first test, why not instead \u003Ca href=\"https://blog.ploeh.dk/2021/12/13/backwards-compatibility-as-a-profunctor\">strengthen the postconditions of the first test case\u003C/a>?\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>Just add the new assertion after the first assertion:\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:code -->\n\u003Cpre class=\"wp-block-code\">\u003Ccode>&#91;Theory]\n&#91;InlineData(884, 18, 47, \"c@example.net\", \"Nick Klimenko\", 2)]\n&#91;InlineData(902, 18, 50, \"emot@example.gov\", \"Emma Otting\", 5)]\npublic async Task DeleteReservation(\n    int days, int hours, int minutes,\n    string email, string name, int quantity)\n{\n    using var api = new LegacyApi();\n    var at = DateTime.Today.AddDays(days).At(hours, minutes)\n        .ToIso8601DateTimeString();\n    var dto = Create.ReservationDto(at, email, name, quantity);\n    var postResp = await api.PostReservation(dto);\n    Uri address = FindReservationAddress(postResp);\n \n    var deleteResp = await api.CreateClient().DeleteAsync(address);\n \n    Assert.True(\n        deleteResp.IsSuccessStatusCode,\n        $\"Actual status code: {deleteResp.StatusCode}.\");\n    var getResp = await api.CreateClient().GetAsync(address);\n    Assert.Equal(HttpStatusCode.NotFound, getResp.StatusCode);\n}\n\u003C/code>\u003C/pre>\n\u003C!-- /wp:code -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>This means that you only have a single test method to maintain instead of two duplicated methods that are almost identical.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>\u003Cem>But,\u003C/em> some of the people I've coached might say, \u003Cem>this test has two assertions!\u003C/em>\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>Indeed. So what? It's one single \u003Cem>test case\u003C/em>: Cancelling a reservation.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>While cancelling a reservation is a single action, we care about multiple outcomes:\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:list -->\n\u003Cul>\u003C!-- wp:list-item -->\n\u003Cli>The status code after a successful \u003Ccode>DELETE\u003C/code> request should be in the \u003Ccode>200\u003C/code> range.\u003C/li>\n\u003C!-- /wp:list-item -->\n\n\u003C!-- wp:list-item -->\n\u003Cli>The reservation resource should be gone.\u003C/li>\n\u003C!-- /wp:list-item -->\u003C/ul>\n\u003C!-- /wp:list -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>Developing the system further, we might add more behaviors that we care about. Perhaps the system should also send an email about the cancellation. We should assert that as well. It's still the same test case, though: Successfully cancelling a reservation.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>There's nothing wrong with multiple assertions in a single test. The above example illustrates the benefits. A single test case can have multiple outcomes that should all be verified.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:heading -->\n\u003Ch2 id=\"h-origins-of-the-single-assertion-notion\">Origins of the single assertion notion\u003C/h2>\n\u003C!-- /wp:heading -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>Where does the \u003Cem>only one assertion per test\u003C/em> notion come from? I don't know, but I can guess.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>The excellent book \u003Ca href=\"https://blog.ploeh.dk/ref/xunit-patterns\">\u003Cem>xUnit Test Patterns\u003C/em>\u003C/a> describes a test smell named \u003Ca href=\"http://xunitpatterns.com/Assertion%20Roulette.html\">Assertion Roulette\u003C/a>. It describes situations where it may be difficult to determine exactly which assertion caused a test failure.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>It looks to me as though the \u003Cem>only one assertion per test\u003C/em> 'rule' stems from a misreading of the Assertion Roulette description. (I may even have contributed to that myself. I don't remember that I have, but to be honest I've produced so much content about unit testing over the decades that I don't want to assume myself free of guilt.)\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>\u003Cem>xUnit Test Patterns\u003C/em> describes two causes of Assertion Roulette:\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:list -->\n\u003Cul>\u003C!-- wp:list-item -->\n\u003Cli>Eager Test: A single test verifies too much functionality.\u003C/li>\n\u003C!-- /wp:list-item -->\n\n\u003C!-- wp:list-item -->\n\u003Cli>Missing Assertion Message\u003C/li>\n\u003C!-- /wp:list-item -->\u003C/ul>\n\u003C!-- /wp:list -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>You have an Eager Test when you're trying to exercise more than one \u003Cem>test case\u003C/em>. You may be trying to simulate a 'session' where a client performs many steps in order to achieve a goal. As Gerard Meszaros writes regarding the test smell, this is appropriate for manual tests, but rarely for automated tests. It's not the number of assertions that cause problems, but that the test does too much.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>The other cause occurs when the assertions are sufficiently similar that you can't tell which one failed, and they have no assertion messages.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>That's not the case with the above example. If the \u003Ccode>Assert.True\u003C/code> assertion fails, the assertion message will tell you:\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:code -->\n\u003Cpre class=\"wp-block-code\">\u003Ccode>Actual status code: NotFound.\nExpected: True\nActual:   False\n\u003C/code>\u003C/pre>\n\u003C!-- /wp:code -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>Likewise, if the \u003Ccode>Assert.Equal\u003C/code> assertion fails, that too will be clear:\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:code -->\n\u003Cpre class=\"wp-block-code\">\u003Ccode>Assert.Equal() Failure\nExpected: NotFound\nActual:   OK\n\u003C/code>\u003C/pre>\n\u003C!-- /wp:code -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>There's no ambiguity.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:heading -->\n\u003Ch2 id=\"h-one-assertion-per-test\">One assertion per test\u003C/h2>\n\u003C!-- /wp:heading -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>Now that you understand that multiple assertions per test are fine, you may be inclined to have a ball adding assertions like there's no tomorrow.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>Usually, however, there's a germ of truth in a persistent notion like the \u003Cem>one test, one assertion\u003C/em> 'rule'. Use good judgement.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>If you consider what an automated test is, it's basically a predicate. It's a statement that we expect a particular outcome. We then compare the actual outcome to the expected outcome to see if they are equal. Thus, in essence, the ideal assertion is this:\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:code -->\n\u003Cpre class=\"wp-block-code\">\u003Ccode>Assert.Equal(expected, actual);\u003C/code>\u003C/pre>\n\u003C!-- /wp:code -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>I can't always attain that ideal, but whenever I can, I feel deep satisfaction. Sometimes, \u003Ccode>expected\u003C/code> and \u003Ccode>actual\u003C/code> are primitive values like integers or strings, but they might also be complex values that represent the subset of program state that the test cares about. \u003Ca href=\"https://blog.ploeh.dk/2021/05/03/structural-equality-for-better-tests\">As long as the objects have structural equality, such an assertion is meaningful\u003C/a>.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>At other times I can't quite find a way to express the verification step as succinctly as that. If I have to add another assertion or two, I'll do that.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:heading -->\n\u003Ch2 id=\"h-conclusion\">Conclusion\u003C/h2>\n\u003C!-- /wp:heading -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>There's this notion that you're only allowed to write one assertion per unit test. It probably originates from real concerns about badly-factored test code, but over the years the nuanced test smell \u003Cem>Assertion Roulette\u003C/em> has become garbled into a simpler, but less helpful 'rule'.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>That 'rule' often gets in the way of maintainable test code. Programmers following the 'rule' resort to gratuitous copying and pasting instead of adding another assertion to an existing test.\u003C/p>\n\u003C!-- /wp:paragraph -->\n\n\u003C!-- wp:paragraph -->\n\u003Cp>If adding a relevant assertion to an existing test is the best way forward, don't let a misunderstood 'rule' stop you.\u003C/p>\n\u003C!-- /wp:paragraph -->","html","2022-11-03T14:00:00.000Z",{"current":882},"multiple-assertions-per-test-are-fine",[884,892,896],{"_createdAt":885,"_id":886,"_rev":887,"_type":888,"_updatedAt":885,"slug":889,"title":891},"2023-05-23T16:43:21Z","wp-tagcat-code-for-a-living","9HpbCsT2tq0xwozQfkc4ih","blogTag",{"current":890},"code-for-a-living","Code for a Living",{"_createdAt":885,"_id":893,"_rev":887,"_type":888,"_updatedAt":885,"slug":894,"title":895},"wp-tagcat-testing",{"current":895},"testing",{"_createdAt":885,"_id":897,"_rev":887,"_type":888,"_updatedAt":885,"slug":898,"title":900},"wp-tagcat-unit-tests",{"current":899},"unit-tests","unit tests","Stop requiring only one assertion per unit test: Multiple assertions are fine",[903,909,915,921],{"_id":904,"publishedAt":905,"slug":906,"sponsored":12,"title":908},"28e560af-f0aa-4d46-bd90-f435ad604aa7","2026-06-26T14:00:27.102Z",{"_type":10,"current":907},"paging-charity-how-can-engineering-leaders-avoid-becoming-bond-villains","Paging Charity! How can engineering leaders avoid becoming Bond villains?",{"_id":910,"publishedAt":911,"slug":912,"sponsored":12,"title":914},"4b22c2a3-3779-4966-93eb-5230391dbdce","2026-06-23T14:08:58.595Z",{"_type":10,"current":913},"your-ai-shipped-a-backend-that-boots-that-is-the-whole-problem","Your AI shipped a backend that boots. That is the whole problem.",{"_id":916,"publishedAt":917,"slug":918,"sponsored":12,"title":920},"5cf362e1-fe7b-45af-b69c-914731c6a052","2026-06-23T14:00:00.000Z",{"_type":10,"current":919},"the-2026-developer-survey-is-now-open-for-human-developers-only","The 2026 Developer Survey is now open (for human developers only)!",{"_id":922,"publishedAt":923,"slug":924,"sponsored":12,"title":926},"30b995f7-7cb9-4dd8-bf71-d0685940a32b","2026-06-19T14:00:00.000Z",{"_type":10,"current":925},"dispatches-from-o-reilly-from-capabilities-to-responsibilities","Dispatches from O'Reilly: From capabilities to responsibilities",{"data":928,"sourceMap":-1},{"count":929,"lastTimestamp":930},22,"2024-12-10T16:15:18Z"]