Diffable typeclass

Better failures messages

The matchers based on typed equality like be_===[T] all take an implicit typeclass instance from the Diffable typeclass:

trait Diffable[-T] {

  def diff(actual: T, expected: T): ComparisonResult

}

trait ComparisonResult {
  def identical: Boolean
  def render: String
  def render(indent: String): String = render
}

The Diffable typeclass produces a ComparisonResult between 2 instances so that:

Diffable[T].diff(t1, t2).identical <==> t1 == t2

The ComparisonResult.render method is then used to produce a user-readable difference between 2 objects of the same type. This typeclass is implemented for the most common types:

Primitives

(1 ==== 2).message

1 != 2

Lists

(List(1, 2, 3) ==== List(1, 5, 3)).message
1
- 2
+ 5
3
(List(1, 2, 3) ==== List(1, 2, 3, 4, 5)).message
1
2
3
+ 4
+ 5

Maps

(Map(1 -> "one", 2 -> "two") ==== Map(2 -> "Two", 3 -> "three")).message
Map(2 -> {'two' != 'Two'},
    added: 3 -> 'three',
    removed: 1 -> 'one')

We can also compare case classes with by adding the specs2-shapeless dependency and importing a special Diffable instance for case classes with CaseClassDiffs:

case class Address(number: Int, street: String)
case class Person(age: Int, name: String, address: Address)
val p1 = Person(44, "me", Address(14, "Best avenue"))
val p2 = Person(27, "you", Address(14, "First street"))

import org.specs2.matcher.CaseClassDiffs._

(p1 ==== p2).message
Person(age: 44 != 27,
       name: 'me' != 'you',
       address: Address(number: 14,
                        street: 'Best avenue' != 'First street'))

Other instances

Since Diffable is a typeclass you are free to provide other instances in scope to display failures differently:

val p1 = Person(44, "me", Address(14, "Best avenue"))
val p2 = Person(27, "you", Address(14, "First street"))

import org.specs2.matcher.CaseClassDiffs._

// display string differences using the edit distance
implicit def stringDiffable: Diffable[String] = new Diffable[String] {

  def diff(s1: String, s2: String): ComparisonResult = {
    if (s1 == s2) PrimitiveIdentical(s1)
    else
      new ComparisonResult {
        def identical = false

        def render = {
          val (s1Diff, s2Diff) = org.specs2.text.StringEditDistance.showDistance(s1, s2)
          s"$s1Diff != $s2Diff"
        }
      }
  }
}

(p1 ==== p2).message
Person(age: 44 != 27,
       name: [me] != [you],
       address: Address(number: 14,
                        street: [Be]st [avenue] != [Fir]st [street]))

You can also reuse the identical method on ComparisonResult to provide a different notion of equality:

import org.specs2.matcher.describe.Diffables._

implicit def approximateInt: Diffable[Int] =
  PrimitiveDiffable.primitive[Int].
    compareWith((i1: Int, i2: Int) => math.abs(i1 - i2) <= 5)

(1 ==== 3).message

1 == '3'