如何使用MOQ为C# SendEmail方法编写参数校验单元测试
现有代码问题说明
你当前的代码无法直接使用Moq做单元测试,核心原因是方法内部直接实例化了ExchangeService和EmailMessage两个外部依赖的实现类,没有做依赖抽象,Moq无法替换方法内部硬编码创建的实例,需要先做最小改造。
方案1:轻量改造(无需新增接口)
适合老项目快速适配单元测试,通过提取虚方法实现可Mock:
public class EmailService { // 提取ExchangeService创建逻辑为虚方法 protected virtual ExchangeService CreateExchangeService(string sender, string password) { var service = new ExchangeService(ExchangeVersion.Exchange2016); service.Credentials = new WebCredentials(sender, password); return service; } // 提取EmailMessage创建逻辑为虚方法 protected virtual EmailMessage CreateEmailMessage(ExchangeService service, string subject, string body, List<string> to, List<string> cc) { var email = new EmailMessage(service); email.ToRecipients.AddRange(to); email.CcRecipients.AddRange(cc); email.Subject = subject; email.Body = new MessageBody(body); return email; } public string SendEmail(string emailNotificationSender, string senderAccountPassword, string emailCcList,string emailBody, string emailSubject, List<string> toReceipients) { try { ExchangeService exchangeService = CreateExchangeService(emailNotificationSender, senderAccountPassword); exchangeService.AutodiscoverUrl(emailNotificationSender, RedirectionUrlValidationCallback); var ccList = emailCcList.Split(';').ToList(); EmailMessage email = CreateEmailMessage(exchangeService, emailSubject, emailBody, toReceipients, ccList); email.Send(); return "Success"; } catch (Exception exception) { OnError(new ErrorRecordEventArgs { ErrorSource = "ExchangeDataStore::ExecuteQuery()", Exception = exception }); } return default; } public static bool RedirectionUrlValidationCallback(string redirectionUrl) { bool result = false; Uri redirectionUri = new Uri(redirectionUrl); if (redirectionUri.Scheme == "https") { result = true; } return result; } // 你的原有OnError逻辑保留 }
方案2:面向抽象改造(可扩展性更高)
适合新项目或者有重构空间的项目,通过依赖注入解耦:
// 定义Exchange服务工厂接口 public interface IExchangeServiceFactory { ExchangeService Create(string senderEmail, string password); } // 定义邮件消息工厂接口 public interface IEmailMessageFactory { EmailMessage Create(ExchangeService service, string subject, string body, List<string> to, List<string> cc); } // 改造原有服务类,依赖注入工厂 public class EmailService { private readonly IExchangeServiceFactory _exchangeFactory; private readonly IEmailMessageFactory _emailFactory; public EmailService(IExchangeServiceFactory exchangeFactory, IEmailMessageFactory emailFactory) { _exchangeFactory = exchangeFactory; _emailFactory = emailFactory; } public string SendEmail(string emailNotificationSender, string senderAccountPassword, string emailCcList,string emailBody, string emailSubject, List<string> toReceipients) { try { ExchangeService exchangeService = _exchangeFactory.Create(emailNotificationSender, senderAccountPassword); exchangeService.AutodiscoverUrl(emailNotificationSender, RedirectionUrlValidationCallback); var ccList = emailCcList.Split(';').ToList(); EmailMessage email = _emailFactory.Create(exchangeService, emailSubject, emailBody, toReceipients, ccList); email.Send(); return "Success"; } catch (Exception exception) { OnError(new ErrorRecordEventArgs { ErrorSource = "ExchangeDataStore::ExecuteQuery()", Exception = exception }); } return default; } // RedirectionUrlValidationCallback和OnError逻辑保留 }
单元测试编写(基于Moq)
这里以方案2为例,方案1的测试逻辑类似,直接MockEmailService的虚方法即可:
using Moq; using Xunit; using FluentAssertions; using Microsoft.Exchange.WebServices.Data; public class EmailServiceTests { private readonly Mock<IExchangeServiceFactory> _mockExchangeFactory; private readonly Mock<IEmailMessageFactory> _mockEmailFactory; private readonly EmailService _testService; public EmailServiceTests() { _mockExchangeFactory = new Mock<IExchangeServiceFactory>(); _mockEmailFactory = new Mock<IEmailMessageFactory>(); _testService = new EmailService(_mockExchangeFactory.Object, _mockEmailFactory.Object); } [Fact] public void SendEmail_Should_Pass_All_Parameters_Correctly() { // 1. 构造测试用例参数 var testSender = "sender@demo.com"; var testPassword = "DemoPass@123"; var testCcList = "cc1@demo.com;cc2@demo.com"; var testBody = "测试邮件正文内容"; var testSubject = "测试邮件主题"; var testToList = new List<string> {"receiver1@demo.com", "receiver2@demo.com"}; var parsedCcList = testCcList.Split(';').ToList(); // 2. Mock依赖返回 var mockExchangeService = new Mock<ExchangeService>(ExchangeVersion.Exchange2016); _mockExchangeFactory.Setup(f => f.Create(testSender, testPassword)) .Returns(mockExchangeService.Object); var mockEmailMessage = new Mock<EmailMessage>(mockExchangeService.Object); mockEmailMessage.Setup(m => m.Send()).Verifiable(); _mockEmailFactory.Setup(f => f.Create( mockExchangeService.Object, testSubject, testBody, testToList, parsedCcList )).Returns(mockEmailMessage.Object) .Verifiable(); // 3. 执行被测方法 var result = _testService.SendEmail(testSender, testPassword, testCcList, testBody, testSubject, testToList); // 4. 断言验证所有参数符合预期 result.Should().Be("Success"); // 验证发件人地址、密码正确传入 _mockExchangeFactory.Verify(f => f.Create(testSender, testPassword), Times.Once); // 验证发件人地址正确传入自动发现方法 mockExchangeService.Verify(s => s.AutodiscoverUrl(testSender, It.IsAny<AutodiscoverRedirectionUrlValidationCallback>()), Times.Once); // 验证主题、正文、收件人、抄送全部正确传入 _mockEmailFactory.VerifyAll(); // 验证邮件发送方法被调用 mockEmailMessage.Verify(m => m.Send(), Times.Once); } [Fact] public void RedirectionUrlValidationCallback_Should_Return_True_Only_For_Https() { EmailService.RedirectionUrlValidationCallback("https://demo.com").Should().BeTrue(); EmailService.RedirectionUrlValidationCallback("http://demo.com").Should().BeFalse(); } }
说明
- 你需要验证的发件人地址、抄送列表、邮件正文、发件人账户密码四个参数,已经全部包含在上述测试的校验逻辑中,只要断言全部通过即可证明参数被正确传递
- 测试需要提前安装NuGet包:
Moq、xUnit/NUnit(根据你的测试框架选择)、FluentAssertions(可选,简化断言写法)
内容的提问来源于stack exchange,提问作者Umapathy
相关产品推荐
相关产品推荐

