Wie kann eine SqlException ausgelöst werden, wenn dies zum Verspotten und Testen von Einheiten erforderlich ist?

83

Ich versuche, einige Ausnahmen in meinem Projekt zu testen, und eine der Ausnahmen, die ich fange, ist SQlException.

Es scheint, dass Sie nicht gehen können, new SqlException()daher bin ich mir nicht sicher, wie ich eine Ausnahme auslösen kann, insbesondere ohne die Datenbank aufzurufen (und da es sich um Komponententests handelt, wird normalerweise empfohlen, die Datenbank nicht aufzurufen, da sie langsam ist).

Ich benutze NUnit und Moq, bin mir aber nicht sicher, wie ich das vortäuschen soll.

Beachten Sie, dass ich bei der Beantwortung einiger Antworten, die anscheinend alle auf ADO.NET basieren, Linq to Sql verwende. Das Zeug ist also wie hinter den Kulissen.

Weitere Informationen wie von @MattHamilton angefordert:

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

Beiträge in der ersten Zeile, wenn versucht wird, ein Modell zu erstellen

 var ex = new Mock<System.Data.SqlClient.SqlException>();
 ex.SetupGet(e => e.Message).Returns("Exception message");
chobo2
quelle
Du hast recht. Ich habe meine Antwort aktualisiert, aber sie ist jetzt wahrscheinlich nicht sehr hilfreich. DbException ist wahrscheinlich die bessere Ausnahme, die es zu fangen gilt.
Matt Hamilton
Die Antworten, die tatsächlich funktionieren, erzeugen eine Vielzahl von resultierenden Ausnahmemeldungen. Es kann hilfreich sein, genau zu definieren, welchen Typ Sie benötigen. Beispiel: "Ich benötige eine SqlException, die die Ausnahmenummer 18487 enthält und angibt, dass das angegebene Kennwort abgelaufen ist." Eine solche Lösung scheint für Unit-Tests besser geeignet zu sein.
Mike Christian

Antworten:

9

Da Sie Linq to Sql verwenden, finden Sie hier ein Beispiel zum Testen des von Ihnen erwähnten Szenarios mit NUnit und Moq. Ich kenne die genauen Details Ihres DataContext nicht und weiß nicht, was Sie darin zur Verfügung haben. Bearbeiten Sie für Ihre Bedürfnisse.

Sie müssen den DataContext mit einer benutzerdefinierten Klasse umschließen. Sie können den DataContext nicht mit Moq verspotten. Sie können SqlException auch nicht verspotten, da es versiegelt ist. Sie müssen es mit Ihrer eigenen Ausnahmeklasse umschließen. Es ist nicht allzu schwierig, diese beiden Dinge zu erreichen.

Beginnen wir mit der Erstellung unseres Tests:

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

Lassen Sie uns den Test implementieren. Lassen Sie uns zuerst unsere Linq-to-Sql-Aufrufe mit dem Repository-Muster umbrechen:

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

Als nächstes erstellen Sie den IDataContextWrapper so, dass Sie diesen Blog-Beitrag zu diesem Thema anzeigen können , meiner unterscheidet sich ein wenig:

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

Erstellen Sie als Nächstes die CustomSqlException-Klasse:

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

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

Hier ist eine Beispielimplementierung des 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
quelle
88

Sie können dies mit Reflection tun. Sie müssen es beibehalten, wenn Microsoft Änderungen vornimmt, aber es funktioniert. Ich habe es gerade getestet:

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

Auf diese Weise können Sie auch die Nummer der SqlException steuern, was wichtig sein kann.

Sam Safran
quelle
2
Dieser Ansatz funktioniert. Sie müssen lediglich genauer festlegen, welche CreateException-Methode Sie verwenden möchten, da zwei Überladungen vorliegen. Ändern Sie den GetMethod-Aufruf in: .GetMethod ("CreateException", BindingFlags.NonPublic | BindingFlags.Static, null, CallingConventions.ExplicitThis, new [] {typeof (SqlErrorCollection), typeof (string)}, new ParameterModifier [] {}) und es funktioniert
Erik Nordenhök
Funktioniert bei mir. Brillant.
Nick Patsaris
4
Verwandelte sich in einen Kern, mit den Korrekturen aus den Kommentaren. gist.github.com/timabell/672719c63364c497377f - Vielen Dank an alle, die mir einen Ausweg aus diesem dunklen, dunklen Ort gegeben haben.
Tim Abell
2
In der Version von Ben J Anderson können Sie zusätzlich zum Fehlercode eine Nachricht angeben. gist.github.com/benjanderson/07e13d9a2068b32c2911
Tony
8
Um dies mit Dotnet-Core 2.0 zum Laufen zu bringen, ändern Sie die zweite Zeile in der NewSqlExceptionMethode wie folgt :SqlError error = Construct<SqlError>(number, (byte)2, (byte)3, "server name", "error message", "proc", 100, null);
Chuck Spencer
74

Ich habe eine Lösung dafür. Ich bin mir nicht sicher, ob es Genie oder Wahnsinn ist.

Der folgende Code erstellt eine neue 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);
}

die Sie dann so verwenden können (dieses Beispiel verwendet Moq)

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

Damit können Sie Ihre SqlException-Fehlerbehandlung in Ihren Repositorys, Handlern und Controllern testen.

Jetzt muss ich mich hinlegen.

Dylan Beattie
quelle
10
Geniale Lösung! Ich habe eine Änderung vorgenommen, um Zeit beim Warten auf die Verbindung zu sparen:new SqlConnection(@"Data Source=.;Database=GUARANTEED_TO_FAIL;Connection Timeout=1")
Joanna Derks
2
Ich liebe die Emotionen, die Sie Ihrer Antwort hinzugefügt haben. lol danke für diese Lösung. Es ist ein Kinderspiel und ich weiß nicht, warum ich anfangs nicht daran gedacht habe. noch einmal Danke.
pqsk
1
Tolle Lösung, stellen Sie einfach sicher, dass Sie keine Datenbank namens GUARANTEED_TO_FAIL auf Ihrem lokalen Computer haben;)
Amit G.
Ein gutes Beispiel für KISS
Lup
Dies ist eine genial verrückte Lösung
Mykhailo Seniutovych
21

Je nach Situation ziehe ich GetUninitializedObject normalerweise dem Aufrufen einer ConstructorInfo vor. Sie müssen sich nur bewusst sein, dass der Konstruktor nicht aufgerufen wird - aus dem MSDN. Anmerkungen: "Da die neue Instanz des Objekts auf Null initialisiert wird und keine Konstruktoren ausgeführt werden, repräsentiert das Objekt möglicherweise keinen Status, der als gültig angesehen wird von diesem Objekt. " Aber ich würde sagen, es ist weniger spröde, als sich auf die Existenz eines bestimmten Konstruktors zu verlassen.

[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
quelle
4
Dies funktionierte für mich und um die Nachricht der Ausnahme zu setzen, sobald Sie das Objekt haben:typeof(SqlException).GetField("_message", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(exception, "my custom sql message");
Phil Cooper
7
Ich habe dies erweitert, um ErrorMessage und ErrorCode wiederzugeben. gist.github.com/benjanderson/07e13d9a2068b32c2911
Ben Anderson
13

Edit Autsch: Ich wusste nicht, dass SqlException versiegelt ist. Ich habe DbException verspottet, eine abstrakte Klasse.

Sie können keine neue SqlException erstellen, aber Sie können eine DbException verspotten, von der SqlException abgeleitet ist. Versuche dies:

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

Ihre Ausnahme wird also ausgelöst, wenn die Methode versucht, die Verbindung zu öffnen.

Wenn Sie erwarten, etwas anderes als die MessageEigenschaft in der verspotteten Ausnahme zu lesen, vergessen Sie nicht, das "get" für diese Eigenschaften zu erwarten (oder Setup, abhängig von Ihrer Moq-Version).

Matt Hamilton
quelle
Sie sollten Erwartungen für "Nummer" hinzufügen, mit denen Sie herausfinden können, um welche Art von Ausnahme es sich handelt (Deadlock, Timeout usw.)
Sam Saffron,
Hmm, wie wäre es, wenn Sie linq to sql verwenden? Ich mache eigentlich kein Open (es ist für mich gemacht).
Chobo2
Wenn Sie Moq verwenden, verspotten Sie vermutlich eine Art Datenbankoperation. Richten Sie es so ein, dass es in diesem Fall geworfen wird.
Matt Hamilton
Also auf die eigentliche Operation (die eigentliche Methode, die die Datenbank aufrufen würde)?
Chobo2
Verspottest du dein Datenbankverhalten? Verspotten Sie Ihre DataContext-Klasse oder so? Unabhängig davon, welche Operation diese Ausnahme auslösen würde, wenn die Datenbankoperation einen Fehler zurückgibt.
Matt Hamilton
4

Ich bin mir nicht sicher, ob dies hilft, scheint aber für diese Person funktioniert zu haben (ziemlich klug).

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

Gefunden unter: http://smartypeeps.blogspot.com/2006/06/how-to-throw-sqlexception-in-c.html

David
quelle
Ich habe es versucht, aber Sie benötigen noch ein offenes SqlConnection-Objekt, um eine SqlException auszulösen.
MusiGenesis
Ich benutze linq to sql, damit ich dieses ado.net-Zeug nicht mache. Es ist alles hinter den Kulissen.
Chobo2
2

Das sollte funktionieren:

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

Es dauert ein bisschen, bis die Ausnahme ausgelöst wird. Ich denke, das würde noch schneller funktionieren:

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

Code von Hacks-R-Us.

Update : Nein, der zweite Ansatz löst eine ArgumentException aus, keine SqlException.

Update 2 : Dies funktioniert viel schneller (die SqlException wird in weniger als einer Sekunde ausgelöst):

SqlConnection bogusConn = new SqlConnection("Data Source=localhost;Initial
    Catalog=myDataBase;User Id=myUsername;Password=myPassword;Connection
    Timeout=1");
bogusConn.Open();
MusiGenesis
quelle
Dies war meine eigene Implementierung, bevor ich auf diese SU-Seite stieß und nach einem anderen Weg suchte, da das Zeitlimit nicht akzeptabel war. Dein Update 2 ist gut, aber es ist immer noch eine Sekunde. Nicht gut für Unit-Test-Sets, da es nicht skaliert.
Jon Davis
2

Ich habe festgestellt, dass Ihre Frage ein Jahr alt ist, aber für die Aufzeichnung möchte ich eine Lösung hinzufügen, die ich kürzlich mit Microsoft Moles entdeckt habe (Referenzen finden Sie hier Microsoft Moles ).

Sobald Sie den System.Data-Namespace geändert haben, können Sie einfach eine SQL-Ausnahme in einer SqlConnection.Open () wie folgt verspotten:

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

Ich hoffe, dies kann jemandem helfen, der diese Frage in Zukunft beantwortet.

FrenchData
quelle
1
Trotz der späten Antwort ist dies wahrscheinlich die sauberste Lösung, insbesondere wenn Sie Maulwürfe bereits für andere Zwecke verwenden.
Amandalishus
1
Nun, Sie müssen das Moles-Framework verwenden, damit dies funktioniert. Nicht ganz ideal, wenn bereits MOQ verwendet wird. Diese Lösung leitet den Aufruf von .NET Framework um. Die Antwort von @ default.kramer ist angemessener. Moles wurde in Visual Studio 2012 Ultimate als "Fakes" und später in VS 2012 Premium über Update 2 veröffentlicht. Ich bin alle dafür, das Fakes-Framework zu verwenden, halte mich aber für die kommenden an jeweils ein spöttisches Framework nach dir. ;)
Mike Christian
1

(Sry, es ist 6 Monate zu spät, ich hoffe, dies wird nicht als Nekroposting angesehen. Ich bin hier gelandet und habe nach einer Möglichkeit gesucht, eine SqlCeException aus einem Mock zu werfen.)

Wenn Sie nur den Code testen müssen, der die Ausnahme behandelt, wäre eine extrem einfache Problemumgehung:

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
quelle
1

Basierend auf all den anderen Antworten habe ich die folgende Lösung erstellt:

    [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
quelle
1

Das ist wirklich alt und hier gibt es einige gute Antworten. Ich verwende Moq und kann abstrakte Klassen nicht verspotten und wollte wirklich keine Reflexion verwenden. Deshalb habe ich meine eigene Ausnahme erstellt, die von DbException abgeleitet ist. So:

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

Wenn Sie InnerException oder was auch immer hinzufügen müssen, fügen Sie natürlich weitere Requisiten, Konstruktoren usw. hinzu.

dann in meinem Test:

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

Hoffentlich hilft dies jedem, der Moq verwendet. Vielen Dank für alle, die hier gepostet haben und mich zu meiner Antwort geführt haben.

rauben
quelle
Wenn Sie für SqlException nichts Spezielles benötigen, funktioniert diese Methode sehr gut.
Ralph Willgoss
1

Ich schlage vor, diese Methode zu verwenden.

    /// <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
quelle
Aus der Überprüfungswarteschlange : Darf ich Sie bitten, Ihrer Antwort einen weiteren Kontext hinzuzufügen. Nur-Code-Antworten sind schwer zu verstehen. Es wird sowohl dem Fragesteller als auch zukünftigen Lesern helfen, wenn Sie Ihrem Beitrag weitere Informationen hinzufügen können.
RBT
Möglicherweise möchten Sie diese Informationen hinzufügen, indem Sie den Beitrag selbst bearbeiten. Post ist ein besserer Ort als Kommentare, um relevante Informationen zur Antwort zu erhalten.
RBT
Dies funktioniert nicht mehr, da SqlExceptiones keinen Konstruktor gibt und errorConstructornull ist.
Emad
1

Diese Lösungen fühlen sich aufgebläht.

Der CTOR ist intern, ja.

(Ohne Reflexion ist dies der einfachste Weg, um diese Ausnahme wirklich zu erstellen.

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

Perphaps gibt es eine andere Methode, für deren Werfen keine Zeitüberschreitung von 1 Sekunde erforderlich ist.

Billy Jake O'Connor
quelle
hah ... so einfach, ich weiß nicht, warum ich nicht daran gedacht habe ... perfekt ohne Ärger und ich kann das überall tun.
hal9000
0

Sie können Reflection verwenden, um ein SqlException-Objekt im Test zu erstellen:

        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.
quelle
Dies wird nicht funktionieren; SqlException enthält keinen Konstruktor. Die Antwort von @ default.kramer funktioniert ordnungsgemäß.
Mike Christian
1
@ MikeChristian Es funktioniert, wenn Sie einen Konstruktor verwenden, der existiert, zBprivate SqlException(string message, SqlErrorCollection errorCollection, Exception innerException, Guid conId)
Shaun Wilde