Alay ve birim testi için gerektiğinde SqlException nasıl atılır?


86

Projemdeki bazı istisnaları test etmeye çalışıyorum ve yakaladığım İstisnalardan biri SQlException.

Görünüşe göre gidemezsiniz, new SqlException()bu yüzden özellikle veritabanını bir şekilde çağırmadan nasıl bir istisna atabileceğimi bilmiyorum (ve bunlar birim testleri olduğundan, genellikle yavaş olduğu için veritabanını çağırmamanız önerilir).

NUnit ve Moq kullanıyorum, ancak bunu nasıl taklit edeceğimi bilmiyorum.

Hepsi ADO.NET'e dayalı gibi görünen bazı yanıtlara yanıt verirken, Linq to Sql kullanıyorum. Yani bu şeyler perde arkası gibi.

@MattHamilton tarafından talep edilen daha fazla bilgi:

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

Mockup yapmaya çalıştığında ilk satıra gönderiler

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

Haklısın. Cevabımı güncelledim, ancak şu anda muhtemelen pek yardımcı olmuyor. DbException muhtemelen yakalanması daha iyi bir istisnadır, bu yüzden bunu göz önünde bulundurun.
Matt Hamilton

Gerçekte işe yarayan yanıtlar, ortaya çıkan çeşitli istisna mesajları üretir. Tam olarak hangi türe ihtiyacınız olduğunu tanımlamak yardımcı olabilir. Örneğin, "Belirtilen parolanın süresinin dolduğunu gösteren 18487 istisna numarasını içeren bir SqlException'a ihtiyacım var." Birim testi için böyle bir çözüm daha uygun görünüyor.
Mike Christian

Yanıtlar:


9

Linq to Sql'yi kullandığınız için, NUnit ve Moq kullanarak bahsettiğiniz senaryoyu test etmenin bir örneğini burada bulabilirsiniz. DataContext'inizin tam ayrıntılarını ve içinde ne bulunduğunu bilmiyorum. İhtiyaçlarınıza göre düzenleyin.

DataContext'i özel bir sınıfla sarmalamanız gerekir, DataContext'i Moq ile alay edemezsiniz. Mühürlü olduğu için SqlException da alay edemezsiniz. Kendi Exception sınıfınızla sarmalamanız gerekecek. Bu iki şeyi başarmak zor değil.

Testimizi oluşturarak başlayalım:

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

Testi uygulayalım, önce Linq to Sql çağrılarımızı depo modelini kullanarak sarmalayalım:

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

Daha sonra IDataContextWrapper'ı bu şekilde oluşturun, konuyla ilgili bu blog gönderisini görüntüleyebilirsiniz, benimki biraz farklıdır:

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

Ardından CustomSqlException sınıfını oluşturun:

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

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

İşte IDataContextWrapper'ın örnek bir uygulaması:

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
}

92

Bunu yansıtma ile yapabilirsiniz, Microsoft değişiklik yaptığında bunu sürdürmeniz gerekecek, ancak işe yarıyor, az önce test ettim:

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

Bu ayrıca, önemli olabilecek SqlException Numarasını kontrol etmenizi sağlar.


2
Bu yaklaşım işe yarıyor, sadece istediğiniz CreateException yöntemiyle daha spesifik olmanız gerekiyor çünkü iki aşırı yükleme var. GetMethod çağrısını şu şekilde değiştirin: .GetMethod ("CreateException", BindingFlags.NonPublic | BindingFlags.Static, null, CallingConventions.ExplicitThis, new [] {typeof (SqlErrorCollection), typeof (string)}, new ParameterModifier [] {}) Ve işe yarıyor
Erik Nordenhök

Benim için çalışıyor. Parlak.
Nick Patsaris

4
Yorumlardan gelen düzeltmelerle ana fikre dönüştü. gist.github.com/timabell/672719c63364c497377f - Bana bu karanlık ve karanlık yerden bir çıkış yolu verdikleri için hepinize çok teşekkürler.
Tim Abell

2
Ben J Anderson'ın sürümü, hata koduna ek olarak mesaj belirtmenize de izin verir. gist.github.com/benjanderson/07e13d9a2068b32c2911
Tony

10
Bunun dotnet-core 2.0 ile çalışmasını sağlamak için, NewSqlExceptionyöntemdeki ikinci satırı şu şekilde değiştirin :SqlError error = Construct<SqlError>(number, (byte)2, (byte)3, "server name", "error message", "proc", 100, null);
Chuck Spencer

75

Buna bir çözümüm var. Dahice mi yoksa delilik mi olduğundan emin değilim.

Aşağıdaki kod yeni bir SqlException oluşturacaktır:

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

daha sonra bu şekilde kullanabilirsiniz (bu örnek Moq kullanıyor)

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

böylece depolarınızda, işleyicilerinizde ve denetleyicilerinizde SqlException hata işlemenizi test edebilirsiniz.

Şimdi gidip uzanmam gerekiyor.


10
Mükemmel çözüm! Bağlantıyı beklerken biraz zaman kazanmak için bir değişiklik yaptım:new SqlConnection(@"Data Source=.;Database=GUARANTEED_TO_FAIL;Connection Timeout=1")
Joanna Derks

2
Cevabına kattığın duyguyu seviyorum. haha bu çözüm için teşekkürler. Bu hiç akıllıca değil ve başlangıçta bunu neden düşünmediğimi bilmiyorum. tekrar teşekkürler.
pqsk

1
Harika çözüm, yerel makinenizde GUARANTEED_TO_FAIL adlı bir veritabanınızın olmadığından emin olun;)
Amit G

Harika bir KISS örneği
Lup

Bu dahice çılgın bir çözüm
Mykhailo Seniutovych

22

Duruma bağlı olarak, genellikle bir ConstructorInfo çağırmak için GetUninitializedObject tercih ederim . Yapıcıyı çağırmadığının farkında olmalısınız - MSDN'den Açıklamalar: "Nesnenin yeni örneği sıfır olarak başlatıldığından ve hiçbir kurucu çalıştırılmadığından, nesne geçerli olarak kabul edilen bir durumu temsil etmeyebilir o nesneyle. " Ancak, belirli bir kurucunun varlığına güvenmekten daha az kırılgan olduğunu söyleyebilirim.

[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
Bu benim için çalıştı ve nesneye sahip olduğunuzda istisna mesajını ayarlamak için çalıştı:typeof(SqlException).GetField("_message", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(exception, "my custom sql message");
Phil Cooper

8
Bunu ErrorMessage ve ErrorCode'u yansıtacak şekilde genişlettim. gist.github.com/benjanderson/07e13d9a2068b32c2911
Ben Anderson

13

Edit Ouch: SqlException'ın mühürlendiğini bilmiyordum. Soyut bir sınıf olan DbException ile alay ediyorum.

Yeni bir SqlException oluşturamazsınız, ancak SqlException'ın türetildiği bir DbException ile dalga geçebilirsiniz. Bunu dene:

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

Dolayısıyla, yöntem bağlantıyı açmaya çalıştığında istisnanız atılır.

MessageAlay konusu istisnadaki özellik dışında herhangi bir şey okumayı bekliyorsanız , bu özelliklerde "get" beklemeyi (veya Moq sürümünüze bağlı olarak Kurulum'u) unutmayın.


"Sayı" için beklentiler eklemelisiniz ki bu da ne tür bir istisna olduğunu anlamanıza olanak tanır (kilitlenme, zaman aşımı vb.)
Sam Saffron

Hmm, linq to sql kullandığınızda ne dersiniz? Aslında bir açılış yapmıyorum (benim için yapıldı).
chobo2

Moq kullanıyorsanız, muhtemelen bir çeşit veritabanı işlemiyle dalga geçiyorsunuzdur. Bu olduğunda fırlatılacak şekilde ayarlayın.
Matt Hamilton

Peki gerçek operasyon (db'yi çağıracak gerçek yöntem)?
chobo2

DB davranışınızla dalga mı geçiyorsunuz? DataContext sınıfınızla dalga geçmek gibi mi? Veritabanı işlemi bir hata döndürürse, hangi işlem bu istisnayı atar.
Matt Hamilton

4

Bunun yardımcı olup olmadığından emin değilim, ancak bu kişi için işe yaramış gibi görünüyor (oldukça zeki).

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

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


Bunu denedim, ancak yine de bir SqlException atılması için açık bir SqlConnection nesnesine ihtiyacınız var.
MusiGenesis

Linq to sql kullanıyorum, bu yüzden bu ado.net şeylerini yapmıyorum. Hepsi perde arkasında.
chobo2

2

Bu çalışmalı:

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

Bunun istisnayı atması biraz zaman alıyor, bu yüzden bunun daha da hızlı çalışacağını düşünüyorum:

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

Size Hacks-R-Us tarafından getirilen kod.

Güncelleme : hayır, ikinci yaklaşım SqlException değil, ArgumentException oluşturur.

Güncelleme 2 : Bu çok daha hızlı çalışır (SqlException bir saniyeden daha kısa sürede atılır):

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

Bu SU sayfasına rastlamadan önce başka bir yol arayan benim kendi uygulamamdı çünkü zaman aşımı kabul edilemezdi. 2. Güncellemeniz iyi ama yine de bir saniye. Ölçeklenmediği için birim test setleri için iyi değildir.
Jon Davis

2

Sorunuzun bir yaşında olduğunu fark ettim, ancak kayıt için yakın zamanda microsoft Moles kullanarak keşfettiğim bir çözümü eklemek istiyorum (referansları burada bulabilirsiniz Microsoft Moles )

System.Data ad alanını değiştirdikten sonra, bir SqlConnection.Open () üzerinde şu şekilde bir SQL istisnası ile alay edebilirsiniz:

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

Umarım bu, gelecekte bu soruyu yanıtlayan birine yardımcı olabilir.


1
Geç cevaba rağmen, bu muhtemelen en temiz çözümdür, özellikle de Moles'i başka amaçlar için kullanıyorsanız.
Amandalishus

1
Bunun çalışması için Moles çerçevesini kullanıyor olmalısınız. MOQ kullanırken tamamen ideal değil. Bu çözüm, .NET Framework çağrısını bozuyor. @ Default.kramer tarafından verilen cevap daha uygundur. Moles, Visual Studio 2012 Ultimate'da "Sahte" olarak ve daha sonra VS 2012 Premium'da Güncelleme 2 aracılığıyla yayınlandı senden sonra. ;)
Mike Christian

2

Bu yöntemi kullanmanızı öneririm.

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

İnceleme kuyruğundan : Cevabınıza biraz daha bağlam eklemenizi rica edebilir miyim? Yalnızca kod yanıtlarını anlamak zordur. Gönderinize daha fazla bilgi ekleyebilirseniz, soruyu soran kişiye ve gelecekteki okuyuculara yardımcı olacaktır.
RBT

Yazının kendisini düzenleyerek bu bilgileri eklemek isteyebilirsiniz. Yanıtla ilgili bilgileri korumak için gönderi yorumlardan daha iyi bir yerdir.
RBT

Bu artık çalışmıyor çünkü bir SqlExceptionkurucuya sahip değil ve errorConstructorboş olacak.
Emad

@Emad, sorunun üstesinden gelmek için ne kullandın?
Sasuke Uchiha

2

Bu çözümler şişkinlik hissi veriyor.

Ctor içseldir, evet.

(Yansıma kullanmadan, bu istisnayı gerçekten yaratmanın en kolay yolu ....

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

Perphaps, atmak için 1 saniyelik zaman aşımı gerektirmeyen başka bir yöntem daha var.


hah ... çok basit Neden bunu düşünmediğimi bilmiyorum ... güçlük çekmeden mükemmel ve bunu her yerde yapabilirim.
hal9000

Bir mesaj ve bir hata kodu ayarlamaya ne dersiniz? Görünüşe göre çözümünüz buna izin vermiyor.
Sasuke Uchiha

@Sasuke Uchiha tabii, öyle değil. Diğer çözümler yapar. Ancak, bu tür bir istisna atmanız gerekiyorsa, yansımadan kaçınmak ve fazla kod yazmamak istiyorsanız, bu çözümü kullanabilirsiniz.
Billy Jake O'Connor

1

(Sry, 6 ay gecikti, umarım bu necroposting olarak kabul edilmez. Buraya bir taklitten SqlCeException'ı nasıl atacağımı aradım).

Sadece istisnayı işleyen kodu test etmeniz gerekiyorsa, ultra basit bir çözüm şu şekilde olacaktır:

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

Diğer tüm cevaplara dayanarak aşağıdaki çözümü oluşturdum:

    [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

Bu gerçekten eski ve burada bazı iyi cevaplar var. Moq kullanıyorum ve Soyut sınıfları alay edemiyorum ve gerçekten yansımayı kullanmak istemedim, bu yüzden DbException'dan türetilen kendi İstisnamı oluşturdum. Yani:

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

Açıkçası, InnerException veya her neyse eklemeniz gerekirse, daha fazla sahne, kurucu vb. ekleyin.

sonra, benim testimde:

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

Neyse ki bu, Moq kullanan herkese yardımcı olacaktır. Beni cevabıma götüren burada paylaşımda bulunan herkese teşekkürler.


SqlException üzerinde belirli bir şeye ihtiyacınız olmadığında, bu yöntem gerçekten iyi çalışır.
Ralph Willgoss

0

Testte SqlException nesnesi oluşturmak için yansımayı kullanabilirsiniz:

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

Bu işe yaramayacak; SqlException yapıcı içermez. @ Default.kramer'dan gelen yanıt düzgün çalışıyor.
Mike Christian

1
@MikeChristian Var olan bir kurucu kullanırsanız işe yarar, örneğinprivate SqlException(string message, SqlErrorCollection errorCollection, Exception innerException, Guid conId)
Shaun Wilde
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.