Jak zgłosić wyjątek SqlException, gdy jest to konieczne do testowania i testowania jednostkowego?

85

Próbuję przetestować kilka wyjątków w moim projekcie i jeden z wyjątków, które łapię, to SQlException.

Wygląda na to, że nie możesz przejść, new SqlException()więc nie jestem pewien, jak mogę zgłosić wyjątek, zwłaszcza bez wywoływania bazy danych (a ponieważ są to testy jednostkowe, zwykle odradza się wywoływanie bazy danych, ponieważ jest wolna).

Używam NUnit i Moq, ale nie wiem, jak to udawać.

Odpowiadając na niektóre odpowiedzi, które wydają się być oparte na ADO.NET, zauważ, że używam Linq to Sql. Więc to jest jak za kulisami.

Więcej informacji na prośbę @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()

Wysyła do pierwszego wiersza, gdy próbuje wykonać makietę

 var ex = new Mock<System.Data.SqlClient.SqlException>();
 ex.SetupGet(e => e.Message).Returns("Exception message");
chobo2
źródło
Masz rację. Zaktualizowałem swoją odpowiedź, ale prawdopodobnie nie jest to teraz zbyt pomocne. DbException jest prawdopodobnie lepszym wyjątkiem do przechwycenia, więc rozważ to.
Matt Hamilton,
Odpowiedzi, które faktycznie działają, powodują powstanie różnorodnych komunikatów o wyjątkach. Pomocne może być dokładne określenie, jakiego typu potrzebujesz. Na przykład „Potrzebuję wyjątku SqlException zawierającego numer wyjątku 18487, wskazujący, że określone hasło wygasło”. Wydaje się, że takie rozwiązanie jest bardziej odpowiednie do testów jednostkowych.
Mike Christian

Odpowiedzi:

9

Ponieważ używasz Linq do Sql, oto próbka testowania wspomnianego scenariusza przy użyciu NUnit i Moq. Nie znam dokładnych szczegółów Twojego DataContext i tego, co w nim masz. Edytuj według swoich potrzeb.

Będziesz musiał opakować DataContext klasą niestandardową, nie możesz Mockować DataContext za pomocą Moq. Nie można również mock SqlException, ponieważ jest on zapieczętowany. Będziesz musiał opakować go własną klasą wyjątków. Osiągnięcie tych dwóch rzeczy nie jest trudne.

Zacznijmy od stworzenia naszego testu:

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

Zaimplementujmy test, najpierw zawińmy nasze wywołania Linq do Sql przy użyciu wzorca repozytorium:

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

Następnie utwórz IDataContextWrapper w ten sposób, możesz wyświetlić ten post na blogu na ten temat, mój trochę się różni:

public interface IDataContextWrapper : IDisposable
{
    Table<T> Table<T>() where T : class;
}

Następnie utwórz klasę CustomSqlException:

public class CustomSqlException : Exception
{
 public CustomSqlException()
 {
 }

 public CustomSqlException(string message, SqlException innerException) : base(message, innerException)
 {
 }
}

Oto przykładowa implementacja 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
}
Dale Ragan
źródło
91

Możesz to zrobić z refleksją, będziesz musiał to utrzymywać, gdy Microsoft wprowadzi zmiany, ale działa, właśnie to przetestowałem:

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

Umożliwia to również kontrolowanie numeru SqlException, który może być ważny.

Sam Saffron
źródło
2
To podejście działa, po prostu musisz być bardziej szczegółowy w przypadku metody CreateException, ponieważ istnieją dwa przeciążenia. Zmień wywołanie GetMethod na: .GetMethod ("CreateException", BindingFlags.NonPublic | BindingFlags.Static, null, CallingConventions.ExplicitThis, new [] {typeof (SqlErrorCollection), typeof (string)}, new ParameterModifier [] {}) I to działa
Erik Nordenhök
Pracuje dla mnie. Znakomity.
Nick Patsaris
4
W podsumowaniu, z poprawkami z komentarzy. gist.github.com/timabell/672719c63364c497377f - Bardzo dziękuję wszystkim za umożliwienie mi wyjścia z tego mrocznego miejsca.
Tim Abell
2
Wersja od Bena J Andersona umożliwia określenie komunikatu oprócz kodu błędu. gist.github.com/benjanderson/07e13d9a2068b32c2911
Tony
9
Aby to zadziałało z dotnet-core 2.0, zmień drugą linię w NewSqlExceptionmetodzie na:SqlError error = Construct<SqlError>(number, (byte)2, (byte)3, "server name", "error message", "proc", 100, null);
Chuck Spencer
75

Mam na to rozwiązanie. Nie jestem pewien, czy to geniusz czy szaleństwo.

Poniższy kod utworzy nowy 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);
}

którego możesz następnie użyć w ten sposób (w tym przykładzie jest używany Moq)

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

aby można było przetestować obsługę błędów SqlException w repozytoriach, programach obsługi i kontrolerach.

Teraz muszę iść i się położyć.

Dylan Beattie
źródło
10
Genialne rozwiązanie! Zrobiłem w nim jedną modyfikację, żeby zaoszczędzić trochę czasu na oczekiwaniu na połączenie:new SqlConnection(@"Data Source=.;Database=GUARANTEED_TO_FAIL;Connection Timeout=1")
Joanna Derks
2
Uwielbiam emocje, które dodałeś do swojej odpowiedzi. lol dzięki za to rozwiązanie. To nie myślenia i nie wiem, dlaczego początkowo o tym nie pomyślałem. jeszcze raz dzięki.
pqsk
1
Świetne rozwiązanie, tylko upewnij się, że nie masz bazy danych o nazwie GUARANTEED_TO_FAIL na swoim lokalnym komputerze;)
Amit G
Świetny przykład KISS
Lup
To genialnie szalone rozwiązanie
Mychajło Seniutowycz
21

W zależności od sytuacji zwykle wolę GetUninitializedObject od wywoływania ConstructorInfo. Musisz tylko mieć świadomość, że nie wywołuje konstruktora - z MSDN Uwagi: „Ponieważ nowe wystąpienie obiektu jest inicjowane na zero i nie są uruchamiane żadne konstruktory, obiekt może nie reprezentować stanu, który jest uważany za prawidłowy przez ten obiekt. " Ale powiedziałbym, że jest mniej kruchy niż poleganie na istnieniu pewnego konstruktora.

[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
źródło
4
To zadziałało dla mnie i aby ustawić wiadomość o wyjątku, gdy już masz obiekt:typeof(SqlException).GetField("_message", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(exception, "my custom sql message");
Phil Cooper
7
Rozszerzyłem to, aby odzwierciedlić ErrorMessage i ErrorCode. gist.github.com/benjanderson/07e13d9a2068b32c2911
Ben Anderson
13

Edytuj Ouch: Nie zdawałem sobie sprawy, że SqlException jest zapieczętowany. Kpiłem z DbException, który jest klasą abstrakcyjną.

Nie można utworzyć nowego SqlException, ale można mockować DbException, z którego SqlException pochodzi. Spróbuj tego:

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

Więc twój wyjątek jest generowany, gdy metoda próbuje otworzyć połączenie.

Jeśli spodziewasz się odczytać cokolwiek innego niż Messagewłaściwość w symulowanym wyjątku, nie zapomnij Oczekiwania (lub Instalatora, w zależności od wersji Moq) polecenia „pobierz” dla tych właściwości.

Matt Hamilton
źródło
powinieneś dodać oczekiwania dotyczące „Numeru”, które pozwolą ci dowiedzieć się, jaki jest typ wyjątku (zakleszczenie, limit czasu itp.)
Sam Saffron
Hmm, a co powiesz na używanie linq do sql? Właściwie nie robię otwarcia (to zrobione dla mnie).
chobo2
Jeśli używasz Moq, prawdopodobnie kpisz z jakiejś operacji na bazie danych. Skonfiguruj go, aby został wyrzucony, gdy tak się stanie.
Matt Hamilton,
A więc w sprawie rzeczywistej operacji (rzeczywistej metody, która wywoła db)?
chobo2,
Czy kpisz ze swojego zachowania db? Na przykład szydzenie z klasy DataContext czy coś? Każda operacja zgłosiłaby ten wyjątek, gdyby operacja bazy danych zwróciła błąd.
Matt Hamilton,
4

Nie jestem pewien, czy to pomaga, ale wydaje się, że zadziałało dla tej osoby (całkiem sprytne).

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

Znalezione pod adresem : http://smartypeeps.blogspot.com/2006/06/how-to-throw-sqlexception-in-c.html

David
źródło
Próbowałem tego, ale nadal potrzebujesz otwartego obiektu SqlConnection, aby uzyskać zgłoszenie SqlException.
MusiGenesis
Używam linq do sql, więc nie robię tego z ado.net. To wszystko za kulisami.
chobo2
2

To powinno działać:

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

Trochę to potrwa, zanim zgłosi wyjątek, więc myślę, że działałoby to jeszcze szybciej:

SqlCommand bogusCommand = new SqlCommand();
bogusCommand.ExecuteScalar();

Kod dostarczony przez Hacks-R-Us.

Aktualizacja : nie, drugie podejście zgłasza ArgumentException, a nie SqlException.

Aktualizacja 2 : to działa znacznie szybciej (SqlException jest rzucany w mniej niż sekundę):

SqlConnection bogusConn = new SqlConnection("Data Source=localhost;Initial
    Catalog=myDataBase;User Id=myUsername;Password=myPassword;Connection
    Timeout=1");
bogusConn.Open();
MusiGenesis
źródło
To była moja własna implementacja, zanim natknąłem się na tę stronę SU, szukając innego sposobu, ponieważ limit czasu był nie do zaakceptowania. Wasza aktualizacja 2 jest dobra, ale to wciąż jedna sekunda. Nie nadaje się do zestawów testów jednostkowych, ponieważ nie jest skalowany.
Jon Davis
2

Zauważyłem, że twoje pytanie ma już rok, ale dla przypomnienia chciałbym dodać rozwiązanie, które odkryłem niedawno przy użyciu Microsoft Moles (możesz znaleźć odniesienia tutaj Microsoft Moles )

Gdy już spolujesz przestrzeń nazw System.Data, możesz po prostu mockować wyjątek SQL w SqlConnection.Open () w następujący sposób:

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

Mam nadzieję, że pomoże to komuś, kto odpowie na to pytanie w przyszłości.

FrenchData
źródło
1
Pomimo późnej odpowiedzi jest to prawdopodobnie najczystsze rozwiązanie, szczególnie jeśli używasz już Moli do innych celów.
Amandalishus
1
Cóż, musisz używać frameworka Moles, aby to działało. Nie do końca idealne, gdy już używasz MOQ. To rozwiązanie omija wywołanie .NET Framework. Bardziej odpowiednia jest odpowiedź @ default.kramer. Moles został wydany w Visual Studio 2012 Ultimate jako „Fakes”, a później w VS 2012 Premium poprzez Update 2. Jestem za korzystaniem z frameworka Fakes, ale trzymam się jednego frameworka na raz, dla dobra tych, którzy mają nadejść po tobie. ;)
Mike Christian
2

Te rozwiązania wydają się rozdęte.

Ktor jest wewnętrzny, tak.

(Bez używania refleksji, najłatwiejszy sposób na autentyczne utworzenie tego wyjątku ....

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

Perphaps jest jeszcze jedna metoda, która nie wymaga limitu czasu 1 sekundy na rzucenie.

Billy Jake O'Connor
źródło
hah ... takie proste, nie wiem, dlaczego o tym nie pomyślałem ... idealne bez kłopotów i mogę to zrobić wszędzie.
hal9000
A co z ustawieniem komunikatu i kodu błędu? Wygląda na to, że Twoje rozwiązanie na to nie pozwala.
Sasuke Uchiha
@Sasuke Uchiha oczywiście, tak nie jest. Inne rozwiązania tak. Ale jeśli po prostu potrzebujesz rzucić tego typu wyjątek, chcesz uniknąć refleksji i nie pisać dużo kodu to możesz skorzystać z tego rozwiązania.
Billy Jake O'Connor
1

(Sry, jest spóźnienie 6 miesięcy, mam nadzieję, że to nie będzie uważane za nekropostę. Wylądowałem tutaj, szukając, jak rzucić SqlCeException z próby).

Jeśli potrzebujesz tylko przetestować kod, który obsługuje wyjątek, bardzo prostym obejściem byłoby:

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());
Grokodile
źródło
1

Na podstawie wszystkich innych odpowiedzi stworzyłem następujące rozwiązanie:

    [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();
    }
khebbie
źródło
1

To jest naprawdę stare i jest tutaj kilka dobrych odpowiedzi. Używam Moq i nie mogę zrobić makiety z klas abstrakcyjnych i naprawdę nie chciałem używać odbicia, więc stworzyłem własny wyjątek pochodzący z DbException. Więc:

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

oczywiście, jeśli chcesz dodać InnerException lub cokolwiek innego, dodaj więcej właściwości, konstruktorów itp.

następnie w moim teście:

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

Hoepfully pomoże to każdemu, kto używa Moq. Dziękuję wszystkim, którzy tutaj napisali, co doprowadziło mnie do mojej odpowiedzi.

Obrabować
źródło
Kiedy nie potrzebujesz niczego konkretnego w SqlException, ta metoda działa naprawdę dobrze.
Ralph Willgoss
1

Proponuję skorzystać z tej metody.

    /// <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() });
    }
Luiz Lanza
źródło
Z kolejki recenzji : Czy mogę prosić o dodanie więcej kontekstu do Twojej odpowiedzi. Odpowiedzi zawierające tylko kod są trudne do zrozumienia. Pomoże to pytającemu i przyszłym czytelnikom, jeśli możesz dodać więcej informacji w swoim poście.
RBT
Możesz chcieć dodać te informacje, edytując sam post. Post jest lepszym miejscem niż komentarze, aby zachować istotne informacje dotyczące odpowiedzi.
RBT
To już nie działa, ponieważ SqlExceptionnie ma konstruktora i errorConstructorbędzie miało wartość null.
Emad
@Emad, czego użyłeś, aby rozwiązać problem?
Sasuke Uchiha
0

Możesz użyć odbicia, aby utworzyć obiekt SqlException w teście:

        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 });
Oleg D.
źródło
To nie zadziała; SqlException nie zawiera konstruktora. Odpowiedź z @ default.kramer działa poprawnie.
Mike Christian,
1
@MikeChristian To działa, jeśli używasz konstruktora, który istnieje, np.private SqlException(string message, SqlErrorCollection errorCollection, Exception innerException, Guid conId)
Shaun Wilde