Як кинути 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, мабуть, є кращим винятком для лову, тому враховуйте його.
Метт Гамільтон,

Відповіді, які насправді працюють, створюють різноманітні повідомлення про винятки. Визначення, який саме тип вам потрібен, може бути корисним. Наприклад, "Мені потрібен SqlException, який містить номер винятку 18487, що вказує, що вказаний пароль минув". Здається, таке рішення більше підходить для модульного тестування.
Mike Christian

Відповіді:


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
}

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, що може бути важливим.


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
Версія від Ben J Anderson дозволяє вказати повідомлення на додаток до коду помилки. gist.github.com/benjanderson/07e13d9a2068b32c2911
Тоні

9
Щоб це працювало з dotnet-core 2.0, змініть другий рядок у NewSqlExceptionметоді на наступний:SqlError error = Construct<SqlError>(number, (byte)2, (byte)3, "server name", "error message", "proc", 100, null);
Чак Спенсер

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
Мені подобаються емоції, які ти додав до своєї відповіді. хай дякую за це рішення. Це ні до чого, і я не знаю, чому спочатку про це не думав. ще раз спасибі.
pqsk

1
Чудове рішення, просто переконайтеся, що на вашій локальній машині немає бази даних GUARANTEED_TO_FAIL;)
Amit G

Прекрасний приклад KISS
Луп

Це геніально божевільне рішення
Михайло Сенютович

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");
Філ Купер

7
Я поширив це, щоб відобразити ErrorMessage та ErrorCode. gist.github.com/benjanderson/07e13d9a2068b32c2911
Бен Андерсон,

13

Edit Ouch: Я не розумів, що 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) "отримати" для цих властивостей.


слід додати очікування щодо "Числа", що дозволяє зрозуміти, що це за виняток (глухий кут, час очікування тощо)
Сем Саффрон,

Хм, як щодо того, коли ви використовуєте linq для sql? Я насправді не роблю відкритий (це зроблено для мене).
chobo2 06

Якщо ви використовуєте Moq, то, мабуть, ви глузуєте над якоюсь операцією з базою даних. Налаштуйте його кидати, коли це станеться.
Метт Гамільтон,

Тож про фактичну операцію (фактичний метод, який би викликав дБ)?
chobo2 07

Ви глузуєте над своєю поведінкою в режимі db? Мовляв, знущання над вашим класом 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, шукаючи інший спосіб, оскільки час очікування був неприйнятним. Ваше оновлення 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;
            };

Сподіваюся, це може допомогти тому, хто поставить це питання в майбутньому.


1
Незважаючи на пізню відповідь, це, мабуть, найчистіше рішення, особливо якщо ви вже використовуєте Moles для інших цілей.
Амандаліш

1
Ну, ви повинні використовувати фреймворк Moles, щоб це функціонувало. Не зовсім ідеально, коли вже використовується MOQ. Це рішення об'їжджає виклик .NET Framework. Відповідь @ default.kramer є більш доречною. Moles був випущений у Visual Studio 2012 Ultimate під назвою "Підробки", а пізніше - у VS 2012 Premium через оновлення 2. Я всі за те, що використовую фейк-фрейм, але дотримуюся однієї насмішкуватої фреймворку заради майбутніх після вас. ;)
Майк Крістіан

2

Ці розчини відчувають роздуття.

Ктор внутрішній, так.

(Без використання роздумів, найпростіший спосіб просто справді створити цей виняток ....

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

Perphaps існує ще один метод, який не вимагає тайм-ауту в 1 секунду.


ха-ха ... так просто, я не знаю, чому я про це не подумав ... ідеально без клопоту, і я можу робити це де завгодно.
hal9000

А як щодо встановлення повідомлення та коду помилки? Здається, ваше рішення цього не дозволяє.
Саске Учіха

@Sasuke Uchiha впевнений, що ні. Інші рішення роблять це. Але якщо вам просто потрібно викинути такий тип винятків, щоб уникнути роздумів і не писати багато коду, ви можете використовувати це рішення.
Біллі Джейк О'Коннор,

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, цей метод працює дуже добре.
Ральф Віллгосс,

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буде нульовим.
Емад

@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)
Шон Уайльд
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.