【问题标题】:Mocking a StreamWriter/determining when to mock模拟 StreamWriter/确定何时模拟
【发布时间】:2012-09-04 05:05:24
【问题描述】:

我有一个使用 StreamWriter 写入文件的类。

public void CreateLog(string errorLogFilePath, StringBuilder errorLogBuilder, string errorMessage)
{
    using (StreamWriter sw = new StreamWriter(errorLogFilePath, true)
    {
        errorLogBuilder.Apend("An error was discovered.");
        //....
        sw.Write(errorLogBuilder.ToString());
    }
}

[问题]

1:是否可以检查.Write()方法是否被调用?

2:我是否需要在 StreamWriter 中包装 MemoryStream 以对其进行测试,而无需实际访问硬盘驱动器。 StreamWriters 构造函数之一接受流,但它指出以下 + UTF-8 编码会影响这一点吗?

使用 UTF-8 编码和默认缓冲区大小为指定流初始化 StreamWriter 类的新实例。

3:你如何确定一个类是否真的在访问 hd 并因此需要被模拟? (对不起,如果最后一个问题听起来很愚蠢,但我真的对此有点困惑。)

【问题讨论】:

    标签: c# c#-4.0 moq


    【解决方案1】:

    让方法写入 TextWriter 而不是 StreamWriter。然后通过传递一个模拟 TextWriter 来测试该方法。当然,在“真实”代码中,您将传入一个使用 new StreamWriter(errorLogFilePath, true) 创建的 StreamWriter。

    这将为您的问题提供以下答案:

    1. 是的
    2. 没有
    3. 如果不反编译其代码,通常无法确定。

    更多细节:

    将方法重构为两个方法:

    public void CreateLog(string errorLogFilePath, StringBuilder errorLogBuilder, string errorMessage) 
    { 
        using (StreamWriter sw = new StreamWriter(errorLogFilePath, true) 
        {
            CreateLog(sw, errorLogBuilder, errorMessage); 
        } 
    } 
    
    public void CreateLog(TextWriter writer, StringBuilder errorLogBuilder, string errorMessage)
    {
        errorLogBuilder.Apend("An error was discovered."); 
        //.... 
        writer.Write(errorLogBuilder.ToString()); 
    }
    

    测试第一个方法以确保它使用适当构造的 StreamWriter 调用第二个方法。测试第二种方法以确保它使用适当的参数在传递的 TextWriter 上调用 Write。现在您已经抽象出对硬盘驱动器的依赖。您的测试不使用硬盘,但您正在测试所有内容。

    【讨论】:

    • 感谢您的回复。关于(3),我指的是 MemoryStream、StreamReader、StreamWriter 等。你怎么知道哪个实际访问了 HD?大约 1 个月前,我问了一个与此类似的问题,Jon Skeet 说我可以将 memoryStream 包装在 StreamReader 中。为什么我不能在这里做同样的事情?
    • @HansRudel 你确实可以在这里做同样的事情(顺便说一下,有一个 StreamWriter 构造函数,它可以让你提供一个流指定编码),但我发现这个方法更简单。使用内存流创建 StreamWriter 将允许您在没有模拟框架的情况下编写测试,但是您使用了 moq 标记,那么为什么不直接模拟抽象类呢?
    • @HansRudel 我忘了写地址(3)。访问 HD 的类是 FileStream。当您将文件路径而不是流传递给 StreamWriter 构造函数时,StreamWriter 会创建一个 FileStream。如果你有一个 StreamWriter 并且想知道它正在写入什么(文件、内存、其他),你应该能够通过检查它的 BaseStream 属性来确定。
    • 嗨,我想我会按照你的建议去做,但我只是想澄清一下这是否可能/我是否真的正确理解了它。编辑:我看到你在上面提到了 q3。所以只要它不接受文件流,我就不需要模拟它。像 StringBuilder 或 MemoryStream 一样,它们都没有将 Stream 作为属性?
    • @HansRudel 基本原则是你应该模拟除被测类之外的所有内容。如果您这样做,那么在一种方法中引入错误可能会导致数十个测试失败。理想情况下,新错误应该会导致一项测试失败。
    【解决方案2】:

    一般来说,你可以:

    使用经过良好测试的日志库(如 NLog、MS Logging Application Block),让您无需开发和维护自己的日志。

    将您的日志记录逻辑(或代码调用消息框、打开文件对话框等)重构为带有接口的服务。这样您就可以拆分测试策略:

    • 在测试登录服务的消费者时:模拟日志接口以确保调用日志方法。这将确保日志记录服务的使用者正确调用日志记录
    • 在测试日志服务实现时,只需确保预期的输出与给定的输入匹配:如果您想将“FOO”写入 bar.log,请有效调用

    IE:

    // arrrange
    File.Delete("bar.log")
    
    // act
    CreateLog("bar.log", errorLogBuilder, "FOO")
    
    // assert
    Assert.IsTrue( File.Exists("bar.log") )
    Assert.IsTrue( File.ReadAllLines("bar.log").First() == "FOO")
    

    关键是确保组件被调用,通过模拟完成。 然后您可以检查该组件是否按预期工作。

    【讨论】:

      【解决方案3】:

      我知道这是一个非常古老的问题,但我在尝试解决类似问题时遇到了这个问题。即,如何伪造 StreamWriter。

      我解决这个问题的方法是没有在方法内部创建 StreamWriter 作为 using 语句的一部分,而是在 ctor 中预先创建(让你的类从 IDisposable 扩展,然后在 Dispose 方法中销毁 StreamWriter反而)。然后在测试时在其顶部注入一个假的:

      internal class FakeStreamWriter : StreamWriter
      {
          public List<string> Writes { get; set; } = new List<string>();
      
          public FakeStreamWriter() : base(new MemoryStream()) { }
      
          public override void Write(string value)
          {
              WriteLine(value);
          }
          public override void WriteLine(string value)
          {
              Writes.Add(value);
          }
      
          public override void Flush()
          {
      
          }
      }
      

      然后我的单元测试方法如下所示:

      public void SmtpStream_Negotiate_EhloResultsCorrectly()
      {
          var ctx = new APIContext();
          var logger = new FakeLogger();
          var writer = new FakeStreamWriter();
          var reader = new FakeStreamReader { Line = "EHLO test.com" };
          var stream = new SmtpStream(logger, ctx, new MemoryStream())
          {
              _writer = writer,
              _reader = reader
          };
      
          Exception ex = null;
      
          try
          {
              stream.Negotiate(ctx);
          }
          catch (Exception x)
          {
              ex = x;
          }
      
          Assert.IsNull(ex);
          Assert.IsTrue(writer.Writes.ElementAt(0) == "250 Hello test.com");
          Assert.IsTrue(writer.Writes.ElementAt(1) == "250 STARTTLS");
      } 
      

      【讨论】:

        猜你喜欢
        • 2018-10-22
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2023-03-09
        • 2017-01-20
        • 1970-01-01
        • 1970-01-01
        • 2010-12-25
        相关资源
        最近更新 更多