Types of Differs

Here we list the kinds of Differs and how you can use them.

The examples below assume the following imports:

import difflicious._
import difflicious.implicits._

Value Differs

For basic types like Int, Double and String we typically can compare them directly e.g. using equals method.

If you have a simple type where you don’t need any advanced diffing, then you can use Differ.useEquals to make a Differ instance for it.

case class MyInt(i: Int)

object MyInt {
  implicit val differ: Differ[MyInt] = Differ.useEquals[MyInt](valueToString = _.toString)
}
MyInt.differ.diff(MyInt(1), MyInt(2))
MyInt(1) -> MyInt(2)

Differs for Algebraic Data Types (enums, sealed traits and case classes)

You can derive Differ for a case class provided that there is a Differ instance for all your fields.

Similarly, you can derive a Differ for a sealed trait (Also called Enums in Scala 3) provided that we’re able to derive a Differ for subclass of the sealed trait (or a Differ instance is already in scope for that subclass)

Case class

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

object Person {
  implicit val differ: Differ[Person] = Differ.derived[Person]
}
Person.differ.diff(
  Person("Alice", 40),
  Person("Alice", 35)
)
Person(
  name: "Alice",
  age: 40 -> 35,
)

Sealed trait / Scala 3 Enum

// Deriving Differ instance for sealed trait
sealed trait HousePet
final case class Dog(name: String, age: Int) extends HousePet
final case class Cat(name: String, livesLeft: Int) extends HousePet

object HousePet {
  implicit val differ: Differ[HousePet] = Differ.derived[HousePet]
}
HousePet.differ.diff(
  Dog("Lucky", 1),
  Cat("Lucky", 1)
)
Dog != Cat
=== Obtained ===
Dog(
  name: "Lucky",
  age: 1,
)
=== Expected ===
Cat(
  name: "Lucky",
  livesLeft: 1,
)

Seq Differ

Differ for sequences allow diffing immutable sequences like Seq, List, and Vector.

By default, Seq Differs will match elements by their index in the sequence.

In the example below

  • Bob’s age
  • Alice isn’t expected to be in list
  • Charles is expected but missing
val alice = Person("Alice", 30)
val bob = Person("Bob", 25)
val bob50 = Person("Bob", 50)
val charles = Person("Charles", 80)

Differ.seqDiffer[List, Person].diff(
  List(alice, bob50),
  List(alice, bob, charles)
)
List(
  Person(
    name: "Alice",
    age: 30,
  ),
  Person(
    name: "Bob",
    age: 50 -> 25,
  ),
  Person(
    name: "Charles",
    age: 80,
  ),
)

Pair by field

In many test scenarios we actually don’t care about order of elements, as long as the two sequences contains the same elements. One example of this is inserting multiple records into a database and then retrieving them , where you expect the same records to be returned by not necessarily in the original order.

In this case, you can configure a Differ to pair by a field instead.

val differByName = Differ[List[Person]].pairBy(_.name)

differByName.diff(
  List(bob50, charles, alice),
  List(alice, bob, charles)
)

When we match by a person’s name instead of index, we can now easily spot that Bob has the wrong age.

List(
  Person(
    name: "Bob",
    age: 50 -> 25,
  ),
  Person(
    name: "Charles",
    age: 80,
  ),
  Person(
    name: "Alice",
    age: 30,
  ),
)

Map differ

Map differ pair entries with the same keys and compare the values. Missing key-values will also be reported in the result.

It requires

  • a ValueDiffer instance for the map key type (for display purposes)
  • a Differ instance for the map value type
Differ[Map[String, Person]].diff(
  Map(
    "a" -> alice,
    "b" -> bob
  ),
  Map(
    "b" -> bob50,
    "c" -> charles
  ),
)
Map(
  "a" -> Person(
      name: "Alice",
      age: 30,
    ),
  "b" -> Person(
      name: "Bob",
      age: 25 -> 50,
    ),
  "c" -> Person(
      name: "Charles",
      age: 80,
    ),
)

Set differ

Set differ can diff two Sets by pairing the set elements and diffing them. By default, the pairing is based on matching elements that are equal to each other (using equals).

However, you most likely want to pair elements using a field on an element instead for better diffs reports (See next section).

Pair by field

For the best error reporting, you want to configure SetDiffer to pair by a field.

val differByName: Differ[Set[Person]] = Differ[Set[Person]].pairBy(_.name)

differByName.diff(
  Set(bob50, charles, alice),
  Set(alice, bob, charles)
)
Set(
  Person(
    name: "Bob",
    age: 50 -> 25,
  ),
  Person(
    name: "Charles",
    age: 80,
  ),
  Person(
    name: "Alice",
    age: 30,
  ),
)

Always ignored Differ

Sometimes for certain types you can’t really compare them (e.g. Something that’s not a plain data structure).

In that case you can use Differ.alwaysIgnore

class CantCompare()

val alwaysIgnoredDiffer: Differ[CantCompare] = Differ.alwaysIgnore[CantCompare]