XUnit测试中模拟IDataReader返回多条记录时遇NullReferenceException的解决方法
我仔细看了你的测试代码和业务逻辑,发现NullReferenceException的根源很明确:你的Repository代码里用了IDataReader的字符串索引器(比如userReader["ID"]),但你完全没给这个索引器做Moq模拟。Moq的Mock<IDataReader>默认不会实现索引器的逻辑,所以代码读取userReader["ID"]时返回的是null,后续强制转换(int)null直接触发了空引用异常。
下面是一步步的修复方案,结合你的代码做具体调整:
1. 核心修复:模拟IDataReader的索引器行为
你需要在SetupAndGetMockDataReader方法中,添加对字符串索引器和整数索引器的模拟,让它们能根据当前行动态返回对应字段值。同时因为要逐行切换,我们可以直接在Read()方法的Setup里管理当前行的索引,不再需要原来的SetupNextRow方法。
修改后的SetupAndGetMockDataReader代码如下:
private Mock<IDataReader> SetupAndGetMockDataReader(List<EShopUser> eShopUsers) { var mockreader = new Mock<IDataReader>(); DataTable table = new DataTable(); table.Columns.Add(new DataColumn("ID", typeof(Int32))); table.Columns.Add(new DataColumn("UserName", typeof(string))); table.Columns.Add(new DataColumn("FirstName", typeof(string))); table.Columns.Add(new DataColumn("LastName", typeof(string))); foreach (EShopUser item in eShopUsers) { DataRow row = table.NewRow(); row["ID"] = item.ID; row["UserName"] = item.UserName; row["FirstName"] = item.FirstName; row["LastName"] = item.LastName; table.Rows.Add(row); } // 跟踪当前读取的行索引 int currentRowIndex = -1; // 模拟Read()方法:移动到下一行,返回是否还有数据 mockreader.Setup(r => r.Read()) .Returns(() => { currentRowIndex++; return currentRowIndex < table.Rows.Count; }); // 模拟字符串索引器:userReader["ColumnName"] mockreader.Setup(r => r[It.IsAny<string>()]) .Returns((string columnName) => { if (currentRowIndex < 0 || currentRowIndex >= table.Rows.Count) throw new InvalidOperationException("当前没有可读取的行"); int ordinal = table.Columns[columnName].Ordinal; return table.Rows[currentRowIndex][ordinal]; }); // 模拟整数索引器:userReader[ordinal](可选,防止后续代码用到) mockreader.Setup(r => r[It.IsAny<int>()]) .Returns((int ordinal) => { if (currentRowIndex < 0 || currentRowIndex >= table.Rows.Count) throw new InvalidOperationException("当前没有可读取的行"); return table.Rows[currentRowIndex][ordinal]; }); // 保留原有的GetOrdinal设置(如果后续业务代码用到的话) for (int i = 0; i < table.Columns.Count; i++) { var name = table.Columns[i].ColumnName; mockreader.Setup(r => r.GetOrdinal(It.Is<string>(n => n == name))).Returns(i); } return mockreader; }
2. 清理冗余代码
现在我们通过索引器动态返回字段值,原来的SetupNextRow方法已经没用了,可以直接删除它。
3. 确保IDbCommand的属性可被赋值
你的Repository代码里会设置cmd.CommandType和cmd.CommandText,需要给Moq的Command对象添加属性允许设置的配置,否则会触发未Setup的异常:
Mock<IDbCommand> command = new Mock<IDbCommand>(); // 允许设置CommandType和CommandText属性 command.SetupProperty(c => c.CommandType); command.SetupProperty(c => c.CommandText);
4. 可选优化:修复Repository的伪异步代码
你的GetAllUsersAsync用Task.Run包裹同步数据库操作,属于“伪异步”写法,会浪费线程资源。可以简化为真正的同步转异步(如果用原生IDbConnection的话):
public Task<object> GetAllUsersAsync() { List<EShopUser> users = new List<EShopUser>(); using (_connection) { _connection.Open(); IDbCommand cmd = _connection.CreateCommand(); cmd.CommandType = System.Data.CommandType.StoredProcedure; cmd.CommandText = "GetAllUser"; IDataReader userReader = cmd.ExecuteReader(); while (userReader.Read()) { users.Add(new EShopUser( (int)userReader["ID"], (string)userReader["UserName"], (string)userReader["FirstName"], (string)userReader["LastName"])); } } return Task.FromResult<object>(users); }
现在运行你的XUnit测试,应该能正常通过了:userReader["ID"]等索引器会正确返回当前行的字段值,不再返回null,也就不会触发空引用异常。
内容来源于stack exchange

