Hoe gooi je een SqlException wanneer dat nodig is voor mocking en unit testing?

Ik probeer enkele uitzonderingen in mijn project te testen en een van de uitzonderingen die ik tegenkom is SQlException.

Het lijkt erop dat je new SqlException()niet kunt gebruiken, dus ik weet niet zeker hoe ik een uitzondering kan maken, vooral zonder de database op de een of andere manier aan te roepen (en aangezien dit eenheidstests zijn, wordt het meestal aangeraden om de database niet aan te roepen omdat deze traag is).

Ik gebruik NUnit en Moq, maar ik weet niet zeker hoe ik dit moet faken.

Als antwoord op enkele van de antwoorden die allemaal op ADO.NET lijken te zijn gebaseerd, moet u er rekening mee houden dat ik Linq to Sql gebruik. Dus dat spul is als achter de schermen.

Meer info op verzoek van @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()

Bericht op de eerste regel wanneer deze probeert te modelleren

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

Antwoord 1, autoriteit 100%

Je kunt dit doen met reflectie, je zult het moeten onderhouden als Microsoft wijzigingen aanbrengt, maar het werkt wel. Ik heb het zojuist getest:

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

Hiermee kunt u ook het nummer van de SqlException bepalen, wat belangrijk kan zijn.


Antwoord 2, autoriteit 92%

Ik heb hier een oplossing voor. Ik weet niet zeker of het geniaal of waanzin is.

De volgende code maakt een nieuwe SqlException aan:

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

die je dan zo kunt gebruiken (dit voorbeeld gebruikt Moq)

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

zodat u uw SqlException-foutafhandeling kunt testen in uw repositories, handlers en controllers.

Nu moet ik gaan liggen.


Antwoord 3, autoriteit 26%

Afhankelijk van de situatie geef ik meestal de voorkeur aan GetUninitializedObjectom een ​​ConstructorInfo aan te roepen. U moet zich er alleen van bewust zijn dat het de constructor niet aanroept – van de MSDN Opmerkingen: “Omdat de nieuwe instantie van het object is geïnitialiseerd op nul en er geen constructors worden uitgevoerd, vertegenwoordigt het object mogelijk geen status die als geldig wordt beschouwd door dat voorwerp.” Maar ik zou zeggen dat het minder broos is dan te vertrouwen op het bestaan ​​van een bepaalde 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;
}

Antwoord 4, autoriteit 14%

BewerkenOuch: ik wist niet dat SqlException verzegeld is. Ik heb de spot gedreven met DbException, wat een abstracte klasse is.

U kunt geen nieuwe SqlException maken, maar u kunt wel een DbException maken, waarvan SqlException is afgeleid. Probeer dit:

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

Dus uw uitzondering wordt gegenereerd wanneer de methode probeert de verbinding te openen.

Als u iets anders verwacht te lezen dan de eigenschap Messageop de bespotte uitzondering, vergeet dan niet om de “get” op die eigenschappen te verwachten (of in te stellen, afhankelijk van uw versie van Moq) .


Antwoord 5, autoriteit 9%

Aangezien je Linq to Sql gebruikt, is hier een voorbeeld van het testen van het scenario dat je noemde met NUnit en Moq. Ik ken de exacte details van uw DataContext niet en wat u daarin beschikbaar heeft. Bewerk voor uw behoeften.

U moet de DataContext omwikkelen met een aangepaste klasse, u kunt de DataContext niet bespotten met Moq. Je kunt SqlException ook niet bespotten, omdat het verzegeld is. Je moet het inpakken met je eigen Exception-klasse. Het is niet zo moeilijk om deze twee dingen te bereiken.

Laten we beginnen met het maken van onze 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);
}

Laten we de test implementeren, laten we eerst onze Linq naar Sql-aanroepen inpakken met behulp van het repositorypatroon:

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

Maak vervolgens de IDataContextWrapper zo, je kunt deze blogberichtover dit onderwerp, de mijne verschilt een beetje:

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

Maak vervolgens de CustomSqlException-klasse:

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

Hier is een voorbeeldimplementatie van 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
}

Antwoord 6, autoriteit 5%

Ik weet niet zeker of dit helpt, maar het lijkt voor deze persoon te hebben gewerkt (best slim).

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

Gevonden op:
http://smartypeeps.blogspot.com/ 2006/06/how-to-throw-sqlception-in-c.html


Antwoord 7, autoriteit 3%

Als u de nieuwe Microsoft.Data.SqlClient-nuget gebruikt, kunt u deze hulpmethode gebruiken:

public static class SqlExceptionCreator
{
    public static SqlException Create(int number)
    {
        Exception? innerEx = null;
        var c = typeof(SqlErrorCollection).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance);
        SqlErrorCollection errors = (c[0].Invoke(null) as SqlErrorCollection)!;
        var errorList = (errors.GetType().GetField("_errors", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(errors) as List<object>)!;
        c = typeof(SqlError).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance);
        var nineC = c.FirstOrDefault(f => f.GetParameters().Length == 9)!;
        SqlError sqlError = (nineC.Invoke(new object?[] { number, (byte)0, (byte)0, "", "", "", (int)0, (uint)0, innerEx}) as SqlError)!;
        errorList.Add(sqlError);
        SqlException ex = (Activator.CreateInstance(typeof(SqlException), BindingFlags.NonPublic | BindingFlags.Instance, null, new object?[] { "test", errors,
            innerEx, Guid.NewGuid() }, null) as SqlException)!;
        return ex;
    }
}

Antwoord 8, autoriteit 2%

Dit zou moeten werken:

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

Dat duurt even voordat de uitzondering wordt gegenereerd, dus ik denk dat dit nog sneller zou werken:

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

Code aangeboden door Hacks-R-Us.

Update: nee, de tweede benadering levert een ArgumentException op, geen SqlException.

Update 2: dit werkt veel sneller (de SqlException wordt in minder dan een seconde gegenereerd):

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

Antwoord 9, autoriteit 2%

Ik heb gemerkt dat uw vraag een jaar oud is, maar voor de goede orde wil ik graag een oplossing toevoegen die ik onlangs heb ontdekt met Microsoft Moles (u kunt hier referenties vinden Microsoft Moles)

Als je eenmaal de System.Data-naamruimte hebt gevormd, kun je eenvoudig een SQL-uitzondering op een SqlConnection.Open() als volgt spotten:

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

Ik hoop dat dit iemand kan helpen die deze vraag in de toekomst beantwoordt.


Antwoord 10, autoriteit 2%

Ik raad aan om deze methode te gebruiken.

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

Antwoord 11, autoriteit 2%

Deze oplossingen voelen opgeblazen.

De ctor is intern, ja.

(Zonder reflectie, de gemakkelijkste manier om deze uitzondering echt te maken….

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

Misschien is er een andere methode die niet de time-out van 1 seconde vereist om te gooien.


Antwoord 12, autoriteit 2%

Ik was alleen succesvol met de aanpak van @jjxtra (die ik heb gestemd), maar de code moest worden gewijzigd omdat ik System.Data.SqlClient gebruik die geen 9 parameterconstructor voor SqlError heeft, en SqlErrorCollection heeft een veld met de naam “errors” (niet “_errors”) van het type ArrayList (niet List<object>).

// Assemblagelocatie: C:\Windows\Microsoft.NET\Framework\v4.0.30319\System.Data.dll

Hier is de aangepaste code die voor mij werkt:

 public static SqlException CreateSqlException(int number)
  {
    Exception? innerEx = null;
    var c = typeof(SqlErrorCollection).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance);
    SqlErrorCollection errors = (c[0].Invoke(null) as SqlErrorCollection);
    ArrayList errorList = (ArrayList)errors.GetType().GetField("errors", BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(errors);
    c = typeof(SqlError).GetConstructors(BindingFlags.NonPublic | BindingFlags.Instance);
    var theC = c.FirstOrDefault(f => f.GetParameters().Length == 8);
    SqlError sqlError = (theC.Invoke(new object?[] { number, (byte)0, (byte)0, "", "", "", (int)0, (uint)0}) as SqlError);
    errorList.Add(sqlError);
    SqlException ex = (Activator.CreateInstance(typeof(SqlException), BindingFlags.NonPublic | BindingFlags.Instance, null, new object[] { "test", errors,
      innerEx, Guid.NewGuid() }, null) as SqlException);
    return ex;
  }

Antwoord 13

(Sry It’s 6 maanden te laat, hoop dat dit zal niet worden beschouwd als necroposting waar ik hier landde op zoek naar hoe een sqlceexception van een mock te gooien).

Als u alleen de code hoeft te testen die de uitzondering verwerkt, zou een ultra eenvoudige oplossing zijn:

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

Antwoord 14

Gebaseerd op alle andere antwoorden die ik de volgende oplossing heb gemaakt:

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

Antwoord 15

Dit is echt oud en er zijn hier goede antwoorden. Ik gebruik MOQ, en ik kan abstracte klassen niet bespotten en echt geen reflectie wilden gebruiken, dus ik heb mijn eigen uitzondering afgeleid van DBEXCeption. Dus:

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

Uiteraard, als u innerException wilt toevoegen, of wat dan ook, meer rekwisieten, constructeurs, enz. Voeg.

Dan, in mijn test:

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

Hoepfully Dit zal iedereen helpen die MOQ gebruikt. Bedankt voor iedereen die hier heeft gepost die me naar mijn antwoord leidde.


Antwoord 16

U kunt reflectie gebruiken om SQLEXCeption-object in de test te maken:

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

Other episodes