Scalatest custom matchers for 'should contain'
Asked Answered
A

2

6

This is a situation I have encountered frequently, but I have not been able to find a solution yet.

Suppose you have a list of persons and you just want to verify the person names. This works:

persons.map(_.name) should contain theSameElementsAs(List("A","B"))

Instead, I would rather write this like

val toName: Person => String = _.name
persons should contain theSameElementsAs(List("A","B")) (after mapping toName)

because this is how you would say this.

Sometimes however, you'd like to use a custom matcher which matches more than just one property of the object. How would it be possible to use

persons should contain(..)

syntax, but somehow be able to use a custom matcher?

Both these situations I could easily solve using JUnit or TestNG using Hamcrest matchers, but I have not found a way to do this with ScalaTest.

I have tried to use the 'after being' syntax from the Explicitly trait, but that's not possible since this takes a 'Normalization' which defines that the 'normalized' method uses the same type for the argument and return type. So it's not possible to change a Person to a String. Also I have not succeeded yet in implementing an 'Explicitly' like trait because it does not like the Equality[.] type I return and/or it does not know anymore what the original list type was, so using '_.name' does not compile.

Any suggestions are welcome.

Ainsley answered 14/1, 2016 at 13:17 Comment(0)
T
7

You can manage something similar via the word decided and moderate abuse of the Equality trait. This is because the Equality trait's areEqual method takes a parameter of the generic type and one of type Any, so you can use that to compare Person with String, and decided by simply takes an Equality object which means you don't have to futz around with Normality.

import org.scalactic.Equality
import org.scalatest.{FreeSpec, Matchers}

final class Test extends FreeSpec with Matchers {

  case class Person(name: String)

  val people = List(Person("Alice"), Person("Eve"))

  val namesBeingEqual = MappingEquality[Person, String](p => p.name)

  "test should pass" in {
    (people should contain theSameElementsAs List("Alice", "Eve"))(
      decided by namesBeingEqual)
  }

  "test should fail" in {
    (people should contain theSameElementsAs List("Alice", "Bob"))(
      decided by namesBeingEqual)
  }

  case class MappingEquality[S, T](map: S => T) extends Equality[S] {

    override def areEqual(s: S, b: Any): Boolean = b match {
      case t: T => map(s) == t
      case _    => false
    }
  }
}

I'm not sure I'd say this is a good idea since it doesn't exactly behave in the way one would expect anything called Equality to behave, but it works.

You can even get the beingMapped syntax you suggest by adding it to after via implicit conversion:

  implicit class AfterExtensions(aft: TheAfterWord) {
    def beingMapped[S, T](map: S => T): Equality[S] = MappingEquality(map)
    }
  }

I did try getting it work with after via the Uniformity trait, which has similar methods involving Any, but ran into problems because the normalization is the wrong way around: I can create a Uniformity[String] object from your example, but not a Uniformity[Person] one. (The reason is that there's a normalized method returning the generic type which is used to construct the Equality object, meaning that in order to compare strings with strings the left-side input must be a string.) This means that the only way to write it is with the expected vs actual values in the opposite order from normally:

"test should succeed" in {
    val mappedToName = MappingUniformity[Person, String](person => person.name)
    (List("Alice", "Eve") should contain theSameElementsAs people)(
      after being mappedToName)
  }

  case class MappingUniformity[S, T](map: S => T) extends Uniformity[T] {

    override def normalizedOrSame(b: Any): Any = b match {
      case s: S => map(s)
      case t: T => t
    }

    override def normalizedCanHandle(b: Any): Boolean =
      b.isInstanceOf[S] || b.isInstanceOf[T]

    override def normalized(s: T): T = s
  }

Definitely not how you'd usually want to write this.

Twofaced answered 30/4, 2018 at 14:48 Comment(0)
B
0

use inspectors

 forAll (xs) { x => x should be < 3 }
Berniecebernier answered 2/11, 2021 at 17:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.