Type safe checking of arbitary properties (custom class) for without boilerplate code

Hello.

according to
https://www.scalatest.org/user_guide/using_matchers#checkingArbitraryProperties one should use HavePropertyMatcher for type safe property testing.

e.g.:
book should have (
title (“Programming in Scala”),
author (List(“Odersky”, “Spoon”, “Venners”)),
pubYear (2008)
)

For that, however, I need to write boiler plate code. For every property (title, author, …) of my class book, I’d need to implement a corresponding matcher method.

Isn’t there any other way to do the same in a more generic way? Or at least, a way to generate that boilerplate code?

The HavePropertyMatcherGenerator doesn’t solve that problem, since it isn’t type safe.

Thanks in advance for any hint.

Kuno

Depending on your degree of eagerness to use this construct…

  1. Only pull it in where the frequency of usage justifies the boilerplate overhead.
  2. Write some helpers to reduce boilerplate.
  3. Consider implementing some macro on top (in contrast to the reflection-based approach used by HavePropertyMatcherGenerator)

Naive suggestion for 2:

def mkPropEqMatcher[T, V](
    name: String, extr: T => V)(
    expVal: V
): HavePropertyMatcher[T, V] =
  (t: T) => 
    HavePropertyMatchResult(extr(t) == expVal, name, expVal, extr(t))

val title = mkPropEqMatcher("title", (_: Book).title) _
val author = mkPropEqMatcher("author", (_: Book).author) _

Hello Patrick,

thanks for your reply. It isn’t exactly what was looking for, however, in your suggestion you brake down the boilerplate code to the minimum. I’ve tried to compile it, but got errors. Here I have a fixed version of your suggestion:

 def mkPropEqMatcher[T,V](name:String, extr: T => V)(expVal : V): HavePropertyMatcher[T,V] =
     new HavePropertyMatcher[T, V] {
       def apply(t: T): HavePropertyMatchResult[V] =
         HavePropertyMatchResult(
           extr(t) == expVal,
           name,
           expVal,
           extr(t)
         )
     }
 
   val title = mkPropEqMatcher("title", (_ : Book).title) _
   val author = mkPropEqMatcher("author", (_ : Book).author) _  

For macro implementation I need to investigate more time. Would I have any advantage comparing to your suggestion?

Worked fine for me (with Scala 2.13.2 and Scalatest 3.2.0, IIRC). AFAICS you have only switched the lambda syntax for a single-method trait (which should be supported since Scala 2.12) for a full-fledged anonymous trait implementation.

I haven’t even thought about what such a macro API might look like, it just felt like a natural path to investigate if you’re not happy with a reflection-based solution. I’ve also never used the “have” matcher approach myself - usually I just compare full case class instances or single members for equality or, rarely, write full-fledged custom matchers. So, it’s a very subjective question and I don’t know, but I’d suspect that for me personally it’d be more work than it’s worth.

Hallo Patrick,
thanks again for your quick response.

Worked fine for me (with Scala 2.13.2 and Scalatest 3.2.0, IIRC). AFAICS you have only switched the lambda syntax for a single-method trait (which should be supported since Scala 2.12) for a full-fledged anonymous trait implementation.

I’m still on Scala 2.11.x due to dependencies to Apache Spark. That’s may explain the compilation error.

I’ve also never used the “have” matcher approach myself - usually I just compare full case class instances or single members for equality or, rarely, write full-fledged custom matchers. So, it’s a very subjective question and I don’t know, but I’d suspect that for me personally it’d be more work than it’s worth.

Yes, it’s a subjective question, however, we work on wide bigdata tables typed checked with case classes. And I think it’s rather a common use case and the large case classes lead to significantly increase of the test setup and decrease readablility. Therefore, we don’t compare the full case class each time. It’s a trait off.

Greets
Kuno

I haven’t been using Spark for quite some time, but - really…?! :face_with_raised_eyebrow:

Sure… The question just is how to tackle this.

Since I brought up the macro approach, I’ve tinkered a bit with it, and I’m not that convinced. The remaining issues with the mkPropEqMatcher convenience method approach is that the type inference requires guidance (i.e. (_: Book)) and the property name must be supplied. I haven’t found a nice way around the first issue in general, so that only leaves room for improvement regarding the property name.

I have come up with a naive, quickhack haveEq() macro that extracts the property name for the common case of chained accessors, just matching on Select instances in the tree and its qualifiers, recursively. (Disclaimer: I’m not fluent with macros at all.)

book should have(
  haveEq((_: Book).title)("Moby Dick"),
  haveEq((_: Book).author.firstName)("Herman")
)

This yields ‘The author.firstName property had value “Hermann”, instead of its expected value “Herman”, […]’. However, a property match like haveEq((_: Book).toString)("") would give you ‘The <unknown> property had value […]’:confused: Not sure that’s worth the effort and a huge improvement over

book should have(
  mkPropEqMatcher("title", (_: Book).title)("Moby Dick"),
  mkPropEqMatcher("author first name", (_: Book).author.firstName)("Herman")
)

Going back to square one, what’s the actual issue you have with

book.title should === ("Moby Dick")
book.author.firstName should === ("Herman")

or

(book.title, book.author.firstName) should === ("Moby Dick", "Herman")

…?

You mention type safety, so I assume that you’re referring to the leniency of universal equality via #equals(), as inherited from Java, as in book.title should === (21). But that’s an issue that probably affects your whole code base, not just the tests. By buying into the cats ecosystem, for example, and relying on Eq rather than #equals(), you could make the above variants type safe by pulling in StrictCatsEquality.

(There’s other libraries providing similar abstractions, and there’s other approaches to type safe equality comparisons, e.g. the SuperSafe plugin.)

To sum up, I’m not sure that Scalatest’s have construct is an ideal approach to type safe (equality) assertions - even more, if one feels seduced to bikeshed it further. It may have its place, but I think its applicability is somewhat limited. (And I’ve learned some new stuff when playing with this question, so it still feels like well-spent time.)

Hello Patrick,

thank you very much for your help. Me too, I’ve learned many things I can think about now :wink:

Greets
Kuno