Como lançar um SqlException quando necessário para simulação e teste de unidade?

86

Estou tentando testar algumas exceções em meu projeto e uma das exceções que pego é SQlException.

Parece que você não pode ir, new SqlException()então não tenho certeza de como posso lançar uma exceção, especialmente sem chamar o banco de dados de alguma forma (e como esses são testes de unidade, geralmente é aconselhável não chamar o banco de dados, pois ele é lento).

Estou usando o NUnit e o Moq, mas não tenho certeza de como fingir.

Respondendo a algumas das respostas que parecem ser baseadas no ADO.NET, observe que estou usando o Linq to Sql. Então, essas coisas são como nos bastidores.

Mais informações solicitadas 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()

Publica na primeira linha quando tenta fazer a maquete

 var ex = new Mock<System.Data.SqlClient.SqlException>();
 ex.SetupGet(e => e.Message).Returns("Exception message");
chobo2
fonte
Você está certo. Atualizei minha resposta, mas provavelmente não é muito útil agora. DbException é provavelmente a melhor exceção a ser capturada, então considere-a.
Matt Hamilton,
As respostas que realmente funcionam produzem uma variedade de mensagens de exceção resultantes. Definir exatamente o tipo de que você precisa pode ser útil. Por exemplo, "I need a SqlException que contém o número de exceção 18487, indicando que a senha especificada expirou." Parece que essa solução é mais apropriada para testes de unidade.
Mike Christian

Respostas:

9

Como você está usando o Linq to Sql, aqui está um exemplo de teste do cenário que você mencionou usando o NUnit e o Moq. Não sei os detalhes exatos do seu DataContext e o que você tem disponível nele. Edite de acordo com suas necessidades.

Você precisará envolver o DataContext com uma classe personalizada, você não pode simular o DataContext com Moq. Você também não pode simular SqlException, porque está lacrado. Você precisará envolvê-lo com sua própria classe de exceção. Não é muito difícil realizar essas duas coisas.

Vamos começar criando nosso teste:

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

Vamos implementar o teste, primeiro vamos envolver nossas chamadas Linq para Sql usando o padrão de repositório:

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

Em seguida, crie o IDataContextWrapper assim, você pode ver esta postagem do blog sobre o assunto, a minha é um pouco diferente:

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

Em seguida, crie a classe CustomSqlException:

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

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

Aqui está um exemplo de implementação do 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
fonte
92

Você pode fazer isso com reflexão, você terá que mantê-lo quando a Microsoft fizer alterações, mas funciona, acabei de testar:

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

Isso também permite que você controle o número da SqlException, que pode ser importante.

Sam Saffron
fonte
2
Essa abordagem funciona, você só precisa ser mais específico com qual método CreateException deseja, pois há duas sobrecargas. Altere a chamada GetMethod para: .GetMethod ("CreateException", BindingFlags.NonPublic | BindingFlags.Static, null, CallingConventions.ExplicitThis, new [] {typeof (SqlErrorCollection), typeof (string)}, novo ParameterModifier [] {}) E funciona
Erik Nordenhök
Funciona para mim. Brilhante.
Nick Patsaris
4
Virou uma essência, com as correções dos comentários. gist.github.com/timabell/672719c63364c497377f - Muito obrigado a todos por me darem uma saída deste lugar escuro e escuro.
Tim Abell
2
A versão de Ben J Anderson permite que você especifique a mensagem, além do código de erro. gist.github.com/benjanderson/07e13d9a2068b32c2911
Tony
10
Para fazer isso funcionar com dotnet-core 2.0, altere a segunda linha no NewSqlExceptionmétodo para ler:SqlError error = Construct<SqlError>(number, (byte)2, (byte)3, "server name", "error message", "proc", 100, null);
Chuck Spencer
75

Eu tenho uma solução para isso. Não tenho certeza se é gênio ou loucura.

O código a seguir criará uma nova 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 você pode usar como tal (este exemplo está usando Moq)

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

para que você possa testar o tratamento de erros SqlException em seus repositórios, manipuladores e controladores.

Agora preciso ir me deitar.

Dylan Beattie
fonte
10
Solução brilhante! Fiz uma modificação para economizar tempo de espera pela conexão:new SqlConnection(@"Data Source=.;Database=GUARANTEED_TO_FAIL;Connection Timeout=1")
Joanna Derks
2
Eu amo a emoção que você adicionou à sua resposta. lol obrigado por esta solução. É um acéfalo e não sei por que não pensei nisso inicialmente. novamente obrigado.
pqsk de
1
Ótima solução, apenas certifique-se de não ter um banco de dados chamado GUARANTEED_TO_FAIL em sua máquina local;)
Amit G
Um ótimo exemplo de KISS
Lup
Esta é uma solução engenhosamente maluca
Mykhailo Seniutovych
22

Dependendo da situação, geralmente prefiro GetUninitializedObject a invocar um ConstructorInfo. Você apenas precisa estar ciente de que ele não chama o construtor - dos comentários do MSDN: "Como a nova instância do objeto é inicializada com zero e nenhum construtor é executado, o objeto pode não representar um estado considerado válido por esse objeto. " Mas eu diria que é menos frágil do que confiar na existência de um certo construtor.

[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
fonte
4
Isso funcionou para mim e para definir a mensagem de exceção assim que você tiver o objeto:typeof(SqlException).GetField("_message", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(exception, "my custom sql message");
Phil Cooper
8
Estendi isso para refletir ErrorMessage e ErrorCode. gist.github.com/benjanderson/07e13d9a2068b32c2911
Ben Anderson
13

Editar Ouch: Eu não percebi que SqlException está selado. Tenho zombado de DbException, que é uma classe abstrata.

Você não pode criar uma nova SqlException, mas pode simular uma DbException, da qual SqlException deriva. Experimente isto:

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

Portanto, sua exceção é lançada quando o método tenta abrir a conexão.

Se você espera ler qualquer coisa diferente da Messagepropriedade na exceção simulada, não se esqueça de esperar (ou configurar, dependendo da sua versão do Moq) o "get" nessas propriedades.

Matt Hamilton
fonte
você deve adicionar expectativas para "Número" que permitem descobrir que tipo de exceção é (impasse, tempo limite etc.)
Sam Saffron
Hmm, e quando você estiver usando linq para sql? Na verdade, eu não faço uma abertura (é feito para mim).
chobo2
Se você estiver usando o Moq, provavelmente está zombando de algum tipo de operação de banco de dados. Configure-o para ser lançado quando isso acontecer.
Matt Hamilton,
Então, na operação real (o método real que chamaria o banco de dados)?
chobo2
Você está zombando de seu comportamento db? Tipo, zombar de sua classe DataContext ou algo assim? Qualquer operação lançaria essa exceção se a operação do banco de dados retornasse um erro.
Matt Hamilton,
4

Não tenho certeza se isso ajuda, mas parece ter funcionado para essa pessoa (muito 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 em: http://smartypeeps.blogspot.com/2006/06/how-to-throw-sqlexception-in-c.html

David
fonte
Eu tentei isso, mas você ainda precisa de um objeto SqlConnection aberto para obter um SqlException lançado.
MusiGenesis
Eu uso linq para sql, então não faço essas coisas ado.net. Está tudo nos bastidores.
chobo2
2

Isso deve funcionar:

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

Isso leva um pouco antes de lançar a exceção, então acho que funcionaria ainda mais rápido:

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

Código trazido a você pela Hacks-R-Us.

Atualizar : não, a segunda abordagem lança uma ArgumentException, não uma SqlException.

Atualização 2 : isso funciona muito mais rápido (a SqlException é lançada em menos de um segundo):

SqlConnection bogusConn = new SqlConnection("Data Source=localhost;Initial
    Catalog=myDataBase;User Id=myUsername;Password=myPassword;Connection
    Timeout=1");
bogusConn.Open();
MusiGenesis
fonte
Esta foi minha própria implementação antes de encontrar esta página SU procurando outra maneira, porque o tempo limite era inaceitável. Sua atualização 2 é boa, mas ainda é um segundo. Não é bom para conjuntos de teste de unidade, pois não é escalonável.
Jon Davis
2

Percebi que sua pergunta tem um ano de idade, mas para registro, gostaria de adicionar uma solução que descobri recentemente usando os Moles da Microsoft (você pode encontrar referências aqui Moles da Microsoft )

Depois de modificar o namespace System.Data, você pode simplesmente simular uma exceção SQL em um 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 isso possa ajudar alguém que tenha essa pergunta no futuro.

FrenchData
fonte
1
Apesar da resposta tardia, esta é provavelmente a solução mais limpa, principalmente se você já estiver usando Moles para outros fins.
Amandalishus
1
Bem, você deve estar usando o framework Moles, para que isso funcione. Não totalmente ideal, quando já estiver usando MOQ. Esta solução está desviando a chamada para o .NET Framework. A resposta por @ default.kramer é mais apropriada. Moles foi lançado no Visual Studio 2012 Ultimate como "Fakes" e, posteriormente, no VS 2012 Premium através da Atualização 2. Eu sou totalmente a favor do framework Fakes, mas me atenha a um framework mocking de cada vez, para o bem daqueles que virão Depois de você. ;)
Mike Christian
2

Eu sugiro 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
fonte
Da fila de revisão : Peço que adicione mais contexto em torno de sua resposta. Respostas apenas em código são difíceis de entender. Ajudará tanto o autor da pergunta quanto os futuros leitores se você puder adicionar mais informações em sua postagem.
RBT
Você pode adicionar essas informações editando a própria postagem. Postar é um lugar melhor do que comentários para manter informações relevantes relacionadas à resposta.
RBT
Isso não funciona mais porque SqlExceptionnão tem um construtor e errorConstructorserá nulo.
Emad
@Emad, o que você usou para superar o problema?
Sasuke Uchiha
2

Essas soluções parecem inchadas.

O ctor é interno, sim.

(Sem usar reflexão, a maneira mais fácil de criar genuinamente essa exceção ....

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

Talvez haja outro método que não exija o tempo limite de 1 segundo para ser lançado.

Billy Jake O'Connor
fonte
hah ... tão simples não sei porque não pensei nisso ... perfeito sem complicações e posso fazer isso em qualquer lugar.
hal9000
Que tal definir uma mensagem e um código de erro? Parece que sua solução não permite isso.
Sasuke Uchiha
@Sasuke Uchiha com certeza, não. Outras soluções sim. Mas se você simplesmente precisa lançar esse tipo de exceção, quer evitar reflexão e não escrever muito código, então você pode usar esta solução.
Billy Jake O'Connor
1

(Sry, está 6 meses atrasado, espero que isso não seja considerado necroposting. Vim aqui procurando como lançar um SqlCeException de um mock).

Se você só precisa testar o código que lida com a exceção, uma solução ultra simples seria:

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

Com base em todas as outras respostas, criei a seguinte solução:

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

Isso é muito antigo e existem algumas boas respostas aqui. Estou usando Moq e não posso simular classes abstratas e realmente não queria usar reflexão, então fiz minha própria exceção derivada de DbException. Então:

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

obviamente, se você precisar adicionar InnerException, ou qualquer outra coisa, adicione mais adereços, construtores, etc.

então, em meu teste:

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

Felizmente, isso ajudará qualquer pessoa que esteja usando o Moq. Obrigado a todos que postaram aqui que me levaram à minha resposta.

Roubar
fonte
Quando você não precisa de nada específico em SqlException, esse método funciona muito bem.
Ralph Willgoss
0

Você pode usar reflexão para criar o objeto SqlException no teste:

        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.
fonte
Isso não funcionará; SqlException não contém nenhum construtor. A resposta de @ default.kramer funciona corretamente.
Mike Christian
1
@MikeChristian Funciona se você usar um construtor que exista, por exemploprivate SqlException(string message, SqlErrorCollection errorCollection, Exception innerException, Guid conId)
Shaun Wilde