Scalactic: Simply Productive

Just released - Scalactic 2.2.6!
And check out Scalactic 3.0.0-RC3, which includes Scala.js support and new org.scalactic.anyvals restricted value types!

The Scalactic library is focused on constructs related to quality that are useful in both production code and tests. Although ScalaTest is tightly integrated with Scalactic, you can use Scalactic with any Scala project and any test framework. The Scalactic library has no dependencies other than Scala itself.

The === and !== operators

Scalactic provides a powerful === operator (and its complement, !==) that allows you to

  • Customize equality for a type outside its class
  • Get compiler errors for suspicious equality comparisons
  • Compare numeric values for equality with a tolerance
  • Normalize values before comparing them for equality

For example, you can compare strings for equality after being normalized by forcing them to lowercase by customizing Equality explicitly, like this:

    import org.scalactic._
    import TripleEquals._
    import StringNormalizations._
    import Explicitly._

    ("Hello" === "hello") (after being lowerCased) // true
    

You can also define implicit Normalization strategies for types and access them by invoking norm methods:

    import NormMethods._

    implicit val strNormalization = lowerCased

    "WHISPER".norm                                // "whisper"
    "WHISPER".norm === "whisper"                  // true
    

The “after being lowerCased” syntax shown previously is provided by Scalactic' Explicitly DSL, which allows you to specify Equality explicitly. You can also define custom Equalitys implicitly:

    implicit val strEquality =
      decided by defaultEquality[String] afterBeing lowerCased

    "Hello" === "hello"                           // true
    "normalized" === "NORMALIZED"                 // true
    

You can compare numeric values for equality with a Tolerance, like this:

    import Tolerance._

    2.00001 === 2.0 +- 0.01                       // true
    

Or you could use TolerantNumerics define an implicit Equality[Double] that compares Doubles with a tolerance:

    import TolerantNumerics._

    implicit val dblEquality = tolerantDoubleEquality(0.01)

    2.00001 === 2.0                               // true
    

A compiler error for an equality comparison that would always yield false looks like:

    import TypeCheckedTripleEquals._

    Some("hi") === "hi"
    error: types Some[String] and String do not adhere to the type constraint
    selected for the === and !== operators; the missing implicit parameter is
    of type org.scalactic.Constraint[Some[String],String]
              Some("hi") === "hi"
                         ^
    

Or and Every

Scalactic provides an “Either with attitude” named Or, designed for functional error handling. Or gives you more convenient chaining of map and flatMap calls (and for expressions) than Either and, when, combined with Every, enables you to accumulate errors. Every is an ordered collection of one or more elements. An Or is either Good or Bad. An Every is either One or Many. Here's an example of accumulating errors with Or and Every:

import org.scalactic._
import Accumulation._

case class Person(name: String, age: Int)

def parseName(input: String): String Or One[ErrorMessage] = {
  val trimmed = input.trim
  if (!trimmed.isEmpty)
    Good(trimmed)
  else
    Bad(One(s""""${input}" is not a valid name"""))
}

def parseAge(input: String): Int Or One[ErrorMessage] = {
  try {
    val age = input.trim.toInt
    if (age >= 0) Good(age) else Bad(One(s""""${age}" is not a valid age"""))
  }
  catch {
    case _: NumberFormatException =>
      Bad(One(s""""${input}" is not a valid integer"""))
  }
}

def parsePerson(inputName: String,
      inputAge: String): Person Or Every[ErrorMessage] = {

  val name = parseName(inputName)
  val age = parseAge(inputAge)
  withGood(name, age) { Person(_, _) }
}

Here are some examples of parsePerson in action:

parsePerson("Bridget Jones", "29")
// Result: Good(Person(Bridget Jones,29))

parsePerson("Bridget Jones", "") // Result: Bad(One("" is not a valid integer))
parsePerson("Bridget Jones", "-29") // Result: Bad(One("-29" is not a valid age))
parsePerson("", "") // Result: Bad(Many("" is not a valid name, "" is not a valid integer))

Or offers several other ways to accumulate errors besides the withGood methods shown in the example above. See the documentation for Or for more information.

Requirements and Snapshots

Scalactic includes a Requirements trait that offers require, requireState, and requireNonNull methods for checking pre-conditions that give descriptive error messages extracted via a macro. Here are some examples:

val a = -1

require(a >= 0)
// throws IllegalArgumentException: -1 was not greater than or equal to 0

requireState(a >= 0)
// throws IllegalStateException: -1 was not greater than or equal to 0

val b: String = null

requireNonNull(a, b)
// throws NullPointerException: b was null

Trait Snapshots offers a snap method that can help you make debug and log messages that include information about the values of variables:

val a = 1
val b = '2'
val c = "3"

snap(a, b, c) // Result: a was 1, b was '2', c was "3"

And more (but not much more)...

Scalactic also includes a TimesOnInt trait that allows you to perform side-effecting loops a specified number of times, like this:

import TimesOnInt._

3 times println("hello ") // Output: hello hello hello

You can also define an alternate String forms for types using Prettifiers and create extractors for Throwables via the Catcher factory.

And that's it: Scalactic is a small, very focused library. Why not give it a try? Just visit the Quick Start page.

Scalactic is brought to you by Bill Venners, with contributions from several other folks. It is sponsored by Artima, Inc.
ScalaTest is free, open-source software released under the Apache 2.0 license.

Copyright © 2009-2016 Artima, Inc. All Rights Reserved.

artima