[{"data":1,"prerenderedAt":3646},["ShallowReactive",2],{"page-\u002Fscala\u002F10-testing":3},{"id":4,"title":5,"body":6,"description":28,"extension":3637,"meta":3638,"navigation":105,"path":3639,"seo":3640,"stem":3644,"__hash__":3645},"content\u002Fscala\u002F10-testing.md","[object Object]",{"type":7,"value":8,"toc":3623},"minimark",[9,14,19,458,462,963,967,1556,1560,2001,2005,2332,2340,2550,2554,2852,2856,3286,3290,3301,3331,3337,3490,3496,3500,3506,3518,3524,3542,3546,3561,3619],[10,11,13],"h1",{"id":12},"_10-testing-scalatest-scalacheck-property-based-testing","10 — Testing: ScalaTest, ScalaCheck & Property-Based Testing",[15,16,18],"h2",{"id":17},"test-styles-choosing-the-right-one","Test Styles — Choosing the Right One",[20,21,23],"code-wrapper",{"language":22},"scala",[24,25,29],"pre",{"className":26,"code":27,"language":22,"meta":28,"style":28},"language-scala shiki shiki-themes github-light github-dark","\u002F\u002F FunSuite — flat, function-style. Good for unit tests.\nimport org.scalatest.funsuite.AnyFunSuite\nimport org.scalatest.matchers.should.Matchers.*\n\nclass CalculatorTest extends AnyFunSuite:\n  test(\"add returns sum of two integers\"):\n    Calculator.add(2, 3) shouldBe 5\n\n  test(\"divide by zero throws\"):\n    assertThrows[ArithmeticException]:\n      Calculator.divide(10, 0)\n\n\u002F\u002F FunSpec — describe\u002Fit style. Good for BDD-ish specs.\nimport org.scalatest.funspec.AnyFunSpec\nclass UserSpec extends AnyFunSpec:\n  describe(\"User\"):\n    it(\"should create with valid email\"):\n      User.create(\"alice@x.com\") shouldBe Right(User(\"alice@x.com\"))\n    it(\"should reject invalid email\"):\n      User.create(\"not-email\") shouldBe Left(\"invalid email\")\n\n\u002F\u002F munit — lightweight, fast, with diffs built in. Recommended for new projects.\nclass MyTest extends munit.FunSuite:\n  test(\"add\"):\n    assertEquals(Calculator.add(2, 3), 5)\n  test(\"divide by zero\"):\n    intercept[ArithmeticException]:\n      Calculator.divide(10, 0)\n","",[30,31,32,41,69,100,107,125,140,165,170,182,196,216,221,227,248,263,276,289,319,331,353,358,364,382,394,419,431,443],"code",{"__ignoreMap":28},[33,34,37],"span",{"class":35,"line":36},"line",1,[33,38,40],{"class":39},"sdCPZ","\u002F\u002F FunSuite — flat, function-style. Good for unit tests.\n",[33,42,44,48,52,56,59,61,64,66],{"class":35,"line":43},2,[33,45,47],{"class":46},"svdQ7","import",[33,49,51],{"class":50},"sIsaT"," org",[33,53,55],{"class":54},"ssxIu",".",[33,57,58],{"class":50},"scalatest",[33,60,55],{"class":54},[33,62,63],{"class":50},"funsuite",[33,65,55],{"class":54},[33,67,68],{"class":50},"AnyFunSuite\n",[33,70,72,74,76,78,80,82,85,87,90,92,95,97],{"class":35,"line":71},3,[33,73,47],{"class":46},[33,75,51],{"class":50},[33,77,55],{"class":54},[33,79,58],{"class":50},[33,81,55],{"class":54},[33,83,84],{"class":50},"matchers",[33,86,55],{"class":54},[33,88,89],{"class":50},"should",[33,91,55],{"class":54},[33,93,94],{"class":50},"Matchers",[33,96,55],{"class":54},[33,98,99],{"class":50},"*\n",[33,101,103],{"class":35,"line":102},4,[33,104,106],{"emptyLinePlaceholder":105},true,"\n",[33,108,110,113,116,119,122],{"class":35,"line":109},5,[33,111,112],{"class":46},"class",[33,114,115],{"class":50}," CalculatorTest",[33,117,118],{"class":46}," extends",[33,120,121],{"class":50}," AnyFunSuite",[33,123,124],{"class":46},":\n",[33,126,128,131,135,138],{"class":35,"line":127},6,[33,129,130],{"class":54},"  test(",[33,132,134],{"class":133},"sJ6F3","\"add returns sum of two integers\"",[33,136,137],{"class":54},")",[33,139,124],{"class":46},[33,141,143,146,149,153,156,159,162],{"class":35,"line":142},7,[33,144,145],{"class":50},"    Calculator",[33,147,148],{"class":54},".add(",[33,150,152],{"class":151},"snvgF","2",[33,154,155],{"class":54},", ",[33,157,158],{"class":151},"3",[33,160,161],{"class":54},") shouldBe ",[33,163,164],{"class":151},"5\n",[33,166,168],{"class":35,"line":167},8,[33,169,106],{"emptyLinePlaceholder":105},[33,171,173,175,178,180],{"class":35,"line":172},9,[33,174,130],{"class":54},[33,176,177],{"class":133},"\"divide by zero throws\"",[33,179,137],{"class":54},[33,181,124],{"class":46},[33,183,185,188,191,194],{"class":35,"line":184},10,[33,186,187],{"class":54},"    assertThrows[",[33,189,190],{"class":50},"ArithmeticException",[33,192,193],{"class":54},"]",[33,195,124],{"class":46},[33,197,199,202,205,208,210,213],{"class":35,"line":198},11,[33,200,201],{"class":50},"      Calculator",[33,203,204],{"class":54},".divide(",[33,206,207],{"class":151},"10",[33,209,155],{"class":54},[33,211,212],{"class":151},"0",[33,214,215],{"class":54},")\n",[33,217,219],{"class":35,"line":218},12,[33,220,106],{"emptyLinePlaceholder":105},[33,222,224],{"class":35,"line":223},13,[33,225,226],{"class":39},"\u002F\u002F FunSpec — describe\u002Fit style. Good for BDD-ish specs.\n",[33,228,230,232,234,236,238,240,243,245],{"class":35,"line":229},14,[33,231,47],{"class":46},[33,233,51],{"class":50},[33,235,55],{"class":54},[33,237,58],{"class":50},[33,239,55],{"class":54},[33,241,242],{"class":50},"funspec",[33,244,55],{"class":54},[33,246,247],{"class":50},"AnyFunSpec\n",[33,249,251,253,256,258,261],{"class":35,"line":250},15,[33,252,112],{"class":46},[33,254,255],{"class":50}," UserSpec",[33,257,118],{"class":46},[33,259,260],{"class":50}," AnyFunSpec",[33,262,124],{"class":46},[33,264,266,269,272,274],{"class":35,"line":265},16,[33,267,268],{"class":54},"  describe(",[33,270,271],{"class":133},"\"User\"",[33,273,137],{"class":54},[33,275,124],{"class":46},[33,277,279,282,285,287],{"class":35,"line":278},17,[33,280,281],{"class":54},"    it(",[33,283,284],{"class":133},"\"should create with valid email\"",[33,286,137],{"class":54},[33,288,124],{"class":46},[33,290,292,295,298,301,303,306,309,312,314,316],{"class":35,"line":291},18,[33,293,294],{"class":50},"      User",[33,296,297],{"class":54},".create(",[33,299,300],{"class":133},"\"alice@x.com\"",[33,302,161],{"class":54},[33,304,305],{"class":50},"Right",[33,307,308],{"class":54},"(",[33,310,311],{"class":50},"User",[33,313,308],{"class":54},[33,315,300],{"class":133},[33,317,318],{"class":54},"))\n",[33,320,322,324,327,329],{"class":35,"line":321},19,[33,323,281],{"class":54},[33,325,326],{"class":133},"\"should reject invalid email\"",[33,328,137],{"class":54},[33,330,124],{"class":46},[33,332,334,336,338,341,343,346,348,351],{"class":35,"line":333},20,[33,335,294],{"class":50},[33,337,297],{"class":54},[33,339,340],{"class":133},"\"not-email\"",[33,342,161],{"class":54},[33,344,345],{"class":50},"Left",[33,347,308],{"class":54},[33,349,350],{"class":133},"\"invalid email\"",[33,352,215],{"class":54},[33,354,356],{"class":35,"line":355},21,[33,357,106],{"emptyLinePlaceholder":105},[33,359,361],{"class":35,"line":360},22,[33,362,363],{"class":39},"\u002F\u002F munit — lightweight, fast, with diffs built in. Recommended for new projects.\n",[33,365,367,369,372,374,377,380],{"class":35,"line":366},23,[33,368,112],{"class":46},[33,370,371],{"class":50}," MyTest",[33,373,118],{"class":46},[33,375,376],{"class":54}," munit.",[33,378,379],{"class":50},"FunSuite",[33,381,124],{"class":46},[33,383,385,387,390,392],{"class":35,"line":384},24,[33,386,130],{"class":54},[33,388,389],{"class":133},"\"add\"",[33,391,137],{"class":54},[33,393,124],{"class":46},[33,395,397,400,403,405,407,409,411,414,417],{"class":35,"line":396},25,[33,398,399],{"class":54},"    assertEquals(",[33,401,402],{"class":50},"Calculator",[33,404,148],{"class":54},[33,406,152],{"class":151},[33,408,155],{"class":54},[33,410,158],{"class":151},[33,412,413],{"class":54},"), ",[33,415,416],{"class":151},"5",[33,418,215],{"class":54},[33,420,422,424,427,429],{"class":35,"line":421},26,[33,423,130],{"class":54},[33,425,426],{"class":133},"\"divide by zero\"",[33,428,137],{"class":54},[33,430,124],{"class":46},[33,432,434,437,439,441],{"class":35,"line":433},27,[33,435,436],{"class":54},"    intercept[",[33,438,190],{"class":50},[33,440,193],{"class":54},[33,442,124],{"class":46},[33,444,446,448,450,452,454,456],{"class":35,"line":445},28,[33,447,201],{"class":50},[33,449,204],{"class":54},[33,451,207],{"class":151},[33,453,155],{"class":54},[33,455,212],{"class":151},[33,457,215],{"class":54},[15,459,461],{"id":460},"fixtures-beforeafter-loan-pattern-and-resource-management","Fixtures — Before\u002FAfter, Loan Pattern, and Resource Management",[20,463,464],{"language":22},[24,465,467],{"className":26,"code":466,"language":22,"meta":28,"style":28},"import org.scalatest.funsuite.AnyFunSuite\nimport org.scalatest.{BeforeAndAfterEach, BeforeAndAfterAll}\n\n\u002F\u002F ❌ ANTI-PATTERN: shared mutable state across tests\nclass BadTest extends AnyFunSuite:\n  var counter = 0                           \u002F\u002F shared across tests — order-dependent!\n  test(\"inc\"):\n    counter += 1\n    assert(counter == 1)                    \u002F\u002F passes IF this runs first\n  test(\"check\"):\n    assert(counter == 0)                    \u002F\u002F fails IF inc ran first\n\n\u002F\u002F ✅ CORRECT: BeforeAndAfterEach for per-test isolation\nclass GoodTest extends AnyFunSuite with BeforeAndAfterEach:\n  private var db: TestDatabase = _\n\n  override def beforeEach(): Unit =\n    db = TestDatabase.inMemory()            \u002F\u002F fresh DB per test\n\n  override def afterEach(): Unit =\n    db.close()                              \u002F\u002F cleanup after each test\n\n  test(\"insert and query\"):\n    db.insert(User(\"Alice\"))\n    db.findAll() shouldBe List(User(\"Alice\"))\n\n\u002F\u002F ✅ CORRECT: Loan pattern for resource management\nclass FileTest extends AnyFunSuite:\n  \u002F\u002F Loan pattern — pass resource to test, guarantee cleanup\n  def withTempFile[T](test: java.io.File => T): T =\n    val file = java.io.File.createTempFile(\"test\", \".tmp\")\n    try test(file)\n    finally file.delete()\n\n  test(\"write and read\"):\n    withTempFile { file =>\n      Files.write(file.toPath, \"hello\".getBytes)\n      Files.readAllLines(file.toPath).get(0) shouldBe \"hello\"\n    }                                       \u002F\u002F file deleted after test, guaranteed\n",[30,468,469,487,511,515,520,533,551,562,573,590,601,614,618,623,642,664,668,690,706,710,727,735,739,750,764,782,786,791,804,810,851,880,889,898,903,915,924,939,954],{"__ignoreMap":28},[33,470,471,473,475,477,479,481,483,485],{"class":35,"line":36},[33,472,47],{"class":46},[33,474,51],{"class":50},[33,476,55],{"class":54},[33,478,58],{"class":50},[33,480,55],{"class":54},[33,482,63],{"class":50},[33,484,55],{"class":54},[33,486,68],{"class":50},[33,488,489,491,493,495,497,500,503,505,508],{"class":35,"line":43},[33,490,47],{"class":46},[33,492,51],{"class":50},[33,494,55],{"class":54},[33,496,58],{"class":50},[33,498,499],{"class":54},".{",[33,501,502],{"class":50},"BeforeAndAfterEach",[33,504,155],{"class":54},[33,506,507],{"class":50},"BeforeAndAfterAll",[33,509,510],{"class":54},"}\n",[33,512,513],{"class":35,"line":71},[33,514,106],{"emptyLinePlaceholder":105},[33,516,517],{"class":35,"line":102},[33,518,519],{"class":39},"\u002F\u002F ❌ ANTI-PATTERN: shared mutable state across tests\n",[33,521,522,524,527,529,531],{"class":35,"line":109},[33,523,112],{"class":46},[33,525,526],{"class":50}," BadTest",[33,528,118],{"class":46},[33,530,121],{"class":50},[33,532,124],{"class":46},[33,534,535,538,542,545,548],{"class":35,"line":127},[33,536,537],{"class":46},"  var",[33,539,541],{"class":540},"sCrzJ"," counter",[33,543,544],{"class":46}," =",[33,546,547],{"class":151}," 0",[33,549,550],{"class":39},"                           \u002F\u002F shared across tests — order-dependent!\n",[33,552,553,555,558,560],{"class":35,"line":142},[33,554,130],{"class":54},[33,556,557],{"class":133},"\"inc\"",[33,559,137],{"class":54},[33,561,124],{"class":46},[33,563,564,567,570],{"class":35,"line":167},[33,565,566],{"class":54},"    counter ",[33,568,569],{"class":46},"+=",[33,571,572],{"class":151}," 1\n",[33,574,575,578,581,584,587],{"class":35,"line":172},[33,576,577],{"class":54},"    assert(counter ",[33,579,580],{"class":46},"==",[33,582,583],{"class":151}," 1",[33,585,586],{"class":54},")                    ",[33,588,589],{"class":39},"\u002F\u002F passes IF this runs first\n",[33,591,592,594,597,599],{"class":35,"line":184},[33,593,130],{"class":54},[33,595,596],{"class":133},"\"check\"",[33,598,137],{"class":54},[33,600,124],{"class":46},[33,602,603,605,607,609,611],{"class":35,"line":198},[33,604,577],{"class":54},[33,606,580],{"class":46},[33,608,547],{"class":151},[33,610,586],{"class":54},[33,612,613],{"class":39},"\u002F\u002F fails IF inc ran first\n",[33,615,616],{"class":35,"line":218},[33,617,106],{"emptyLinePlaceholder":105},[33,619,620],{"class":35,"line":223},[33,621,622],{"class":39},"\u002F\u002F ✅ CORRECT: BeforeAndAfterEach for per-test isolation\n",[33,624,625,627,630,632,634,637,640],{"class":35,"line":229},[33,626,112],{"class":46},[33,628,629],{"class":50}," GoodTest",[33,631,118],{"class":46},[33,633,121],{"class":50},[33,635,636],{"class":46}," with",[33,638,639],{"class":50}," BeforeAndAfterEach",[33,641,124],{"class":46},[33,643,644,647,650,653,656,659,661],{"class":35,"line":250},[33,645,646],{"class":46},"  private",[33,648,649],{"class":46}," var",[33,651,652],{"class":540}," db",[33,654,655],{"class":46},":",[33,657,658],{"class":50}," TestDatabase",[33,660,544],{"class":46},[33,662,663],{"class":54}," _\n",[33,665,666],{"class":35,"line":265},[33,667,106],{"emptyLinePlaceholder":105},[33,669,670,673,676,679,682,684,687],{"class":35,"line":278},[33,671,672],{"class":46},"  override",[33,674,675],{"class":46}," def",[33,677,678],{"class":50}," beforeEach",[33,680,681],{"class":54},"()",[33,683,655],{"class":46},[33,685,686],{"class":50}," Unit",[33,688,689],{"class":46}," =\n",[33,691,692,695,698,700,703],{"class":35,"line":291},[33,693,694],{"class":54},"    db ",[33,696,697],{"class":46},"=",[33,699,658],{"class":50},[33,701,702],{"class":54},".inMemory()            ",[33,704,705],{"class":39},"\u002F\u002F fresh DB per test\n",[33,707,708],{"class":35,"line":321},[33,709,106],{"emptyLinePlaceholder":105},[33,711,712,714,716,719,721,723,725],{"class":35,"line":333},[33,713,672],{"class":46},[33,715,675],{"class":46},[33,717,718],{"class":50}," afterEach",[33,720,681],{"class":54},[33,722,655],{"class":46},[33,724,686],{"class":50},[33,726,689],{"class":46},[33,728,729,732],{"class":35,"line":355},[33,730,731],{"class":54},"    db.close()                              ",[33,733,734],{"class":39},"\u002F\u002F cleanup after each test\n",[33,736,737],{"class":35,"line":360},[33,738,106],{"emptyLinePlaceholder":105},[33,740,741,743,746,748],{"class":35,"line":366},[33,742,130],{"class":54},[33,744,745],{"class":133},"\"insert and query\"",[33,747,137],{"class":54},[33,749,124],{"class":46},[33,751,752,755,757,759,762],{"class":35,"line":384},[33,753,754],{"class":54},"    db.insert(",[33,756,311],{"class":50},[33,758,308],{"class":54},[33,760,761],{"class":133},"\"Alice\"",[33,763,318],{"class":54},[33,765,766,769,772,774,776,778,780],{"class":35,"line":396},[33,767,768],{"class":54},"    db.findAll() shouldBe ",[33,770,771],{"class":50},"List",[33,773,308],{"class":54},[33,775,311],{"class":50},[33,777,308],{"class":54},[33,779,761],{"class":133},[33,781,318],{"class":54},[33,783,784],{"class":35,"line":421},[33,785,106],{"emptyLinePlaceholder":105},[33,787,788],{"class":35,"line":433},[33,789,790],{"class":39},"\u002F\u002F ✅ CORRECT: Loan pattern for resource management\n",[33,792,793,795,798,800,802],{"class":35,"line":445},[33,794,112],{"class":46},[33,796,797],{"class":50}," FileTest",[33,799,118],{"class":46},[33,801,121],{"class":50},[33,803,124],{"class":46},[33,805,807],{"class":35,"line":806},29,[33,808,809],{"class":39},"  \u002F\u002F Loan pattern — pass resource to test, guarantee cleanup\n",[33,811,813,816,819,822,825,828,831,834,837,840,843,845,847,849],{"class":35,"line":812},30,[33,814,815],{"class":46},"  def",[33,817,818],{"class":50}," withTempFile",[33,820,821],{"class":54},"[",[33,823,824],{"class":50},"T",[33,826,827],{"class":54},"](",[33,829,830],{"class":540},"test",[33,832,833],{"class":54},": java.io.",[33,835,836],{"class":50},"File",[33,838,839],{"class":46}," =>",[33,841,842],{"class":50}," T",[33,844,137],{"class":54},[33,846,655],{"class":46},[33,848,842],{"class":50},[33,850,689],{"class":46},[33,852,854,857,860,862,865,867,870,873,875,878],{"class":35,"line":853},31,[33,855,856],{"class":46},"    val",[33,858,859],{"class":540}," file",[33,861,544],{"class":46},[33,863,864],{"class":54}," java.io.",[33,866,836],{"class":50},[33,868,869],{"class":54},".createTempFile(",[33,871,872],{"class":133},"\"test\"",[33,874,155],{"class":54},[33,876,877],{"class":133},"\".tmp\"",[33,879,215],{"class":54},[33,881,883,886],{"class":35,"line":882},32,[33,884,885],{"class":46},"    try",[33,887,888],{"class":54}," test(file)\n",[33,890,892,895],{"class":35,"line":891},33,[33,893,894],{"class":46},"    finally",[33,896,897],{"class":54}," file.delete()\n",[33,899,901],{"class":35,"line":900},34,[33,902,106],{"emptyLinePlaceholder":105},[33,904,906,908,911,913],{"class":35,"line":905},35,[33,907,130],{"class":54},[33,909,910],{"class":133},"\"write and read\"",[33,912,137],{"class":54},[33,914,124],{"class":46},[33,916,918,921],{"class":35,"line":917},36,[33,919,920],{"class":54},"    withTempFile { file ",[33,922,923],{"class":46},"=>\n",[33,925,927,930,933,936],{"class":35,"line":926},37,[33,928,929],{"class":50},"      Files",[33,931,932],{"class":54},".write(file.toPath, ",[33,934,935],{"class":133},"\"hello\"",[33,937,938],{"class":54},".getBytes)\n",[33,940,942,944,947,949,951],{"class":35,"line":941},38,[33,943,929],{"class":50},[33,945,946],{"class":54},".readAllLines(file.toPath).get(",[33,948,212],{"class":151},[33,950,161],{"class":54},[33,952,953],{"class":133},"\"hello\"\n",[33,955,957,960],{"class":35,"line":956},39,[33,958,959],{"class":54},"    }                                       ",[33,961,962],{"class":39},"\u002F\u002F file deleted after test, guaranteed\n",[15,964,966],{"id":965},"property-based-testing-with-scalacheck-generators-shrinking","Property-Based Testing with ScalaCheck — Generators & Shrinking",[20,968,969],{"language":22},[24,970,972],{"className":26,"code":971,"language":22,"meta":28,"style":28},"import org.scalacheck.{Gen, Prop, Arbitrary}\nimport org.scalacheck.Prop.forAll\nimport org.scalatest.funsuite.AnyFunSuite\nimport org.scalatestplus.scalacheck.Checkers\n\n\u002F\u002F Property: a statement that should hold for ALL valid inputs\nclass ListPropertyTest extends AnyFunSuite with Checkers:\n\n  test(\"reverse of reverse equals identity\"):\n    check((list: List[Int]) => list.reverse.reverse == list)\n\n  test(\"head of sorted list is minimum\"):\n    check((list: List[Int]) =>\n      list.nonEmpty ==> (list.sorted.head == list.min)  \u002F\u002F ==> is implication\n    )\n\n  test(\"map + filter = filter + map for commutative ops\"):\n    check((list: List[Int], f: Int => Int) =>\n      list.map(f).filter(_ > 0) == list.filter(_ > 0).map(f)  \u002F\u002F WRONG property — catches bug!\n    )\n\n\u002F\u002F Custom generators — control the generated data distribution\nobject Generators:\n  val userIdGen: Gen[Long] = Gen.choose(1L, 999_999_999L)\n  val emailGen: Gen[String] = for\n    local \u003C- Gen.alphaStr.filter(_.nonEmpty)\n    domain \u003C- Gen.oneOf(\"gmail.com\", \"yahoo.com\", \"example.org\")\n  yield s\"$local@$domain\"\n\n  \u002F\u002F Generate case class instances\n  val userGen: Gen[User] = for\n    id   \u003C- userIdGen\n    name \u003C- Gen.alphaStr.filter(_.nonEmpty)\n    email \u003C- emailGen\n  yield User(id, name, email)\n\n  \u002F\u002F Generate lists of a specific size range\n  val userListGen: Gen[List[User]] = Gen.listOf(userGen)\n\n\u002F\u002F Shrinking — ScalaCheck automatically finds the MINIMAL failing case\n\u002F\u002F If a property fails for List(4, 8, 15, 16, 23, 42), ScalaCheck tries:\n\u002F\u002F   List(4, 8, 15, 16, 23) → still fails? → List(4, 8, 15, 16) → ... → List(0)\n\u002F\u002F The minimal counterexample is shown in the test output.\n\u002F\u002F This is the #1 value of PBT — it finds edge cases you'd never think to test.\n",[30,973,974,1002,1021,1039,1059,1063,1068,1086,1090,1101,1133,1137,1148,1166,1185,1190,1194,1205,1239,1266,1270,1274,1279,1289,1327,1350,1363,1390,1413,1417,1422,1443,1453,1464,1474,1484,1488,1493,1522,1526,1532,1538,1544,1550],{"__ignoreMap":28},[33,975,976,978,980,982,985,987,990,992,995,997,1000],{"class":35,"line":36},[33,977,47],{"class":46},[33,979,51],{"class":50},[33,981,55],{"class":54},[33,983,984],{"class":50},"scalacheck",[33,986,499],{"class":54},[33,988,989],{"class":50},"Gen",[33,991,155],{"class":54},[33,993,994],{"class":50},"Prop",[33,996,155],{"class":54},[33,998,999],{"class":50},"Arbitrary",[33,1001,510],{"class":54},[33,1003,1004,1006,1008,1010,1012,1014,1016,1018],{"class":35,"line":43},[33,1005,47],{"class":46},[33,1007,51],{"class":50},[33,1009,55],{"class":54},[33,1011,984],{"class":50},[33,1013,55],{"class":54},[33,1015,994],{"class":50},[33,1017,55],{"class":54},[33,1019,1020],{"class":50},"forAll\n",[33,1022,1023,1025,1027,1029,1031,1033,1035,1037],{"class":35,"line":71},[33,1024,47],{"class":46},[33,1026,51],{"class":50},[33,1028,55],{"class":54},[33,1030,58],{"class":50},[33,1032,55],{"class":54},[33,1034,63],{"class":50},[33,1036,55],{"class":54},[33,1038,68],{"class":50},[33,1040,1041,1043,1045,1047,1050,1052,1054,1056],{"class":35,"line":102},[33,1042,47],{"class":46},[33,1044,51],{"class":50},[33,1046,55],{"class":54},[33,1048,1049],{"class":50},"scalatestplus",[33,1051,55],{"class":54},[33,1053,984],{"class":50},[33,1055,55],{"class":54},[33,1057,1058],{"class":50},"Checkers\n",[33,1060,1061],{"class":35,"line":109},[33,1062,106],{"emptyLinePlaceholder":105},[33,1064,1065],{"class":35,"line":127},[33,1066,1067],{"class":39},"\u002F\u002F Property: a statement that should hold for ALL valid inputs\n",[33,1069,1070,1072,1075,1077,1079,1081,1084],{"class":35,"line":142},[33,1071,112],{"class":46},[33,1073,1074],{"class":50}," ListPropertyTest",[33,1076,118],{"class":46},[33,1078,121],{"class":50},[33,1080,636],{"class":46},[33,1082,1083],{"class":50}," Checkers",[33,1085,124],{"class":46},[33,1087,1088],{"class":35,"line":167},[33,1089,106],{"emptyLinePlaceholder":105},[33,1091,1092,1094,1097,1099],{"class":35,"line":172},[33,1093,130],{"class":54},[33,1095,1096],{"class":133},"\"reverse of reverse equals identity\"",[33,1098,137],{"class":54},[33,1100,124],{"class":46},[33,1102,1103,1106,1109,1112,1114,1116,1119,1122,1125,1128,1130],{"class":35,"line":184},[33,1104,1105],{"class":54},"    check((",[33,1107,1108],{"class":540},"list",[33,1110,1111],{"class":54},": ",[33,1113,771],{"class":50},[33,1115,821],{"class":54},[33,1117,1118],{"class":50},"Int",[33,1120,1121],{"class":54},"]) ",[33,1123,1124],{"class":46},"=>",[33,1126,1127],{"class":54}," list.reverse.reverse ",[33,1129,580],{"class":46},[33,1131,1132],{"class":54}," list)\n",[33,1134,1135],{"class":35,"line":198},[33,1136,106],{"emptyLinePlaceholder":105},[33,1138,1139,1141,1144,1146],{"class":35,"line":218},[33,1140,130],{"class":54},[33,1142,1143],{"class":133},"\"head of sorted list is minimum\"",[33,1145,137],{"class":54},[33,1147,124],{"class":46},[33,1149,1150,1152,1154,1156,1158,1160,1162,1164],{"class":35,"line":223},[33,1151,1105],{"class":54},[33,1153,1108],{"class":540},[33,1155,1111],{"class":54},[33,1157,771],{"class":50},[33,1159,821],{"class":54},[33,1161,1118],{"class":50},[33,1163,1121],{"class":54},[33,1165,923],{"class":46},[33,1167,1168,1171,1174,1177,1179,1182],{"class":35,"line":229},[33,1169,1170],{"class":54},"      list.nonEmpty ",[33,1172,1173],{"class":46},"==>",[33,1175,1176],{"class":54}," (list.sorted.head ",[33,1178,580],{"class":46},[33,1180,1181],{"class":54}," list.min)  ",[33,1183,1184],{"class":39},"\u002F\u002F ==> is implication\n",[33,1186,1187],{"class":35,"line":250},[33,1188,1189],{"class":54},"    )\n",[33,1191,1192],{"class":35,"line":265},[33,1193,106],{"emptyLinePlaceholder":105},[33,1195,1196,1198,1201,1203],{"class":35,"line":278},[33,1197,130],{"class":54},[33,1199,1200],{"class":133},"\"map + filter = filter + map for commutative ops\"",[33,1202,137],{"class":54},[33,1204,124],{"class":46},[33,1206,1207,1209,1211,1213,1215,1217,1219,1222,1225,1227,1229,1231,1234,1237],{"class":35,"line":291},[33,1208,1105],{"class":54},[33,1210,1108],{"class":540},[33,1212,1111],{"class":54},[33,1214,771],{"class":50},[33,1216,821],{"class":54},[33,1218,1118],{"class":50},[33,1220,1221],{"class":54},"], ",[33,1223,1224],{"class":540},"f",[33,1226,1111],{"class":54},[33,1228,1118],{"class":50},[33,1230,839],{"class":46},[33,1232,1233],{"class":50}," Int",[33,1235,1236],{"class":54},") ",[33,1238,923],{"class":46},[33,1240,1241,1244,1247,1249,1251,1253,1256,1258,1260,1263],{"class":35,"line":321},[33,1242,1243],{"class":54},"      list.map(f).filter(_ ",[33,1245,1246],{"class":46},">",[33,1248,547],{"class":151},[33,1250,1236],{"class":54},[33,1252,580],{"class":46},[33,1254,1255],{"class":54}," list.filter(_ ",[33,1257,1246],{"class":46},[33,1259,547],{"class":151},[33,1261,1262],{"class":54},").map(f)  ",[33,1264,1265],{"class":39},"\u002F\u002F WRONG property — catches bug!\n",[33,1267,1268],{"class":35,"line":333},[33,1269,1189],{"class":54},[33,1271,1272],{"class":35,"line":355},[33,1273,106],{"emptyLinePlaceholder":105},[33,1275,1276],{"class":35,"line":360},[33,1277,1278],{"class":39},"\u002F\u002F Custom generators — control the generated data distribution\n",[33,1280,1281,1284,1287],{"class":35,"line":366},[33,1282,1283],{"class":46},"object",[33,1285,1286],{"class":50}," Generators",[33,1288,124],{"class":46},[33,1290,1291,1294,1297,1299,1302,1304,1307,1310,1312,1314,1317,1320,1322,1325],{"class":35,"line":384},[33,1292,1293],{"class":46},"  val",[33,1295,1296],{"class":540}," userIdGen",[33,1298,655],{"class":46},[33,1300,1301],{"class":50}," Gen",[33,1303,821],{"class":54},[33,1305,1306],{"class":50},"Long",[33,1308,1309],{"class":54},"] ",[33,1311,697],{"class":46},[33,1313,1301],{"class":50},[33,1315,1316],{"class":54},".choose(",[33,1318,1319],{"class":151},"1L",[33,1321,155],{"class":54},[33,1323,1324],{"class":151},"999_999_999L",[33,1326,215],{"class":54},[33,1328,1329,1331,1334,1336,1338,1340,1343,1345,1347],{"class":35,"line":396},[33,1330,1293],{"class":46},[33,1332,1333],{"class":540}," emailGen",[33,1335,655],{"class":46},[33,1337,1301],{"class":50},[33,1339,821],{"class":54},[33,1341,1342],{"class":50},"String",[33,1344,1309],{"class":54},[33,1346,697],{"class":46},[33,1348,1349],{"class":46}," for\n",[33,1351,1352,1355,1358,1360],{"class":35,"line":421},[33,1353,1354],{"class":54},"    local ",[33,1356,1357],{"class":46},"\u003C-",[33,1359,1301],{"class":50},[33,1361,1362],{"class":54},".alphaStr.filter(_.nonEmpty)\n",[33,1364,1365,1368,1370,1372,1375,1378,1380,1383,1385,1388],{"class":35,"line":433},[33,1366,1367],{"class":54},"    domain ",[33,1369,1357],{"class":46},[33,1371,1301],{"class":50},[33,1373,1374],{"class":54},".oneOf(",[33,1376,1377],{"class":133},"\"gmail.com\"",[33,1379,155],{"class":54},[33,1381,1382],{"class":133},"\"yahoo.com\"",[33,1384,155],{"class":54},[33,1386,1387],{"class":133},"\"example.org\"",[33,1389,215],{"class":54},[33,1391,1392,1395,1398,1401,1404,1407,1410],{"class":35,"line":445},[33,1393,1394],{"class":46},"  yield",[33,1396,1397],{"class":46}," s",[33,1399,1400],{"class":133},"\"",[33,1402,1403],{"class":54},"$local",[33,1405,1406],{"class":133},"@",[33,1408,1409],{"class":54},"$domain",[33,1411,1412],{"class":133},"\"\n",[33,1414,1415],{"class":35,"line":806},[33,1416,106],{"emptyLinePlaceholder":105},[33,1418,1419],{"class":35,"line":812},[33,1420,1421],{"class":39},"  \u002F\u002F Generate case class instances\n",[33,1423,1424,1426,1429,1431,1433,1435,1437,1439,1441],{"class":35,"line":853},[33,1425,1293],{"class":46},[33,1427,1428],{"class":540}," userGen",[33,1430,655],{"class":46},[33,1432,1301],{"class":50},[33,1434,821],{"class":54},[33,1436,311],{"class":50},[33,1438,1309],{"class":54},[33,1440,697],{"class":46},[33,1442,1349],{"class":46},[33,1444,1445,1448,1450],{"class":35,"line":882},[33,1446,1447],{"class":54},"    id   ",[33,1449,1357],{"class":46},[33,1451,1452],{"class":54}," userIdGen\n",[33,1454,1455,1458,1460,1462],{"class":35,"line":891},[33,1456,1457],{"class":54},"    name ",[33,1459,1357],{"class":46},[33,1461,1301],{"class":50},[33,1463,1362],{"class":54},[33,1465,1466,1469,1471],{"class":35,"line":900},[33,1467,1468],{"class":54},"    email ",[33,1470,1357],{"class":46},[33,1472,1473],{"class":54}," emailGen\n",[33,1475,1476,1478,1481],{"class":35,"line":905},[33,1477,1394],{"class":46},[33,1479,1480],{"class":50}," User",[33,1482,1483],{"class":54},"(id, name, email)\n",[33,1485,1486],{"class":35,"line":917},[33,1487,106],{"emptyLinePlaceholder":105},[33,1489,1490],{"class":35,"line":926},[33,1491,1492],{"class":39},"  \u002F\u002F Generate lists of a specific size range\n",[33,1494,1495,1497,1500,1502,1504,1506,1508,1510,1512,1515,1517,1519],{"class":35,"line":941},[33,1496,1293],{"class":46},[33,1498,1499],{"class":540}," userListGen",[33,1501,655],{"class":46},[33,1503,1301],{"class":50},[33,1505,821],{"class":54},[33,1507,771],{"class":50},[33,1509,821],{"class":54},[33,1511,311],{"class":50},[33,1513,1514],{"class":54},"]] ",[33,1516,697],{"class":46},[33,1518,1301],{"class":50},[33,1520,1521],{"class":54},".listOf(userGen)\n",[33,1523,1524],{"class":35,"line":956},[33,1525,106],{"emptyLinePlaceholder":105},[33,1527,1529],{"class":35,"line":1528},40,[33,1530,1531],{"class":39},"\u002F\u002F Shrinking — ScalaCheck automatically finds the MINIMAL failing case\n",[33,1533,1535],{"class":35,"line":1534},41,[33,1536,1537],{"class":39},"\u002F\u002F If a property fails for List(4, 8, 15, 16, 23, 42), ScalaCheck tries:\n",[33,1539,1541],{"class":35,"line":1540},42,[33,1542,1543],{"class":39},"\u002F\u002F   List(4, 8, 15, 16, 23) → still fails? → List(4, 8, 15, 16) → ... → List(0)\n",[33,1545,1547],{"class":35,"line":1546},43,[33,1548,1549],{"class":39},"\u002F\u002F The minimal counterexample is shown in the test output.\n",[33,1551,1553],{"class":35,"line":1552},44,[33,1554,1555],{"class":39},"\u002F\u002F This is the #1 value of PBT — it finds edge cases you'd never think to test.\n",[15,1557,1559],{"id":1558},"mock-free-testing-with-traits-the-functional-core","Mock-Free Testing with Traits — The Functional Core",[20,1561,1562],{"language":22},[24,1563,1565],{"className":26,"code":1564,"language":22,"meta":28,"style":28},"\u002F\u002F ❌ ANTI-PATTERN: mocking everything — tests test the mocks, not the logic\nclass BadUserServiceTest extends AnyFunSuite with MockFactory:\n  test(\"get user\"):\n    val mockRepo = mock[UserRepository]\n    (mockRepo.find _).expects(42).returning(Some(User(\"Alice\")))\n    \u002F\u002F This test ONLY verifies that find(42) was called — not that the logic works\n\n\u002F\u002F ✅ CORRECT: design for testability — inject traits, use fakes (not mocks)\ntrait UserRepository:\n  def find(id: Long): Option[User]\n  def save(user: User): Long\n\n\u002F\u002F Fake — in-memory implementation for tests\nclass FakeUserRepo extends UserRepository:\n  private val store = scala.collection.mutable.Map.empty[Long, User]\n  private var nextId = 1L\n  def find(id: Long): Option[User] = store.get(id)\n  def save(user: User): Long =\n    val id = nextId; nextId += 1; store(id) = user; id\n\nclass UserServiceTest extends AnyFunSuite:\n  val repo = FakeUserRepo()                 \u002F\u002F real logic, no mocking framework\n  val service = UserService(repo)\n\n  test(\"create and find\"):\n    val id = service.create(\"Alice\")\n    service.find(id) shouldBe Some(User(\"Alice\"))\n\n  test(\"find non-existent returns None\"):\n    service.find(999) shouldBe None\n\n\u002F\u002F This tests the SERVICE logic with a fast, deterministic in-memory repo.\n\u002F\u002F No mock framework needed. Tests are robust to refactoring (no \"expects\" to update).\n",[30,1566,1567,1572,1590,1601,1619,1644,1649,1653,1658,1668,1697,1720,1724,1729,1742,1771,1785,1816,1839,1863,1867,1880,1897,1912,1916,1927,1942,1959,1963,1974,1987,1991,1996],{"__ignoreMap":28},[33,1568,1569],{"class":35,"line":36},[33,1570,1571],{"class":39},"\u002F\u002F ❌ ANTI-PATTERN: mocking everything — tests test the mocks, not the logic\n",[33,1573,1574,1576,1579,1581,1583,1585,1588],{"class":35,"line":43},[33,1575,112],{"class":46},[33,1577,1578],{"class":50}," BadUserServiceTest",[33,1580,118],{"class":46},[33,1582,121],{"class":50},[33,1584,636],{"class":46},[33,1586,1587],{"class":50}," MockFactory",[33,1589,124],{"class":46},[33,1591,1592,1594,1597,1599],{"class":35,"line":71},[33,1593,130],{"class":54},[33,1595,1596],{"class":133},"\"get user\"",[33,1598,137],{"class":54},[33,1600,124],{"class":46},[33,1602,1603,1605,1608,1610,1613,1616],{"class":35,"line":102},[33,1604,856],{"class":46},[33,1606,1607],{"class":540}," mockRepo",[33,1609,544],{"class":46},[33,1611,1612],{"class":54}," mock[",[33,1614,1615],{"class":50},"UserRepository",[33,1617,1618],{"class":54},"]\n",[33,1620,1621,1624,1627,1630,1633,1635,1637,1639,1641],{"class":35,"line":109},[33,1622,1623],{"class":54},"    (mockRepo.find _).expects(",[33,1625,1626],{"class":151},"42",[33,1628,1629],{"class":54},").returning(",[33,1631,1632],{"class":50},"Some",[33,1634,308],{"class":54},[33,1636,311],{"class":50},[33,1638,308],{"class":54},[33,1640,761],{"class":133},[33,1642,1643],{"class":54},")))\n",[33,1645,1646],{"class":35,"line":127},[33,1647,1648],{"class":39},"    \u002F\u002F This test ONLY verifies that find(42) was called — not that the logic works\n",[33,1650,1651],{"class":35,"line":142},[33,1652,106],{"emptyLinePlaceholder":105},[33,1654,1655],{"class":35,"line":167},[33,1656,1657],{"class":39},"\u002F\u002F ✅ CORRECT: design for testability — inject traits, use fakes (not mocks)\n",[33,1659,1660,1663,1666],{"class":35,"line":172},[33,1661,1662],{"class":46},"trait",[33,1664,1665],{"class":50}," UserRepository",[33,1667,124],{"class":46},[33,1669,1670,1672,1675,1677,1680,1682,1684,1686,1688,1691,1693,1695],{"class":35,"line":184},[33,1671,815],{"class":46},[33,1673,1674],{"class":50}," find",[33,1676,308],{"class":54},[33,1678,1679],{"class":540},"id",[33,1681,1111],{"class":54},[33,1683,1306],{"class":50},[33,1685,137],{"class":54},[33,1687,655],{"class":46},[33,1689,1690],{"class":50}," Option",[33,1692,821],{"class":54},[33,1694,311],{"class":50},[33,1696,1618],{"class":54},[33,1698,1699,1701,1704,1706,1709,1711,1713,1715,1717],{"class":35,"line":198},[33,1700,815],{"class":46},[33,1702,1703],{"class":50}," save",[33,1705,308],{"class":54},[33,1707,1708],{"class":540},"user",[33,1710,1111],{"class":54},[33,1712,311],{"class":50},[33,1714,137],{"class":54},[33,1716,655],{"class":46},[33,1718,1719],{"class":50}," Long\n",[33,1721,1722],{"class":35,"line":218},[33,1723,106],{"emptyLinePlaceholder":105},[33,1725,1726],{"class":35,"line":223},[33,1727,1728],{"class":39},"\u002F\u002F Fake — in-memory implementation for tests\n",[33,1730,1731,1733,1736,1738,1740],{"class":35,"line":229},[33,1732,112],{"class":46},[33,1734,1735],{"class":50}," FakeUserRepo",[33,1737,118],{"class":46},[33,1739,1665],{"class":50},[33,1741,124],{"class":46},[33,1743,1744,1746,1749,1752,1754,1757,1760,1763,1765,1767,1769],{"class":35,"line":250},[33,1745,646],{"class":46},[33,1747,1748],{"class":46}," val",[33,1750,1751],{"class":540}," store",[33,1753,544],{"class":46},[33,1755,1756],{"class":54}," scala.collection.mutable.",[33,1758,1759],{"class":50},"Map",[33,1761,1762],{"class":54},".empty[",[33,1764,1306],{"class":50},[33,1766,155],{"class":54},[33,1768,311],{"class":50},[33,1770,1618],{"class":54},[33,1772,1773,1775,1777,1780,1782],{"class":35,"line":265},[33,1774,646],{"class":46},[33,1776,649],{"class":46},[33,1778,1779],{"class":540}," nextId",[33,1781,544],{"class":46},[33,1783,1784],{"class":151}," 1L\n",[33,1786,1787,1789,1791,1793,1795,1797,1799,1801,1803,1805,1807,1809,1811,1813],{"class":35,"line":278},[33,1788,815],{"class":46},[33,1790,1674],{"class":50},[33,1792,308],{"class":54},[33,1794,1679],{"class":540},[33,1796,1111],{"class":54},[33,1798,1306],{"class":50},[33,1800,137],{"class":54},[33,1802,655],{"class":46},[33,1804,1690],{"class":50},[33,1806,821],{"class":54},[33,1808,311],{"class":50},[33,1810,1309],{"class":54},[33,1812,697],{"class":46},[33,1814,1815],{"class":54}," store.get(id)\n",[33,1817,1818,1820,1822,1824,1826,1828,1830,1832,1834,1837],{"class":35,"line":291},[33,1819,815],{"class":46},[33,1821,1703],{"class":50},[33,1823,308],{"class":54},[33,1825,1708],{"class":540},[33,1827,1111],{"class":54},[33,1829,311],{"class":50},[33,1831,137],{"class":54},[33,1833,655],{"class":46},[33,1835,1836],{"class":50}," Long",[33,1838,689],{"class":46},[33,1840,1841,1843,1846,1848,1851,1853,1855,1858,1860],{"class":35,"line":321},[33,1842,856],{"class":46},[33,1844,1845],{"class":540}," id",[33,1847,544],{"class":46},[33,1849,1850],{"class":54}," nextId; nextId ",[33,1852,569],{"class":46},[33,1854,583],{"class":151},[33,1856,1857],{"class":54},"; store(id) ",[33,1859,697],{"class":46},[33,1861,1862],{"class":54}," user; id\n",[33,1864,1865],{"class":35,"line":333},[33,1866,106],{"emptyLinePlaceholder":105},[33,1868,1869,1871,1874,1876,1878],{"class":35,"line":355},[33,1870,112],{"class":46},[33,1872,1873],{"class":50}," UserServiceTest",[33,1875,118],{"class":46},[33,1877,121],{"class":50},[33,1879,124],{"class":46},[33,1881,1882,1884,1887,1889,1891,1894],{"class":35,"line":360},[33,1883,1293],{"class":46},[33,1885,1886],{"class":540}," repo",[33,1888,544],{"class":46},[33,1890,1735],{"class":50},[33,1892,1893],{"class":54},"()                 ",[33,1895,1896],{"class":39},"\u002F\u002F real logic, no mocking framework\n",[33,1898,1899,1901,1904,1906,1909],{"class":35,"line":366},[33,1900,1293],{"class":46},[33,1902,1903],{"class":540}," service",[33,1905,544],{"class":46},[33,1907,1908],{"class":50}," UserService",[33,1910,1911],{"class":54},"(repo)\n",[33,1913,1914],{"class":35,"line":384},[33,1915,106],{"emptyLinePlaceholder":105},[33,1917,1918,1920,1923,1925],{"class":35,"line":396},[33,1919,130],{"class":54},[33,1921,1922],{"class":133},"\"create and find\"",[33,1924,137],{"class":54},[33,1926,124],{"class":46},[33,1928,1929,1931,1933,1935,1938,1940],{"class":35,"line":421},[33,1930,856],{"class":46},[33,1932,1845],{"class":540},[33,1934,544],{"class":46},[33,1936,1937],{"class":54}," service.create(",[33,1939,761],{"class":133},[33,1941,215],{"class":54},[33,1943,1944,1947,1949,1951,1953,1955,1957],{"class":35,"line":433},[33,1945,1946],{"class":54},"    service.find(id) shouldBe ",[33,1948,1632],{"class":50},[33,1950,308],{"class":54},[33,1952,311],{"class":50},[33,1954,308],{"class":54},[33,1956,761],{"class":133},[33,1958,318],{"class":54},[33,1960,1961],{"class":35,"line":445},[33,1962,106],{"emptyLinePlaceholder":105},[33,1964,1965,1967,1970,1972],{"class":35,"line":806},[33,1966,130],{"class":54},[33,1968,1969],{"class":133},"\"find non-existent returns None\"",[33,1971,137],{"class":54},[33,1973,124],{"class":46},[33,1975,1976,1979,1982,1984],{"class":35,"line":812},[33,1977,1978],{"class":54},"    service.find(",[33,1980,1981],{"class":151},"999",[33,1983,161],{"class":54},[33,1985,1986],{"class":50},"None\n",[33,1988,1989],{"class":35,"line":853},[33,1990,106],{"emptyLinePlaceholder":105},[33,1992,1993],{"class":35,"line":882},[33,1994,1995],{"class":39},"\u002F\u002F This tests the SERVICE logic with a fast, deterministic in-memory repo.\n",[33,1997,1998],{"class":35,"line":891},[33,1999,2000],{"class":39},"\u002F\u002F No mock framework needed. Tests are robust to refactoring (no \"expects\" to update).\n",[15,2002,2004],{"id":2003},"integration-testing-tagged-separated-and-parallel","Integration Testing — Tagged, Separated, and Parallel",[20,2006,2007],{"language":22},[24,2008,2010],{"className":26,"code":2009,"language":22,"meta":28,"style":28},"import org.scalatest.{Tag, BeforeAndAfterAll}\nimport org.scalatest.funsuite.AnyFunSuite\n\n\u002F\u002F Tags for selective execution in CI\nobject UnitTest extends Tag(\"unit\")\nobject IntegrationTest extends Tag(\"integration\")\nobject SlowTest extends Tag(\"slow\")\n\nclass UserDBTest extends AnyFunSuite with BeforeAndAfterAll:\n  private var db: PostgresContainer = _\n\n  override def beforeAll(): Unit =\n    db = PostgresContainer.start()          \u002F\u002F Testcontainers — real Postgres in Docker\n\n  override def afterAll(): Unit =\n    db.stop()\n\n  test(\"insert and query real DB\", IntegrationTest):\n    val repo = PostgresUserRepo(db.jdbcUrl)\n    repo.save(User(\"Alice\"))\n    repo.findAll() shouldBe List(User(\"Alice\"))\n\n  test(\"connection retry on transient failure\", IntegrationTest, SlowTest):\n    \u002F\u002F takes 30s+ — only run in nightly CI, not on every push\n\n\u002F\u002F sbt commands for tagged execution:\n\u002F\u002F   sbt \"testOnly * -- -n unit\"              # only unit tests\n\u002F\u002F   sbt \"testOnly * -- -n integration -l slow\"  # integration but not slow\n\u002F\u002F   sbt \"Test\u002Fserial\"                        # run tests serially (for shared resources)\n",[30,2011,2012,2033,2051,2055,2060,2079,2097,2115,2119,2137,2154,2158,2175,2189,2193,2210,2215,2219,2235,2249,2262,2279,2283,2303,2308,2312,2317,2322,2327],{"__ignoreMap":28},[33,2013,2014,2016,2018,2020,2022,2024,2027,2029,2031],{"class":35,"line":36},[33,2015,47],{"class":46},[33,2017,51],{"class":50},[33,2019,55],{"class":54},[33,2021,58],{"class":50},[33,2023,499],{"class":54},[33,2025,2026],{"class":50},"Tag",[33,2028,155],{"class":54},[33,2030,507],{"class":50},[33,2032,510],{"class":54},[33,2034,2035,2037,2039,2041,2043,2045,2047,2049],{"class":35,"line":43},[33,2036,47],{"class":46},[33,2038,51],{"class":50},[33,2040,55],{"class":54},[33,2042,58],{"class":50},[33,2044,55],{"class":54},[33,2046,63],{"class":50},[33,2048,55],{"class":54},[33,2050,68],{"class":50},[33,2052,2053],{"class":35,"line":71},[33,2054,106],{"emptyLinePlaceholder":105},[33,2056,2057],{"class":35,"line":102},[33,2058,2059],{"class":39},"\u002F\u002F Tags for selective execution in CI\n",[33,2061,2062,2064,2067,2069,2072,2074,2077],{"class":35,"line":109},[33,2063,1283],{"class":46},[33,2065,2066],{"class":50}," UnitTest",[33,2068,118],{"class":46},[33,2070,2071],{"class":50}," Tag",[33,2073,308],{"class":54},[33,2075,2076],{"class":133},"\"unit\"",[33,2078,215],{"class":54},[33,2080,2081,2083,2086,2088,2090,2092,2095],{"class":35,"line":127},[33,2082,1283],{"class":46},[33,2084,2085],{"class":50}," IntegrationTest",[33,2087,118],{"class":46},[33,2089,2071],{"class":50},[33,2091,308],{"class":54},[33,2093,2094],{"class":133},"\"integration\"",[33,2096,215],{"class":54},[33,2098,2099,2101,2104,2106,2108,2110,2113],{"class":35,"line":142},[33,2100,1283],{"class":46},[33,2102,2103],{"class":50}," SlowTest",[33,2105,118],{"class":46},[33,2107,2071],{"class":50},[33,2109,308],{"class":54},[33,2111,2112],{"class":133},"\"slow\"",[33,2114,215],{"class":54},[33,2116,2117],{"class":35,"line":167},[33,2118,106],{"emptyLinePlaceholder":105},[33,2120,2121,2123,2126,2128,2130,2132,2135],{"class":35,"line":172},[33,2122,112],{"class":46},[33,2124,2125],{"class":50}," UserDBTest",[33,2127,118],{"class":46},[33,2129,121],{"class":50},[33,2131,636],{"class":46},[33,2133,2134],{"class":50}," BeforeAndAfterAll",[33,2136,124],{"class":46},[33,2138,2139,2141,2143,2145,2147,2150,2152],{"class":35,"line":184},[33,2140,646],{"class":46},[33,2142,649],{"class":46},[33,2144,652],{"class":540},[33,2146,655],{"class":46},[33,2148,2149],{"class":50}," PostgresContainer",[33,2151,544],{"class":46},[33,2153,663],{"class":54},[33,2155,2156],{"class":35,"line":198},[33,2157,106],{"emptyLinePlaceholder":105},[33,2159,2160,2162,2164,2167,2169,2171,2173],{"class":35,"line":218},[33,2161,672],{"class":46},[33,2163,675],{"class":46},[33,2165,2166],{"class":50}," beforeAll",[33,2168,681],{"class":54},[33,2170,655],{"class":46},[33,2172,686],{"class":50},[33,2174,689],{"class":46},[33,2176,2177,2179,2181,2183,2186],{"class":35,"line":223},[33,2178,694],{"class":54},[33,2180,697],{"class":46},[33,2182,2149],{"class":50},[33,2184,2185],{"class":54},".start()          ",[33,2187,2188],{"class":39},"\u002F\u002F Testcontainers — real Postgres in Docker\n",[33,2190,2191],{"class":35,"line":229},[33,2192,106],{"emptyLinePlaceholder":105},[33,2194,2195,2197,2199,2202,2204,2206,2208],{"class":35,"line":250},[33,2196,672],{"class":46},[33,2198,675],{"class":46},[33,2200,2201],{"class":50}," afterAll",[33,2203,681],{"class":54},[33,2205,655],{"class":46},[33,2207,686],{"class":50},[33,2209,689],{"class":46},[33,2211,2212],{"class":35,"line":265},[33,2213,2214],{"class":54},"    db.stop()\n",[33,2216,2217],{"class":35,"line":278},[33,2218,106],{"emptyLinePlaceholder":105},[33,2220,2221,2223,2226,2228,2231,2233],{"class":35,"line":291},[33,2222,130],{"class":54},[33,2224,2225],{"class":133},"\"insert and query real DB\"",[33,2227,155],{"class":54},[33,2229,2230],{"class":50},"IntegrationTest",[33,2232,137],{"class":54},[33,2234,124],{"class":46},[33,2236,2237,2239,2241,2243,2246],{"class":35,"line":321},[33,2238,856],{"class":46},[33,2240,1886],{"class":540},[33,2242,544],{"class":46},[33,2244,2245],{"class":50}," PostgresUserRepo",[33,2247,2248],{"class":54},"(db.jdbcUrl)\n",[33,2250,2251,2254,2256,2258,2260],{"class":35,"line":333},[33,2252,2253],{"class":54},"    repo.save(",[33,2255,311],{"class":50},[33,2257,308],{"class":54},[33,2259,761],{"class":133},[33,2261,318],{"class":54},[33,2263,2264,2267,2269,2271,2273,2275,2277],{"class":35,"line":355},[33,2265,2266],{"class":54},"    repo.findAll() shouldBe ",[33,2268,771],{"class":50},[33,2270,308],{"class":54},[33,2272,311],{"class":50},[33,2274,308],{"class":54},[33,2276,761],{"class":133},[33,2278,318],{"class":54},[33,2280,2281],{"class":35,"line":360},[33,2282,106],{"emptyLinePlaceholder":105},[33,2284,2285,2287,2290,2292,2294,2296,2299,2301],{"class":35,"line":366},[33,2286,130],{"class":54},[33,2288,2289],{"class":133},"\"connection retry on transient failure\"",[33,2291,155],{"class":54},[33,2293,2230],{"class":50},[33,2295,155],{"class":54},[33,2297,2298],{"class":50},"SlowTest",[33,2300,137],{"class":54},[33,2302,124],{"class":46},[33,2304,2305],{"class":35,"line":384},[33,2306,2307],{"class":39},"    \u002F\u002F takes 30s+ — only run in nightly CI, not on every push\n",[33,2309,2310],{"class":35,"line":396},[33,2311,106],{"emptyLinePlaceholder":105},[33,2313,2314],{"class":35,"line":421},[33,2315,2316],{"class":39},"\u002F\u002F sbt commands for tagged execution:\n",[33,2318,2319],{"class":35,"line":433},[33,2320,2321],{"class":39},"\u002F\u002F   sbt \"testOnly * -- -n unit\"              # only unit tests\n",[33,2323,2324],{"class":35,"line":445},[33,2325,2326],{"class":39},"\u002F\u002F   sbt \"testOnly * -- -n integration -l slow\"  # integration but not slow\n",[33,2328,2329],{"class":35,"line":806},[33,2330,2331],{"class":39},"\u002F\u002F   sbt \"Test\u002Fserial\"                        # run tests serially (for shared resources)\n",[15,2333,2335,2336,2339],{"id":2334},"async-testing-future-based-assertions","Async Testing — ",[30,2337,2338],{},"Future","-Based Assertions",[20,2341,2342],{"language":22},[24,2343,2345],{"className":26,"code":2344,"language":22,"meta":28,"style":28},"import org.scalatest.funsuite.AsyncFunSuite\nimport scala.concurrent.Future\n\n\u002F\u002F AsyncFunSuite: test bodies return Future[Assertion] — no Await needed\nclass UserServiceAsyncTest extends AsyncFunSuite:\n  import scala.concurrent.ExecutionContext.Implicits.global\n\n  test(\"async user creation\"):\n    for\n      id   \u003C- service.create(\"Alice\")       \u002F\u002F returns Future[Long]\n      user \u003C- service.find(id)              \u002F\u002F returns Future[Option[User]]\n    yield assert(user == Some(User(\"Alice\")))\n    \u002F\u002F The test framework handles awaiting — the Future completes the test\n\n  test(\"async error handling\"):\n    recoverToSucceededIf[IllegalArgumentException]:\n      service.create(\"\")                    \u002F\u002F should fail with IAE\n",[30,2346,2347,2366,2383,2387,2392,2406,2432,2436,2447,2452,2469,2482,2505,2510,2514,2525,2537],{"__ignoreMap":28},[33,2348,2349,2351,2353,2355,2357,2359,2361,2363],{"class":35,"line":36},[33,2350,47],{"class":46},[33,2352,51],{"class":50},[33,2354,55],{"class":54},[33,2356,58],{"class":50},[33,2358,55],{"class":54},[33,2360,63],{"class":50},[33,2362,55],{"class":54},[33,2364,2365],{"class":50},"AsyncFunSuite\n",[33,2367,2368,2370,2373,2375,2378,2380],{"class":35,"line":43},[33,2369,47],{"class":46},[33,2371,2372],{"class":50}," scala",[33,2374,55],{"class":54},[33,2376,2377],{"class":50},"concurrent",[33,2379,55],{"class":54},[33,2381,2382],{"class":50},"Future\n",[33,2384,2385],{"class":35,"line":71},[33,2386,106],{"emptyLinePlaceholder":105},[33,2388,2389],{"class":35,"line":102},[33,2390,2391],{"class":39},"\u002F\u002F AsyncFunSuite: test bodies return Future[Assertion] — no Await needed\n",[33,2393,2394,2396,2399,2401,2404],{"class":35,"line":109},[33,2395,112],{"class":46},[33,2397,2398],{"class":50}," UserServiceAsyncTest",[33,2400,118],{"class":46},[33,2402,2403],{"class":50}," AsyncFunSuite",[33,2405,124],{"class":46},[33,2407,2408,2411,2413,2415,2417,2419,2422,2424,2427,2429],{"class":35,"line":127},[33,2409,2410],{"class":46},"  import",[33,2412,2372],{"class":50},[33,2414,55],{"class":54},[33,2416,2377],{"class":50},[33,2418,55],{"class":54},[33,2420,2421],{"class":50},"ExecutionContext",[33,2423,55],{"class":54},[33,2425,2426],{"class":50},"Implicits",[33,2428,55],{"class":54},[33,2430,2431],{"class":50},"global\n",[33,2433,2434],{"class":35,"line":142},[33,2435,106],{"emptyLinePlaceholder":105},[33,2437,2438,2440,2443,2445],{"class":35,"line":167},[33,2439,130],{"class":54},[33,2441,2442],{"class":133},"\"async user creation\"",[33,2444,137],{"class":54},[33,2446,124],{"class":46},[33,2448,2449],{"class":35,"line":172},[33,2450,2451],{"class":46},"    for\n",[33,2453,2454,2457,2459,2461,2463,2466],{"class":35,"line":184},[33,2455,2456],{"class":54},"      id   ",[33,2458,1357],{"class":46},[33,2460,1937],{"class":54},[33,2462,761],{"class":133},[33,2464,2465],{"class":54},")       ",[33,2467,2468],{"class":39},"\u002F\u002F returns Future[Long]\n",[33,2470,2471,2474,2476,2479],{"class":35,"line":198},[33,2472,2473],{"class":54},"      user ",[33,2475,1357],{"class":46},[33,2477,2478],{"class":54}," service.find(id)              ",[33,2480,2481],{"class":39},"\u002F\u002F returns Future[Option[User]]\n",[33,2483,2484,2487,2490,2492,2495,2497,2499,2501,2503],{"class":35,"line":218},[33,2485,2486],{"class":46},"    yield",[33,2488,2489],{"class":54}," assert(user ",[33,2491,580],{"class":46},[33,2493,2494],{"class":50}," Some",[33,2496,308],{"class":54},[33,2498,311],{"class":50},[33,2500,308],{"class":54},[33,2502,761],{"class":133},[33,2504,1643],{"class":54},[33,2506,2507],{"class":35,"line":223},[33,2508,2509],{"class":39},"    \u002F\u002F The test framework handles awaiting — the Future completes the test\n",[33,2511,2512],{"class":35,"line":229},[33,2513,106],{"emptyLinePlaceholder":105},[33,2515,2516,2518,2521,2523],{"class":35,"line":250},[33,2517,130],{"class":54},[33,2519,2520],{"class":133},"\"async error handling\"",[33,2522,137],{"class":54},[33,2524,124],{"class":46},[33,2526,2527,2530,2533,2535],{"class":35,"line":265},[33,2528,2529],{"class":54},"    recoverToSucceededIf[",[33,2531,2532],{"class":50},"IllegalArgumentException",[33,2534,193],{"class":54},[33,2536,124],{"class":46},[33,2538,2539,2542,2545,2547],{"class":35,"line":278},[33,2540,2541],{"class":54},"      service.create(",[33,2543,2544],{"class":133},"\"\"",[33,2546,586],{"class":54},[33,2548,2549],{"class":39},"\u002F\u002F should fail with IAE\n",[15,2551,2553],{"id":2552},"test-parallelism-race-conditions-in-tests","Test Parallelism & Race Conditions in Tests",[20,2555,2556],{"language":22},[24,2557,2559],{"className":26,"code":2558,"language":22,"meta":28,"style":28},"import org.scalatest.funsuite.AnyFunSuite\n\n\u002F\u002F ⚠️ ScalaTest runs tests in PARALLEL by default (different test classes in parallel).\n\u002F\u002F Within a class, tests are sequential UNLESS ParallelTestExecution is mixed in.\n\nimport org.scalatest.ParallelTestExecution\nclass ParallelTest extends AnyFunSuite with ParallelTestExecution:\n  \u002F\u002F These tests MAY run concurrently — don't share mutable state!\n  test(\"a\"): assert(1 + 1 == 2)\n  test(\"b\"): assert(2 + 2 == 4)\n\n\u002F\u002F ❌ ANTI-PATTERN: shared DB across parallel test classes\nclass TestA extends AnyFunSuite:\n  test(\"insert user\"):\n    sharedDb.save(User(\"Alice\"))            \u002F\u002F races with TestB\nclass TestB extends AnyFunSuite:\n  test(\"count users\"):\n    assert(sharedDb.count() == 0)           \u002F\u002F flaky — depends on TestA's timing\n\n\u002F\u002F ✅ CORRECT: per-class isolated DB (Testcontainers or in-memory)\nclass TestA extends AnyFunSuite with BeforeAndAfterAll:\n  private var db: TestDatabase = _\n  override def beforeAll(): Unit = db = TestDatabase.fresh()\n  \u002F\u002F Each class gets its own DB — no cross-test interference\n",[30,2560,2561,2579,2583,2588,2593,2597,2612,2630,2635,2665,2691,2695,2700,2713,2724,2741,2754,2765,2780,2784,2789,2805,2821,2847],{"__ignoreMap":28},[33,2562,2563,2565,2567,2569,2571,2573,2575,2577],{"class":35,"line":36},[33,2564,47],{"class":46},[33,2566,51],{"class":50},[33,2568,55],{"class":54},[33,2570,58],{"class":50},[33,2572,55],{"class":54},[33,2574,63],{"class":50},[33,2576,55],{"class":54},[33,2578,68],{"class":50},[33,2580,2581],{"class":35,"line":43},[33,2582,106],{"emptyLinePlaceholder":105},[33,2584,2585],{"class":35,"line":71},[33,2586,2587],{"class":39},"\u002F\u002F ⚠️ ScalaTest runs tests in PARALLEL by default (different test classes in parallel).\n",[33,2589,2590],{"class":35,"line":102},[33,2591,2592],{"class":39},"\u002F\u002F Within a class, tests are sequential UNLESS ParallelTestExecution is mixed in.\n",[33,2594,2595],{"class":35,"line":109},[33,2596,106],{"emptyLinePlaceholder":105},[33,2598,2599,2601,2603,2605,2607,2609],{"class":35,"line":127},[33,2600,47],{"class":46},[33,2602,51],{"class":50},[33,2604,55],{"class":54},[33,2606,58],{"class":50},[33,2608,55],{"class":54},[33,2610,2611],{"class":50},"ParallelTestExecution\n",[33,2613,2614,2616,2619,2621,2623,2625,2628],{"class":35,"line":142},[33,2615,112],{"class":46},[33,2617,2618],{"class":50}," ParallelTest",[33,2620,118],{"class":46},[33,2622,121],{"class":50},[33,2624,636],{"class":46},[33,2626,2627],{"class":50}," ParallelTestExecution",[33,2629,124],{"class":46},[33,2631,2632],{"class":35,"line":167},[33,2633,2634],{"class":39},"  \u002F\u002F These tests MAY run concurrently — don't share mutable state!\n",[33,2636,2637,2639,2642,2644,2646,2649,2652,2655,2657,2660,2663],{"class":35,"line":172},[33,2638,130],{"class":54},[33,2640,2641],{"class":133},"\"a\"",[33,2643,137],{"class":54},[33,2645,655],{"class":46},[33,2647,2648],{"class":54}," assert(",[33,2650,2651],{"class":151},"1",[33,2653,2654],{"class":46}," +",[33,2656,583],{"class":151},[33,2658,2659],{"class":46}," ==",[33,2661,2662],{"class":151}," 2",[33,2664,215],{"class":54},[33,2666,2667,2669,2672,2674,2676,2678,2680,2682,2684,2686,2689],{"class":35,"line":184},[33,2668,130],{"class":54},[33,2670,2671],{"class":133},"\"b\"",[33,2673,137],{"class":54},[33,2675,655],{"class":46},[33,2677,2648],{"class":54},[33,2679,152],{"class":151},[33,2681,2654],{"class":46},[33,2683,2662],{"class":151},[33,2685,2659],{"class":46},[33,2687,2688],{"class":151}," 4",[33,2690,215],{"class":54},[33,2692,2693],{"class":35,"line":198},[33,2694,106],{"emptyLinePlaceholder":105},[33,2696,2697],{"class":35,"line":218},[33,2698,2699],{"class":39},"\u002F\u002F ❌ ANTI-PATTERN: shared DB across parallel test classes\n",[33,2701,2702,2704,2707,2709,2711],{"class":35,"line":223},[33,2703,112],{"class":46},[33,2705,2706],{"class":50}," TestA",[33,2708,118],{"class":46},[33,2710,121],{"class":50},[33,2712,124],{"class":46},[33,2714,2715,2717,2720,2722],{"class":35,"line":229},[33,2716,130],{"class":54},[33,2718,2719],{"class":133},"\"insert user\"",[33,2721,137],{"class":54},[33,2723,124],{"class":46},[33,2725,2726,2729,2731,2733,2735,2738],{"class":35,"line":250},[33,2727,2728],{"class":54},"    sharedDb.save(",[33,2730,311],{"class":50},[33,2732,308],{"class":54},[33,2734,761],{"class":133},[33,2736,2737],{"class":54},"))            ",[33,2739,2740],{"class":39},"\u002F\u002F races with TestB\n",[33,2742,2743,2745,2748,2750,2752],{"class":35,"line":265},[33,2744,112],{"class":46},[33,2746,2747],{"class":50}," TestB",[33,2749,118],{"class":46},[33,2751,121],{"class":50},[33,2753,124],{"class":46},[33,2755,2756,2758,2761,2763],{"class":35,"line":278},[33,2757,130],{"class":54},[33,2759,2760],{"class":133},"\"count users\"",[33,2762,137],{"class":54},[33,2764,124],{"class":46},[33,2766,2767,2770,2772,2774,2777],{"class":35,"line":291},[33,2768,2769],{"class":54},"    assert(sharedDb.count() ",[33,2771,580],{"class":46},[33,2773,547],{"class":151},[33,2775,2776],{"class":54},")           ",[33,2778,2779],{"class":39},"\u002F\u002F flaky — depends on TestA's timing\n",[33,2781,2782],{"class":35,"line":321},[33,2783,106],{"emptyLinePlaceholder":105},[33,2785,2786],{"class":35,"line":333},[33,2787,2788],{"class":39},"\u002F\u002F ✅ CORRECT: per-class isolated DB (Testcontainers or in-memory)\n",[33,2790,2791,2793,2795,2797,2799,2801,2803],{"class":35,"line":355},[33,2792,112],{"class":46},[33,2794,2706],{"class":50},[33,2796,118],{"class":46},[33,2798,121],{"class":50},[33,2800,636],{"class":46},[33,2802,2134],{"class":50},[33,2804,124],{"class":46},[33,2806,2807,2809,2811,2813,2815,2817,2819],{"class":35,"line":360},[33,2808,646],{"class":46},[33,2810,649],{"class":46},[33,2812,652],{"class":540},[33,2814,655],{"class":46},[33,2816,658],{"class":50},[33,2818,544],{"class":46},[33,2820,663],{"class":54},[33,2822,2823,2825,2827,2829,2831,2833,2835,2837,2840,2842,2844],{"class":35,"line":366},[33,2824,672],{"class":46},[33,2826,675],{"class":46},[33,2828,2166],{"class":50},[33,2830,681],{"class":54},[33,2832,655],{"class":46},[33,2834,686],{"class":50},[33,2836,544],{"class":46},[33,2838,2839],{"class":54}," db ",[33,2841,697],{"class":46},[33,2843,658],{"class":50},[33,2845,2846],{"class":54},".fresh()\n",[33,2848,2849],{"class":35,"line":384},[33,2850,2851],{"class":39},"  \u002F\u002F Each class gets its own DB — no cross-test interference\n",[15,2853,2855],{"id":2854},"assertion-deep-dive-matchers-and-custom-assertions","Assertion Deep-Dive — Matchers and Custom Assertions",[20,2857,2858],{"language":22},[24,2859,2861],{"className":26,"code":2860,"language":22,"meta":28,"style":28},"import org.scalatest.matchers.should.Matchers.*\nimport org.scalatest.matchers.be\n\n\u002F\u002F Structural equality — checks field by field (case class equals)\nUser(\"Alice\") shouldBe User(\"Alice\")        \u002F\u002F ✓\nUser(\"Alice\") should be(User(\"Alice\"))      \u002F\u002F same thing\n\n\u002F\u002F Collection matchers\nList(1, 2, 3) should contain(2)\nList(1, 2, 3) should contain theSameElementsAs List(3, 2, 1)  \u002F\u002F order-independent\nMap(\"a\" -> 1, \"b\" -> 2) should contain key \"a\"\nMap(\"a\" -> 1) should contain value 1\n\n\u002F\u002F Option\u002FEither matchers\nSome(42) shouldBe defined\nNone shouldBe empty\nRight(42) shouldBe Right(42)\nLeft(\"err\") shouldBe Left(\"err\")\n\n\u002F\u002F Exception assertions\nassertThrows[ArithmeticException](10 \u002F 0)\nthe[ArithmeticException] thrownBy (10 \u002F 0) should have message \"\u002F by zero\"\n\n\u002F\u002F Custom matcher for domain-specific assertions\ndef beWithinTolerance(expected: Double, tolerance: Double) =\n  be >= (expected - tolerance) and be \u003C= (expected + tolerance)\n\n3.14159 should beWithinTolerance(3.14, 0.01)  \u002F\u002F → passes\n",[30,2862,2863,2889,2908,2912,2917,2939,2962,2966,2971,2994,3033,3060,3078,3082,3087,3098,3106,3124,3143,3147,3152,3170,3192,3196,3201,3233,3261,3265],{"__ignoreMap":28},[33,2864,2865,2867,2869,2871,2873,2875,2877,2879,2881,2883,2885,2887],{"class":35,"line":36},[33,2866,47],{"class":46},[33,2868,51],{"class":50},[33,2870,55],{"class":54},[33,2872,58],{"class":50},[33,2874,55],{"class":54},[33,2876,84],{"class":50},[33,2878,55],{"class":54},[33,2880,89],{"class":50},[33,2882,55],{"class":54},[33,2884,94],{"class":50},[33,2886,55],{"class":54},[33,2888,99],{"class":50},[33,2890,2891,2893,2895,2897,2899,2901,2903,2905],{"class":35,"line":43},[33,2892,47],{"class":46},[33,2894,51],{"class":50},[33,2896,55],{"class":54},[33,2898,58],{"class":50},[33,2900,55],{"class":54},[33,2902,84],{"class":50},[33,2904,55],{"class":54},[33,2906,2907],{"class":50},"be\n",[33,2909,2910],{"class":35,"line":71},[33,2911,106],{"emptyLinePlaceholder":105},[33,2913,2914],{"class":35,"line":102},[33,2915,2916],{"class":39},"\u002F\u002F Structural equality — checks field by field (case class equals)\n",[33,2918,2919,2921,2923,2925,2927,2929,2931,2933,2936],{"class":35,"line":109},[33,2920,311],{"class":50},[33,2922,308],{"class":54},[33,2924,761],{"class":133},[33,2926,161],{"class":54},[33,2928,311],{"class":50},[33,2930,308],{"class":54},[33,2932,761],{"class":133},[33,2934,2935],{"class":54},")        ",[33,2937,2938],{"class":39},"\u002F\u002F ✓\n",[33,2940,2941,2943,2945,2947,2950,2952,2954,2956,2959],{"class":35,"line":127},[33,2942,311],{"class":50},[33,2944,308],{"class":54},[33,2946,761],{"class":133},[33,2948,2949],{"class":54},") should be(",[33,2951,311],{"class":50},[33,2953,308],{"class":54},[33,2955,761],{"class":133},[33,2957,2958],{"class":54},"))      ",[33,2960,2961],{"class":39},"\u002F\u002F same thing\n",[33,2963,2964],{"class":35,"line":142},[33,2965,106],{"emptyLinePlaceholder":105},[33,2967,2968],{"class":35,"line":167},[33,2969,2970],{"class":39},"\u002F\u002F Collection matchers\n",[33,2972,2973,2975,2977,2979,2981,2983,2985,2987,2990,2992],{"class":35,"line":172},[33,2974,771],{"class":50},[33,2976,308],{"class":54},[33,2978,2651],{"class":151},[33,2980,155],{"class":54},[33,2982,152],{"class":151},[33,2984,155],{"class":54},[33,2986,158],{"class":151},[33,2988,2989],{"class":54},") should contain(",[33,2991,152],{"class":151},[33,2993,215],{"class":54},[33,2995,2996,2998,3000,3002,3004,3006,3008,3010,3013,3015,3017,3019,3021,3023,3025,3027,3030],{"class":35,"line":184},[33,2997,771],{"class":50},[33,2999,308],{"class":54},[33,3001,2651],{"class":151},[33,3003,155],{"class":54},[33,3005,152],{"class":151},[33,3007,155],{"class":54},[33,3009,158],{"class":151},[33,3011,3012],{"class":54},") should contain theSameElementsAs ",[33,3014,771],{"class":50},[33,3016,308],{"class":54},[33,3018,158],{"class":151},[33,3020,155],{"class":54},[33,3022,152],{"class":151},[33,3024,155],{"class":54},[33,3026,2651],{"class":151},[33,3028,3029],{"class":54},")  ",[33,3031,3032],{"class":39},"\u002F\u002F order-independent\n",[33,3034,3035,3037,3039,3041,3044,3046,3048,3050,3052,3054,3057],{"class":35,"line":198},[33,3036,1759],{"class":50},[33,3038,308],{"class":54},[33,3040,2641],{"class":133},[33,3042,3043],{"class":46}," ->",[33,3045,583],{"class":151},[33,3047,155],{"class":54},[33,3049,2671],{"class":133},[33,3051,3043],{"class":46},[33,3053,2662],{"class":151},[33,3055,3056],{"class":54},") should contain key ",[33,3058,3059],{"class":133},"\"a\"\n",[33,3061,3062,3064,3066,3068,3070,3072,3075],{"class":35,"line":218},[33,3063,1759],{"class":50},[33,3065,308],{"class":54},[33,3067,2641],{"class":133},[33,3069,3043],{"class":46},[33,3071,583],{"class":151},[33,3073,3074],{"class":54},") should contain value ",[33,3076,3077],{"class":151},"1\n",[33,3079,3080],{"class":35,"line":223},[33,3081,106],{"emptyLinePlaceholder":105},[33,3083,3084],{"class":35,"line":229},[33,3085,3086],{"class":39},"\u002F\u002F Option\u002FEither matchers\n",[33,3088,3089,3091,3093,3095],{"class":35,"line":250},[33,3090,1632],{"class":50},[33,3092,308],{"class":54},[33,3094,1626],{"class":151},[33,3096,3097],{"class":54},") shouldBe defined\n",[33,3099,3100,3103],{"class":35,"line":265},[33,3101,3102],{"class":50},"None",[33,3104,3105],{"class":54}," shouldBe empty\n",[33,3107,3108,3110,3112,3114,3116,3118,3120,3122],{"class":35,"line":278},[33,3109,305],{"class":50},[33,3111,308],{"class":54},[33,3113,1626],{"class":151},[33,3115,161],{"class":54},[33,3117,305],{"class":50},[33,3119,308],{"class":54},[33,3121,1626],{"class":151},[33,3123,215],{"class":54},[33,3125,3126,3128,3130,3133,3135,3137,3139,3141],{"class":35,"line":291},[33,3127,345],{"class":50},[33,3129,308],{"class":54},[33,3131,3132],{"class":133},"\"err\"",[33,3134,161],{"class":54},[33,3136,345],{"class":50},[33,3138,308],{"class":54},[33,3140,3132],{"class":133},[33,3142,215],{"class":54},[33,3144,3145],{"class":35,"line":321},[33,3146,106],{"emptyLinePlaceholder":105},[33,3148,3149],{"class":35,"line":333},[33,3150,3151],{"class":39},"\u002F\u002F Exception assertions\n",[33,3153,3154,3157,3159,3161,3163,3166,3168],{"class":35,"line":355},[33,3155,3156],{"class":54},"assertThrows[",[33,3158,190],{"class":50},[33,3160,827],{"class":54},[33,3162,207],{"class":151},[33,3164,3165],{"class":46}," \u002F",[33,3167,547],{"class":151},[33,3169,215],{"class":54},[33,3171,3172,3175,3177,3180,3182,3184,3186,3189],{"class":35,"line":360},[33,3173,3174],{"class":54},"the[",[33,3176,190],{"class":50},[33,3178,3179],{"class":54},"] thrownBy (",[33,3181,207],{"class":151},[33,3183,3165],{"class":46},[33,3185,547],{"class":151},[33,3187,3188],{"class":54},") should have message ",[33,3190,3191],{"class":133},"\"\u002F by zero\"\n",[33,3193,3194],{"class":35,"line":366},[33,3195,106],{"emptyLinePlaceholder":105},[33,3197,3198],{"class":35,"line":384},[33,3199,3200],{"class":39},"\u002F\u002F Custom matcher for domain-specific assertions\n",[33,3202,3203,3206,3209,3211,3214,3216,3219,3221,3224,3226,3228,3230],{"class":35,"line":396},[33,3204,3205],{"class":46},"def",[33,3207,3208],{"class":50}," beWithinTolerance",[33,3210,308],{"class":54},[33,3212,3213],{"class":540},"expected",[33,3215,1111],{"class":54},[33,3217,3218],{"class":50},"Double",[33,3220,155],{"class":54},[33,3222,3223],{"class":540},"tolerance",[33,3225,1111],{"class":54},[33,3227,3218],{"class":50},[33,3229,1236],{"class":54},[33,3231,3232],{"class":46},"=\n",[33,3234,3235,3238,3241,3244,3247,3250,3253,3255,3258],{"class":35,"line":421},[33,3236,3237],{"class":54},"  be ",[33,3239,3240],{"class":46},">=",[33,3242,3243],{"class":54}," (expected ",[33,3245,3246],{"class":46},"-",[33,3248,3249],{"class":54}," tolerance) and be ",[33,3251,3252],{"class":46},"\u003C=",[33,3254,3243],{"class":54},[33,3256,3257],{"class":46},"+",[33,3259,3260],{"class":54}," tolerance)\n",[33,3262,3263],{"class":35,"line":433},[33,3264,106],{"emptyLinePlaceholder":105},[33,3266,3267,3270,3273,3276,3278,3281,3283],{"class":35,"line":445},[33,3268,3269],{"class":151},"3.14159",[33,3271,3272],{"class":54}," should beWithinTolerance(",[33,3274,3275],{"class":151},"3.14",[33,3277,155],{"class":54},[33,3279,3280],{"class":151},"0.01",[33,3282,3029],{"class":54},[33,3284,3285],{"class":39},"\u002F\u002F → passes\n",[15,3287,3289],{"id":3288},"tips-tricks","💡 Tips & Tricks",[3291,3292,3293,3300],"p",{},[3294,3295,3296,3299],"strong",{},[30,3297,3298],{},"sbt ~test"," for continuous testing",": Watches for file changes and re-runs affected tests — instant feedback loop during development.",[20,3302,3304],{"language":3303},"bash",[24,3305,3308],{"className":3306,"code":3307,"language":3303,"meta":28,"style":28},"language-bash shiki shiki-themes github-light github-dark","sbt \"~testOnly *UserService*\"           # re-run user service tests on every save\nsbt \"~Test\u002Fcompile\"                      # just compile tests on save (faster, no execution)\n",[30,3309,3310,3321],{"__ignoreMap":28},[33,3311,3312,3315,3318],{"class":35,"line":36},[33,3313,3314],{"class":50},"sbt",[33,3316,3317],{"class":133}," \"~testOnly *UserService*\"",[33,3319,3320],{"class":39},"           # re-run user service tests on every save\n",[33,3322,3323,3325,3328],{"class":35,"line":43},[33,3324,3314],{"class":50},[33,3326,3327],{"class":133}," \"~Test\u002Fcompile\"",[33,3329,3330],{"class":39},"                      # just compile tests on save (faster, no execution)\n",[3291,3332,3333,3336],{},[3294,3334,3335],{},"Test data builders for complex domain objects",": Use the Builder pattern or a test data factory to create valid objects without verbose constructor calls.",[20,3338,3339],{"language":22},[24,3340,3342],{"className":26,"code":3341,"language":22,"meta":28,"style":28},"object UserBuilder:\n  def apply(name: String = \"test-user\", email: String = \"test@x.com\", roles: List[String] = Nil): User =\n    User(name = name, email = email, roles = roles)\n\n\u002F\u002F In tests:\nUserBuilder(email = \"alice@x.com\")         \u002F\u002F uses defaults for other fields\nUserBuilder(roles = List(\"admin\"))         \u002F\u002F only override what matters\n",[30,3343,3344,3353,3416,3439,3443,3448,3467],{"__ignoreMap":28},[33,3345,3346,3348,3351],{"class":35,"line":36},[33,3347,1283],{"class":46},[33,3349,3350],{"class":50}," UserBuilder",[33,3352,124],{"class":46},[33,3354,3355,3357,3360,3362,3365,3367,3369,3371,3374,3376,3379,3381,3383,3385,3388,3390,3393,3395,3397,3399,3401,3403,3405,3408,3410,3412,3414],{"class":35,"line":43},[33,3356,815],{"class":46},[33,3358,3359],{"class":50}," apply",[33,3361,308],{"class":54},[33,3363,3364],{"class":540},"name",[33,3366,1111],{"class":54},[33,3368,1342],{"class":50},[33,3370,544],{"class":46},[33,3372,3373],{"class":133}," \"test-user\"",[33,3375,155],{"class":54},[33,3377,3378],{"class":540},"email",[33,3380,1111],{"class":54},[33,3382,1342],{"class":50},[33,3384,544],{"class":46},[33,3386,3387],{"class":133}," \"test@x.com\"",[33,3389,155],{"class":54},[33,3391,3392],{"class":540},"roles",[33,3394,1111],{"class":54},[33,3396,771],{"class":50},[33,3398,821],{"class":54},[33,3400,1342],{"class":50},[33,3402,1309],{"class":54},[33,3404,697],{"class":46},[33,3406,3407],{"class":50}," Nil",[33,3409,137],{"class":54},[33,3411,655],{"class":46},[33,3413,1480],{"class":50},[33,3415,689],{"class":46},[33,3417,3418,3421,3424,3426,3429,3431,3434,3436],{"class":35,"line":71},[33,3419,3420],{"class":50},"    User",[33,3422,3423],{"class":54},"(name ",[33,3425,697],{"class":46},[33,3427,3428],{"class":54}," name, email ",[33,3430,697],{"class":46},[33,3432,3433],{"class":54}," email, roles ",[33,3435,697],{"class":46},[33,3437,3438],{"class":54}," roles)\n",[33,3440,3441],{"class":35,"line":102},[33,3442,106],{"emptyLinePlaceholder":105},[33,3444,3445],{"class":35,"line":109},[33,3446,3447],{"class":39},"\u002F\u002F In tests:\n",[33,3449,3450,3453,3456,3458,3461,3464],{"class":35,"line":127},[33,3451,3452],{"class":50},"UserBuilder",[33,3454,3455],{"class":54},"(email ",[33,3457,697],{"class":46},[33,3459,3460],{"class":133}," \"alice@x.com\"",[33,3462,3463],{"class":54},")         ",[33,3465,3466],{"class":39},"\u002F\u002F uses defaults for other fields\n",[33,3468,3469,3471,3474,3476,3479,3481,3484,3487],{"class":35,"line":142},[33,3470,3452],{"class":50},[33,3472,3473],{"class":54},"(roles ",[33,3475,697],{"class":46},[33,3477,3478],{"class":50}," List",[33,3480,308],{"class":54},[33,3482,3483],{"class":133},"\"admin\"",[33,3485,3486],{"class":54},"))         ",[33,3488,3489],{"class":39},"\u002F\u002F only override what matters\n",[3291,3491,3492,3495],{},[3294,3493,3494],{},"Property-based tests for invariants, example-based for regressions",": Use ScalaCheck for universal invariants (reverse.reverse == id), use explicit examples for specific bug regressions (with a comment referencing the issue number).",[15,3497,3499],{"id":3498},"️-edge-cases-gotchas","⚠️ Edge Cases & Gotchas",[3291,3501,3502,3505],{},[3294,3503,3504],{},"Flaky tests reveal real bugs",": If a test passes\u002Ffails randomly, it's usually a concurrency bug (race condition, timing dependency) — investigate, don't ignore.",[3291,3507,3508,3514,3515,3517],{},[3294,3509,3510,3513],{},[30,3511,3512],{},"beforeAll"," runs once per class, not per test",": Expensive setup (DB containers) goes here. But if tests are parallel, ",[30,3516,3512],{}," may run while another class's tests are still running — use distinct databases.",[3291,3519,3520,3523],{},[3294,3521,3522],{},"Mock frameworks can hide integration bugs",": A test with mocks only verifies that the right calls were made, not that the real dependency actually works. Always have integration tests with real dependencies.",[3291,3525,3526,3529,3530,3533,3534,3537,3538,3541],{},[3294,3527,3528],{},"ScalaCheck generators can be slow",": Generating large lists (Gen.listOfN(100000, ...)) is expensive. Use ",[30,3531,3532],{},"Gen.listOfN"," with reasonable sizes, or configure ",[30,3535,3536],{},"PropertyCheckConfig"," with ",[30,3539,3540],{},"minSuccessful = 50"," instead of the default 100.",[15,3543,3545],{"id":3544},"quick-quiz","🧠 Quick Quiz",[3291,3547,3548,3549,3552,3553,3556,3557,3560],{},"What's the difference between a ",[3294,3550,3551],{},"mock",", a ",[3294,3554,3555],{},"stub",", and a ",[3294,3558,3559],{},"fake","?",[3562,3563,3564,3568,3613],"details",{},[3565,3566,3567],"summary",{},"Answer",[3569,3570,3571,3590,3600],"ul",{},[3572,3573,3574,3577,3578,3581,3582,3586,3587,137],"li",{},[3294,3575,3576],{},"Mock",": A test double that verifies interactions — \"was ",[30,3579,3580],{},"find(42)"," called?\" It records calls and lets you assert on them. The test is about the ",[3583,3584,3585],"em",{},"interaction",", not the result. (e.g., ScalaMock's ",[30,3588,3589],{},"mock[UserRepository]",[3572,3591,3592,3595,3596,3599],{},[3294,3593,3594],{},"Stub",": A test double that returns canned responses — ",[30,3597,3598],{},"find(42) => Some(User(\"Alice\"))",". It doesn't verify interactions, just provides predetermined outputs. Simpler than a mock.",[3572,3601,3602,3605,3606,3609,3610,3612],{},[3294,3603,3604],{},"Fake",": A real working implementation that's simplified for testing — ",[30,3607,3608],{},"FakeUserRepo"," with an in-memory ",[30,3611,1759],{},". It has real logic (find actually looks up the map), but isn't production-ready (no persistence, no concurrency). Fakes are preferred in functional Scala because they test real behavior without a mock framework's coupling.",[3291,3614,3615,3618],{},[3294,3616,3617],{},"Best practice",": Prefer fakes > stubs > mocks. Fakes test the most real behavior. Mocks couple tests to implementation details (every refactor breaks the mock expectations).",[3620,3621,3622],"style",{},"html pre.shiki code .sdCPZ, html code.shiki .sdCPZ{--shiki-default:#6A737D;--shiki-github-dark:#6A737D}html pre.shiki code .svdQ7, html code.shiki .svdQ7{--shiki-default:#D73A49;--shiki-github-dark:#F97583}html pre.shiki code .sIsaT, html code.shiki .sIsaT{--shiki-default:#6F42C1;--shiki-github-dark:#B392F0}html pre.shiki code .ssxIu, html code.shiki .ssxIu{--shiki-default:#24292E;--shiki-github-dark:#E1E4E8}html pre.shiki code .sJ6F3, html code.shiki .sJ6F3{--shiki-default:#032F62;--shiki-github-dark:#9ECBFF}html pre.shiki code .snvgF, html code.shiki .snvgF{--shiki-default:#005CC5;--shiki-github-dark:#79B8FF}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 .github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html.github-dark .shiki span {color: var(--shiki-github-dark);background: var(--shiki-github-dark-bg);font-style: var(--shiki-github-dark-font-style);font-weight: var(--shiki-github-dark-font-weight);text-decoration: var(--shiki-github-dark-text-decoration);}html pre.shiki code .sCrzJ, html code.shiki .sCrzJ{--shiki-default:#E36209;--shiki-github-dark:#FFAB70}",{"title":28,"searchDepth":43,"depth":43,"links":3624},[3625,3626,3627,3628,3629,3630,3632,3633,3634,3635,3636],{"id":17,"depth":43,"text":18},{"id":460,"depth":43,"text":461},{"id":965,"depth":43,"text":966},{"id":1558,"depth":43,"text":1559},{"id":2003,"depth":43,"text":2004},{"id":2334,"depth":43,"text":3631},"Async Testing — Future-Based Assertions",{"id":2552,"depth":43,"text":2553},{"id":2854,"depth":43,"text":2855},{"id":3288,"depth":43,"text":3289},{"id":3498,"depth":43,"text":3499},{"id":3544,"depth":43,"text":3545},"md",{},"\u002Fscala\u002F10-testing",{"title":3641,"description":28},{"Scala — Testing":3642,"description":3643},"ScalaTest, ScalaCheck & Property-Based Testing","Production testing patterns — test fixtures, property-based testing with ScalaCheck, mock-free design with traits, parallel test execution, and CI integration with sbt and munit.","scala\u002F10-testing","hN4DNi5iN_2st5mI58p7cvddlQ2qbWZvpGvjK8BIC_k",1789924654149]