Come generare un'eccezione SqlException quando necessario per il mocking e il test di unità?


86

Sto cercando di testare alcune eccezioni nel mio progetto e una delle eccezioni che prendo è SQlException.

Sembra che tu non possa andare, new SqlException()quindi non sono sicuro di come posso lanciare un'eccezione soprattutto senza chiamare in qualche modo il database (e poiché questi sono test unitari, di solito si consiglia di non chiamare il database poiché è lento).

Sto usando NUnit e Moq, ma non sono sicuro di come fingere questo.

Rispondendo ad alcune delle risposte che sembrano essere tutte basate su ADO.NET, nota che sto usando Linq to Sql. Quindi quella roba è come dietro le quinte.

Maggiori informazioni come richiesto da @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()

Pubblica nella prima riga quando tenta di eseguire il mockup

 var ex = new Mock<System.Data.SqlClient.SqlException>();
 ex.SetupGet(e => e.Message).Returns("Exception message");

Hai ragione. Ho aggiornato la mia risposta, ma probabilmente non è molto utile ora. DbException è probabilmente l'eccezione migliore da catturare, quindi considerala.
Matt Hamilton

Le risposte che funzionano effettivamente producono una varietà di messaggi di eccezione risultanti. Può essere utile definire esattamente il tipo di cui hai bisogno. Ad esempio "Ho bisogno di una SqlException che contiene il numero di eccezione 18487, che indica che la password specificata è scaduta". Sembra che una tale soluzione sia più appropriata per i test unitari.
Mike Christian

Risposte:


9

Poiché stai usando Linq to Sql, ecco un esempio di test dello scenario che hai menzionato usando NUnit e Moq. Non conosco i dettagli esatti del tuo DataContext e cosa hai a disposizione in esso. Modifica per le tue esigenze.

Avrai bisogno di racchiudere il DataContext con una classe personalizzata, non puoi simulare il DataContext con Moq. Non puoi nemmeno simulare SqlException, perché è sealed. Dovrai racchiuderlo con la tua classe di eccezione. Non è difficile realizzare queste due cose.

Cominciamo creando il nostro test:

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

Implementiamo il test, per prima cosa racchiudiamo le nostre chiamate da Linq a Sql usando il pattern del repository:

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

Quindi crea l'IDataContextWrapper in questo modo, puoi visualizzare questo post del blog sull'argomento, il mio differisce un po ':

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

Quindi creare la classe CustomSqlException:

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

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

Ecco un'implementazione di esempio di 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

Puoi farlo con la riflessione, dovrai mantenerlo quando Microsoft apporta modifiche, ma funziona, l'ho appena testato:

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

Ciò consente anche di controllare il numero di SqlException, che può essere importante.


2
Questo approccio funziona, devi solo essere più specifico con il metodo CreateException che desideri poiché ci sono due overload. Modificare la chiamata GetMethod in: .GetMethod ("CreateException", BindingFlags.NonPublic | BindingFlags.Static, null, CallingConventions.ExplicitThis, new [] {typeof (SqlErrorCollection), typeof (string)}, new ParameterModifier [] {}) e funziona
Erik Nordenhök

Per me va bene. Brillante.
Nick Patsaris,

4
Trasformato in un succo, con le correzioni dai commenti. gist.github.com/timabell/672719c63364c497377f - Mille grazie a tutti per avermi dato una via d'uscita da questo luogo buio e buio.
Tim Abell

2
La versione di Ben J Anderson consente di specificare il messaggio oltre al codice di errore. gist.github.com/benjanderson/07e13d9a2068b32c2911
Tony

10
Per farlo funzionare con dotnet-core 2.0, cambia la seconda riga del NewSqlExceptionmetodo in:SqlError error = Construct<SqlError>(number, (byte)2, (byte)3, "server name", "error message", "proc", 100, null);
Chuck Spencer

75

Ho una soluzione a questo. Non sono sicuro se sia genio o follia.

Il codice seguente creerà una nuova 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);
}

che puoi quindi usare in questo modo (questo esempio utilizza Moq)

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

in modo da poter testare la gestione degli errori SqlException nei tuoi repository, gestori e controller.

Ora devo andare a sdraiarmi.


10
Ottima soluzione! Ho apportato una modifica per risparmiare tempo nell'attesa della connessione:new SqlConnection(@"Data Source=.;Database=GUARANTEED_TO_FAIL;Connection Timeout=1")
Joanna Derks

2
Amo l'emozione che hai aggiunto alla tua risposta. lol grazie per questa soluzione. È un gioco da ragazzi e non so perché inizialmente non ci ho pensato. grazie ancora.
pqsk

1
Ottima soluzione, assicurati solo di non avere un database chiamato GUARANTEED_TO_FAIL sulla tua macchina locale;)
Amit G

Un ottimo esempio di KISS
Lup

Questa è una soluzione ingegnosamente folle
Mykhailo Seniutovych

22

A seconda della situazione, di solito preferisco GetUninitializedObject per invocare un ConstructorInfo. Devi solo essere consapevole del fatto che non chiama il costruttore - dalle osservazioni di MSDN: "Poiché la nuova istanza dell'oggetto è inizializzata a zero e non vengono eseguiti costruttori, l'oggetto potrebbe non rappresentare uno stato considerato valido da quell'oggetto. " Ma direi che è meno fragile che fare affidamento sull'esistenza di un certo costruttore.

[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
Questo ha funzionato per me e per impostare il messaggio dell'eccezione una volta ottenuto l'oggetto:typeof(SqlException).GetField("_message", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(exception, "my custom sql message");
Phil Cooper

8
Ho esteso questo per riflettere ErrorMessage e ErrorCode. gist.github.com/benjanderson/07e13d9a2068b32c2911
Ben Anderson

13

Modifica Ouch: non mi ero reso conto che SqlException è sigillato. Ho preso in giro DbException, che è una classe astratta.

Non è possibile creare una nuova SqlException, ma è possibile simulare una DbException, da cui deriva SqlException. Prova questo:

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

Quindi la tua eccezione viene generata quando il metodo tenta di aprire la connessione.

Se ti aspetti di leggere qualcosa di diverso dalla Messageproprietà sull'eccezione derisa, non dimenticare di Expect (o Setup, a seconda della tua versione di Moq) il "get" su quelle proprietà.


dovresti aggiungere le aspettative per "Numero" che ti consentono di capire che tipo di eccezione è (deadlock, timeout, ecc.)
Sam Saffron

Hmm che ne dici di quando usi linq a sql? In realtà non faccio un open (è fatto per me).
chobo2

Se stai usando Moq, presumibilmente stai prendendo in giro una sorta di operazione di database. Impostalo per essere lanciato quando ciò accade.
Matt Hamilton

Quindi sull'operazione effettiva (il metodo effettivo che chiamerebbe sul db)?
chobo2

Stai prendendo in giro il tuo comportamento db? Tipo, deridere la tua classe DataContext o qualcosa del genere? Qualunque operazione genererebbe questa eccezione se l'operazione del database restituisse un errore.
Matt Hamilton

4

Non sono sicuro che questo aiuti, ma sembra che abbia funzionato per questa persona (abbastanza intelligente).

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

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


Ho provato questo, ma hai ancora bisogno di un oggetto SqlConnection aperto per ottenere una SqlException generata.
MusiGenesis

Uso linq a sql quindi non faccio queste cose ado.net. È tutto dietro le quinte.
chobo2

2

Questo dovrebbe funzionare:

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

Ci vuole un po 'prima che generi l'eccezione, quindi penso che funzionerebbe ancora più velocemente:

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

Codice fornito da Hacks-R-Us.

Aggiornamento : no, il secondo approccio genera un'ArgumentException, non una SqlException.

Aggiornamento 2 : funziona molto più velocemente (la SqlException viene lanciata in meno di un secondo):

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

Questa era la mia implementazione prima di imbattermi in questa pagina SU cercando un altro modo perché il timeout era inaccettabile. Il tuo aggiornamento 2 è buono ma è ancora un secondo. Non va bene per i set di unit test in quanto non scala.
Jon Davis

2

Ho notato che la tua domanda è vecchia di un anno, ma per la cronaca vorrei aggiungere una soluzione che ho scoperto di recente utilizzando microsoft Moles (puoi trovare riferimenti qui Microsoft Moles )

Dopo aver modificato lo spazio dei nomi System.Data, puoi semplicemente simulare un'eccezione SQL su SqlConnection.Open () in questo modo:

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

Spero che questo possa aiutare qualcuno che risponde a questa domanda in futuro.


1
Nonostante la risposta tardiva, questa è probabilmente la soluzione più pulita, in particolare se stai già utilizzando Moles per altri scopi.
Amandalishus

1
Bene, devi usare il framework Moles, perché funzioni. Non del tutto ideale, quando si utilizza già MOQ. Questa soluzione sta deviando la chiamata a .NET Framework. La risposta di @ default.kramer è più appropriata. Moles è stato rilasciato in Visual Studio 2012 Ultimate come "Fakes" e successivamente in VS 2012 Premium tramite l'aggiornamento 2. Sono d'accordo con l'utilizzo del framework Fakes, ma attenersi a un framework beffardo alla volta, per il bene di quelli a venire Dopo di te. ;)
Mike Christian

2

Suggerisco di utilizzare questo metodo.

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

Dalla coda di revisione : posso chiederti di aggiungere un po 'più di contesto alla tua risposta. Le risposte di solo codice sono difficili da capire. Aiuterà sia il richiedente che i futuri lettori se puoi aggiungere ulteriori informazioni nel tuo post.
RBT

Potresti voler aggiungere queste informazioni modificando il post stesso. Il post è un posto migliore dei commenti per mantenere le informazioni pertinenti relative alla risposta.
RBT

Questo non funziona più perché SqlExceptionnon ha un costruttore e errorConstructorsarà nullo.
Emad

@Emad, cosa hai usato per superare il problema?
Sasuke Uchiha

2

Queste soluzioni sembrano gonfie.

Il ctor è interno, sì.

(Senza usare la riflessione, il modo più semplice per creare veramente questa eccezione ...

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

Forse c'è un altro metodo che non richiede il timeout di 1 secondo per essere lanciato.


hah ... così semplice non so perché non ci ho pensato ... perfetto senza problemi e posso farlo ovunque.
hal9000

Che ne dici di impostare un messaggio e un codice di errore? Sembra che la tua soluzione non lo permetta.
Sasuke Uchiha

@Sasuke Uchiha certo, non è così. Altre soluzioni lo fanno. Ma se hai semplicemente bisogno di lanciare questo tipo di eccezione, vuoi evitare la riflessione e non scrivere molto codice, puoi usare questa soluzione.
Billy Jake O'Connor

1

(Sry, è in ritardo di 6 mesi, spero che questo non sia considerato necroposting Sono atterrato qui alla ricerca di come lanciare un'eccezione SqlCeException da una simulazione).

Se hai solo bisogno di testare il codice che gestisce l'eccezione, una soluzione estremamente semplice sarebbe:

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

Sulla base di tutte le altre risposte ho creato la seguente soluzione:

    [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

Questo è davvero vecchio e ci sono alcune buone risposte qui. Sto usando Moq e non posso simulare classi astratte e davvero non volevo usare la riflessione, quindi ho creato la mia eccezione derivata da DbException. Così:

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

ovviamente, se devi aggiungere InnerException, o qualsiasi altra cosa, aggiungi più oggetti di scena, costruttori, ecc.

quindi, nel mio test:

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

Con entusiasmo questo aiuterà chiunque stia usando Moq. Grazie a tutti quelli che hanno postato qui che mi hanno portato alla mia risposta.


Quando non hai bisogno di nulla di specifico su SqlException, questo metodo funziona davvero bene.
Ralph Willgoss

0

È possibile utilizzare la reflection per creare l'oggetto SqlException nel test:

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

Questo non funzionerà; SqlException non contiene alcun costruttore. La risposta da @ default.kramer funziona correttamente.
Mike Christian

1
@MikeChristian Funziona se usi un costuttore esistente, ad esprivate SqlException(string message, SqlErrorCollection errorCollection, Exception innerException, Guid conId)
Shaun Wilde
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.