Scala
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Scala
-
Section 4: Functional Scala
-
Section 5: Collections in Depth
-
Section 6: Type System
-
Section 7: Concurrency and Ecosystem
-
Section 8: Practical Projects
-
Section 9: Interview Practice
-
Section 10: Data Structures and Algorithms in Scala
-
Section 11: More Practice Exercises
-
Section 12: Advanced Functional Patterns
-
Section 13: More Ecosystem
-
Section 14: Scala Collections Library Deep Dive
-
Section 15: Scala Standard Library Deep Dive
-
Section 16: Akka Ecosystem Deep Dive
-
Section 17: Cats and Cats Effect Deep Dive
-
Section 18: Play Framework Deep Dive
-
Section 19: Apache Spark with Scala Deep Dive
-
Section 20: Scala Build Tools Deep Dive
-
Section 21: Scala 3 Specific Features
-
90: Union and Intersection Types
-
Section 22: Scala Testing Deep Dive
-
Section 23: Functional Domain Modeling
-
Section 24: More Data Structures and Algorithms in Scala
-
Section 25: Scala for Data Engineering
-
Section 26: More Practical Projects
-
Section 27: More Interview and Review
-
Section 28: ZIO Ecosystem Deep Dive
-
Section 29: Scala for Machine Learning
-
Section 30: Scala Microservices Architecture
-
Section 31: Scala Type System Deep Dive
-
Section 32: More Practice and Drills
-
Section 33: Scala Performance Deep Dive
-
Section 34: Scala Ecosystem Tooling
-
Section 35: Scala for Reactive Systems
-
Section 36: More Real-World Case Studies
-
Section 37: Scala for Financial Systems
-
Section 38: Scala GraphQL and gRPC
-
Section 39: More Final Projects
-
Section 40: More Interview and Final Review
-
Section 41: Scala for Streaming Data
-
Section 42: Scala Security Practices
-
Section 43: More Language Deep Dive
-
Section 44: Scala Command-Line Tools
-
Section 45: Scala Documentation and Style
-
Section 46: Scala Dependency Management
-
Section 47: More Practical Backend Patterns
-
Section 48: Scala for Event-Driven Architecture
-
Section 49: More Practice Drills Round 2
-
Section 50: Scala Compiler Deep Dive
-
Section 51: Scala for Web Frontends
-
Section 52: More Data Engineering Practice
-
Section 53: Scala Observability
-
Section 54: More Advanced Practice Projects
-
Section 55: Scala for Legacy Java Integration
-
Section 56: More Testing Practice
-
Section 57: Final Mastery Review
-
Section 58: Scala History and Ecosystem Context
-
Section 59: More Concurrency Patterns
-
Section 60: Scala for Configuration Management
-
Section 61: More Domain Modeling Practice
94: Property-Based Testing with ScalaCheck
A few years ago, I was reviewing a PR for a colleague who had written a custom function to merge two sorted lists. He had a great set of unit tests: he tested two empty lists, two lists of equal length, and one empty list with one populated one. Everything passed. I felt confident, we merged it, and two days later, the production logs started screaming. It turned out that when the lists contained duplicate values in a specific alternating pattern, the merge logic skipped an element. We had missed a case that was mathematically possible but didn't occur to us during the "brainstorming" phase of writing unit tests.
That's the fundamental weakness of example-based testing: you are limited by your own imagination. You only test the scenarios you can think of. Property-based testing (PBT), and specifically ScalaCheck, flips this on its head. Instead of providing specific inputs and expected outputs, you define properties—invariants that should always hold true regardless of the input—and you let the library try its hardest to prove you wrong by throwing thousands of random inputs at your code.
Defining Invariants Over Examples
When you use ScalaCheck, you stop thinking about "If I give it X, I should get Y" and start thinking about "No matter what I give it, Z should always be true." For instance, if you're writing a function to reverse a list, you don't need to test List(1, 2, 3). Instead, you define a property: reversing a list twice should return the original list. This is an invariant.
import org.scalacheck.Properties
import org.scalacheck.Gen
object ListProperties extends Properties("ListProperties") {
property("reversing twice returns original") = forAll { (list: List[Int]) =>
list.reverse.reverse == list
}
}
In the example above, forAll is where the magic happens. ScalaCheck looks at the type List[Int] and automatically generates a vast array of test cases: empty lists, lists with one element, lists with a million elements, and lists with huge integers. If it finds a case that returns false, it doesn't just tell you it failed; it tells you exactly which input caused the crash.
Generating Domain-Specific Data
Generic types like Int or String are great, but in the real world, your functions usually expect data that fits a specific shape. You wouldn't want to test a "User Age" field with negative numbers or strings that are ten megabytes long. This is where Gen comes in. You can create custom generators to constrain the data ScalaCheck produces.
I've found that the most powerful way to use generators is by combining them. If you have a User case class, you don't want to manually instantiate it 100 times; you build a generator that composes smaller generators together.
case class User(id: Int, username: String, age: Int)
val userGen: Gen[User] = for {
id <- Gen.choose(1, 10000)
username <- Gen.alphaNumStr
age <- Gen.choose(18, 120)
} yield User(id, username, age)
property("user age is always within legal bounds") = forAll(userGen) { user =>
user.age >= 18 && user.age <= 120
}
By narrowing the scope of the input, you spend less time debugging "impossible" failures and more time finding the actual edge cases in your business logic.
The Power of Shrinking
One of the most frustrating things about random testing is when the library finds a failure but the input is a massive, messy string of 500 characters that is impossible to debug. This is why ScalaCheck's "shrinking" process is a lifesaver. When ScalaCheck finds a failing input, it doesn't just stop. It attempts to "shrink" that input to the smallest possible value that still triggers the failure.
If a list of 100 integers causes your function to throw an exception, ScalaCheck will try removing elements or reducing the size of the integers until it finds the minimal example. Instead of a 100-element list, it might tell you: "This fails specifically when the list contains exactly one negative number." This turns a needle-in-a-haystack debugging session into a trivial fix. It's honestly a bit like having a tiny, malicious agent whose only job is to find the most efficient way to break your code.
📋 Practical Task
Testing a Balanced Parentheses Parser
You have been handed a function called isBalanced(input: String): Boolean. This function is supposed to return true if the parentheses in a string are properly balanced (e.g., "(())"` is true, "()()"` is true, but "(()"` is false).
Your task is to write a ScalaCheck property test to verify this function. However, simply testing a few strings isn't enough. You must:
- Create a custom
Gen[String]that generates strings containing only parentheses'('and')'. - Write a property that verifies a "known truth": if you take any balanced string and wrap it in another pair of parentheses (e.g.,
"()"becomes"(())"), the resulting string must also be balanced. - Write a second property that verifies that any string with an odd length can never be balanced.
Implement the Properties class and run it against the provided isBalanced implementation to see if you can find any edge cases where the logic fails.
There are no comments for now.