在需要进行模拟和单元测试时,如何抛出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.