You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何优化Scalatest中case类集合对比的错误输出格式?

问题:Scalatest中Case类集合对比的错误输出优化

当使用Scalatest测试框架对比case类集合时,默认错误输出可读性极差——所有元素挤在一行,大型case类的输出更是混乱,必须手动格式化才能定位差异。

示例代码

case class Test(a: String, b: String, c: String)

"lists" should "match" in {
  val l1 = List(
    Test("a", "b", "c"),
    Test("d", "e", "f"),
    Test("g", "h", "i")
  )

  val l2 = List(
    Test("a", "b", "c"),
    Test("d", "E", "f"),
    Test("g", "h", "i")
  )

  l1 should contain theSameElementsAs l2
}

默认错误输出

lists
- should match *** FAILED ***
  List(Test("a", "b", "c"), Test("d", "e", "f"), Test("g", "h", "i")) did not contain the same elements as List(Test("a", "b", "c"), Test("d", "E", "f"), Test("g", "h", "i")) (Test.scala:52)

期望输出格式

lists
- should match *** FAILED ***
  List(
    Test("a", "b", "c"),
    Test("d", "e", "f"),
    Test("g", "h", "i")
  )
  did not contain the same elements as
  List(
    Test("a", "b", "c"),
    Test("d", "E", "f"),
    Test("g", "h", "i")
  )
  (Test.scala:52)

理想情况下还能直接标注出差异字段。


解决方案(无需更换测试框架)

完全可以通过Scalatest的自定义匹配器、内置格式化工具或Diffing功能实现需求,以下是具体方案:

1. 自定义匹配器(推荐:实现格式化+差异标注)

手动实现匹配器,重写错误消息的生成逻辑,对集合进行换行格式化,并直接标注差异元素:

import org.scalatest.matchers.{MatchResult, Matcher}
import org.scalatest.wordspec.AnyWordSpec

case class Test(a: String, b: String, c: String)

class FormattedListMatchSpec extends AnyWordSpec {
  // 自定义匹配器:格式化输出集合+标注差异
  def containTheSameElementsAsFormatted(expected: List[Test]): Matcher[List[Test]] = 
    new Matcher[List[Test]] {
      override def apply(actual: List[Test]): MatchResult = {
        val actualSet = actual.toSet
        val expectedSet = expected.toSet
        val missingElements = expectedSet -- actualSet
        val extraElements = actualSet -- expectedSet

        // 格式化集合输出
        def formatList(list: List[Test]): String = 
          list.map(_.toString).mkString("List(\n  ", ",\n  ", "\n)")

        val formattedActual = formatList(actual)
        val formattedExpected = formatList(expected)

        // 构建错误消息
        val failureMsg = 
          if (missingElements.nonEmpty || extraElements.nonEmpty) {
            s"""$formattedActual
               |did not contain the same elements as
               |$formattedExpected
               |
               |差异详情:
               |- 缺失元素:${missingElements.mkString(", ")}
               |- 多余元素:${extraElements.mkString(", ")}""".stripMargin
          } else {
            s"$formattedActual did contain the same elements as $formattedExpected"
          }

        MatchResult(
          actualSet == expectedSet,
          failureMsg,
          failureMsg.replace("did not contain", "did contain")
        )
      }
    }

  "lists" should "match with formatted output" in {
    val l1 = List(
      Test("a", "b", "c"),
      Test("d", "e", "f"),
      Test("g", "h", "i")
    )

    val l2 = List(
      Test("a", "b", "c"),
      Test("d", "E", "f"),
      Test("g", "h", "i")
    )

    l1 should containTheSameElementsAsFormatted(l2)
  }
}

2. 利用Scalatest内置的PrettyPrinting

混入PrettyPrinting特质,使用pretty方法自动格式化对象输出,简化自定义匹配器的格式化逻辑:

import org.scalatest.PrettyPrinting
import org.scalatest.matchers.{MatchResult, Matcher}
import org.scalatest.wordspec.AnyWordSpec

case class Test(a: String, b: String, c: String)

class PrettyPrintMatchSpec extends AnyWordSpec with PrettyPrinting {
  def containTheSameElementsAsPretty(expected: List[Test]): Matcher[List[Test]] = 
    new Matcher[List[Test]] {
      override def apply(actual: List[Test]): MatchResult = {
        val actualSet = actual.toSet
        val expectedSet = expected.toSet
        val isMatch = actualSet == expectedSet

        val failureMsg = s"""${pretty(actual)}
                           |did not contain the same elements as
                           |${pretty(expected)}""".stripMargin

        MatchResult(isMatch, failureMsg, failureMsg.replace("did not contain", "did contain"))
      }
    }

  // 测试用例同上...
}

3. 使用Scalatest的Diffing功能生成结构化差异

Scalatest的Diffing模块可以生成更精细的字段级差异对比,适合复杂case类:

import org.scalatest.diff.Diffing
import org.scalatest.matchers.{MatchResult, Matcher}
import org.scalatest.wordspec.AnyWordSpec

case class Test(a: String, b: String, c: String)

class DiffingMatchSpec extends AnyWordSpec {
  def containTheSameElementsAsWithDiff(expected: List[Test]): Matcher[List[Test]] = 
    new Matcher[List[Test]] {
      override def apply(actual: List[Test]): MatchResult = {
        val actualSet = actual.toSet
        val expectedSet = expected.toSet
        val isMatch = actualSet == expectedSet

        val diffMsg = if (!isMatch) {
          val diff = Diffing.diff(actual.toSeq.sorted, expected.toSeq.sorted)
          s"""集合不匹配:
             |实际值:${actual.mkString("[", ", ", "]")}
             |期望值:${expected.mkString("[", ", ", "]")}
             |差异:
             |${diff}""".stripMargin
        } else {
          "集合元素完全匹配"
        }

        MatchResult(isMatch, diffMsg, diffMsg)
      }
    }

  // 测试用例同上...
}

内容的提问来源于stack exchange,提问作者fungtional

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 13:45:37