93

我正在尝试在我的项目中测试一些异常,我捕获的异常之一是SQlException.

看来您不能去new SqlException(),所以我不确定如何抛出异常,尤其是在不以某种方式调用数据库的情况下(并且由于这些是单元测试,因此通常建议不要调用数据库,因为它很慢)。

我正在使用 NUnit 和 Moq,但我不知道如何伪造它。

回应一些似乎都是基于ADO.NET的答案,注意我使用的是Linq to Sql。所以这些东西就像在幕后。

@MattHamilton 要求的更多信息:

System.ArgumentException : Type to mock must be an interface or an abstract or non-sealed class.       
  at Moq.Mock`1.CheckParameters()
  at Moq.Mock`1..ctor(MockBehavior behavior, Object[] args)
  at Moq.Mock`1..ctor(MockBehavior behavior)
  at Moq.Mock`1..ctor()

尝试模拟时发布到第一行

 var ex = new Mock<System.Data.SqlClient.SqlException>();
 ex.SetupGet(e => e.Message).Returns("Exception message");
4

16 回答 16

95

你可以通过反射来做到这一点,当微软做出改变时你必须维护它,但它确实有效,我刚刚测试过它:

public class SqlExceptionCreator
{
    private static T Construct<T>(params object[] p)
    {
        var ctors = typeof(T).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance);
        return (T)ctors.First(ctor => ctor.GetParameters().Length == p.Length).Invoke(p);
    }

    internal static SqlException NewSqlException(int number = 1)
    {
        SqlErrorCollection collection = Construct<SqlErrorCollection>();
        SqlError error = Construct<SqlError>(number, (byte)2, (byte)3, "server name", "error message", "proc", 100);

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


        return typeof(SqlException)
            .GetMethod("CreateException", BindingFlags.NonPublic | BindingFlags.Static,
                null,
                CallingConventions.ExplicitThis,
                new[] { typeof(SqlErrorCollection), typeof(string) },
                new ParameterModifier[] { })
            .Invoke(null, new object[] { collection, "7.0.0" }) as SqlException;
    }
}      

这也允许您控制 SqlException 的数量,这可能很重要。

于 2009-09-06T23:44:44.920 回答
87

我有一个解决方案。我不确定这是天才还是疯狂。

以下代码将创建一个新的 SqlException:

public SqlException MakeSqlException() {
    SqlException exception = null;
    try {
        SqlConnection conn = new SqlConnection(@"Data Source=.;Database=GUARANTEED_TO_FAIL;Connection Timeout=1");
        conn.Open();
    } catch(SqlException ex) {
        exception = ex;
    }
    return(exception);
}

然后你可以像这样使用它(这个例子是使用最小起订量)

mockSqlDataStore
    .Setup(x => x.ChangePassword(userId, It.IsAny<string>()))
    .Throws(MakeSqlException());

这样您就可以在存储库、处理程序和控制器中测试您的 SqlException 错误处理。

现在我需要去躺下。

于 2011-05-20T16:44:14.093 回答
26

根据情况,我通常更喜欢GetUninitializedObject而不是调用 ConstructorInfo。您只需要知道它不调用构造函数 - 来自 MSDN 备注:“因为对象的新实例被初始化为零并且没有运行构造函数,所以对象可能不代表被视为有效的状态由那个对象。” 但我会说它比依赖某个构造函数的存在更脆弱。

[TestMethod]
[ExpectedException(typeof(System.Data.SqlClient.SqlException))]
public void MyTestMethod()
{
    throw Instantiate<System.Data.SqlClient.SqlException>();
}

public static T Instantiate<T>() where T : class
{
    return System.Runtime.Serialization.FormatterServices.GetUninitializedObject(typeof(T)) as T;
}
于 2011-07-19T17:16:46.037 回答
13

编辑哎哟:我没有意识到 SqlException 是密封的。我一直在嘲笑 DbException,这是一个抽象类。

您不能创建新的 SqlException,但可以模拟 DbException,SqlException 派生自该 DbException。试试这个:

var ex = new Mock<DbException>();
ex.ExpectGet(e => e.Message, "Exception message");

var conn = new Mock<SqlConnection>();
conn.Expect(c => c.Open()).Throws(ex.Object);

因此,当方法尝试打开连接时会引发异常。

如果您希望读取Message模拟异常上的属性以外的任何内容,请不要忘记期望(或设置,取决于您的 Moq 版本)这些属性的“获取”。

于 2009-09-06T23:20:45.883 回答
9

由于您使用的是 Linq to Sql,因此这里是使用 NUnit 和 Moq 测试您提到的场景的示例。我不知道您的 DataContext 的确切细节以及您在其中可用的内容。根据您的需要进行编辑。

您将需要使用自定义类包装 DataContext,您不能使用 Moq 模拟 DataContext。您也不能模拟 SqlException,因为它是密封的。你需要用你自己的异常类来包装它。完成这两件事并不难。

让我们从创建我们的测试开始:

[Test]
public void FindBy_When_something_goes_wrong_Should_handle_the_CustomSqlException()
{
    var mockDataContextWrapper = new Mock<IDataContextWrapper>();
    mockDataContextWrapper.Setup(x => x.Table<User>()).Throws<CustomSqlException>();

    IUserResository userRespoistory = new UserRepository(mockDataContextWrapper.Object);
    // Now, because we have mocked everything and we are using dependency injection.
    // When FindBy is called, instead of getting a user, we will get a CustomSqlException
    // Now, inside of FindBy, wrap the call to the DataContextWrapper inside a try catch
    // and handle the exception, then test that you handled it, like mocking a logger, then passing it into the repository and verifying that logMessage was called
    User user = userRepository.FindBy(1);
}

让我们实现测试,首先让我们使用存储库模式包装我们的 Linq to Sql 调用:

public interface IUserRepository
{
    User FindBy(int id);
}

public class UserRepository : IUserRepository
{
    public IDataContextWrapper DataContextWrapper { get; protected set; }

    public UserRepository(IDataContextWrapper dataContextWrapper)
    {
        DataContextWrapper = dataContextWrapper;
    }

    public User FindBy(int id)
    {
        return DataContextWrapper.Table<User>().SingleOrDefault(u => u.UserID == id);
    }
}

接下来像这样创建 IDataContextWrapper,您可以查看有关该主题的这篇文,我的有点不同:

public interface IDataContextWrapper : IDisposable
{
    Table<T> Table<T>() where T : class;
}

接下来创建 CustomSqlException 类:

public class CustomSqlException : Exception
{
 public CustomSqlException()
 {
 }

 public CustomSqlException(string message, SqlException innerException) : base(message, innerException)
 {
 }
}

这是 IDataContextWrapper 的示例实现:

public class DataContextWrapper<T> : IDataContextWrapper where T : DataContext, new()
{
 private readonly T _db;

 public DataContextWrapper()
 {
        var t = typeof(T);
     _db = (T)Activator.CreateInstance(t);
 }

 public DataContextWrapper(string connectionString)
 {
     var t = typeof(T);
     _db = (T)Activator.CreateInstance(t, connectionString);
 }

 public Table<TableName> Table<TableName>() where TableName : class
 {
        try
        {
            return (Table<TableName>) _db.GetTable(typeof (TableName));
        }
        catch (SqlException exception)
        {
            // Wrap the SqlException with our custom one
            throw new CustomSqlException("Ooops...", exception);
        }
 }

 // IDispoable Members
}
于 2009-09-07T06:22:30.810 回答
5

不确定这是否有帮助,但似乎对这个人有用(非常聪明)。

try
{
    SqlCommand cmd =
        new SqlCommand("raiserror('Manual SQL exception', 16, 1)",DBConn);
    cmd.ExecuteNonQuery();
}
catch (SqlException ex)
{
    string msg = ex.Message; // msg = "Manual SQL exception"
}

发现于: http ://smartypeeps.blogspot.com/2006/06/how-to-throw-sqlexception-in-c.html

于 2009-09-06T23:18:38.540 回答
3

如果您使用的是新的 Microsoft.Data.SqlClient nuget,则可以使用此辅助方法:

public static class SqlExceptionCreator
{
    public static SqlException Create(int number)
    {
        Exception? innerEx = null;
        var c = typeof(SqlErrorCollection).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance);
        SqlErrorCollection errors = (c[0].Invoke(null) as SqlErrorCollection)!;
        var errorList = (errors.GetType().GetField("_errors", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(errors) as List<object>)!;
        c = typeof(SqlError).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance);
        var nineC = c.FirstOrDefault(f => f.GetParameters().Length == 9)!;
        SqlError sqlError = (nineC.Invoke(new object?[] { number, (byte)0, (byte)0, "", "", "", (int)0, (uint)0, innerEx}) as SqlError)!;
        errorList.Add(sqlError);
        SqlException ex = (Activator.CreateInstance(typeof(SqlException), BindingFlags.NonPublic | BindingFlags.Instance, null, new object?[] { "test", errors,
            innerEx, Guid.NewGuid() }, null) as SqlException)!;
        return ex;
    }
}
于 2021-01-11T17:07:57.633 回答
2

这应该有效:

SqlConnection bogusConn = 
    new SqlConnection("Data Source=myServerAddress;Initial
    Catalog=myDataBase;User Id=myUsername;Password=myPassword;");
bogusConn.Open();

在引发异常之前需要一些时间,所以我认为这会更快:

SqlCommand bogusCommand = new SqlCommand();
bogusCommand.ExecuteScalar();

Hacks-R-Us 为您带来的代码。

更新:不,第二种方法会引发 ArgumentException,而不是 SqlException。

更新 2:这工作得更快(SqlException 在不到一秒的时间内抛出):

SqlConnection bogusConn = new SqlConnection("Data Source=localhost;Initial
    Catalog=myDataBase;User Id=myUsername;Password=myPassword;Connection
    Timeout=1");
bogusConn.Open();
于 2009-09-06T23:15:20.047 回答
2

我注意到您的问题已经存在一年了,但作为记录,我想添加一个我最近使用 microsoft Moles 发现的解决方案(您可以在此处找到参考资料Microsoft Moles

一旦你使用了 System.Data 命名空间,你可以简单地在 SqlConnection.Open() 上模拟一个 SQL 异常,如下所示:

//Create a delegate for the SqlConnection.Open method of all instances
        //that raises an error
        System.Data.SqlClient.Moles.MSqlConnection.AllInstances.Open =
            (a) =>
            {
                SqlException myException = new System.Data.SqlClient.Moles.MSqlException();
                throw myException;
            };

我希望这可以帮助将来遇到这个问题的人。

于 2010-09-25T21:57:56.673 回答
2

我建议使用这种方法。

    /// <summary>
    /// Method to simulate a throw SqlException
    /// </summary>
    /// <param name="number">Exception number</param>
    /// <param name="message">Exception message</param>
    /// <returns></returns>
    public static SqlException CreateSqlException(int number, string message)
    {
        var collectionConstructor = typeof(SqlErrorCollection)
            .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, //visibility
                null, //binder
                new Type[0],
                null);
        var addMethod = typeof(SqlErrorCollection).GetMethod("Add", BindingFlags.NonPublic | BindingFlags.Instance);
        var errorCollection = (SqlErrorCollection)collectionConstructor.Invoke(null);
        var errorConstructor = typeof(SqlError).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null,
            new[]
            {
                typeof (int), typeof (byte), typeof (byte), typeof (string), typeof(string), typeof (string),
                typeof (int), typeof (uint)
            }, null);
        var error =
            errorConstructor.Invoke(new object[] { number, (byte)0, (byte)0, "server", "errMsg", "proccedure", 100, (uint)0 });
        addMethod.Invoke(errorCollection, new[] { error });
        var constructor = typeof(SqlException)
            .GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, //visibility
                null, //binder
                new[] { typeof(string), typeof(SqlErrorCollection), typeof(Exception), typeof(Guid) },
                null); //param modifiers
        return (SqlException)constructor.Invoke(new object[] { message, errorCollection, new DataException(), Guid.NewGuid() });
    }
于 2017-05-10T13:28:58.333 回答
2

这些解决方案感觉臃肿。

ctor是内部的,是的。

(不使用反射,最简单的方法就是真正创建这个异常......

   instance.Setup(x => x.MyMethod())
            .Callback(() => new SqlConnection("Server=pleasethrow;Database=anexception;Connection Timeout=1").Open());

也许还有另一种方法不需要 1 秒的超时来抛出。

于 2020-06-05T08:39:09.923 回答
2

我只使用@jjxtra 的方法(我赞成)成功了,但是代码需要修改,因为我使用的是 System.Data.SqlClient,它没有 SqlError 的 9 参数构造函数,并且 SqlErrorCollection 有一个名为“errors”的字段(不是“_errors”),它是 ArrayList 类型(不是 List<object>)。

// 组装位置:C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Data.dll

这是适用于我的修改后的代码:

  public static SqlException CreateSqlException(int number)
  {
    Exception? innerEx = null;
    var c = typeof(SqlErrorCollection).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance);
    SqlErrorCollection errors = (c[0].Invoke(null) as SqlErrorCollection);
    ArrayList errorList = (ArrayList)errors.GetType().GetField("errors", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(errors);
    c = typeof(SqlError).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance);
    var theC = c.FirstOrDefault(f => f.GetParameters().Length == 8);
    SqlError sqlError = (theC.Invoke(new object?[] { number, (byte)0, (byte)0, "", "", "", (int)0, (uint)0}) as SqlError);
    errorList.Add(sqlError);
    SqlException ex = (Activator.CreateInstance(typeof(SqlException), BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { "test", errors,
      innerEx, Guid.NewGuid() }, null) as SqlException);
    return ex;
  }
于 2021-04-02T16:23:16.767 回答
1

(抱歉,已经晚了 6 个月,希望这不会被视为 necroposting 我来到这里寻找如何从模拟中抛出 SqlCeException)。

如果您只需要测试处理异常的代码,一个超简单的解决方法是:

public void MyDataMethod(){
    try
    {
        myDataContext.SubmitChanges();
    }
    catch(Exception ex)
    {
        if(ex is SqlCeException || ex is TestThrowableSqlCeException)
        {
            // handle ex
        }
        else
        {
            throw;
        }
    }
}



public class TestThrowableSqlCeException{
   public TestThrowableSqlCeException(string message){}
   // mimic whatever properties you needed from the SqlException:
}

var repo = new Rhino.Mocks.MockReposity();
mockDataContext = repo.StrictMock<IDecoupleDataContext>();
Expect.Call(mockDataContext.SubmitChanges).Throw(new TestThrowableSqlCeException());
于 2010-03-09T02:01:07.260 回答
1

基于所有其他答案,我创建了以下解决方案:

    [Test]
    public void Methodundertest_ExceptionFromDatabase_Logs()
    {
        _mock
            .Setup(x => x.MockedMethod(It.IsAny<int>(), It.IsAny<string>()))
            .Callback(ThrowSqlException);

        _service.Process(_batchSize, string.Empty, string.Empty);

        _loggermock.Verify(x => x.Error(It.IsAny<string>(), It.IsAny<SqlException>()));
    }

    private static void ThrowSqlException() 
    {
        var bogusConn =
            new SqlConnection(
                "Data Source=localhost;Initial Catalog = myDataBase;User Id = myUsername;Password = myPassword;Connection Timeout = 1");
        bogusConn.Open();
    }
于 2010-11-09T09:45:35.093 回答
1

这真的很老,这里有一些很好的答案。我正在使用 Moq,我无法模拟抽象类并且真的不想使用反射,所以我制作了自己的从 DbException 派生的异常。所以:

public class MockDbException : DbException {
  public MockDbException(string message) : base (message) {}
}   

显然,如果您需要添加 InnerException 或其他任何内容,请添加更多道具、构造函数等。

然后,在我的测试中:

MyMockDatabase.Setup(q => q.Method()).Throws(new MockDbException(myMessage));

希望这将帮助任何使用 Moq 的人。感谢所有在这里发帖的人,他们让我得到了答案。

于 2015-10-12T15:13:52.553 回答
0

您可以使用反射在测试中创建 SqlException 对象:

        ConstructorInfo errorsCi = typeof(SqlErrorCollection).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[]{}, null);
        var errors = errorsCi.Invoke(null);

        ConstructorInfo ci = typeof(SqlException).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(string), typeof(SqlErrorCollection) }, null);
        var sqlException = (SqlException)ci.Invoke(new object[] { "Exception message", errors });
于 2011-07-19T16:57:51.803 回答