Search code examples
scalatypeclassscalatestspecs2

How to unit-test different typeclass instances for Numeric?


Let's say I have two instances of the typeclass Numeric.

class Money(c: String, x: Long, y: Int)
class Quantity(c: String, x: Long, y: Int)
implicit val numericMoney: Numeric[Money] = new Numeric[Money]
implicit val numericQuantity: Numeric[Quantity] = new Numeric[Quantity]

Money and Quantity should behave the same in the Numeric instance. I have scalaTest tests which check that Money behaves correctly.

e.g.

import implicits.NumericMoney.numericMoney._

class MoneyOpsSpec extends WordSpec with Matchers {

  val max = Money("", Long.MaxValue, 999999999)
  val min = Money("", Long.MinValue, -999999999)

  "A Money object" when {
    "zero" should {
      "be neutral element under addition" in {
        zero + Money("", 15, 50) should ===(Money("", 15, 50))
        Money("", 15, 50) + zero should ===(Money("", 15, 50))
      }
      "be neutral element under subtraction" in {
        zero - Money("", 15, 50) should ===(Money("", -15, -50))
        Money("", 15, 50) - zero should ===(Money("", 15, 50))
      }
      "be invariant under negation" in {
        -zero should ===(zero)
      }
    }
  }
}

Quantity spec should be executed in the same way. Can I implement a generic spec and use Money and Quantity as an input for that spec? Or do scalaTest or specs2 have someting to make sure that a Numeric typeclass instance behaves correctly? I can switch testing frameworks easily.


Solution

  • Can I implement a generic spec and use Money and Quantity as an input for that spec?

    Sure. Just take the implicit as a constructor argument. Not tested, but should be approximately (with minimal changes):

    abstract class NumOpsSpec[T](implicit num: Numeric[T], tag: ClassTag[T]) extends WordSpec with Matchers {
      import num._
    
      val max: T
      val min: T
      val someElement: T
    
      s"A ${tag.runtimeClass.simpleName} object" when {
        "zero" should {
          "be neutral element under addition" in {
            zero + someElement should ===(someElement)
            someElement + zero should ===(someElement)
          }
          "be neutral element under subtraction" in {
            zero - someElement should ===(- someElement)
            someElement - zero should ===(someElement)
          }
          "be invariant under negation" in {
            -zero should ===(zero)
          }
        }
      }
    }
    
    class MoneyOpsSpec extends NumOpsSpec[Money] {
      override val max = Money("", Long.MaxValue, 999999999)
      override val min = Money("", Long.MinValue, -999999999)
      override val someElement = Money("", 15, 50)
    }
    
    class QuantityOpsSpec extends NumOpsSpec[Quantity] {
      override val max = ???
      override val min = ???
      override val someElement = ???
    }
    

    You could also look into https://github.com/typelevel/discipline for testing typeclass laws in general.