Moq和SqlException的抛出

46

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

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

然而,这将无法编译,因为SqlException的构造函数是internal:

  

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

现在,我可以更改它以使其抛出 Exception,但对我来说这并不起作用,因为我的方法应该在抛出 SqlException 时返回一个状态代码,并在抛出其他异常时返回另一个。这就是我的单元测试所测试的内容。

是否有任何方法可以在不更改正在测试的方法的逻辑或不测试此场景的情况下实现这一点?


1
可能是重复的问题:如何抛出 SqlException(需要模拟) - Joachim Isaksson
1
你可以使用反射来访问内部方法CreateException,从而创建一个SQLException。https://dev59.com/uXM_5IYBdhLWcg3wt1k0 .... http://msdn.microsoft.com/en-us/library/ms229394(v=vs.100).aspx ... 然后只需使用lambda表达式来创建并抛出它。 - Colin Smith
6个回答

76

如果您需要针对异常的NumberMessage属性进行测试,请使用类似下面这样(使用反射)的构建器:

using System;
using System.Data.SqlClient;  // .NetCore using Microsoft.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); // .NetCore should be 8 not 7
        SqlError error = firstSqlErrorCtor.Invoke(
            new object[] 
            { 
                this.errorNumber, 
                new byte(), 
                new byte(), 
                string.Empty, 
                string.Empty, 
                string.Empty, 
                new int() 
            //,new Exception()  // for .NetCore 
            }) 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库):

那么,您可以让一个存储库模拟对象(例如)像这样抛出异常(该示例使用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);

1
谢谢你! :D - Stephan Møller
@StephanRyer 我已经有了代码...只是想分享一下。 - Aage
1
你真是位绅士兼学者。你需要将这个上传到 Github,这样我就可以给它点赞了。 - Matthew Mark Miller
14
我来翻译一下:发现了这个宝石。如果使用 corefx,需要将 GetParameters.Count() 更改为 8 并在参数列表中添加 new Exception() - jmzagorski
6
这是最全面的答案,符合问题背后的原始动机 -> SQL异常和单元测试。很棒的答案 - 并额外加分使用了建造者模式。 - adelpreore
显示剩余2条评论

58

这个应该可以工作:

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有以下注意事项:

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

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


有没有办法设置异常消息? - Aage
@bump 可能可以通过反射实现,但根据底层结构,这可能相当困难(例如获取属性的后备字段并设置这些字段)。我不确定设置消息会给你什么,除非您有基于异常消息陈述的不同逻辑运行,并且您需要测试它。 - docmanhattan
我确实需要测试一下。通过反射(和私有构造函数)我成功地做到了。谢谢。 - Aage
1
请看我的答案,其中包含设置了“Number”和“Message”属性的SqlException示例。 - Aage
它能正常工作,但由此引发的“SqlException”是有缺陷的。例如,尝试调用.ToString()方法,你可能不会喜欢结果。 - Tipx

7

我刚刚尝试了一下,对我有效:

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 返回 void,会怎样? - Jesus is Lord
1
现在我想起来了,无论如何你可能想要使用.Callback。正在更新答案。 - Steve Czetty

2

我使用未初始化对象方法生成具有消息的SqlException是最简单的方法:

Original Answer翻译成:"最初的回答"

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);

0

在发现这个问题/答案之前,我已经写了这个。对于只想要特定编号的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;
}

0

公共类SqlExceptionMock { public static SqlException ThrowSqlException(int errorNumber, string message = null) { var ex = (SqlException)FormatterServices.GetUninitializedObject(typeof(SqlException)); var errors = GenerateSqlErrorCollection(errorNumber, message); SetPrivateFieldValue(ex, "_errors", errors); return ex; }

    private static SqlErrorCollection GenerateSqlErrorCollection(int errorNumber, string message)
    {
        var t = typeof(SqlErrorCollection);
        var col = (SqlErrorCollection)FormatterServices.GetUninitializedObject(t);
        SetPrivateFieldValue(col, "_errors", new List<object>());
        var sqlError = GenerateSqlError(errorNumber, message);
        var method = t.GetMethod(
          "Add",
          BindingFlags.NonPublic | BindingFlags.Instance);
        method.Invoke(col, new object[] { sqlError });
        return col;
    }

    private static SqlError GenerateSqlError(int errorNumber, string message)
    {
        var sqlError = (SqlError)FormatterServices.GetUninitializedObject(typeof(SqlError));

        SetPrivateFieldValue(sqlError, "_number", errorNumber);
        if (!string.IsNullOrEmpty(message)) SetPrivateFieldValue(sqlError, "_message", message);
        return sqlError;
    }

    private static void SetPrivateFieldValue(object obj, string field, object val)
    {
        var member = obj.GetType().GetField(
          field,
          System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance
          );
        member?.SetValue(obj, val);
    }
}

1
由于您当前的回答写得不明确,请[编辑]以添加更多细节,帮助他人了解如何解决所提出的问题。您可以在帮助中心中找到有关编写良好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接