ScalaTest deep comparison best practices
Asked Answered
H

2

5

I'm trying to write a unit test for a function that returns a tuple of a class that contains an array.

A simple assert(out === expectedOut) or out should be(expectedOut) does not compare the contents of the classes on the LHS and RHS because of the Array. Is there a neat way to do that in ScalaTest?

I've looked at custom matchers but I'm not sure if that's the best way for what I'm trying to do. So any info from experience of the experts would be much appreciated.

Edit: Here is a case where that does not seem to be the case:

object Utils {
  case class Product(id: Int, prices: Array[Int])


  def getProductInfo(id: Int, prices: Array[Int]): Option[Product] = {
    val sortedPrices = prices.sortWith(_ < _)
    Some(Product(id, sortedPrices))
  }
}

---

import org.scalatest._
import Utils._

class DataProcessorSpec extends FlatSpec with Matchers with OptionValues {
  val id = 12345
  val priceList = Array(10,20,30)

  val prod = Utils.getProductInfo(id, priceList)
  val expectedProd = Some(Utils.Product(id, priceList))

  "A DataProcessorSpec" should "return the correct product information" in {
     prod should be(expectedProd)
  }
}

The test fails because the sortWith causes a new array to be created and is thus pointing to a different memory location, as far as I can tell.

Hortensehortensia answered 6/4, 2016 at 11:24 Comment(1)
Can you please provide a snippet of code? You could use matchers as Tzach pointed but I'm not entirely sure what exactly you're trying to assert here.Transmission
G
7

UPDATE: with the code example, this is clearer:

Comparison fails because shoud be uses the case class's equals function to perform the comparison, and case classes don't compare arrays "deeply" - which means, as you suspected, that different instances will not be equal (see more info here).

Workarounds:

  1. Verify equality of each "part" of the case class separately:

    prod.get.prices should be(expectedProd.get.prices)
    prod.get.id should be(expectedProd.get.id)
    
  2. If using Array is not a must, you can change the case class to use Seq[Int], which would make the test pass, because a Seq's implementation of equals is "deep"

Comparing Arrays:

When compared "on their own", arrays are compared as expected ("deeply") by Matchers' should be:

arr1 should be(arr2) // true if contents is the same

If you just want to compare the contents, without verifying that out is indeed an Array, you can use theSameElementsInOrderAs:

arr1 should contain theSameElementsInOrderAs arr2
Grovergroves answered 6/4, 2016 at 11:31 Comment(3)
Cool, thanks! Is there a way to have it also work on Options?Hortensehortensia
For Options you can simple use out should be(expectedOut), the comparison is "deep". Actually, that would work for arrays too (!)Grovergroves
Unless I'm missing something that doesn't work (see edited code sample above).Hortensehortensia
C
0

As Tzach Zohar said in his answer, should be uses the case class's equals that doesn't compare arrays "deeply".

You can create a function that will deeply compare any 2 case classes (and any other Product lawful inheritors):

def deepEquals(x: Product, y: Product): Unit = {
  if (x.getClass == y.getClass) {
    x.productIterator.zip(y.productIterator)
      .foreach {
        case (xElem: Product, yElem: Product) => deepEquals(xElem, yElem)
        case (xElem, yElem) => xElem should be(yElem)
      }
  } else {
    x should be(y) // Will fail, with an appropriate error
  }
}

Caveat: This function uses .getClass to check if the 2 values are of the same type, but due to type erasure, type parameters are not checked, so in some cases type-parametered values might be determined equal even if they aren't of the same type.

Comras answered 28/1, 2021 at 10:46 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.