【问题标题】:Moq and throwing a SqlException起订量并抛出 SqlException
【发布时间】:2012-08-12 04:51:27
【问题描述】:

我有以下代码来测试当某个名称被传递给我的方法时,它会抛出一个 SQL 异常(这是有原因的,虽然这听起来有点奇怪)。

   mockAccountDAL.Setup(m => m.CreateAccount(It.IsAny<string>(), 
"Display Name 2", It.IsAny<string>())).Throws<SqlException>();

但是,这不会编译,因为 SqlException 的构造函数是内部的:

'System.Data.SqlClient.SqlException' 必须是非抽象类型 一个公共无参数构造函数,以便将其用作参数 泛型类型或方法中的“TException” 'Moq.Language.IThrows.Throws()'

现在,我可以更改它以声明它应该抛出Exception,但这对我不起作用,因为如果它是SqlException,我的方法应该返回一个状态码,如果它是任何其他状态码则返回另一个例外。这就是我的单元测试正在测试的内容。

有没有什么方法可以在不改变我正在测试的方法的逻辑或不测试这个场景的情况下实现这一点?

【问题讨论】:

标签: c# .net unit-testing mocking moq


【解决方案1】:

这应该可行:

using System.Runtime.Serialization;

var exception = FormatterServices.GetUninitializedObject(typeof(SqlException)) 
                as SqlException;

mockAccountDAL.Setup(m => m.CreateAccount(It.IsAny<string>(), "Display Name 2", 
                     It.IsAny<string>())).Throws(exception);

但是,使用 GetUninitializedObject 有以下警告:

因为对象的新实例被初始化为零并且没有 构造函数运行时,对象可能不代表一个状态 被该对象视为有效。

如果这会导致任何问题,您可能可以使用一些更复杂的反射魔法来创建它,但这种方式可能是最简单的(如果可行的话)。

【讨论】:

  • 不会有设置异常消息的方法吧?
  • @bump 这可能通过反射实现,但取决于底层结构,这可能很难做到(即获取属性的支持字段并设置它们)。我不确定该消息为您提供了什么设置,除非您根据异常消息的说明运行不同的逻辑并且您需要对其进行测试。
  • 我确实需要测试一下。能够通过反射(和私有构造函数)来做到这一点。谢谢。
  • 查看我的答案以获取具有NumberMessage 属性集的SqlException 示例。
  • It works™,但由此产生的 SqlException 功能失调。例如,尝试在其上调用 .ToString(),您可能不喜欢结果。
【解决方案2】:

我刚刚尝试过,它对我有用:

private static void ThrowSqlException()
{
    using (var cxn = new SqlConnection("Connection Timeout=1"))
    {
        cxn.Open();
    }
}

// ...
mockAccountDAL.Setup(m => m.CreateAccount(It.IsAny<string>),
                     "Display Name 2", It.IsAny<string>()))
              .Callback(() => ThrowSqlException());

【讨论】:

  • 如果CreateAccount 返回无效怎么办?
  • 可能想在任何情况下使用.Callback,现在我考虑了一下。正在更新答案。
【解决方案3】:

如果您需要异常的NumberMessage 属性的测试用例,您可以使用这样的构建器(使用反射):

using System;
using System.Data.SqlClient;
using System.Linq;
using System.Reflection;

public class SqlExceptionBuilder
{
    private int errorNumber;
    private string errorMessage;

    public SqlException Build()
    {
        SqlError error = this.CreateError();
        SqlErrorCollection errorCollection = this.CreateErrorCollection(error);
        SqlException exception = this.CreateException(errorCollection);

        return exception;
    }

    public SqlExceptionBuilder WithErrorNumber(int number)
    {
        this.errorNumber = number;
        return this;
    }

    public SqlExceptionBuilder WithErrorMessage(string message)
    {
        this.errorMessage = message;
        return this;
    }

    private SqlError CreateError()
    {
        // Create instance via reflection...
        var ctors = typeof(SqlError).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance);
        var firstSqlErrorCtor = ctors.FirstOrDefault(
            ctor =>
            ctor.GetParameters().Count() == 7); // Need a specific constructor!
        SqlError error = firstSqlErrorCtor.Invoke(
            new object[] 
            { 
                this.errorNumber, 
                new byte(), 
                new byte(), 
                string.Empty, 
                string.Empty, 
                string.Empty, 
                new int() 
            }) as SqlError;

        return error;
    }
 
    private SqlErrorCollection CreateErrorCollection(SqlError error)
    {
        // Create instance via reflection...
        var sqlErrorCollectionCtor = typeof(SqlErrorCollection).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)[0];
        SqlErrorCollection errorCollection = sqlErrorCollectionCtor.Invoke(new object[] { }) as SqlErrorCollection;

        // Add error...
        typeof(SqlErrorCollection).GetMethod("Add", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(errorCollection, new object[] { error });

        return errorCollection;
    }

    private SqlException CreateException(SqlErrorCollection errorCollection)
    {
        // Create instance via reflection...
        var ctor = typeof(SqlException).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance)[0];
        SqlException sqlException = ctor.Invoke(
            new object[] 
            { 
                // With message and error collection...
                this.errorMessage, 
                errorCollection,
                null,
                Guid.NewGuid() 
            }) as SqlException;

        return sqlException;
    }
}

然后您可以让存储库模拟(例如)抛出这样的异常(此示例使用 Moq 库):

using Moq;

var sqlException = 
    new SqlExceptionBuilder().WithErrorNumber(50000)
        .WithErrorMessage("Database exception occured...")
        .Build();
var repoStub = new Mock<IRepository<Product>>(); // Or whatever...
repoStub.Setup(stub => stub.GetById(1))
    .Throws(sqlException);

【讨论】:

  • 谢谢你! :D
  • @StephanRyer 我有代码......只是想我会分享
  • 你是一个绅士和一个学者。你需要把它放到 Github 上,这样我才能给它加星标。
  • 遇到了这颗宝石。如果使用corefx,则需要将GetParameters.Count() 更改为8,并将new Exception() 添加到参数列表中
  • 这是最全面的答案,并且适合问题背后的原始动机 -> sql异常和单元测试。很好的答案 - 以及构建者模式的额外积分。
【解决方案4】:

对我来说,使用未初始化对象方法生成 SqlException 是最简单的方法:

const string sqlErrorMessage = "MyCustomMessage";
var sqlException = FormatterServices.GetUninitializedObject(typeof(SqlException)) as SqlException;
var messageField = typeof(SqlException).GetField("_message", BindingFlags.NonPublic | BindingFlags.Instance);
messageField.SetValue(sqlException, sqlErrorMessage);

【讨论】:

    【解决方案5】:

    我在找到这个问题/答案之前写了这个。对于只想要特定编号的 SQL 异常的人来说可能很有用。

    private static SqlException CreateSqlExceptionWithNumber(int errorNumber)
    {
        var sqlErrorCollectionCtor = typeof(SqlErrorCollection).GetConstructor(
            BindingFlags.NonPublic | BindingFlags.Instance,
            null,
            CallingConventions.Any,
            new Type[0],
            null);
    
        var sqlErrorCollection = (SqlErrorCollection)sqlErrorCollectionCtor.Invoke(new object[0]);
    
        var errors = new ArrayList();
    
        var sqlError = (SqlError)FormatterServices.GetSafeUninitializedObject(typeof(SqlError));
    
        typeof(SqlError)
            .GetField("number", BindingFlags.NonPublic | BindingFlags.Instance)
            ?.SetValue(sqlError, errorNumber);
    
        errors.Add(sqlError);
    
        typeof(SqlErrorCollection)
            .GetField("errors", BindingFlags.NonPublic | BindingFlags.Instance)
            ?.SetValue(sqlErrorCollection, errors);
    
        var exception = (SqlException)FormatterServices.GetUninitializedObject(typeof(SqlException));
    
        typeof(SqlException)
            .GetField("_errors", BindingFlags.NonPublic | BindingFlags.Instance)
            ?.SetValue(exception, sqlErrorCollection);
        
        return exception;
    }
    

    【讨论】:

      猜你喜欢
      • 2014-11-11
      • 1970-01-01
      • 2012-08-13
      • 2015-11-23
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多