¿Cómo lanzar una SqlException cuando sea necesario para simulaciones y pruebas unitarias?

85

Estoy tratando de probar algunas excepciones en mi proyecto y una de las excepciones que capturo es SQlException.

Parece que no puede ir, new SqlException()así que no estoy seguro de cómo puedo lanzar una excepción, especialmente sin llamar de alguna manera a la base de datos (y dado que estas son pruebas unitarias, generalmente se recomienda no llamar a la base de datos ya que es lenta).

Estoy usando NUnit y Moq, pero no estoy seguro de cómo fingir esto.

Respondiendo a algunas de las respuestas que parecen estar todas basadas en ADO.NET, tenga en cuenta que estoy usando Linq para Sql. Entonces esas cosas son como detrás de escena.

Más información solicitada por @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()

Publicaciones en la primera línea cuando intenta maquetarse

 var ex = new Mock<System.Data.SqlClient.SqlException>();
 ex.SetupGet(e => e.Message).Returns("Exception message");
chobo2
fuente
Tienes razón. Actualicé mi respuesta, pero probablemente no sea muy útil ahora. Sin embargo, DbException es probablemente la mejor excepción para detectar, así que considérelo.
Matt Hamilton
Las respuestas que realmente funcionan producen una variedad de mensajes de excepción resultantes. Definir exactamente qué tipo necesita puede resultar útil. Por ejemplo, "Necesito una SqlException que contenga el número de excepción 18487, que indica que la contraseña especificada ha caducado". Parece que esta solución es más apropiada para pruebas unitarias.
Mike Christian

Respuestas:

9

Dado que está utilizando Linq to Sql, aquí hay una muestra de prueba del escenario que mencionó utilizando NUnit y Moq. No conozco los detalles exactos de su DataContext y lo que tiene disponible en él. Edite según sus necesidades.

Deberá envolver el DataContext con una clase personalizada, no puede simular el DataContext con Moq. Tampoco puede burlarse de SqlException, porque está sellado. Deberá envolverlo con su propia clase de excepción. No es difícil lograr estas dos cosas.

Comencemos creando nuestra prueba:

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

Implementemos la prueba, primero envolvemos nuestras llamadas de Linq a Sql usando el patrón de repositorio:

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

A continuación, cree el IDataContextWrapper así, puede ver esta publicación de blog sobre el tema, la mía difiere un poco:

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

A continuación, cree la clase CustomSqlException:

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

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

Aquí hay una implementación de muestra de 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
fuente
91

Puede hacer esto con reflexión, tendrá que mantenerlo cuando Microsoft haga cambios, pero funciona, lo acabo de probar:

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

Esto también le permite controlar el número de SqlException, que puede ser importante.

Sam Saffron
fuente
2
Este enfoque funciona, solo necesita ser más específico con el método CreateException que desea, ya que hay dos sobrecargas. Cambie la llamada GetMethod a: .GetMethod ("CreateException", BindingFlags.NonPublic | BindingFlags.Static, null, CallingConventions.ExplicitThis, new [] {typeof (SqlErrorCollection), typeof (string)}, new ParameterModifier [] {}) Y funciona
Erik Nordenhök
Funciona para mi. Brillante.
Nick Patsaris
4
Convertido en una esencia, con las correcciones de los comentarios. gist.github.com/timabell/672719c63364c497377f - Muchas gracias a todos por darme una salida de este oscuro lugar oscuro.
Tim Abell
2
La versión de Ben J Anderson le permite especificar el mensaje además del código de error. gist.github.com/benjanderson/07e13d9a2068b32c2911
Tony
9
Para que esto funcione con dotnet-core 2.0, cambie la segunda línea en el NewSqlExceptionmétodo para que lea:SqlError error = Construct<SqlError>(number, (byte)2, (byte)3, "server name", "error message", "proc", 100, null);
Chuck Spencer
75

Tengo una solución para esto. No estoy seguro de si es genialidad o locura.

El siguiente código creará una nueva 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);
}

que luego puede usar así (este ejemplo está usando Moq)

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

para que pueda probar su manejo de errores SqlException en sus repositorios, manejadores y controladores.

Ahora necesito acostarme.

Dylan Beattie
fuente
10
¡Solución brillante! Hice una modificación para ahorrar algo de tiempo esperando la conexión:new SqlConnection(@"Data Source=.;Database=GUARANTEED_TO_FAIL;Connection Timeout=1")
Joanna Derks
2
Me encanta la emoción que agregaste a tu respuesta. jajaja gracias por esta solución. Es una obviedad y no sé por qué no pensé en esto inicialmente. gracias otra véz.
pqsk
1
Gran solución, solo asegúrese de no tener una base de datos llamada GUARANTEED_TO_FAIL en su máquina local;)
Amit G
Un gran ejemplo de KISS
Lup
Esta es una solución ingeniosamente loca
Mykhailo Seniutovych
21

Dependiendo de la situación, normalmente prefiero GetUninitializedObject a invocar un ConstructorInfo. Solo debe tener en cuenta que no llama al constructor, de las observaciones de MSDN: "Debido a que la nueva instancia del objeto se inicializa a cero y no se ejecuta ningún constructor, es posible que el objeto no represente un estado que se considere válido por ese objeto ". Pero yo diría que es menos frágil que confiar en la existencia de cierto constructor.

[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
fuente
4
Esto funcionó para mí, y para establecer el mensaje de la excepción una vez que tenga el objeto:typeof(SqlException).GetField("_message", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(exception, "my custom sql message");
Phil Cooper
7
Extendí esto para reflejar ErrorMessage y ErrorCode. gist.github.com/benjanderson/07e13d9a2068b32c2911
Ben Anderson
13

Editar Ouch: No me di cuenta de que SqlException está sellada. Me he estado burlando de DbException, que es una clase abstracta.

No puede crear una nueva SqlException, pero puede simular una DbException, de la que se deriva SqlException. Prueba esto:

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

Entonces, su excepción se lanza cuando el método intenta abrir la conexión.

Si espera leer otra cosa que no sea la Messagepropiedad en la excepción simulada, no olvide Esperar (o Configurar, dependiendo de su versión de Moq) el "obtener" en esas propiedades.

Matt Hamilton
fuente
debe agregar expectativas para "Número" que le permitan averiguar qué tipo de excepción es (punto muerto, tiempo de espera, etc.)
Sam Saffron
Hmm, ¿qué tal cuando usas linq para sql? En realidad, no hago una apertura (está hecho para mí).
Chobo2
Si está utilizando Moq, presumiblemente se está burlando de algún tipo de operación de base de datos. Configúrelo para que se lance cuando eso suceda.
Matt Hamilton
Entonces, ¿en la operación real (el método real que llamaría a la base de datos)?
chobo2
¿Te estás burlando de tu comportamiento de db? ¿Burlarse de su clase DataContext o algo así? Cualquier operación produciría esta excepción si la operación de la base de datos devolviera un error.
Matt Hamilton
4

No estoy seguro de si esto ayuda, pero parece haber funcionado para esta persona (bastante inteligente).

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

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

David
fuente
Intenté esto, pero aún necesita un objeto SqlConnection abierto para que se lance una SqlException.
MusiGenesis
Uso linq para sql, así que no hago estas cosas de ado.net. Todo está detrás de escena.
Chobo2
2

Esto debería funcionar:

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

Eso toma un poco antes de que arroje la excepción, así que creo que esto funcionaría aún más rápido:

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

Código presentado por Hacks-R-Us.

Actualización : no, el segundo enfoque arroja una ArgumentException, no una SqlException.

Actualización 2 : esto funciona mucho más rápido (la SqlException se lanza en menos de un segundo):

SqlConnection bogusConn = new SqlConnection("Data Source=localhost;Initial
    Catalog=myDataBase;User Id=myUsername;Password=myPassword;Connection
    Timeout=1");
bogusConn.Open();
MusiGenesis
fuente
Esta fue mi propia implementación antes de encontrarme con esta página SU buscando otra forma porque el tiempo de espera era inaceptable. Tu Actualización 2 es buena pero aún es un segundo. No es bueno para conjuntos de pruebas unitarias, ya que no escala.
Jon Davis
2

Me di cuenta de que su pregunta tiene un año, pero para que conste, me gustaría agregar una solución que descubrí recientemente usando microsoft Moles (puede encontrar referencias aquí Microsoft Moles )

Una vez que haya modelado el espacio de nombres System.Data, simplemente puede simular una excepción SQL en un SqlConnection.Open () como este:

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

Espero que esto pueda ayudar a alguien que tenga esta pregunta en el futuro.

FrenchData
fuente
1
A pesar de la respuesta tardía, esta es probablemente la solución más limpia, especialmente si ya está usando Moles para otros fines.
Amandalishus
1
Bueno, debes estar usando el marco Moles para que esto funcione. No es del todo ideal, cuando ya se usa MOQ. Esta solución está desviando la llamada a .NET Framework. La respuesta de @ default.kramer es más apropiada. Moles fue lanzado en Visual Studio 2012 Ultimate como "Fakes", y más tarde en VS 2012 Premium a través de la Actualización 2. Estoy a favor de usar el marco de Fakes, pero me quedo con un marco de burla a la vez, por el bien de los que vendrán. Después de ti. ;)
Mike Christian
2

Estas soluciones se sienten hinchadas.

El ctor es interno, sí.

(Sin usar la reflexión, la forma más fácil de crear realmente esta excepción ...

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

Perphaps hay otro método que no requiere el tiempo de espera de 1 segundo para lanzar.

Billy Jake O'Connor
fuente
ja ... tan simple que no sé por qué no pensé en esto ... perfecto sin problemas y puedo hacer esto en cualquier lugar.
hal9000
¿Qué hay de configurar un mensaje y un código de error? Parece que tu solución no lo permite.
Sasuke Uchiha
@ Sasuke Uchiha seguro, no es así. Otras soluciones lo hacen. Pero si simplemente necesita lanzar este tipo de excepción, desea evitar la reflexión y no escribir mucho código, puede usar esta solución.
Billy Jake O'Connor
1

(Sry es 6 meses tarde, espero que esto no se considere necroposting. Aterricé aquí buscando cómo lanzar una SqlCeException desde una simulación).

Si solo necesita probar el código que maneja la excepción, una solución alternativa ultra simple sería:

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

Basado en todas las otras respuestas, creé la siguiente solución:

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

Esto es muy antiguo y aquí hay algunas buenas respuestas. Estoy usando Moq, y no puedo simular clases abstractas y realmente no quería usar la reflexión, así que hice mi propia excepción derivada de DbException. Entonces:

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

obviamente, si necesita agregar InnerException, o lo que sea, agregue más accesorios, constructores, etc.

luego, en mi prueba:

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

Con suerte, esto ayudará a cualquiera que esté usando Moq. Gracias a todos los que publicaron aquí que me llevaron a mi respuesta.

Robar
fuente
Cuando no necesita nada específico en SqlException, este método funciona muy bien.
Ralph Willgoss
1

Sugiero usar este método.

    /// <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
fuente
De la cola de revisión : ¿Puedo solicitarle que agregue más contexto en torno a su respuesta? Las respuestas de solo código son difíciles de entender. Ayudará tanto al autor de la pregunta como a los futuros lectores si puede agregar más información en su publicación.
RBT
Es posible que desee agregar esta información editando la publicación en sí. La publicación es un lugar mejor que los comentarios para mantener información relevante relacionada con la respuesta.
RBT
Esto ya no funciona porque SqlExceptionno tiene un constructor y errorConstructorserá nulo.
Emad
@Emad, ¿qué usaste para superar el problema?
Sasuke Uchiha
0

Puede usar la reflexión para crear el objeto SqlException en la prueba:

        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.
fuente
Esto no funcionará; SqlException no contiene ningún constructor. La respuesta de @ default.kramer funciona correctamente.
Mike Christian
1
@MikeChristian Funciona si usa un constructor que existe, por ejemploprivate SqlException(string message, SqlErrorCollection errorCollection, Exception innerException, Guid conId)
Shaun Wilde