如何在需要进行模拟和单元测试时抛出SqlException?

时间:2009-09-06 23:11:43

标签: .net asp.net-mvc moq sqlexception

我正在尝试在我的项目中测试一些异常,其中一个例外是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");

16 个答案:

答案 0 :(得分:75)

你可以用反射做到这一点,你必须在微软进行更改时维护它,但它确实有效我只是测试了它:

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的数量,这可能很重要。

答案 1 :(得分:61)

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

以下代码将创建一个新的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);
}

然后您可以这样使用(此示例使用Moq)

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

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

现在我需要去躺下。

答案 2 :(得分:17)

根据具体情况,我通常更喜欢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;
}

答案 3 :(得分:13)

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

您无法创建新的SqlException,但您可以模拟SbException派生自的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属性以外的任何内容,请不要忘记Expect(或安装程序,具体取决于您的Moq版本)对这些属性的“获取”。

答案 4 :(得分:9)

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

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

让我们从创建测试开始:

[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包装到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,你可以在这个主题上查看这个blog post,我的有点不同:

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
}

答案 5 :(得分: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为您带来的代码。

更新:nope,第二种方法抛出ArgumentException,而不是SqlException。

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

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

答案 6 :(得分:2)

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

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

答案 7 :(得分: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;
            };

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

答案 8 :(得分:2)

这些解决方案感到肿。

ctor是内部的,是的。

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

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

表示,还有另一种方法不需要一秒钟的超时就可以抛出。

答案 9 :(得分:1)

(Sry已经晚了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());

答案 10 :(得分: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();
    }

答案 11 :(得分:1)

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

MyMockDatabase.Setup(q => q.Method()).Throws(new MockDbException(myMessage));
显然,如果你需要添加InnerException,或者其他什么,可以添加更多的道具,构造函数等。

然后,在我的测试中:

{{1}}

这可以帮助任何使用Moq的人。感谢所有发布在这里的人,让我得到了答案。

答案 12 :(得分:1)

我建议使用这种方法。

int numberofmove = steps.count();

答案 13 :(得分:1)

如果您使用的是新的 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;
    }
}

答案 14 :(得分: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 });

答案 15 :(得分:0)

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

// 程序集位置: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;
  }