A comprehensive property-based testing library for Scala and Java applications that enables developers to specify program properties as testable assertions and automatically generates test cases to verify these properties.
—
ScalaCheck's Properties framework enables organizing related properties into testable suites with command-line runners, batch execution support, and hierarchical organization. Property collections provide structure for large test suites and enable systematic testing of complex systems.
The fundamental class for organizing and executing collections of named properties.
open class Properties(val name: String) {
val name: String
def properties: Seq[(String, Prop)]
def check(prms: Test.Parameters = Test.Parameters.default): Unit
def main(args: Array[String]): Unit
def include(ps: Properties): Unit
def include(ps: Properties, prefix: String): Unit
def overrideParameters(p: Test.Parameters): Test.Parameters
}Usage Examples:
object MathProperties extends Properties("Mathematics") {
property("addition commutative") = forAll { (a: Int, b: Int) =>
a + b == b + a
}
property("multiplication identity") = forAll { (a: Int) =>
a * 1 == a
}
property("division by self") = forAll { (a: Int) =>
(a != 0) ==> (a / a == 1)
}
}
// Check all properties
MathProperties.check()
// Run from command line
MathProperties.main(Array("--verbosity", "2"))The mechanism for adding properties to collections using assignment syntax.
sealed class PropertySpecifier {
def update(propName: String, p: => Prop): Unit
}
val property: PropertySpecifierUsage Examples:
object StringProperties extends Properties("String Operations") {
// Using property specifier with assignment syntax
property("reverse twice") = forAll { (s: String) =>
s.reverse.reverse == s
}
property("concatenation length") = forAll { (s1: String, s2: String) =>
(s1 + s2).length == s1.length + s2.length
}
property("empty string identity") = forAll { (s: String) =>
s + "" == s && "" + s == s
}
}Advanced property specification that allows explicit seed control for reproducible testing.
sealed class PropertyWithSeedSpecifier {
def update(propName: String, optSeed: Option[String], p: => Prop): Unit
}
val propertyWithSeed: PropertyWithSeedSpecifierUsage Examples:
object ReproducibleTests extends Properties("Reproducible") {
// Property with explicit seed for reproducibility
propertyWithSeed("deterministic test", Some("SGVsbG8gV29ybGQ")) = forAll { (x: Int) =>
x + 0 == x
}
// Property without seed (will use random seed but show it on failure)
propertyWithSeed("debug test", None) = forAll { (data: ComplexData) =>
processData(data).isValid
}
}Mechanism for composing larger test suites from smaller property collections.
def include(ps: Properties): Unit
def include(ps: Properties, prefix: String): UnitUsage Examples:
object CoreLogic extends Properties("Core") {
property("basic invariant") = forAll { (x: Int) => x == x }
}
object AdvancedLogic extends Properties("Advanced") {
property("complex invariant") = forAll { (data: ComplexType) =>
validateComplex(data)
}
}
object AllTests extends Properties("Complete Test Suite") {
// Include other property collections
include(CoreLogic)
include(AdvancedLogic, "advanced.")
// Additional properties specific to this suite
property("integration test") = forAll { (a: Int, b: String) =>
integrate(a, b).nonEmpty
}
}
// This will run all properties from included collections plus local properties
AllTests.check()Override default test parameters for specific property collections.
def overrideParameters(p: Test.Parameters): Test.ParametersUsage Examples:
object PerformanceTests extends Properties("Performance") {
// Custom parameters for this collection
override def overrideParameters(p: Test.Parameters): Test.Parameters = {
p.withMinSuccessfulTests(10000)
.withWorkers(4)
.withMaxSize(1000)
}
property("large data processing") = forAll { (data: List[Int]) =>
processLargeDataset(data).size <= data.size
}
}
object QuickTests extends Properties("Quick Smoke Tests") {
override def overrideParameters(p: Test.Parameters): Test.Parameters = {
p.withMinSuccessfulTests(10)
.withMaxSize(20)
}
property("smoke test") = forAll { (x: Int) => x + 1 > x }
}Built-in command-line runner with parameter parsing and result reporting.
def main(args: Array[String]): UnitUsage Examples:
object CommandLineTests extends Properties("CLI Tests") {
property("always passes") = Prop.passed
property("sometimes fails") = forAll { (x: Int) => x != 42 }
}
// Command line usage:
// scala CommandLineTests --minSuccessfulTests 1000 --workers 4
// scala CommandLineTests --verbosity 2 --maxDiscardRatio 10
// In application code:
CommandLineTests.main(Array(
"--minSuccessfulTests", "500",
"--workers", "2",
"--verbosity", "1"
))// Domain-specific property collections
object UserManagement extends Properties("User Management") {
property("user creation") = forAll { (name: String, email: String) =>
val user = createUser(name, email)
user.name == name && user.email == email
}
property("user validation") = forAll { (user: User) =>
validateUser(user).isValid ==> user.isComplete
}
}
object OrderProcessing extends Properties("Order Processing") {
property("order total calculation") = forAll { (items: List[OrderItem]) =>
val order = Order(items)
order.total >= 0 && order.total == items.map(_.price).sum
}
}
object DatabaseOperations extends Properties("Database") {
property("CRUD operations") = forAll { (entity: Entity) =>
val saved = database.save(entity)
val retrieved = database.findById(saved.id)
retrieved.contains(saved)
}
}
// Top-level test suite
object ApplicationTestSuite extends Properties("E-Commerce Application") {
include(UserManagement, "users.")
include(OrderProcessing, "orders.")
include(DatabaseOperations, "db.")
property("end-to-end workflow") = forAll { (user: User, items: List[Item]) =>
val order = createOrder(user, items)
processPayment(order).isSuccess ==> order.status == OrderStatus.Paid
}
}trait TestEnvironment {
def databaseUrl: String
def apiEndpoint: String
}
object LocalTestEnv extends TestEnvironment {
def databaseUrl = "jdbc:h2:mem:test"
def apiEndpoint = "http://localhost:8080"
}
object StagingTestEnv extends TestEnvironment {
def databaseUrl = "jdbc:postgresql://staging-db:5432/app"
def apiEndpoint = "https://staging-api.example.com"
}
abstract class EnvironmentProperties(name: String, env: TestEnvironment) extends Properties(name) {
override def overrideParameters(p: Test.Parameters): Test.Parameters = {
p.withTestCallback(ConsoleReporter(1))
.withMinSuccessfulTests(if (env == LocalTestEnv) 100 else 1000)
}
}
object LocalTests extends EnvironmentProperties("Local Tests", LocalTestEnv) {
property("database connection") = Prop {
connectToDatabase(LocalTestEnv.databaseUrl).isSuccess
}
}
object StagingTests extends EnvironmentProperties("Staging Tests", StagingTestEnv) {
property("api availability") = Prop {
httpGet(StagingTestEnv.apiEndpoint + "/health").status == 200
}
}object PropertyCategories {
// Fast unit tests
object UnitTests extends Properties("Unit Tests") {
override def overrideParameters(p: Test.Parameters): Test.Parameters = {
p.withMinSuccessfulTests(100).withMaxSize(50)
}
property("string operations") = forAll { (s: String) =>
s.toLowerCase.toUpperCase.toLowerCase == s.toLowerCase
}
}
// Slower integration tests
object IntegrationTests extends Properties("Integration Tests") {
override def overrideParameters(p: Test.Parameters): Test.Parameters = {
p.withMinSuccessfulTests(50).withWorkers(2)
}
property("service integration") = forAll { (request: ServiceRequest) =>
val response = callExternalService(request)
response.isValid && response.correlationId == request.id
}
}
// Performance-focused tests
object PerformanceTests extends Properties("Performance Tests") {
override def overrideParameters(p: Test.Parameters): Test.Parameters = {
p.withMinSuccessfulTests(1000)
.withMaxSize(10000)
.withWorkers(Runtime.getRuntime.availableProcessors())
}
property("large data processing") = forAll { (data: List[DataRecord]) =>
val start = System.currentTimeMillis()
val result = processLargeDataset(data)
val duration = System.currentTimeMillis() - start
result.size == data.size && duration < 5000 // Under 5 seconds
}
}
}
// Master suite that includes all categories
object AllTests extends Properties("Complete Test Suite") {
include(PropertyCategories.UnitTests, "unit.")
include(PropertyCategories.IntegrationTests, "integration.")
include(PropertyCategories.PerformanceTests, "performance.")
}object TestAnalyzer extends Properties("Analyzer") {
property("analyzed property") = forAll { (data: TestData) =>
classify(data.size == 0, "empty") {
classify(data.size < 10, "small", "large") {
collect(data.category) {
processTestData(data).isValid
}
}
}
}
}
// Custom test runner with result analysis
object CustomTestRunner {
def runWithAnalysis(props: Properties): Unit = {
val results = Test.checkProperties(
Test.Parameters.default.withTestCallback(ConsoleReporter(2)),
props
)
val (passed, failed) = results.partition(_._2.passed)
println(s"\n=== Test Results ===")
println(s"Passed: ${passed.size}")
println(s"Failed: ${failed.size}")
if (failed.nonEmpty) {
println(s"\nFailed Properties:")
failed.foreach { case (name, result) =>
println(s" ✗ $name")
result.status match {
case Test.Failed(args, labels) =>
println(s" Args: ${args.map(_.arg).mkString(", ")}")
if (labels.nonEmpty) println(s" Labels: ${labels.mkString(", ")}")
case Test.PropException(_, ex, _) =>
println(s" Exception: ${ex.getMessage}")
case _ =>
}
}
}
// Analyze collected data
results.foreach { case (name, result) =>
if (result.freqMap.total > 0) {
println(s"\n$name - Collected Data:")
result.freqMap.getRatios.take(5).foreach { case (item, ratio) =>
println(f" $item: ${ratio * 100}%.1f%%")
}
}
}
}
}
// Usage
CustomTestRunner.runWithAnalysis(ApplicationTestSuite)object ConditionalTests extends Properties("Conditional Tests") {
// Only run expensive tests if environment variable is set
if (sys.env.get("RUN_EXPENSIVE_TESTS").contains("true")) {
property("expensive computation") = forAll { (data: LargeDataSet) =>
expensiveComputation(data).isOptimal
}
}
// Platform-specific tests
if (System.getProperty("os.name").toLowerCase.contains("linux")) {
property("linux-specific feature") = forAll { (path: String) =>
linuxSpecificOperation(path).isSuccess
}
}
// Database-dependent tests
try {
connectToTestDatabase()
property("database operations") = forAll { (entity: Entity) =>
saveToDatabase(entity).isSuccess
}
} catch {
case _: Exception =>
println("Skipping database tests - database not available")
}
}abstract class ModuleTests(moduleName: String) extends Properties(s"$moduleName Module") {
def unitTests: Properties
def integrationTests: Properties
include(unitTests, "unit.")
include(integrationTests, "integration.")
}
object AuthenticationModule extends ModuleTests("Authentication") {
object unitTests extends Properties("Auth Unit Tests") {
property("password hashing") = forAll { (password: String) =>
val hashed = hashPassword(password)
hashed != password && verifyPassword(password, hashed)
}
}
object integrationTests extends Properties("Auth Integration Tests") {
property("login flow") = forAll { (credentials: LoginCredentials) =>
val token = authenticate(credentials)
token.isValid ==> validateToken(token).isSuccess
}
}
}
object PaymentModule extends ModuleTests("Payment") {
object unitTests extends Properties("Payment Unit Tests") {
property("amount calculations") = forAll { (amount: BigDecimal, tax: Double) =>
val total = calculateTotal(amount, tax)
total >= amount && total > 0
}
}
object integrationTests extends Properties("Payment Integration Tests") {
property("payment processing") = forAll { (payment: PaymentRequest) =>
val result = processPayment(payment)
result.status == PaymentStatus.Success ==> result.transactionId.nonEmpty
}
}
}
object ApplicationModules extends Properties("All Modules") {
include(AuthenticationModule)
include(PaymentModule)
override def overrideParameters(p: Test.Parameters): Test.Parameters = {
p.withMinSuccessfulTests(200).withWorkers(3)
}
}Install with Tessl CLI
npx tessl i tessl/maven-org-scalacheck--scalacheck-2-12