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


85

我正在尝试测试项目中的一些异常,而我捕获的异常之一是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");

你是对的。我已经更新了我的回复,但是现在可能不是很有帮助。虽然DbException可能是更好的捕获例外,所以请考虑一下。
— Matt Hamilton 2009年

实际起作用的答案会产生各种结果异常消息。准确定义所需的类型可能会有所帮助。例如:“我需要一个包含异常号18487的SqlException,它指示指定的密码已过期。” 似乎这样的解决方案更适合单元测试。
— Mike Christian

Answers:


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,您可以查看有关此主题的博客文章,我的观点有所不同:

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
}

91

您可以通过反射来做到这一点,当Microsoft进行更改时,您将必须维护它,但是我刚刚对其进行了测试:

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


2
这种方法有效,您只需要更加具体地指定所需的CreateException方法,因为有两个重载。将GetMethod调用更改为:.GetMethod(“ CreateException”,BindingFlags.NonPublic | BindingFlags.Static,null,CallingConventions.ExplicitThis,new [] {typeof(SqlErrorCollection),typeof(string)},新的ParameterModifier [] {})和它有效
— ErikNordenhök'13

为我工作。辉煌。
— Nick Patsaris

4
转为要点,并附有评论中的更正。gist.github.com/timabell/672719c63364c497377f-非常感谢大家为我提供了一条远离黑暗之地的出路。
— 蒂姆·阿贝尔2014年

2
Ben J Anderson的版本允许您指定错误代码以及错误代码。 gist.github.com/benjanderson/07e13d9a2068b32c2911
— Tony

9
要使其与dotnet-core 2.0一起使用,请将NewSqlException方法的第二行更改为:SqlError error = Construct<SqlError>(number, (byte)2, (byte)3, "server name", "error message", "proc", 100, null);
— Chuck Spencer

75

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

以下代码将创建一个新的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错误处理。

现在我需要躺下。


10
出色的解决方案!我对其进行了修改,以节省一些等待连接的时间:new SqlConnection(@"Data Source=.;Database=GUARANTEED_TO_FAIL;Connection Timeout=1")
— 乔安娜·德克斯

2
我喜欢您添加到答案中的情感。大声笑谢谢您的解决方案。这是没有道理的,而且我不知道为什么我最初没有想到这一点。再次感谢。
— 2013年

1
很棒的解决方案,只需确保您的本地计算机上没有名为GUARANTEED_TO_FAIL的数据库即可;)
— Amit G

吻的一个很好的例子
— Lup

这
— 真是个

21

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

4
这对我有用,并在您拥有对象后设置异常消息:typeof(SqlException).GetField("_message", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(exception, "my custom sql message");
— Phil Cooper

7
我将其扩展以反映出ErrorMessage和ErrorCode。gist.github.com/benjanderson/07e13d9a2068b32c2911
— Ben Anderson

13

编辑哎哟:我不知道SQLEXCEPTION密封。我一直在嘲笑DbException,这是一个抽象类。

您不能创建新的SqlException,但可以模拟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模拟异常上的属性以外的其他内容,请不要忘记对这些属性的“ get”(期望,或进行设置,具体取决于您的Moq版本)。


您应该添加对“数字”的期望,以使您知道异常的类型是什么(死锁,超时等)
— Sam Saffron 2009年

嗯,当您使用linq to sql怎么样?我实际上并不做公开赛(为我完成了比赛)。
— chobo2

如果您使用的是Moq,那么大概是在模拟某种数据库操作。将其设置为在发生这种情况时抛出。
— Matt Hamilton 2009年

那么在实际操作上(将在db上调用的实际方法)?
— chobo2

您是否在嘲笑数据库行为?例如,嘲笑您的DataContext类?如果数据库操作返回错误,则任何操作都会引发此异常。
— 马特·汉密尔顿,

4

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

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


我试过了,但是您仍然需要一个开放的SqlConnection对象才能引发SqlException。
— MusiGenesis

我将linq用于sql,所以我不这样做ado.net。一切都在幕后。
— chobo2

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

这是我自己的实现,在我遇到此SU页面以寻找另一种方法之前,因为超时是不可接受的。您的Update 2很好,但仍然是一秒钟。不适合单元测试集,因为它无法扩展。
— 乔恩·戴维斯

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

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


1
尽管答案很晚,但这可能是最干净的解决方案,尤其是如果您已经将Moles用于其他目的。
— Amandalishus

1
好吧,您必须使用Moles框架,此功能才能起作用。当已经使用最小起订量时,并不完全理想。此解决方案绕过了对.NET Framework的调用。@ default.kramer的答案更合适。Moles在Visual Studio 2012 Ultimate中以“ Fakes”的形式发布,后来在VS 2012 Premium中通过Update 2发行。我全都使用Fakes框架,但是为了以后的使用,一次只能使用一个模拟框架。在你之后。;)
— Mike Christian

2

这些解决方案感到肿。

ctor是内部的,是的。

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

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

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


呵呵...这么简单,我不知道为什么我没想到这个...完美无烦恼,我可以在任何地方做到这一点。
— hal9000

设置消息和错误代码该怎么办?您的解决方案似乎不允许这样做。
— 浦佐助

@Sasuke Uchiha当然可以,不是。其他解决方案也可以。但是,如果您只需要抛出这种类型的异常,而要避免反射并且不编写太多代码,则可以使用此解决方案。
— Billy Jake O'Connor

1

(很抱歉,迟到了6个月,希望这不会被视为我在这里寻找如何从模拟中抛出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());

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

1

这真的很老,这里有一些很好的答案。我正在使用Moq,并且无法模拟Abstract类,并且确实不想使用反射,因此我将自己的Exception派生自DbException。所以:

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

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

然后,在我的测试中:

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

希望这会对使用Moq的所有人有所帮助。感谢所有张贴在这里的人,这使我得到了答案。


当您不需要SqlException上的任何特定内容时,此方法非常有效。
— 拉尔夫·威戈斯

1

我建议使用这种方法。

    /// <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() });
    }

在审核队列中:我可以请您在答案周围添加更多背景信息。仅代码的答案很难理解。如果您可以在帖子中添加更多信息,它将对询问者和将来的读者都有帮助。
— RBT

您可能需要通过编辑帖子本身来添加此信息。发布要比评论更好,以保持与答案有关的相关信息。
— RBT

这不再起作用,因为SqlException它没有构造函数,并且errorConstructor将为null。
— Emad

@Emad,您用什么来克服这个问题?
— 浦佐助

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

这行不通;SqlException不包含构造函数。@ default.kramer的答案正常工作。
— Mike Christian

1
@MikeChristian如果使用现有的构造函数,则可以正常工作,例如private SqlException(string message, SqlErrorCollection errorCollection, Exception innerException, Guid conId)
— Shaun Wilde
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.