【问题标题】:Asserting an exception thrown from a mock object constructor断言从模拟对象构造函数抛出的异常
【发布时间】:2026-02-15 16:50:01
【问题描述】:

假设:VS2010、.NET 4、C#、NUnit、Moq

我是 TDD 新手,在进行项目时遇到了这个问题。

给定班级:

public abstract class MyFileType
{                
    public MyFileType(String fullPathToFile)
    {
        if (!File.Exists(fullPathToFile))
        {
            throw new FileNotFoundException();
        }

        // method continues

    }
}

我正在尝试使用方法对其进行测试:

[Test]
[ExpectedException(typeof(System.IO.FileNotFoundException))]
public void MyFileType_CreationWithNonexistingPath_ExceptionThrown()
{
    String nonexistingPath = "C:\\does\\not\\exist\\file.ext";
    var mock = new Mock<MyFileType>(nonexistingPath);
}

测试失败,NUnit 报告从未抛出异常。

我确实找到了一个 section in the NUnit docs 谈论断言异常,但这些示例似乎不是我想要做的。我还在开始使用 NUnit 和 Moq,所以我可能会走错路。

更新:

为了帮助阐明为什么这个例子使用了一个抽象类,它是一系列文件类型的基类,其中只有数据的加载和处理在子类类型之间会有所不同。我最初的想法是将打开/设置的逻辑放入一个基类中,因为它对所有类型都是相同的。

【问题讨论】:

  • 我不确定这是否与您的问题直接相关,但是将您的字符串定义更改为使用 @"" 语法更具可读性且不易出错。例如,您可以简单地输入String nonexistingPath = @"C:\does\not\exist\file.ext";,而不是String nonexistingPath = "C:\\does\\not\\exist\\file.ext";
  • 感谢您的提示。我已合并更改。

标签: c# unit-testing tdd nunit moq


【解决方案1】:

在您引用 mock.Object 之前,不会调用构造函数。这应该会触发您期望的异常。

附带说明一下,让构造函数抛出除使用异常之外的异常(例如各种 ArgumentException 派生类)通常是不好的做法。大多数开发人员不希望“new”抛出异常,除非他们已经做了一些事情非常错误;不存在的文件是一种可以合法发生的异常,超出程序的控制范围,因此您可能希望将其设为静态工厂方法,而不是像“FromFileName”一样。编辑:鉴于这是一个基类构造函数,这也不是真正适用的,因此您可能需要考虑在哪里进行此检查的最佳位置。毕竟,文件可能随时停止存在,因此签入构造函数甚至可能没有意义(无论如何您都需要签入所有相关方法。)

【讨论】:

  • 我同意我对这个问题的最初尝试有构造函数做太多工作的代码味道。
  • +1 表示不从 ctor 抛出异常。 FxCop 有一个规则会标记此类代码。
【解决方案2】:

我今天遇到了类似的问题。我使用以下解决方案解决了这个问题:

[Test]
[ExpectedException(typeof(System.IO.FileNotFoundException))]
public void MyFileType_CreationWithNonexistingPath_ExceptionThrown()
{
    String nonexistingPath = "C:\\does\\not\\exist\\file.ext";
    var mock = new Mock<MyFileType>(nonexistingPath);
    try
    {
        var target = mock.Object;
    }
    catch(TargetInvocationException e)
    {
        if (e.InnerException != null)
        {
            throw e.InnerException;
        }
        throw;
    }
}

【讨论】:

    【解决方案3】:

    如果你必须让这个类成为一个抽象类,那么我们应该按照它的本意来实现它(简单): MSDN:an abstract class

    因此,同意(与 alexanderb)此处可能不需要模拟以及 .Throws NUnit Assert 扩展上的 Stecy,您可以在测试中创建一个调用基类的类,如下所示:

    using System;
    using System.IO;
    
    namespace fileFotFoundException {
        public abstract class MyFile {
    
            protected MyFile(String fullPathToFile) {
                if (!File.Exists(fullPathToFile)) throw new FileNotFoundException();
            }
        }
    }
    
    namespace fileFotFoundExceptionTests {
        using fileFotFoundException;
        using NUnit.Framework;
    
        public class SubClass : MyFile {
            public SubClass(String fullPathToFile) : base(fullPathToFile) {
                // If we have to have it as an abstract class...
            }
        }
    
        [TestFixture]
        public class MyFileTests {
    
            [Test]
            public void MyFile_CreationWithNonexistingPath_ExceptionThrown() {
                const string nonExistingPath = "C:\\does\\not\\exist\\file.ext";
    
                Assert.Throws<FileNotFoundException>(() => new SubClass(nonExistingPath));
            }
        }
    }
    

    【讨论】:

    • 我接受这篇文章作为答案,因为它确实回答了我原来的问题。
    【解决方案4】:

    假设您使用的是最新版本的 NUnit(您应该),那么 ExpectedException 属性已被弃用。

    您应该改用以下内容:

    var exception = Assert.Throws<FileNotFoundException> (() => new MyFileType (nonExistingPath));
    Assert.That (exception, Is.Not.Null);  // Or you can check for exception text...
    

    那里不需要使用模拟。 事实上,模拟在您的示例中并没有什么有趣的地方。

    【讨论】:

    • 我使用的是最新版本的 NUnit,所以感谢您对弃用的提醒。在尝试您的解决方案时,我遇到了与亚历山大的解决方案相同的错误——无法创建抽象类“MyFileType”的实例。
    【解决方案5】:

    如果您尝试测试 MyFileType 类,如果文件不存在,它会引发异常,为什么要创建模拟。你的代码应该很简单

    [Test]
    [ExpectedException(typeof(System.IO.FileNotFoundException))]
    public void MyFileType_CreationWithNonexistingPath_ExceptionThrown()
    {
        // arrange
        var nonexistingPath = "C:\\does\\not\\exist\\file.ext";
    
        // act / assert
        var mock = new MyFileType(nonexistingPath);
    }
    

    【讨论】:

    • MyFileType 是抽象的这一事实是否禁止我使用您所展示的“新”运算符?
    • 哎呀,对不起,我错过了。当然,你不能在那里做“新”。但这也意味着你不应该直接测试这个类,因为它是抽象的。它有一些行为,但这种行为必须在继承自 MyFileType 的类上进行测试。 TDD,也可能指向设计问题。如果某些东西很难测试,那 99% 就是设计错误。在您的情况下,如果类是抽象的,那么将逻辑放入构造函数是个坏主意。
    【解决方案6】:

    answer by Karol Tyl 可以通过使用Fluent Assertions 来改进:

    Func<MyFileType> createMyFileType = () => mock.Object;
    
    createMyFileType.Should().Throw<TargetInvokationException>().WithInnerException<FileNotFoundException>();
    

    【讨论】: