116 votes

Comment lancer une SqlException lorsque cela est nécessaire pour le mocking et les tests unitaires ?

J'essaie de tester certaines exceptions dans mon projet et l'une des exceptions que j'attrape est la suivante SQlException .

Il semble que tu ne puisses pas aller new SqlException() Je ne suis donc pas sûr de pouvoir lever une exception, notamment sans appeler la base de données (et comme il s'agit de tests unitaires, il est généralement conseillé de ne pas appeler la base de données, car cela est lent).

J'utilise NUnit et Moq, mais je ne sais pas comment simuler cela.

Pour répondre à certaines des réponses qui semblent toutes basées sur ADO.NET, notez que j'utilise Linq to Sql. Donc ce genre de choses se passe dans les coulisses.

Plus d'informations comme demandé par @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()

Les messages à la première ligne lorsqu'il tente de faire une maquette

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

1 votes

Vous avez raison. J'ai mis à jour ma réponse, mais elle n'est probablement pas très utile maintenant. DbException est probablement la meilleure exception à attraper, alors pensez-y.

0 votes

Les réponses qui fonctionnent réellement produisent une variété de messages d'exception. Il peut être utile de définir exactement le type dont vous avez besoin. Par exemple : "J'ai besoin d'une SqlException qui contient le numéro d'exception 18487, indiquant que le mot de passe spécifié a expiré." Il semble qu'une telle solution soit plus appropriée pour les tests unitaires.

115voto

Dylan Beattie Points 23222

J'ai une solution à ce problème. Je ne sais pas si c'est du génie ou de la folie.

Le code suivant va créer une nouvelle 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 vous pouvez ensuite utiliser comme suit (cet exemple utilise Moq)

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

afin que vous puissiez tester votre gestion des erreurs SqlException dans vos référentiels, gestionnaires et contrôleurs.

Maintenant, je dois aller m'allonger.

15 votes

Brillante solution ! J'y ai apporté une modification pour gagner du temps en attendant la connexion : new SqlConnection(@"Data Source=.;Database=GUARANTEED_TO_FAIL;Connection Timeout=1")

3 votes

J'adore l'émotion que tu as ajouté à ta réponse. lol merci pour cette solution. C'est une évidence et je ne sais pas pourquoi je n'y ai pas pensé au début. Encore merci.

4 votes

Excellente solution, assurez-vous simplement que vous n'avez pas une base de données appelée GUARANTEED_TO_FAIL sur votre machine locale ;)

102voto

Sam Saffron Points 56236

Vous pouvez faire cela avec la réflexion, vous devrez la maintenir lorsque Microsoft apportera des changements, mais cela fonctionne, je viens de le tester :

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

Cela vous permet également de contrôler le numéro de l'exception SqlException, ce qui peut être important.

2 votes

Cette approche fonctionne, vous devez juste être plus spécifique avec la méthode CreateException que vous voulez car il y a deux surcharges. Changez l'appel GetMethod en : .GetMethod("CreateException", BindingFlags.NonPublic | BindingFlags.Static, null, CallingConventions.ExplicitThis, new[] {typeof (SqlErrorCollection), typeof (string)}, new ParameterModifier[] {}) Et cela fonctionne

0 votes

Ça marche pour moi. Brillant.

5 votes

Transformé en un résumé, avec les corrections des commentaires. gist.github.com/timabell/672719c63364c497377f - Merci à tous de m'avoir permis de sortir de cet endroit sombre.

36voto

default.kramer Points 3119

Selon la situation, je préfère généralement GetUninitializedObject pour invoquer un ConstructorInfo. Vous devez juste être conscient qu'il n'appelle pas le constructeur - d'après les remarques du MSDN : "Parce que la nouvelle instance de l'objet est initialisée à zéro et qu'aucun constructeur n'est exécuté, l'objet peut ne pas représenter un état qui est considéré comme valide par cet objet." Mais je dirais que c'est moins fragile que de s'appuyer sur l'existence d'un certain constructeur.

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

7 votes

Cela a fonctionné pour moi, et pour définir le message de l'exception une fois que vous avez l'objet : typeof(SqlException).GetField("_message", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(exception, "my custom sql message");

10 votes

Je l'ai étendu pour refléter les ErrorMessage et ErrorCode. gist.github.com/benjanderson/07e13d9a2068b32c2911

13voto

Matt Hamilton Points 98268

Modifier Ouch : Je n'avais pas réalisé que SqlException était scellé. J'ai fait une simulation de DbException, qui est une classe abstraite.

Vous ne pouvez pas créer une nouvelle exception SqlException, mais vous pouvez simuler une exception DbException, dont l'exception SqlException dérive. Essayez ceci :

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

Ainsi, votre exception est levée lorsque la méthode tente d'ouvrir la connexion.

Si vous pensez lire autre chose que le Message sur l'exception simulée, n'oubliez pas d'attendre (ou de configurer, selon votre version de Moq) le "get" sur ces propriétés.

0 votes

Vous devriez ajouter des attentes pour "Number" qui vous permettent de déterminer le type d'exception (deadlock, timeout etc)

0 votes

Hmm, et quand vous utilisez linq to sql ? Je ne fais pas vraiment d'ouverture (c'est fait pour moi).

0 votes

Si vous utilisez Moq, vous devez probablement simuler une sorte d'opération de base de données. Configurez-le pour qu'il soit lancé lorsque cela se produit.

9voto

Dale Ragan Points 14495

Puisque vous utilisez Linq to Sql, voici un exemple de test du scénario que vous avez mentionné en utilisant NUnit et Moq. Je ne connais pas les détails exacts de votre DataContext et de ce que vous y avez à disposition. Modifiez pour vos besoins.

Vous devrez envelopper le DataContext avec une classe personnalisée, vous ne pouvez pas simuler le DataContext avec Moq. Vous ne pouvez pas non plus simuler SqlException, car elle est scellée. Vous devrez l'envelopper avec votre propre classe d'exception. Il n'est pas très difficile d'accomplir ces deux choses.

Commençons par créer notre 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);
}

Mettons en œuvre le test, d'abord nous enveloppons nos appels Linq to Sql en utilisant le modèle de dépôt :

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

Ensuite, créez l'IDataContextWrapper comme suit, vous pouvez voir ceci article de blog sur le sujet, le mien diffère un peu :

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

Créez ensuite la classe CustomSqlException :

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

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

Voici un exemple d'implémentation de l'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
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X