Làm thế nào để ném một SqlException khi cần thiết để chế nhạo và thử nghiệm đơn vị?


85

Tôi đang cố gắng kiểm tra một số ngoại lệ trong dự án của mình và một trong những ngoại lệ mà tôi bắt gặp là SQlException.

Có vẻ như bạn không thể đi new SqlException()nên tôi không chắc làm thế nào tôi có thể ném một ngoại lệ đặc biệt là bằng cách nào đó mà không gọi cơ sở dữ liệu (và vì đây là các bài kiểm tra đơn vị nên thường không nên gọi cơ sở dữ liệu vì nó chậm).

Tôi đang sử dụng NUnit và Moq, nhưng tôi không chắc làm thế nào để giả mạo điều này.

Trả lời một số câu trả lời dường như tất cả đều dựa trên ADO.NET, hãy lưu ý rằng tôi đang sử dụng Linq to Sql. Vì vậy, những thứ đó giống như hậu trường.

Thông tin thêm theo yêu cầu của @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()

Đăng lên dòng đầu tiên khi nó cố gắng mô phỏng

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

Bạn đúng. Tôi đã cập nhật câu trả lời của mình, nhưng có lẽ bây giờ nó không hữu ích lắm. DbException có lẽ là ngoại lệ tốt hơn để bắt mặc dù vậy, vì vậy hãy xem xét nó.
Matt Hamilton

Các câu trả lời thực sự hoạt động tạo ra nhiều thông báo ngoại lệ kết quả. Xác định chính xác loại bạn cần có thể hữu ích. Ví dụ: "Tôi cần một SqlException có chứa số ngoại lệ 18487, cho biết mật khẩu được chỉ định đã hết hạn." Có vẻ như một giải pháp như vậy là thích hợp hơn cho kiểm thử đơn vị.
Mike Christian

Câu trả lời:


9

Vì bạn đang sử dụng Linq to Sql, đây là một mẫu thử nghiệm kịch bản bạn đã đề cập bằng cách sử dụng NUnit và Moq. Tôi không biết chi tiết chính xác về DataContext của bạn và những gì bạn có sẵn trong đó. Chỉnh sửa cho nhu cầu của bạn.

Bạn sẽ cần phải bọc DataContext bằng một lớp tùy chỉnh, bạn không thể Mock DataContext bằng Moq. Bạn cũng không thể giả lập SqlException, vì nó đã được niêm phong. Bạn sẽ cần phải bọc nó bằng lớp Ngoại lệ của riêng mình. Không khó để thực hiện được hai điều này.

Hãy bắt đầu bằng cách tạo thử nghiệm của chúng tôi:

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

Hãy triển khai thử nghiệm, trước tiên hãy kết hợp các lệnh gọi Linq thành Sql của chúng ta bằng cách sử dụng mẫu kho lưu trữ:

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

Tiếp theo, tạo IDataContextWrapper như vậy, bạn có thể xem bài đăng trên blog này về chủ đề, của tôi khác một chút:

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

Tiếp theo, tạo lớp CustomSqlException:

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

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

Đây là một triển khai mẫu của 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

Bạn có thể làm điều này với sự phản chiếu, bạn sẽ phải duy trì nó khi Microsoft thực hiện thay đổi, nhưng nó hoạt động mà tôi vừa thử nghiệm nó:

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

Điều này cũng cho phép bạn kiểm soát Số lượng SqlException, điều này có thể quan trọng.


2
Cách tiếp cận này hoạt động, bạn chỉ cần cụ thể hơn với phương thức CreateException bạn muốn vì có hai quá tải. Thay đổi lệnh gọi GetMethod thành: .GetMethod ("CreateException", BindingFlags.NonPublic | BindingFlags.Static, null, CallingConventions.ExplicitThis, mới [] {typeof (SqlErrorCollection), typeof (string)}, ParameterModifier mới [] {}) Và nó hoạt động
Erik Nordenhök

Làm việc cho tôi. Xuất sắc.
Nick Patsaris

4
Được chuyển thành ý chính, với các chỉnh sửa từ các nhận xét. gist.github.com/timabell/672719c63364c497377f - Cảm ơn tất cả vì đã cho tôi một lối thoát khỏi nơi tăm tối tăm tối này.
Tim Abell

2
Phiên bản từ Ben J Anderson cho phép bạn chỉ định thông báo ngoài mã lỗi. gist.github.com/benjanderson/07e13d9a2068b32c2911
Tony

9
Để có được điều này để làm việc với DotNet-core 2.0, thay đổi dòng thứ hai trong NewSqlExceptionphương pháp để đọc:SqlError error = Construct<SqlError>(number, (byte)2, (byte)3, "server name", "error message", "proc", 100, null);
Chuck Spencer

75

Tôi có một giải pháp cho điều này. Tôi không chắc đó là thiên tài hay sự điên rồ.

Đoạn mã sau sẽ tạo một SqlException mới:

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

sau đó bạn có thể sử dụng như vậy (ví dụ này đang sử dụng Moq)

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

để bạn có thể kiểm tra việc xử lý lỗi SqlException trong kho lưu trữ, trình xử lý và trình điều khiển của bạn.

Bây giờ tôi cần phải đi và nằm xuống.


10
Giải pháp tuyệt vời! Tôi đã thực hiện một sửa đổi đối với nó để tiết kiệm thời gian chờ kết nối:new SqlConnection(@"Data Source=.;Database=GUARANTEED_TO_FAIL;Connection Timeout=1")
Joanna Derks

2
Tôi thích cảm xúc mà bạn đã thêm vào câu trả lời của mình. lol cảm ơn cho giải pháp này. Không có trí tuệ và tôi không biết tại sao tôi không nghĩ đến điều này ban đầu. một lần nữa cám ơn.
pqsk

1
Giải pháp tuyệt vời, chỉ cần đảm bảo rằng bạn không có cơ sở dữ liệu có tên là GUARANTEED_TO_FAIL trên máy cục bộ của mình;)
Amit G

Một ví dụ tuyệt vời về KISS
Lup

Đây là giải pháp khéo léo điên rồ
Mykhailo Seniutovych Ngày

21

Tùy thuộc vào tình huống, tôi thường thích GetUninitializedObject gọi một ConstructorInfo. Bạn chỉ cần lưu ý rằng nó không gọi hàm tạo - từ Ghi chú MSDN: "Bởi vì phiên bản mới của đối tượng được khởi tạo bằng 0 và không có hàm tạo nào được chạy, đối tượng có thể không đại diện cho trạng thái được coi là hợp lệ bởi đối tượng đó. " Nhưng tôi muốn nói rằng nó ít giòn hơn là dựa vào sự tồn tại của một hàm tạo nhất định.

[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
Điều này làm việc cho tôi, và để thiết lập các thông điệp của ngoại lệ khi bạn có đối tượng:typeof(SqlException).GetField("_message", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(exception, "my custom sql message");
Phil Cooper

7
Tôi đã mở rộng điều này để phản ánh ErrorMessage và ErrorCode. gist.github.com/benjanderson/07e13d9a2068b32c2911
Ben Anderson

13

Biên tập Ouch: Tôi không nhận ra rằng SqlException đã được niêm phong. Tôi đã chế nhạo DbException, là một lớp trừu tượng.

Bạn không thể tạo một SqlException mới, nhưng bạn có thể giả lập một DbException, mà SqlException bắt nguồn từ đó. Thử đi:

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

Vì vậy, ngoại lệ của bạn được ném ra khi phương thức cố gắng mở kết nối.

Nếu bạn muốn đọc bất kỳ thứ gì khác ngoài thuộc Messagetính trong ngoại lệ bị chế nhạo thì đừng quên Mong đợi (hoặc Thiết lập, tùy thuộc vào phiên bản Moq của bạn) "lấy" trên các thuộc tính đó.


bạn nên thêm những kỳ vọng cho "Số" cho phép bạn tìm ra những gì loại ngoại lệ được là (bế tắc, thời gian chờ vv)
Sam Saffron

Hmm làm thế nào về khi bạn sử dụng linq để sql? Tôi không thực sự mở (nó được thực hiện cho tôi).
chobo2

Nếu bạn đang sử dụng Moq thì có lẽ bạn đang chế nhạo một số loại hoạt động cơ sở dữ liệu. Thiết lập nó để ném khi điều đó xảy ra.
Matt Hamilton

Vì vậy, trên hoạt động thực tế (phương thức thực tế sẽ gọi trên db)?
chobo2 07/09/09

Bạn đang chế giễu hành vi db của bạn? Giống như, chế nhạo lớp DataContext của bạn hay gì đó? Bất kỳ hoạt động nào sẽ ném ra ngoại lệ này nếu hoạt động cơ sở dữ liệu trả về lỗi.
Matt Hamilton

4

Không chắc liệu điều này có giúp ích không, nhưng có vẻ như đã hiệu quả với người này (khá thông minh).

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

Tìm thấy tại: http://smartypeeps.blogspot.com/2006/06/how-to-throw-sqlexception-in-c.html


Tôi đã thử điều này, nhưng bạn vẫn cần một đối tượng SqlConnection đang mở để có được một SqlException được ném.
MusiGenesis

Tôi sử dụng linq để sql vì vậy tôi không làm những thứ này ado.net. Tất cả đằng sau hậu trường của nó.
chobo2

2

Điều này sẽ hoạt động:

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

Điều đó mất một chút trước khi nó ném ngoại lệ, vì vậy tôi nghĩ rằng điều này sẽ hoạt động nhanh hơn:

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

Mã do Hacks-R-Us mang đến cho bạn.

Cập nhật : nope, cách tiếp cận thứ hai ném một ArgumentException, không phải một SqlException.

Cập nhật 2 : điều này hoạt động nhanh hơn nhiều (SqlException được ném trong vòng chưa đầy một giây):

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

Đây là cách thực hiện của riêng tôi trước khi tôi xem trang SU này đang tìm cách khác vì thời gian chờ là không thể chấp nhận được. Bản cập nhật 2 của bạn tốt nhưng vẫn còn một giây. Không tốt cho các tập thử nghiệm đơn vị vì nó không mở rộng.
Jon Davis

2

Tôi nhận thấy rằng câu hỏi của bạn đã được một năm, nhưng tôi muốn bổ sung một giải pháp mà tôi đã phát hiện gần đây bằng cách sử dụng microsoft Moles (bạn có thể tìm tài liệu tham khảo tại đây Microsoft Moles )

Khi bạn đã di chuyển không gian tên System.Data, bạn có thể chỉ cần giả lập một ngoại lệ SQL trên SqlConnection.Open () như sau:

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

Tôi hy vọng điều này có thể giúp ai đó có câu hỏi này trong tương lai.


1
Mặc dù câu trả lời muộn, đây có lẽ là giải pháp sạch sẽ nhất, đặc biệt nếu bạn đang sử dụng Nốt ruồi cho các mục đích khác.
Amandalishus

1
Chà, bạn phải đang sử dụng khung công tác Moles để điều này hoạt động. Không hoàn toàn lý tưởng, khi đã sử dụng MOQ. Giải pháp này đang tách lời gọi đến .NET Framework. Câu trả lời theo @ default.kramer thích hợp hơn. Nốt ruồi đã được phát hành trong Visual Studio 2012 Ultimate với tên "Fakes", và sau đó trong VS 2012 Premium thông qua Bản cập nhật 2. Tất cả tôi đều sử dụng khuôn khổ Fakes, nhưng hãy kiên trì với một khuôn khổ chế nhạo tại một thời điểm, vì lợi ích của những người sau này sau bạn. ;)
Mike Christian

2

Các giải pháp này cảm thấy đầy hơi.

Ctor là nội bộ, có.

(Không cần sử dụng phản chiếu, cách dễ nhất để tạo ngoại lệ này một cách thực sự ....

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

Có lẽ có một phương pháp khác không yêu cầu thời gian chờ 1 giây để ném.


hah ... rất đơn giản tôi không biết tại sao tôi không nghĩ ra điều này ... hoàn hảo mà không phức tạp và tôi có thể làm điều này ở bất cứ đâu.
hal9000

Còn việc đặt thông báo và mã lỗi thì sao? Có vẻ như giải pháp của bạn không cho phép điều đó.
Sasuke Uchiha

@Sasuke Uchiha chắc chắn, nó không. Các giải pháp khác có. Nhưng nếu bạn chỉ cần ném loại ngoại lệ này, muốn tránh phản chiếu và không viết nhiều mã thì bạn có thể sử dụng giải pháp này.
Billy Jake O'Connor

1

(Sry đã muộn 6 tháng, hy vọng điều này sẽ không bị coi là cần thiết. Tôi đã hạ cánh ở đây để tìm cách ném SqlCeException từ một mô hình).

Nếu bạn chỉ cần kiểm tra mã xử lý ngoại lệ, cách giải quyết cực kỳ đơn giản sẽ là:

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

Dựa trên tất cả các câu trả lời khác, tôi đã tạo ra giải pháp sau:

    [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

Điều này thực sự cũ và có một số câu trả lời tốt ở đây. Tôi đang sử dụng Moq và tôi không thể mô phỏng các lớp Trừu tượng và thực sự không muốn sử dụng phản chiếu, vì vậy tôi đã tạo Ngoại lệ của riêng mình bắt nguồn từ DbException. Vì thế:

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

rõ ràng, nếu bạn cần thêm InnerException, hoặc bất cứ điều gì, hãy thêm nhiều đạo cụ, hàm tạo, v.v.

sau đó, trong thử nghiệm của tôi:

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

Rất may điều này sẽ giúp ích cho bất kỳ ai đang sử dụng Moq. Cảm ơn tất cả mọi người đã đăng ở đây đã dẫn tôi đến câu trả lời của mình.


Khi bạn không cần bất kỳ thứ gì cụ thể trên SqlException, phương pháp này thực sự hoạt động tốt.
Ralph Willgoss

1

Tôi đề nghị sử dụng phương pháp này.

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

Từ hàng đợi xem xét : Tôi có thể yêu cầu bạn thêm một số ngữ cảnh xung quanh câu trả lời của bạn. Câu trả lời chỉ có mã rất khó hiểu. Nó sẽ giúp ích cho cả người hỏi và người đọc trong tương lai nếu bạn có thể thêm thông tin vào bài đăng của mình.
RBT

Bạn có thể muốn thêm thông tin này bằng cách chỉnh sửa chính bài đăng. Đăng là nơi tốt hơn là bình luận để duy trì thông tin liên quan đến câu trả lời.
RBT

Điều này không còn hoạt động vì SqlExceptionkhông có hàm tạo và errorConstructorsẽ là null.
Emad

@Emad, bạn đã sử dụng gì để khắc phục sự cố?
Sasuke Uchiha

0

Bạn có thể sử dụng phản chiếu để tạo đối tượng SqlException trong thử nghiệm:

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

Điều này sẽ không hoạt động; SqlException không chứa hàm tạo. Câu trả lời từ @ default.kramer hoạt động đúng.
Mike Christian

1
@MikeChristian Nó không làm việc nếu bạn sử dụng một constuctor tồn tại ví dụprivate SqlException(string message, SqlErrorCollection errorCollection, Exception innerException, Guid conId)
Shaun Wilde
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.