Development Tip

모의 및 단위 테스트에 필요할 때 SqlException을 던지는 방법은 무엇입니까?

yourdevel 2020. 10. 25. 13:12
반응형

모의 및 단위 테스트에 필요할 때 SqlException을 던지는 방법은 무엇입니까?


내 프로젝트에서 몇 가지 예외를 테스트하려고하는데 내가 잡은 예외 중 하나는 SQlException.

갈 수없는 것 new SqlException()같아서 특히 어떻게 든 데이터베이스를 호출하지 않고 예외를 던질 수있는 방법을 모르겠습니다 (그리고 이들은 단위 테스트이기 때문에 일반적으로 느리기 때문에 데이터베이스를 호출하지 않는 것이 좋습니다).

NUnit과 Moq를 사용하고 있지만 어떻게 가짜인지 잘 모르겠습니다.

모두 ADO.NET을 기반으로하는 것처럼 보이는 일부 답변에 대한 응답으로 Linq to Sql을 사용하고 있습니다. 그래서 그 물건은 무대 뒤와 같습니다.

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

목업을 시도 할 때 첫 번째 줄에 게시

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

Linq to Sql을 사용하고 있으므로 다음은 NUnit 및 Moq를 사용하여 언급 한 시나리오 테스트 샘플입니다. 귀하의 DataContext에 대한 정확한 세부 사항과 귀하가 사용할 수있는 내용을 모르겠습니다. 필요에 맞게 편집하십시오.

사용자 정의 클래스로 DataContext를 래핑해야하며 Moq로 DataContext를 Mock 할 수 없습니다. 봉인되어 있기 때문에 SqlException을 조롱 할 수 없습니다. 고유 한 Exception 클래스로 래핑해야합니다. 이 두 가지를 달성하는 것은 어렵지 않습니다.

테스트를 작성하여 시작하겠습니다.

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

테스트를 구현하고 먼저 리포지토리 패턴을 사용하여 Linq에서 Sql로 호출을 래핑하겠습니다.

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

다음으로 IDataContextWrapper를 생성하면 주제에 대한 블로그 게시물볼 수 있습니다 .

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

다음으로 CustomSqlException 클래스를 만듭니다.

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

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

다음은 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
}

리플렉션을 사용하여이 작업을 수행 할 수 있습니다. Microsoft가 변경할 때 유지 관리해야하지만 방금 테스트 한 결과 작동합니다.

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

또한 중요 할 수있는 SqlException의 수를 제어 할 수 있습니다.


이에 대한 해결책이 있습니다. 천재인지 광기인지 잘 모르겠습니다.

다음 코드는 새로운 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);
}

그런 다음 그렇게 사용할 수 있습니다 (이 예제는 Moq를 사용합니다)

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

리포지토리, 핸들러 및 컨트롤러에서 SqlException 오류 처리를 테스트 할 수 있습니다.

이제 가서 누워 야합니다.


상황에 따라 일반적으로 ConstructorInfo 를 호출 하는 것 보다 GetUninitializedObject선호합니다 . 생성자를 호출하지 않는다는 점만 알고 있어야합니다. MSDN 설명에서 "개체의 새 인스턴스가 0으로 초기화되고 생성자가 실행되지 않기 때문에 개체가 유효한 것으로 간주되는 상태를 나타내지 않을 수 있습니다. 그 개체에 의해. " 그러나 특정 생성자의 존재에 의존하는 것보다 덜 깨지기 쉽다고 말하고 싶습니다.

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

Ouch 편집 : SqlException이 봉인되어 있다는 것을 몰랐습니다. 추상 클래스 인 DbException을 조롱했습니다.

새 SqlException을 만들 수는 없지만 SqlException이 파생 된 DbException을 모의 처리 할 수 ​​있습니다. 이 시도:

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

따라서 메서드가 연결을 열려고 할 때 예외가 발생합니다.

Message모의 예외에 대한 속성 이외의 다른 것을 읽으려면 해당 속성에 대한 "가져 오기"를 기대 (또는 Moq 버전에 따라 설정)하는 것을 잊지 마십시오.


이것이 도움이되는지 확실하지 않지만이 사람을 위해 일한 것 같습니다 (매우 영리함).

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

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


이것은 작동합니다.

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

예외가 발생하기 전에 약간의 시간이 걸리므로 이것이 더 빨리 작동 할 것이라고 생각합니다.

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

Hacks-R-Us가 제공하는 코드.

업데이트 : 아니요, 두 번째 방법은 SqlException이 아닌 ArgumentException을 throw합니다.

업데이트 2 : 이것은 훨씬 빠르게 작동합니다 (SqlException이 1 초 이내에 발생합니다).

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

귀하의 질문은 1 년이 지난 것으로 확인되었지만 기록을 위해 최근에 Microsoft Moles를 사용하여 발견 한 솔루션을 추가하고 싶습니다 ( Microsoft Moles에서 참조를 찾을 수 있음 ).

System.Data 네임 스페이스를 몰딩했으면 다음과 같이 SqlConnection.Open ()에서 SQL 예외를 모의 처리 할 수 ​​있습니다.

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

앞으로이 질문에 답하는 사람에게 도움이되기를 바랍니다.


(6 개월 늦었습니다. 모의에서 SqlCeException을 던지는 방법을 찾기 위해 여기에 착륙 한 네크로 포스팅으로 간주되지 않기를 바랍니다.)

예외를 처리하는 코드를 테스트해야하는 경우 매우 간단한 해결 방법은 다음과 같습니다.

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

다른 모든 답변을 바탕으로 다음 솔루션을 만들었습니다.

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

This is really old and there are some good answers here. I am using Moq, and I can't mock up Abstract classes and really didn't want to use reflection, so I made my own Exception derived from DbException. So:

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

obviously, if you need to add InnerException, or whatever, add more props, constructors, etc.

then, in my test:

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

Hoepfully this will help anyone that's using Moq. Thanks for everyone that posted in here that led me to my answer.


I suggest using this method.

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

You could use reflection to create SqlException object in the test:

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

참고URL : https://stackoverflow.com/questions/1386962/how-to-throw-a-sqlexception-when-needed-for-mocking-and-unit-testing

반응형