如何使用AssertJ展示不匹配字符串的具体字符差异?
问题
我在为Kotlin的CSV生成函数编写单元测试时遇到排查瓶颈:测试断言失败,但AssertJ默认输出的预期与实际字符串看起来完全一致。我怀疑是字符编码或不可见字符(如换行符、空格)导致的问题,但无法定位具体差异点。请问如何让测试输出显示字符串中具体不匹配的字符?
相关测试代码及错误输出如下:
internal fun List<Contact>.buildCsv(dp: INT293DependencyProvider):File { val file = File("/tmp/${dp.outputFileName}.csv") file.bufferedWriter(Charsets.ISO_8859_1).use { out -> out.write(RaveOutput.HEADER) this.forEach { out.write(it.toCsvRow()) } } return file } @Test fun `Test generate csv from contacts`() { mockContext(300).value().let { context -> testInt293DependencyProvider(context).let { dp -> javaClass.classLoader.getResourceAsStream("rave-contacts.csv").use { stream -> val expected = IOUtils.toString(stream, Charsets.ISO_8859_1) val contacts = listOf( Contact("123456", "foo", "bar", "foo@bar.com", "5704445555", Type.EMPLOYEE, "fbar"), Contact("654321", "john", "doe", "jdoe@bucknell.edu", "+15556667777", Type.STUDENT, "jdoe") ) val output = contacts.buildCsv(dp) val actual = IOUtils.toString(output.inputStream(), Charsets.ISO_8859_1) assertNotNull(actual) assertThat(actual).isEqualTo(expected) } } } }
错误输出:
[ERROR] Test.Test generate csv from contacts expected: "Unique Loader ID,Last Name,First Name,Registration Email Address,Email Address 2,Email Address 3,Site Username,Role,Mobile Phone 1,Mobile Phone 2,Mobile Phone 3,Mobile 1 Voice,Mobile 2 Voice,Mobile 3 Voice,Mobile Carrier 1,Mobile Carrier 2,Mobile Carrier 3,Landline Phone 1,Landline 1 Extension,Landline Phone 2,Landline 2 Extension,Landline Phone 3,Landline 3 Extension,Language Preference,Notify by SMS,Notify by Email,String Attribute 1,String Attribute 2,String Attribute 3,String Attribute 4,String Attribute 5,String Attribute 6,String Attribute 7,String Attribute 8,String Attribute 9,String Attribute 10,String Attribute 11,String Attribute 12,String Attribute 13,String Attribute 14,String Attribute 15,String Attribute 16,String Attribute 17,String Attribute 18,String Attribute 19,String Attribute 20,Integer Attribute 1,Integer Attribute 2,Integer Attribute 3,Integer Attribute 4,Integer Attribute 5,True/False Attribute 1,True/False Attribute 2,True/False Attribute 3,True/False Attribute 4,True/False Attribute 5,Date Attribute 1,Date Attribute 2,Date Attribute 3,Date Attribute 4,Date Attribute 5 123456,bar,foo,foo@bar.com,,,fbar,EMPLOYEE,5704445555,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 654321,doe,john,jdoe@bucknell.edu,,,jdoe,STUDENT,+15556667777,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,," but was: "Unique Loader ID,Last Name,First Name,Registration Email Address,Email Address 2,Email Address 3,Site Username,Role,Mobile Phone 1,Mobile Phone 2,Mobile Phone 3,Mobile 1 Voice,Mobile 2 Voice,Mobile 3 Voice,Mobile Carrier 1,Mobile Carrier 2,Mobile Carrier 3,Landline Phone 1,Landline 1 Extension,Landline Phone 2,Landline 2 Extension,Landline Phone 3,Landline 3 Extension,Language Preference,Notify by SMS,Notify by Email,String Attribute 1,String Attribute 2,String Attribute 3,String Attribute 4,String Attribute 5,String Attribute 6,String Attribute 7,String Attribute 8,String Attribute 9,String Attribute 10,String Attribute 11,String Attribute 12,String Attribute 13,String Attribute 14,String Attribute 15,String Attribute 16,String Attribute 17,String Attribute 18,String Attribute 19,String Attribute 20,Integer Attribute 1,Integer Attribute 2,Integer Attribute 3,Integer Attribute 4,Integer Attribute 5,True/False Attribute 1,True/False Attribute 2,True/False Attribute 3,True/False Attribute 4,True/False Attribute 5,Date Attribute 1,Date Attribute 2,Date Attribute 3,Date Attribute 4,Date Attribute 5 123456,bar,foo,foo@bar.com,,,fbar,EMPLOYEE,5704445555,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,, 654321,doe,john,jdoe@bucknell.edu,,,jdoe,STUDENT,+15556667777,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,"
解决方案
方法1:自定义AssertJ字符串表示,显示字符细节
使用AssertJ的withRepresentation方法,将字符串转换为包含字符编码、控制字符名称的格式,直接暴露不可见差异:
assertThat(actual) .withRepresentation(ToStringRepresentation { s -> s.map { c -> when (c) { '\n' -> "\\n (0x0A)" '\r' -> "\\r (0x0D)" '\t' -> "\\t (0x09)" else -> if (c.code < 32 || c.code > 126) { String.format("0x%02X", c.code) } else { c.toString() } } }.joinToString("") }) .isEqualTo(expected)
方法2:手动遍历对比,定位差异位置
编写辅助函数遍历两个字符串,找到第一个不匹配的位置并输出字符编码信息:
fun findStringDifference(expected: String, actual: String) { val minLength = minOf(expected.length, actual.length) for (i in 0 until minLength) { if (expected[i] != actual[i]) { println("差异索引:$i") println("预期字符:'${expected[i]}' (0x${expected[i].code.toString(16).padStart(2, '0')})") println("实际字符:'${actual[i]}' (0x${actual[i].code.toString(16).padStart(2, '0')})") return } } if (expected.length != actual.length) { println("字符串长度不一致:预期${expected.length},实际${actual.length}") println("额外字符:${if (expected.length > actual.length) expected.substring(minLength) else actual.substring(minLength)}") } else { println("无可见字符差异,但字符串不相等(可能为编码问题)") } } // 测试中调用 findStringDifference(expected, actual) assertThat(actual).isEqualTo(expected)
方法3:快速排查换行符差异
换行符(Windows的\r\n vs Linux的\n)是这类问题的常见诱因,可直接使用AssertJ的归一化方法:
assertThat(actual).isEqualToNormalizingNewlines(expected)
或打印每行的长度,间接验证换行符类型:
println("预期行信息:${expected.lines().joinToString("|") { "$it [长度:${it.length}]" }}") println("实际行信息:${actual.lines().joinToString("|") { "$it [长度:${it.length}]" }}")
方法4:检查字符串长度与编码一致性
先打印两个字符串的长度,若长度不一致,说明存在隐藏字符:
println("预期字符串长度:${expected.length},实际字符串长度:${actual.length}")
同时确认输入文件rave-contacts.csv的实际编码确实为ISO-8859-1,避免读取时的编码转换误差。
内容的提问来源于stack exchange,提问作者pbuchheit
相关产品推荐
相关产品推荐

