モックとユニットテストに必要なときにSqlExceptionをスローする方法は?


85

プロジェクトでいくつかの例外をテストしようとしていますが、キャッチした例外の1つはSQlExceptionです。

行くことができないようですnew SqlException()ので、特にデータベースを呼び出さずに例外をスローする方法がわかりません(これらは単体テストであるため、通常はデータベースを呼び出さないことをお勧めします)。

NUnitとMoqを使用していますが、これを偽造する方法がわかりません。

すべてADO.NETに基づいているように見えるいくつかの回答に応答して、Linq toSqlを使用していることに注意してください。そのため、舞台裏のようなものです。

@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はおそらくキャッチするのに適した例外なので、考慮してください。
マットハミルトン

実際に機能する回答は、さまざまな結果の例外メッセージを生成します。必要なタイプを正確に定義すると役立つ場合があります。例:「指定されたパスワードの有効期限が切れていることを示す例外番号18487を含むSqlExceptionが必要です。」このようなソリューションは、単体テストに適しているようです。
マイククリスチャン

回答:


9

Linq to Sqlを使用しているため、NUnitとMoqを使用して言及したシナリオをテストするサンプルを次に示します。DataContextの正確な詳細と、DataContextで利用できるものがわかりません。必要に応じて編集します。

DataContextをカスタムクラスでラップする必要があります。MoqでDataContextをモックすることはできません。SqlExceptionは封印されているため、モックすることもできません。独自のExceptionクラスでラップする必要があります。これら2つのことを達成することは難しくありません。

テストを作成することから始めましょう:

[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 toSql呼び出しをラップします。

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
このアプローチは機能します。2つのオーバーロードがあるため、必要なCreateExceptionメソッドをより具体的にする必要があります。GetMethod呼び出しを次のように変更します。.GetMethod( "CreateException"、BindingFlags.NonPublic | BindingFlags.Static、null、CallingConventions.ExplicitThis、new [] {typeof(SqlErrorCollection)、typeof(string)}、new ParameterModifier [] {})そしてそれは動作します
エリックNordenhökを

私のために働きます。鮮やかさ。
Nick Patsaris 2013

4
コメントからの訂正で、要点に変わりました。 gist.github.com/timabell/672719c63364c497377f-この暗くて暗い場所から抜け出す方法を教えてくれたすべての人に感謝します。
ティム・アベル

2
Ben J Andersonのバージョンでは、エラーコードに加えてメッセージを指定できます。 gist.github.com/benjanderson/07e13d9a2068b32c2911
Tony

9
DOTNETコア2.0で動作するようにこれを取得するには、第二行を変更NewSqlException読み取るための方法:SqlError error = Construct<SqlError>(number, (byte)2, (byte)3, "server name", "error message", "proc", 100, null);
チャック・スペンサー

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
素晴らしいソリューション!私は、接続を待っているにいくつかの時間を節約するためにそれに1つの修正をした:new SqlConnection(@"Data Source=.;Database=GUARANTEED_TO_FAIL;Connection Timeout=1")
ジョアンナDerks

2
私はあなたがあなたの答えに加えた感情が大好きです。笑この解決策をありがとう。それは簡単なことではなく、なぜ私が最初にこれを考えなかったのか分かりません。もう一度ありがとう。
pqsk 2013年

1
優れたソリューションです。ローカルマシンにGUARANTEED_TO_FAILというデータベースがないことを確認してください;)
Amit G

KISSの素晴らしい例
LUP

これは独創的に狂った解決策です
Mykhailo Seniutovych

21

状況にもよりますが、私は通常、ConstructorInfoを呼び出すよりもGetUninitializedObjectを好みます。コンストラクターを呼び出さないことに注意する必要があります-MSDNの備考から:「オブジェクトの新しいインスタンスはゼロに初期化され、コンストラクターは実行されないため、オブジェクトは有効と見なされる状態を表していない可能性がありますそのオブジェクトによって。」しかし、特定のコンストラクターの存在に依存するよりも脆弱ではないと思います。

[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");
フィル・クーパー

7
これを拡張して、ErrorMessageとErrorCodeを反映させました。gist.github.com/benjanderson/07e13d9a2068b32c2911
ベンアンダーソン

13

編集痛い:私は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 to sqlを使用する場合はどうですか?私は実際にはオープンをしません(それは私のために行われます)。
chobo2 2009

Moqを使用している場合は、おそらく何らかのデータベース操作をモックしていると思われます。それが発生したときにスローされるように設定します。
マットハミルトン

では、実際の操作(dbを呼び出す実際のメソッド)では?
chobo2 2009

データベースの動作をあざけっていますか?たとえば、DataContextクラスなどをモックしますか?データベース操作がエラーを返した場合、どの操作でもこの例外がスローされます。
マットハミルトン

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 2009

linq to sqlを使用しているので、このado.netの処理は行いません。そのすべての舞台裏。
chobo2 2009

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によって提供されたコード。

更新:いいえ、2番目のアプローチはSqlExceptionではなくArgumentExceptionをスローします。

更新2:これははるかに高速に動作します(SqlExceptionは1秒未満でスローされます):

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

タイムアウトが受け入れられなかったため、別の方法を探してこのSUページに出くわす前は、これは私自身の実装でした。Update 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
遅い答えにもかかわらず、これはおそらく最もクリーンな解決策です。特に、すでに他の目的でMolesを使用している場合はそうです。
アマンダリシュス2011年

1
これが機能するには、Molesフレームワークを使用している必要があります。すでにMOQを使用している場合、完全に理想的ではありません。このソリューションは、.NETFrameworkへの呼び出しを迂回しています。@ default.kramerによる答えがより適切です。Molesは、Visual Studio 2012 Ultimateで「Fakes」としてリリースされ、その後、アップデート2を介してVS 2012Premiumでリリースされました。お先にどうぞ。;)
マイククリスチャン

2

これらのソリューションは肥大化したように感じます。

ctorは内部です、はい。

(リフレクションを使用せずに、この例外を真に作成する最も簡単な方法...。

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

Perphapsは、スローするのに1秒のタイムアウトを必要としない別の方法があります。


ハァッ...とてもシンプルな理由がわからない...面倒なことなく完璧で、どこでもこれを行うことができます。
HAL9000

メッセージとエラーコードの設定はどうですか?あなたのソリューションはそれを許可していないようです。
うちはサスケ

@うちはサスケ確かに、そうではありません。他の解決策はそうします。しかし、単にこのタイプの例外をスローする必要があり、リフレクションを避け、多くのコードを記述したくない場合は、このソリューションを使用できます。
ビリージェイクオコナー

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を使用していますが、抽象クラスをモックアップできず、リフレクションを使用したくなかったため、DbExceptionから派生した独自の例外を作成しました。そう:

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

明らかに、InnerExceptionなどを追加する必要がある場合は、小道具やコンストラクターなどを追加します。

次に、私のテストでは:

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

うまくいけば、これはMoqを使用している人に役立つでしょう。私の答えに導いてくれたここに投稿してくれたすべての人に感謝します。


SqlExceptionに特別なものが必要ない場合、このメソッドは非常にうまく機能します。
ラルフウィルゴス2017

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 2017年

投稿自体を編集して、この情報を追加することをお勧めします。回答に関連する関連情報を維持するには、コメントよりも投稿の方が適しています。
RBT 2017年

SqlExceptionコンストラクターがなく、errorConstructornullになるため、これは機能しなくなりました。
エマド

@Emad、問題を克服するために何を使用しましたか?
うちはサスケ

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からの回答は正しく機能します。
マイククリスチャン

1
@MikeChristianたとえば、存在するコンストラクターを使用する場合は機能しますprivate SqlException(string message, SqlErrorCollection errorCollection, Exception innerException, Guid conId)
Shaun Wilde
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.