使用MSTest编写C#单元测试:是否需验证SQL及参数?
我正在用MSTest写C#单元测试,目标是确保GetMemberName函数能正常获取数据。待测试的函数代码如下:
public static GetMemberName(string member_id) { DataTable dt = new DataTable(); string memberName; String sql = @"SELECT name from MEMBER_GROUP where Member_Id = @Member_Id"; dt = SqlProvider.dbGetDataTable("MY_Table", sql, SqlParameterBuilder.AddParameter("Member_Id ", member_id)); if ((dt != null) && dt.Rows.Count > 0) { for (int i = 0; i <= dt.Rows.Count - 1; i++) { memberName = dt.Rows[i][0].ToString(); } } return memberName; }
注:当member_id为“123”时,SqlParameterBuilder.AddParameter("Member_Id ", member_id)会在构建器中添加参数,使SQL语句等效于@"SELECT name from MEMBER_GROUP where Member_Id = '123'"。
我的困惑是:我是否需要验证sql变量是否等于@"SELECT name from MEMBER_GROUP where Member_Id = @Member_Id",以及验证SqlParameterBuilder.AddParameter("Member_Id ", member_id)是否正常工作(也就是验证dbGetDataTable函数的参数),还是只需伪造dbGetDataTable的返回值即可?如果需要验证,该怎么操作?
核心原则:聚焦单元测试的目标
单元测试的核心是验证当前函数的逻辑正确性,而非依赖组件的内部实现,所以分两种场景处理:
1. 优先选择:伪造dbGetDataTable的返回值
如果你的测试目标只是确保GetMemberName能根据dbGetDataTable的返回值正确处理并返回成员名称,只需要伪造dbGetDataTable的返回结果即可:
- 当返回非空且有数据的
DataTable时,验证函数返回正确的成员名; - 当返回空表或
null时,验证函数的返回结果(原代码中memberName未初始化,这里其实是个潜在bug,测试时能直接发现); - 当返回多行数据时,验证函数返回最后一行的名称(原代码的循环会覆盖
memberName,这也是可以测试出来的)。
这种方式符合单元测试的“隔离性”要求,不需要关心SqlProvider或SqlParameterBuilder的内部逻辑,只需要确保当前函数对依赖的返回值处理正确。
2. 什么时候需要验证SQL和参数?
如果你的需求是确保GetMemberName确实使用了正确的SQL语句和参数(比如防止有人误改SQL导致逻辑错误),可以做参数验证,但前提是你能对SqlProvider进行Mock:
操作步骤:
- 先将
SqlProvider抽象成接口(比如ISqlProvider),让SqlProvider实现该接口,再通过依赖注入传入GetMemberName所在的类; - 使用MSTest配合Mock框架(比如Moq)创建
ISqlProvider的Mock实例; - 调用
GetMemberName后,验证Mock的dbGetDataTable方法是否被调用,且传入的参数符合预期:- 验证第二个参数(SQL语句)是否等于
@"SELECT name from MEMBER_GROUP where Member_Id = @Member_Id"; - 验证第三个参数(SQL参数)是否包含名称为
Member_Id(注意原代码里参数名末尾有空格)、值为传入member_id的SqlParameter。
- 验证第二个参数(SQL语句)是否等于
示例代码(基于Moq):
// 定义抽象接口 public interface ISqlProvider { DataTable dbGetDataTable(string tableName, string sql, params SqlParameter[] parameters); } // 重构GetMemberName所在类,依赖ISqlProvider public class MemberService { private readonly ISqlProvider _sqlProvider; public MemberService(ISqlProvider sqlProvider) { _sqlProvider = sqlProvider; } public string GetMemberName(string member_id) { DataTable dt = new DataTable(); string memberName = null; // 修复未初始化的问题 String sql = @"SELECT name from MEMBER_GROUP where Member_Id = @Member_Id"; dt = _sqlProvider.dbGetDataTable("MY_Table", sql, SqlParameterBuilder.AddParameter("Member_Id ", member_id)); if ((dt != null) && dt.Rows.Count > 0) { for (int i = 0; i <= dt.Rows.Count - 1; i++) { memberName = dt.Rows[i][0].ToString(); } } return memberName; } } // 单元测试代码 [TestClass] public class MemberServiceTests { [TestMethod] public void GetMemberName_UsesCorrectSqlAndParameters() { // 准备Mock对象 var mockSqlProvider = new Mock<ISqlProvider>(); mockSqlProvider.Setup(p => p.dbGetDataTable(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<SqlParameter[]>())) .Returns(new DataTable()); var service = new MemberService(mockSqlProvider.Object); string testMemberId = "123"; // 执行测试方法 service.GetMemberName(testMemberId); // 验证参数是否符合预期 mockSqlProvider.Verify(p => p.dbGetDataTable( "MY_Table", @"SELECT name from MEMBER_GROUP where Member_Id = @Member_Id", It.Is<SqlParameter[]>(paramsArr => paramsArr.Length == 1 && paramsArr[0].ParameterName == "Member_Id " && paramsArr[0].Value.ToString() == testMemberId ) ), Times.Once); } }
额外提醒
原代码里有两个潜在问题,测试时可以顺便覆盖:
memberName未初始化,如果dt为空或无数据,函数会返回未赋值的字符串,可能引发编译警告或运行时问题;- 循环遍历
dt.Rows时,每次都会覆盖memberName,最终返回的是最后一行的名称,这如果是预期逻辑没问题,否则需要调整。
内容的提问来源于stack exchange,提问作者bobo

