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


85

내 프로젝트에서 몇 가지 예외를 테스트하려고하는데 내가 잡은 예외 중 하나는 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");

네가 옳아. 답장을 업데이트했지만 지금은별로 도움이되지 않을 것입니다. DbException은 아마도 잡는 데 더 나은 예외 일 것이므로 고려하십시오.
Matt Hamilton

실제로 작동하는 답변은 다양한 결과 예외 메시지를 생성합니다. 필요한 유형을 정확히 정의하면 도움이 될 수 있습니다. 예 : "지정된 암호가 만료되었음을 나타내는 예외 번호 18487이 포함 된 SqlException이 필요합니다." 이러한 솔루션은 단위 테스트에 더 적합합니다.
Mike Christian

답변:


9

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

사용자 정의 클래스로 DataContext를 래핑해야하며 Moq로 DataContext를 모의 할 수 없습니다. 봉인되어 있기 때문에 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
}

91

리플렉션을 사용하여이 작업을 수행 할 수 있습니다. 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의 수를 제어 할 수 있습니다.


2
이 접근 방식은 작동합니다. 두 개의 오버로드가 있으므로 원하는 CreateException 메서드를 더 구체적으로 지정하면됩니다. GetMethod 호출을 .GetMethod ( "CreateException", BindingFlags.NonPublic | BindingFlags.Static, null, CallingConventions.ExplicitThis, new [] {typeof (SqlErrorCollection), typeof (string)}, new ParameterModifier [] {})로 변경합니다. 작동합니다
Erik Nordenhök 2013-08-07

나를 위해 작동합니다. 훌륭한.
Nick Patsaris 2013 년

4
댓글의 수정으로 요점으로 바뀌 었습니다. gist.github.com/timabell/672719c63364c497377f- 이 어둡고 어두운 곳에서 탈출 할 수있게 해주신 모든 분들께 감사드립니다.
Tim Abell 2014 년

2
Ben J Anderson의 버전을 사용하면 오류 코드와 함께 메시지를 지정할 수 있습니다. gist.github.com/benjanderson/07e13d9a2068b32c2911
Tony

9
dotnet-core 2.0에서이 작업을 수행하려면 NewSqlException메서드 의 두 번째 줄을 다음과 같이 변경하십시오 .SqlError error = Construct<SqlError>(number, (byte)2, (byte)3, "server name", "error message", "proc", 100, null);
Chuck Spencer

75

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

다음 코드는 새로운 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 오류 처리를 테스트 할 수 있습니다.

이제 가서 누워 야합니다.


10
훌륭한 솔루션! 연결 대기 시간을 절약하기 위해 한 가지 수정 작업을 수행했습니다.new SqlConnection(@"Data Source=.;Database=GUARANTEED_TO_FAIL;Connection Timeout=1")
Joanna Derks

2
나는 당신의 대답에 당신이 더한 감정을 사랑합니다. 이 솔루션에 감사드립니다. 그것은 생각할 필요가 없으며 왜 처음에 이것을 생각하지 않았는지 모르겠습니다. 다시 감사합니다.
pqsk

1
훌륭한 솔루션입니다. 로컬 컴퓨터에 GUARANTEED_TO_FAIL이라는 데이터베이스가 없는지 확인하십시오.;)
Amit G

KISS의 좋은 예
Lup

이것은 독창적으로 미친 해결책입니다
Mykhailo Seniutovych

21

상황에 따라 일반적으로 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;
}

4
이것은 나를 위해 일했고, 일단 객체가 있으면 예외 메시지를 설정했습니다.typeof(SqlException).GetField("_message", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(exception, "my custom sql message");
Phil Cooper

7
ErrorMessage와 ErrorCode를 반영하기 위해 이것을 확장했습니다. gist.github.com/benjanderson/07e13d9a2068b32c2911
벤 앤더슨

13

편집하다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 버전에 따라 설정)하는 것을 잊지 마십시오.


예외 유형 (교착 상태, 시간 초과 등)을 파악할 수 있도록 "번호"에 대한 기대치를 추가해야합니다.
Sam Saffron

linq를 SQL에 사용할 때 어떻습니까? 나는 실제로 공개하지 않습니다.
chobo2

Moq를 사용하는 경우 아마도 일종의 데이터베이스 작업을 조롱하고있을 것입니다. 그럴 때 던져 지도록 설정하십시오.
Matt Hamilton

그래서 실제 작업 (db에서 호출하는 실제 메서드)에서?
chobo2

DB 행동을 조롱하고 있습니까? 예를 들어, DataContext 클래스 또는 무언가를 조롱합니까? 데이터베이스 작업이 오류를 반환하면 어떤 작업에서이 예외를 throw합니다.
Matt Hamilton

4

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

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


나는 이것을 시도했지만 SqlException을 던지기 위해서는 여전히 열린 SqlConnection 객체가 필요합니다.
MusiGenesis

나는 linq to sql을 사용 하므로이 ado.net 작업을 수행하지 않습니다. 그 모든 뒤에서.
chobo2

2

이것은 작동합니다.

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

시간 초과가 허용되지 않았기 때문에 다른 방법을 찾는이 SU 페이지를 발견하기 전에 이것은 내 자신의 구현이었습니다. 업데이트 2는 좋지만 여전히 1 초입니다. 확장되지 않기 때문에 단위 테스트 세트에 적합하지 않습니다.
Jon Davis

2

귀하의 질문이 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;
            };

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


1
늦은 답변에도 불구하고 이것은 아마도 가장 깨끗한 해결책 일 것입니다. 특히 이미 다른 목적으로 두더지를 사용하고 있다면 더욱 그렇습니다.
Amandalishus 2011

1
음, 이것이 작동하려면 두더지 프레임 워크를 사용해야합니다. 이미 MOQ를 사용할 때 완전히 이상적이지는 않습니다. 이 솔루션은 .NET Framework에 대한 호출을 우회합니다. @ default.kramer의 대답이 더 적절합니다. Moles는 Visual Studio 2012 Ultimate에서 "Fakes"로 출시되었으며 나중에 업데이트 2를 통해 VS 2012 Premium에서 릴리스되었습니다. 저는 모두 Fakes 프레임 워크를 사용하고 있지만 앞으로 올 것을 위해 한 번에 하나의 조롱 프레임 워크를 고수합니다. 당신 후. ;)
Mike Christian

2

이러한 솔루션은 부풀어 오른 느낌입니다.

ctor는 내부입니다.

(반사를 사용하지 않고 진정으로이 예외를 만드는 가장 쉬운 방법입니다 ....

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

Perphaps는 던지기 위해 1 초의 시간 제한을 요구하지 않는 또 다른 방법이 있습니다.


하 ... 너무 간단해서 왜 이렇게 생각하지 않았는지 모르겠네요 ... 번거 로움없이 완벽하고 어디서든 할 수 있습니다.
hal9000

메시지와 오류 코드를 설정하는 것은 어떻습니까? 귀하의 솔루션이이를 허용하지 않는 것 같습니다.
Sasuke Uchiha

@Sasuke Uchiha 확실히, 그렇지 않습니다. 다른 솔루션도 있습니다. 그러나 이러한 유형의 예외를 던지고 반사를 피하고 많은 코드를 작성하지 않으려면이 솔루션을 사용할 수 있습니다.
Billy Jake O'Connor

1

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

1

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

    [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

이것은 정말 오래되었고 여기에 좋은 답변이 있습니다. 저는 Moq를 사용하고 있으며 Abstract 클래스를 모의 할 수없고 리플렉션을 사용하고 싶지 않았기 때문에 DbException에서 파생 된 고유 한 Exception을 만들었습니다. 그래서:

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

분명히 InnerException을 추가해야하는 경우에는 추가 소품, 생성자 등을 추가합니다.

그런 다음 내 테스트에서 :

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

Hoepfully 이것은 Moq를 사용하는 모든 사람들에게 도움이 될 것입니다. 여기에 게시 한 모든 사람에게 감사드립니다.


SqlException에 대한 특정 사항이 필요하지 않은 경우이 메서드가 정말 잘 작동합니다.
Ralph Willgoss

1

이 방법을 사용하는 것이 좋습니다.

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

검토 대기열에서 : 답변에 대한 컨텍스트를 더 추가해 주시기 바랍니다. 코드 전용 답변은 이해하기 어렵습니다. 게시물에 더 많은 정보를 추가 할 수 있다면 질문하는 사람과 미래의 독자 모두에게 도움이 될 것입니다.
RBT

게시물 자체를 편집하여이 정보를 추가 할 수 있습니다. 답변과 관련된 관련 정보를 유지하려면 댓글보다 게시물이 더 좋습니다.
RBT

SqlException생성자 errorConstructor가없고 null 이기 때문에 더 이상 작동 하지 않습니다 .
Emad

@Emad, 문제를 극복하기 위해 무엇을 사용 했습니까?
Sasuke Uchiha

0

리플렉션을 사용하여 테스트에서 SqlException 개체를 만들 수 있습니다.

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

이것은 작동하지 않습니다. SqlException에 생성자가 없습니다. @ default.kramer의 답변이 제대로 작동합니다.
Mike Christian

1
당신은 예를 들어, 존재하는 constructor에 사용하는 경우 @MikeChristian이 작업을 수행private SqlException(string message, SqlErrorCollection errorCollection, Exception innerException, Guid conId)
숀 와일드
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.