Как выбросить SqlException, когда это необходимо для имитации и модульного тестирования?

86

Я пытаюсь протестировать некоторые исключения в своем проекте, и одно из них, которое я улавливаю, - 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");
chobo2
источник
Вы правы. Я обновил свой ответ, но, вероятно, сейчас он не очень полезен. DbException, вероятно, лучшее исключение, которое нужно уловить, так что учитывайте его.
Мэтт Гамильтон,
Ответы, которые действительно работают, приводят к появлению множества сообщений об исключениях. Может оказаться полезным определение того, какой именно тип вам нужен. Например, «Мне нужно SqlException с номером исключения 18487, указывающим, что срок действия указанного пароля истек». Кажется, такое решение больше подходит для модульного тестирования.
Майк Кристиан,

Ответы:

9

Поскольку вы используете Linq to Sql, вот образец тестирования упомянутого вами сценария с использованием NUnit и Moq. Я не знаю точных деталей вашего DataContext и того, что у вас есть в нем. Отредактируйте под свои нужды.

Вам нужно будет обернуть DataContext настраиваемым классом, вы не можете имитировать DataContext с помощью Moq. Вы также не можете издеваться над 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
}
Дейл Рэган
источник
92

Вы можете сделать это с помощью отражения, вам придется поддерживать его, когда 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, что может быть важно.

Сэм Шафран
источник
2
Этот подход работает, вам просто нужно уточнить, какой метод CreateException вам нужен, поскольку есть две перегрузки. Измените вызов GetMethod на: .GetMethod ("CreateException", BindingFlags.NonPublic | BindingFlags.Static, null, CallingConventions.ExplicitThis, new [] {typeof (SqlErrorCollection), typeof (string)}, new ParameterModifier [] {}) И это работает
Эрик Норденхок
Работает на меня. Блестяще.
Ник Патсарис,
4
Переведено в суть с поправками из комментариев. gist.github.com/timabell/672719c63364c497377f - Большое спасибо всем за то, что дали мне выход из этого темного темного места.
Тим Абелл
2
Версия от Бена Дж. Андерсона позволяет указывать сообщение в дополнение к коду ошибки. gist.github.com/benjanderson/07e13d9a2068b32c2911
Тони,
10
Чтобы это работало с 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
Мне нравится эмоция, которую вы добавили в свой ответ. lol спасибо за это решение. Это ежу понятно, и я не знаю, почему я не подумал об этом изначально. еще раз спасибо.
pqsk
1
Отличное решение, просто убедитесь, что у вас нет базы данных с именем GUARANTEED_TO_FAIL на вашем локальном компьютере;)
Амит Джи
Отличный пример KISS
Lup
Это гениально безумное решение
Михаил Сенютович
22

В зависимости от ситуации я обычно предпочитаю 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;
}
default.kramer
источник
4
Это сработало для меня и для установки сообщения об исключении, когда у вас есть объект:typeof(SqlException).GetField("_message", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(exception, "my custom sql message");
Фил Купер
8
Я расширил это, чтобы отразить ErrorMessage и ErrorCode. gist.github.com/benjanderson/07e13d9a2068b32c2911
Бен Андерсон,
13

Изменить Ой: я не понимал, что SqlException запечатан. Я издевался над DbException, абстрактным классом.

Вы не можете создать новое исключение SqlException, но можете имитировать DbException, от которого происходит SqlException. Попробуй это:

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) «получить» для этих свойств.

Мэтт Гамильтон
источник
вам следует добавить ожидания для «Number», которые позволят вам определить тип исключения (тупик, тайм-аут и т. д.)
Сэм Саффрон,
Хм, а как насчет того, чтобы использовать linq to sql? Я вообще-то не открываю (это сделано за меня).
chobo2
Если вы используете Moq, то, вероятно, вы имитируете какую-то операцию с базой данных. Настройте его, чтобы его бросили, когда это произойдет.
Мэтт Гамильтон,
Итак, о фактической операции (фактический метод, который будет вызывать базу данных)?
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 to 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();
MusiGenesis
источник
Это была моя собственная реализация до того, как я наткнулся на эту страницу SU в поисках другого пути, потому что тайм-аут был недопустим. Ваше обновление 2 хорошее, но еще одна секунда. Не подходит для наборов модульных тестов, поскольку не масштабируется.
Джон Дэвис
2

Я заметил, что вашему вопросу один год, но для записи я хотел бы добавить решение, которое я недавно обнаружил с помощью Microsoft Moles (вы можете найти ссылки здесь Microsoft Moles )

После того, как вы смоделировали пространство имен System.Data, вы можете просто имитировать исключение SQL в SqlConnection.Open () следующим образом:

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

Надеюсь, это поможет кому-то, кто задаст этот вопрос в будущем.

FrenchData
источник
1
Несмотря на поздний ответ, это, вероятно, самое чистое решение, особенно если вы уже используете Moles для других целей.
Amandalishus
1
Что ж, вы должны использовать структуру Moles, чтобы это работало. Не совсем идеально, если уже используется MOQ. Это решение отклоняет вызов .NET Framework. Ответ @ default.kramer более уместен. Moles был выпущен в Visual Studio 2012 Ultimate как «Подделки», а затем в VS 2012 Premium с обновлением 2. Я полностью за использование фреймворка Fakes, но придерживаюсь одного фреймворка за раз, ради тех, кто придет. после вас. ;)
Майк Кристиан
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() });
    }
Луис Ланца
источник
Из очереди на рассмотрение : Могу я попросить вас добавить дополнительный контекст к вашему ответу. Ответы только на коде трудно понять. Если вы добавите больше информации в свой пост, это поможет как спрашивающему, так и будущим читателям.
RBT
Вы можете добавить эту информацию, отредактировав сам пост. Сообщение - лучшее место, чем комментарии, для сохранения актуальной информации, относящейся к ответу.
RBT
Это больше не работает, потому SqlExceptionчто не имеет конструктора и errorConstructorбудет иметь значение null.
Эмад
@ Эмад, что вы использовали для решения проблемы?
Саске Учиха
2

Эти решения кажутся раздутыми.

Ctor внутренний, да.

(Без использования отражения, самый простой способ действительно создать это исключение ....

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

Perphaps есть еще один метод, для выполнения которого не требуется тайм-аут в 1 секунду.

Билли Джейк О'Коннор
источник
ха ... так просто, я не знаю, почему я не подумал об этом ... идеально без проблем, и я могу сделать это где угодно.
hal9000
А как насчет настройки сообщения и кода ошибки? Похоже, ваше решение этого не позволяет.
Саске Учиха
@ Саске Учиха уверен, что это не так. Другие решения делают. Но если вам просто нужно выбросить этот тип исключения, вы хотите избежать отражения и не писать много кода, вы можете использовать это решение.
Билли Джейк О'Коннор,
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, и я не могу смоделировать абстрактные классы и действительно не хотел использовать отражение, поэтому я создал собственное исключение, производное от DbException. Так:

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

очевидно, если вам нужно добавить InnerException или что-то еще, добавьте дополнительные реквизиты, конструкторы и т. д.

тогда в моем тесте:

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

Надеюсь, это поможет всем, кто использует Moq. Спасибо всем, кто разместил здесь, что привело меня к моему ответу.

Роб
источник
Когда вам не нужно ничего особенного в SqlException, этот метод работает очень хорошо.
Ральф Уиллгосс,
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 работает правильно.
Майк Кристиан,
1
@MikeChristian Это работает, если вы используете существующий конструктор, например private SqlException(string message, SqlErrorCollection errorCollection, Exception innerException, Guid conId)
Шон Уайлд