自定义EscapingHtmlConverter单元测试失败,请求排查原因
问题排查:自定义HTML转义JsonConverter单元测试返回Null
问题背景
为实现局部字符串HTML转义(而非全局转义),创建了EscapingHtmlConverter类,并通过[JsonConverter(typeof(EscapingHtmlConverter))]标记模型属性。但编写Xunit单元测试时,执行失败,实际结果为Null,怀疑ReadJson方法参数传递或逻辑存在问题。
问题代码
自定义Converter代码
using Newtonsoft.Json; using System; using System.Collections.Generic; using System.Text; namespace Shared { public class EscapingHtmlConverter : JsonConverter { public EscapingHtmlConverter() { } public override bool CanRead => true; public override bool CanWrite => false; public override bool CanConvert(Type objectType) => objectType == typeof(string); public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.String) { var res = ((string)reader.Value)?.Trim() == string.Empty ? null : ((string)reader.Value)?.Trim(); return res == null ? null : JsonConvert.SerializeObject( res, Formatting.None, new JsonSerializerSettings { StringEscapeHandling = StringEscapeHandling.EscapeHtml }); } return reader.Value; } public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) { throw new NotImplementedException(); } } }
单元测试代码
using Xunit; using Shared; using System; using System.Collections.Generic; using System.Text; using Models; using Newtonsoft.Json; using Newtonsoft.Json.Linq; namespace Shared.Tests { public class EscapingHtmlConverterTests { [Fact()] public void ReadJsonTest() { Model model = new Model() { PropertyToNotBeEscaped = "Something that do not need to be escaped", PropertyToBeEscaped = "Some string <with> [special] 'characters' & more", }; var expectedResult = JsonConvert.SerializeObject(model.PropertyToBeEscaped, Formatting.None, new JsonSerializerSettings { StringEscapeHandling = StringEscapeHandling.EscapeHtml }); JsonReader reader = new JTokenReader(JToken.Parse(JsonConvert.SerializeObject(model.PropertyToBeEscaped))); var serializer = new JsonSerializer(); var converter = new EscapingHtmlConverter(); // just for debugging Assert.NotNull(model.PropertyToBeEscaped); Assert.NotNull(reader); Assert.NotNull(serializer); // Act var result = converter.ReadJson(reader, model.propertyTobeEscaped.GetType(), "Some string <with> [special] 'characters' & more", serializer); Assert.Equal(expectedResult, result?.ToString()); } } }
测试失败信息
Message: Assert.Equal() Failure Expected: "Some string \u003cwith\u003e [special] \u0027characters\u0027 \u0026 more" Actual: (null)
问题分析与修复
1. 核心问题:JsonReader未读取任何Token
创建JTokenReader后,默认处于初始位置,没有读取任何Token,此时reader.TokenType为JsonToken.None,不会进入if (reader.TokenType == JsonToken.String)分支,直接返回reader.Value(即Null)。
2. 次要问题:属性名拼写错误
测试代码中调用model.propertyTobeEscaped.GetType(),但模型属性实际为PropertyToBeEscaped(大小写不匹配),虽然此处不直接导致Null,但属于低级错误。
3. Converter逻辑冗余(不符合需求)
原ReadJson方法中调用JsonConvert.SerializeObject会输出带双引号的转义字符串,这不符合局部转义后作为属性值的需求——实际应该直接返回转义后的字符串内容,而非序列化后的带引号结果。
修复后的代码
修复单元测试
[Fact()] public void ReadJsonTest() { Model model = new Model() { PropertyToNotBeEscaped = "Something that do not need to be escaped", PropertyToBeEscaped = "Some string <with> [special] 'characters' & more", }; // 修正预期:直接获取转义后的字符串(去掉序列化带来的双引号) var expectedResult = JsonConvert.ToString(model.PropertyToBeEscaped, StringEscapeHandling.EscapeHtml).Trim('"'); JsonReader reader = new JTokenReader(JToken.Parse(JsonConvert.SerializeObject(model.PropertyToBeEscaped))); var serializer = new JsonSerializer(); var converter = new EscapingHtmlConverter(); // 关键:移动到第一个有效Token reader.Read(); // Act:修正属性名大小写,existingValue传Null(符合JsonConverter调用逻辑) var result = converter.ReadJson(reader, model.PropertyToBeEscaped.GetType(), null, serializer); Assert.Equal(expectedResult, result?.ToString()); }
修复自定义Converter逻辑
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) { if (reader.TokenType == JsonToken.String) { var rawValue = ((string)reader.Value)?.Trim(); if (string.IsNullOrEmpty(rawValue)) { return null; } // 直接转义HTML特殊字符,返回纯字符串 return JsonConvert.ToString(rawValue, StringEscapeHandling.EscapeHtml).Trim('"'); } return reader.Value; }
关键说明
- 必须调用
reader.Read(),让Reader定位到第一个有效Token,否则无法进入字符串处理分支。 - 避免用
JsonConvert.SerializeObject处理单个字符串,否则会额外添加双引号,不符合属性值的转义需求。 - 保持代码中属性名大小写一致,避免低级拼写错误。
内容的提问来源于stack exchange,提问作者Sasa Jovanovic
相关产品推荐
相关产品推荐

