如何在Visual Studio中对SharePoint自定义Web部件执行NUnit单元测试?
作为SharePoint开发者,我完全理解你想给Web部件加单元测试的需求——毕竟NUnit这类工具能帮我们提前发现问题,不用每次都部署到SharePoint环境里测试。SharePoint Web部件的测试难点在于它依赖SharePoint对象模型(SPWeb、SPList这些),直接跑测试会需要真实的SharePoint环境,所以我们得用**模拟框架(比如Moq)**来隔离这些依赖,结合你熟悉的NUnit来写测试用例。
下面我会结合你的场景,一步步给出可运行的示例:
第一步:改造Web部件代码,提升可测试性
首先,我们需要对原有的Web部件做一点小改造,让它更容易被测试——核心思路是把依赖的SharePoint对象抽成可替换的属性,避免直接硬编码SPContext.Current.Web。比如:
using Microsoft.SharePoint; using System.Web.UI.WebControls; public class MyCustomWebPart : WebPart { // 声明控件(假设你已经在ascx或代码中初始化了这些控件) protected TextBox txtTitle; protected DropDownList ddlCategory; protected GridView gvItems; protected Button btnSave; // 可注入的SPWeb属性,测试时传入模拟对象 private SPWeb _web; public SPWeb Web { get => _web ?? SPContext.Current.Web; set => _web = value; } protected void Page_Load(object sender, EventArgs e) { if (!IsPostBack) { PopulateCategoryDropdown(); BindItemsGridView(); } } // 抽离下拉框填充逻辑,方便单独测试 protected void PopulateCategoryDropdown() { var categoriesList = Web.Lists.TryGetList("Categories"); if (categoriesList == null) return; ddlCategory.DataSource = categoriesList.Items.GetDataTable(); ddlCategory.DataTextField = "Title"; ddlCategory.DataValueField = "ID"; ddlCategory.DataBind(); } // 抽离GridView绑定逻辑 protected void BindItemsGridView() { var itemsList = Web.Lists.TryGetList("Items"); if (itemsList == null) return; var query = new SPQuery { Query = "<OrderBy><FieldRef Name='Title' Ascending='True'/></OrderBy>" }; var items = itemsList.GetItems(query); gvItems.DataSource = items.GetDataTable(); gvItems.DataBind(); } protected void btnSave_Click(object sender, EventArgs e) { var itemsList = Web.Lists.TryGetList("Items"); if (itemsList == null) return; var newItem = itemsList.AddItem(); newItem["Title"] = txtTitle.Text; newItem["Category"] = new SPFieldLookupValue(int.Parse(ddlCategory.SelectedValue)); newItem.Update(); } }
第二步:编写单元测试用例(NUnit + Moq)
首先,你需要安装必要的NuGet包:NUnit、Moq、Microsoft.SharePoint(如果你的项目已经引用了SharePoint SDK,可以忽略)。
接下来是测试类,我们会覆盖Page_Load首次加载、GridView绑定、按钮保存这几个核心场景:
using NUnit.Framework; using Moq; using Microsoft.SharePoint; using System; using System.Web.UI.WebControls; using System.Reflection; [TestFixture] public class MyCustomWebPartTests { private Mock<SPWeb> _mockWeb; private Mock<SPList> _mockCategoriesList; private Mock<SPList> _mockItemsList; private MyCustomWebPart _webPart; private DropDownList _mockDdlCategory; private GridView _mockGvItems; // 每个测试前初始化模拟对象 [SetUp] public void Setup() { // 初始化SharePoint对象的模拟实例 _mockWeb = new Mock<SPWeb>(); _mockCategoriesList = new Mock<SPList>(); _mockItemsList = new Mock<SPList>(); // 设置Web的Lists.TryGetList返回模拟的列表 _mockWeb.Setup(w => w.Lists.TryGetList("Categories")).Returns(_mockCategoriesList.Object); _mockWeb.Setup(w => w.Lists.TryGetList("Items")).Returns(_mockItemsList.Object); // 创建Web部件实例,注入模拟的SPWeb _webPart = new MyCustomWebPart(); _webPart.Web = _mockWeb.Object; // 初始化模拟控件(因为控件是protected,用反射赋值) _mockDdlCategory = new DropDownList(); _mockGvItems = new GridView(); AssignProtectedField(_webPart, "ddlCategory", _mockDdlCategory); AssignProtectedField(_webPart, "gvItems", _mockGvItems); } // 辅助方法:给protected字段赋值 private void AssignProtectedField(object target, string fieldName, object value) { var field = target.GetType().GetField(fieldName, BindingFlags.Instance | BindingFlags.NonPublic); field.SetValue(target, value); } #region Page_Load测试用例 [Test] public void Page_Load_OnFirstLoad_PopulatesCategoryDropdownCorrectly() { // 1. 准备测试数据:模拟Categories列表的项 var mockItems = new Mock<SPListItemCollection>(); var categoryTable = new System.Data.DataTable(); categoryTable.Columns.Add("ID", typeof(int)); categoryTable.Columns.Add("Title", typeof(string)); categoryTable.Rows.Add(1, "文档类"); categoryTable.Rows.Add(2, "图片类"); mockItems.Setup(i => i.GetDataTable()).Returns(categoryTable); _mockCategoriesList.Setup(l => l.Items).Returns(mockItems.Object); // 2. 模拟Page不是回发状态 var mockPage = new Mock<System.Web.UI.Page>(); mockPage.Setup(p => p.IsPostBack).Returns(false); _webPart.Page = mockPage.Object; // 3. 执行Page_Load方法 _webPart.Page_Load(null, EventArgs.Empty); // 4. 断言下拉框已正确绑定数据 Assert.AreEqual(2, _mockDdlCategory.Items.Count); Assert.AreEqual("文档类", _mockDdlCategory.Items[0].Text); Assert.AreEqual("1", _mockDdlCategory.Items[0].Value); } [Test] public void Page_Load_OnFirstLoad_BindsItemsGridView() { // 1. 模拟Items列表的查询结果 var mockQueryItems = new Mock<SPListItemCollection>(); var itemsTable = new System.Data.DataTable(); itemsTable.Columns.Add("Title", typeof(string)); itemsTable.Rows.Add("项目文档1"); itemsTable.Rows.Add("项目图片1"); mockQueryItems.Setup(i => i.GetDataTable()).Returns(itemsTable); _mockItemsList.Setup(l => l.GetItems(It.IsAny<SPQuery>())).Returns(mockQueryItems.Object); // 2. 模拟非回发状态 var mockPage = new Mock<System.Web.UI.Page>(); mockPage.Setup(p => p.IsPostBack).Returns(false); _webPart.Page = mockPage.Object; // 3. 执行Page_Load _webPart.Page_Load(null, EventArgs.Empty); // 4. 断言GridView数据源正确 var dataSource = _mockGvItems.DataSource as System.Data.DataTable; Assert.IsNotNull(dataSource); Assert.AreEqual(2, dataSource.Rows.Count); } #endregion #region 按钮点击测试用例 [Test] public void btnSave_Click_CreatesAndUpdatesNewListItem() { // 1. 模拟输入控件的值 var mockTxtTitle = new TextBox { Text = "新测试项" }; AssignProtectedField(_webPart, "txtTitle", mockTxtTitle); _mockDdlCategory.SelectedValue = "1"; // 2. 模拟列表AddItem返回新的ListItem var mockNewItem = new Mock<SPListItem>(); _mockItemsList.Setup(l => l.AddItem()).Returns(mockNewItem.Object); // 3. 执行按钮点击事件 _webPart.btnSave_Click(null, EventArgs.Empty); // 4. 断言新项的属性已设置,且Update方法被调用 mockNewItem.Verify(i => i["Title"] = "新测试项", Times.Once); mockNewItem.Verify(i => i["Category"] = It.IsAny<SPFieldLookupValue>(), Times.Once); mockNewItem.Verify(i => i.Update(), Times.Once); } #endregion }
关键注意事项
- 隔离SharePoint依赖:通过
Web属性注入SPWeb,避免直接依赖SPContext.Current,这样测试时可以用Moq模拟SPWeb、SPList等对象,不需要真实的SharePoint环境。 - 反射访问protected控件:Web部件的控件通常是protected的,测试时需要用反射来设置或验证这些控件的状态。
- 模拟Page状态:通过
Mock<Page>来控制IsPostBack属性,测试不同的Page_Load分支逻辑。 - 验证方法调用:对于保存逻辑,我们不需要验证数据是否真的写入SharePoint,只需要验证
AddItem()、Update()等方法是否被正确调用,以及参数是否符合预期。
内容的提问来源于stack exchange,提问作者Sharp
相关产品推荐
相关产品推荐

