【问题标题】:How do I make a mockup of System.Net.Mail MailMessage?如何制作 System.Net.Mail MailMessage 的模型?
【发布时间】:2010-11-06 16:20:17
【问题描述】:

所以我的代码中有一些 SMTP 内容,我正在尝试对该方法进行单元测试。

所以我一直在尝试模拟 MailMessage,但它似乎从来没有工作过。我认为这些方法都不是虚拟的或抽象的,所以我不能使用 moq 来模拟它:(。

所以我想我必须手动完成,这就是我卡住的地方。

*手工我的意思是了解接口和包装器,但让 moq 仍然模拟接口。

我不知道如何编写我的接口和包装器(一个将实现具有实际 MailMessage 代码的接口的类,因此当我的真实代码运行时,它实际上会完成它需要做的事情)。

所以首先我不确定如何设置我的界面。让我们看一下我必须模拟的字段之一。

MailMessage mail = new MailMessage();

mail.To.Add("test@hotmail.com");

所以这是我必须伪造的第一件事。

所以看着它我知道“To”是一个属性,通过在“To”上按 F12 将我带到这一行:

public MailAddressCollection To { get; }

所以它是 MailAddressCollection 属性。但是有些方法允许我走得更远并做“添加”。

所以现在我的问题是在我的界面中我要做什么?

我做财产吗?这个属性应该是 MailAddressCollection 吗?

或者我应该有类似的方法吗?

void MailAddressCollection To(string email);

or 

void string To.Add(string email);

那么我的包装器会是什么样子?

所以你可以看到我很困惑。因为他们太多了。我猜我只是模拟我正在使用的那些。

编辑代码

我想在真正意义上我只需要测试更多的异常,但我想测试以确保所有东西都被发送然后它会得到响应 = 成功。

string response = null;
            try
            {

                MembershipUser userName = Membership.GetUser(user);

                string newPassword = userName.ResetPassword(securityAnswer);

                MailMessage mail = new MailMessage();

                mail.To.Add(userName.Email);

                mail.From = new MailAddress(ConfigurationManager.AppSettings["FROMEMAIL"]);
                mail.Subject = "Password Reset";

                string body = userName + " Your Password has been reset. Your new temporary password is: " + newPassword;

                mail.Body = body;
                mail.IsBodyHtml = false;


                SmtpClient smtp = new SmtpClient();

                smtp.Host = ConfigurationManager.AppSettings["SMTP"];
                smtp.Credentials = new System.Net.NetworkCredential(ConfigurationManager.AppSettings["FROMEMAIL"], ConfigurationManager.AppSettings["FROMPWD"]);

                smtp.EnableSsl = true;

                smtp.Port = Convert.ToInt32(ConfigurationManager.AppSettings["FROMPORT"]);

                smtp.Send(mail);

                response = "Success";
            }
            catch (ArgumentNullException ex)
            {
                response = ex.Message;

            }
            catch (ArgumentException ex)
            {
                response = ex.Message;

            }
            catch (ConfigurationErrorsException ex)
            {
                response = ex.Message;
            }
            catch (ObjectDisposedException ex)
            {
                response = ex.Message;
            }
            catch (InvalidOperationException ex)
            {
                response = ex.Message;
            }
            catch (SmtpFailedRecipientException ex)
            {
                response = ex.Message;
            }
            catch (SmtpException ex)
            {
                response = ex.Message;
            }



            return response;

        } 

谢谢

【问题讨论】:

    标签: c# asp.net-mvc unit-testing mocking


    【解决方案1】:

    为什么要模拟 MailMessage? SmtpClient 接收 MailMessages 并将其发送出去;那是我想为测试目的包装的类。因此,如果您正在编写某种类型的下订单系统,如果您尝试测试下订单时 OrderService 是否始终发送电子邮件,那么您将有一个类似于以下的类:

    class OrderService : IOrderSerivce 
    {
        private IEmailService _mailer;
        public OrderService(IEmailService mailSvc) 
        {
            this. _mailer = mailSvc;
        }
    
        public void SubmitOrder(Order order) 
        {
            // other order-related code here
    
            System.Net.Mail.MailMessage confirmationEmail = ... // create the confirmation email
            _mailer.SendEmail(confirmationEmail);
        } 
    
    }
    

    使用 IEmailService 包装 SmtpClient 的默认实现:

    这样,当您编写单元测试时,您测试的是使用 SmtpClient/EmailMessage 类的代码的行为,而不是 SmtpClient/EmailMessage 类本身的行为:

    public Class When_an_order_is_placed
    {
        [Setup]
        public void TestSetup() {
            Order o = CreateTestOrder();
            mockedEmailService = CreateTestEmailService(); // this is what you want to mock
            IOrderService orderService = CreateTestOrderService(mockedEmailService);
            orderService.SubmitOrder(o);
        } 
    
        [Test]
        public void A_confirmation_email_should_be_sent() {
            Assert.IsTrue(mockedEmailService.SentMailMessage != null);
        }
    
    
        [Test]
        public void The_email_should_go_to_the_customer() {
            Assert.IsTrue(mockedEmailService.SentMailMessage.To.Contains("test@hotmail.com"));
        }
    
    }
    

    编辑:为了解决下面的 cmets,您需要两个单独的 EmailService 实现——只有一个会使用 SmtpClient,您将在应用程序代码中使用它:

    class EmailService : IEmailService {
        private SmtpClient client;
    
        public EmailService() {
            client = new SmtpClient();
            object settings = ConfigurationManager.AppSettings["SMTP"];
            // assign settings to SmtpClient, and set any other behavior you 
            // from SmtpClient in your application, such as ssl, host, credentials, 
            // delivery method, etc
        }
    
        public void SendEmail(MailMessage message) {
            client.Send(message);
        }
    
    }
    

    您的模拟/伪造电子邮件服务(您不需要模拟框架,但它有帮助)不会触及 SmtpClient 或 SmtpSettings;它只会记录这样一个事实,即在某些时候,一封电子邮件通过 SendEmail 传递给它。然后,您可以使用它来测试是否调用了 SendEmail,以及使用哪些参数:

    class MockEmailService : IEmailService {
        private EmailMessage sentMessage;;
    
        public SentMailMessage { get { return sentMessage; } }
    
        public void SendEmail(MailMessage message) {
            sentMessage = message;
        }
    
    }
    

    电子邮件是否已发送到 SMTP 服务器并已送达的实际测试应超出单元测试的范围。您需要知道这是否有效,并且您可以设置第二组测试来专门对此进行测试(通常称为集成测试),但这些测试是与测试应用程序核心行为的代码分开的不同测试。

    【讨论】:

    • 嗯,我想我明白你在说什么,但请仔细检查一下。您是说因为 MailMessage 是为我编写的所有代码,因此我不必测试,并且由于 MailMessage 与我的测试实际上无关,而且 Mailmessage 并没有真正引起任何依赖关系,它实际上只是 smpt.send( ) 这会创建依赖关系吗?所以真的我只需要伪造发送部分来打破任何依赖关系吗?我的想法是但 MailMessage 是一个依赖项,但现在我看它我可以看到它确实不是。这是正确的吗?你想说什么?
    • OH 还有一件事我使用 ConfigurationManager.AppSettings["SMTP"];那么我应该模拟 ConfigurationManager 来打破对 AppConfig 文件的依赖,还是应该只制作一个 appConfig 文件?
    • 对。您不想测试 MailMessage 或 SmtpClient;这不是你的代码。您希望对使用 MailMessage 和 SmtpClient 的所有内容进行单元测试,以确保: a) 他们正在创建具有正确字段的 MailMessage; b) 实际上是在发送电子邮件。
    • 对于 ConfigurationManater.AppSettings["SMTP"],这将是您的“真实”实现 IEmailService 的属性,它使用 SmtpClient。但它不是IEmailService本身的属性,也不是任何使用SmtpClient的代码的属性,所以不需要测试。换句话说,当您执行单元测试时,ConfigurationManager.AppSettings["SMTP"] 甚至不会被触及。
    • 是的,如果您使用模拟框架,您可以跳过制作 MockEmailService。您需要设置该 Mock 以验证 SendEmail 方法是否已被调用,并且电子邮件是否包含您期望的字段。查看 Moq 快速入门 (code.google.com/p/moq/wiki/QuickStart) 的回调部分,特别是第二个示例
    【解决方案2】:

    你最终会在这里模拟几个不同的类(至少两个)。首先,您需要一个围绕 MailMessage 类的包装器。我会为包装器创建一个接口,然后让包装器实现该接口。在您的测试中,您将模拟界面。其次,您将提供一个模拟实现作为 MailAddressCollection 的模拟接口的期望。由于 MailAddressCollection 实现了Collection<MailAddress>,这应该是相当简单的。如果由于附加属性(我没有检查)而模拟 MailAddressCollection 存在问题,您可以让包装器将其返回为 IList<MailAddress>,它作为一个接口应该很容易模拟。

    public interface IMailMessageWrapper
    {
        MailAddressCollection To { get; }
    }
    
    public class MailMessageWrapper
    {
        private MailMessage Message { get; set; }
    
        public MailMessageWrapper( MailMessage message )
        {
            this.Message = message;
        }
    
        public MailAddressCollection To
        {
            get { return this.Message.To; }
        }
    }
    
    // RhinoMock syntax, sorry -- but I don't use Moq
    public void MessageToTest()
    {
         var message = MockRepository.GenerateMock<IMailMessageWrapper>()
         var to = MockRepository.GenerateMock<MailAddressCollection>();
    
         var expectedAddress = "test@example.com";
    
         message.Expect( m => m.To ).Return( to ).Repeat.Any();
         to.Expect( t => t.Add( expectedAddress ) );
         ...
    }
    

    【讨论】:

    • 谢谢 我不确定我现在是否需要实际模拟它。这非常有用,因为当我模拟一些东西时,我就可以遵循这个。不过,我不确定。如果我想使用 To.Add(); 会发生什么?我必须用那个做财产吗?在旁注中,您知道他们是如何做到的,因此他们可以执行 Mail.To.Add(...) 就像他们如何在“To”之后显示“add”方法一样我什至看到他们在之后做了一个属性一个属性。
    【解决方案3】:

    免责声明:我在 Typemock 工作 您可以使用 Typemock Isolator 仅在一行代码中简单地伪造该类,而不是寻找一些 hack:

    var fakeMailMessage = Isolate.Fake.Instance<MailMessage>();
    

    然后你可以使用 Isolate.WhenCalled

    设置它的行为

    【讨论】:

    • 是的,我听说 typemock 很好,太糟糕了,它要花钱:( 看看最便宜的版本就好像 89 美元。不知道你会得到什么,但现在我可能买不起这样的东西当我下学并有工作或做合同时,购买它是值得的。
    • @chobo2 你不得不承认,使用 Isolator 会产生最优雅且易于用户理解的代码来解决这个问题。我想质量是有代价的。您是否使用记事本编程,因为 Visual Studio 需要花钱(远远超过 90 美元)
    【解决方案4】:

    在 .NET 4.0 中,您可以使用“duck-typing”来传递另一个类,而不是“System.Net.Mail”。 但在早期版本中,恐怕没有其他方法,只能围绕“System.Net.Mail”和模拟类创建包装器。

    如果是另一种(更好的)方法,我想学习它:)。

    编辑:

    public interface IMailWrapper {
        /* members used from System.Net.Mail class */
    }
    
    public class MailWrapper {
        private System.Net.Mail original;
        public MailWrapper( System.Net.Mail original ) {
            this.original = original;
        }
    
        /* members used from System.Net.Mail class delegated to "original" */
    }
    
    public class MockMailWrapper {
       /* mocked members used from System.Net.Mail class */
    }
    
    
    void YourMethodUsingMail( IMailWrapper mail ) {
        /* do something */
    }
    

    【讨论】:

    • 那我该怎么做呢?就像在我的包装器中一样,我应该拥有 // Mail Here public MyConstructor() { Mail = new MailMessage(0; } public void To.Add(string email) { Mail.To.Add(email); } 的属性然后执行我需要的每一种方法都一样?或者我可以用其他方法吗?
    • 是的,我觉得没有别的办法了,怎么测试呢。
    猜你喜欢
    • 2022-11-05
    • 2011-11-16
    • 2017-03-30
    • 2016-11-02
    • 2018-04-13
    • 1970-01-01
    • 2010-10-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多