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

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
}

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.


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.


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

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.


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


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

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.


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.


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

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

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.


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

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

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
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.