Mock Objekt - Wie generiere ich eine SQLException ohne Datenbank?

Published Freitag, 4. Juni 2010 09:27
Folgendes Problem: Wie kann man für Unit Tests bestimmte SQL Exceptions provozieren? Vielleicht sogar noch ohne Datenbank? Bei mir war das Problem, dass wir standardisierte Retry-Mechanismen verwenden, die aber bei bestimmten Fehlerszenarien keinen Sinn machen. So haben wir diverse SQL Fehlermeldungen / Errorcodes definiert, bei denen kein Retry stattfinden soll. Wie testet man nun so etwas? Hier die Antwort:

Mit Hilfe der folgenden Klasse kann man beliebige SqlExceptions erzeugen lassen, und somit Datanbankfehlerszenarien testen, ohne die Fehler mit einer Datenbank reproduzieren zu lassen.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Data.SqlClient;
using System.Reflection;

namespace Testing.MockObjects
{
    /// <summary>
    /// Hilfsklasse, um SQL Exceptions zu generieren. Diese Klasse bietet nur statische
    /// Methoden an.
    /// </summary>
    public class MockSqlException
    {
        #region Constructor

        /// <summary>
        /// Standard Konstruktor, nicht öffentlich, da nur statische Methoden verfügbar sein sollen.
        /// </summary>
        protected MockSqlException()
        {}

        #endregion

        #region Public methods

        /// <summary>
        /// Erstellt eine SqlException über Reflection.
        /// </summary>
        /// <param name="errorMessage">Die Fehlermeldung, die für die SqlException.Message
        /// Eigenschaft vorgesehen werden soll.</param>
        /// <param name="errorNumber">Die Sqlspezifische Fehlernummer.</param>
        /// <returns>Die generierte SqlException.</returns>
        public static SqlException CreateSqlException(string errorMessage, int errorNumber)
        {
            // Eine SqlException enthält immer eine SqlErrorCollection
            SqlErrorCollection collection = GetErrorCollection();

            // Wir brauchen nur einen Fehler, den wir hiermit generieren
            SqlError error = GetError(errorNumber, errorMessage);

            // Dieser Fehler wird der Collection über die Eigenschaft "add" hinzugefügt
            MethodInfo addMethod = collection.GetType().GetMethod("Add", BindingFlags.NonPublic | BindingFlags.Instance);
            addMethod.Invoke(collection, new object[] { error });

            // Hier wird der Konstruktor der SqlException aufgerufen und die SqlErrorCollection
            // als Parameter übergeben (Standard Konstruktor)
            Type[] types = new Type[] { typeof(string), typeof(SqlErrorCollection) };
            object[] parameters = new object[] { errorMessage, collection };
            ConstructorInfo constructor = typeof(SqlException).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, types, null);
            SqlException exception = (SqlException)constructor.Invoke(parameters);

            // Die SQLException wird zurückgegeben
            return exception;
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Generiert über Reflection einen SqlError, der einer SqlErrorCollection hinzugefügt werden kann.
        /// </summary>
        /// <param name="errorCode">Die Sqlspezifische Fehlernummer.</param>
        /// <param name="message">>Die Fehlermeldung, die für die SqlException.Message
        /// Eigenschaft vorgesehen werden soll.</param>
        /// <returns>Das generierte SqlError Objetkt.</returns>
        private static SqlError GetError(int errorCode, string message)
        {
            // generiert über den Konstruktor und dessen Argumente eine SqlError Instanz
            object[] parameters = new object[] { errorCode, (byte)0, (byte)10, "server", message, "procedure", 0 };
            Type[] types = new Type[] { typeof(int), typeof(byte), typeof(byte), typeof(string), typeof(string), typeof(string), typeof(int) };
            ConstructorInfo constructor = typeof(SqlError).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, types, null);
            
            return  (SqlError)constructor.Invoke(parameters);
        }
        
        /// <summary>
        /// Generiert über Reflection eine leere SqlErrorCollection Instanz.
        /// </summary>
        /// <returns>Die generierte SqlErrorCollection Instanz.</returns>
        private static SqlErrorCollection GetErrorCollection()
        {
            // generiert über den Konstruktor eine SqlErrorCollection Instanz
            ConstructorInfo constructor = typeof(SqlErrorCollection).
            GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { }, null);
            SqlErrorCollection collection = (SqlErrorCollection)constructor.Invoke(new object[] { });

            return collection;
        }

        #endregion
    }
}


Wie aus dem Code gut zu erkennen ist, wird der ganze Aufbau der SqlException über Reflection getätigt, weil die SqlException leider nicht öffentlich zugänglich ist.

Die SqlException ist auch immer so aufgebaut, dass sie eine ErrorCollection entgegennimmt, deshalb der umständliche Weg über die Collection. Weiterhin wichtig ist, dass die Eigenschaft SqlException.ErrorCode NICHT den ErrorCode vom SqlServer liefert, sondern den HRESULT, mit dem meist nichts anzufangen ist. Für den SqlErrorCode muss man die Eigenschaft SqlException.Number abfragen. Witzigerweise entspricht der SqlErrorcode der SqlException immer des SqlErrorCodes des ersten SqlErrors aus der Collection (hardcoded!).

Ein Anwendungsbeispiel ist dann z.b. folgender:
        static void Main(string[] args)
        {
            try
            {
                throw MockSqlException.CreateSqlException(
                    "The conversion from datetime data type to smalldatetime data type resulted in a smalldatetime overflow error",
                    298);
            }
            catch (SqlException sqlex)
            {
                int test = sqlex.ErrorCode;
                Console.WriteLine(String.Format("Eine SQL Exception ist aufgetreten: ErrorCode: {0} Message {1}", sqlex.Number, sqlex.Message));
            }

            Console.WriteLine("If this is displayed, a sql exception was thrown, and everything is fine ;-)");
            Console.ReadKey();
        }


Übrigens eine Auflistung aller SQL Fehlermeldungen findet man in der Datenbank selbst:
In SQL 2005: sys.messages
In SQL 2000: master.dbo.sysmessages

Mit Hilfe des Verfahrens lassen sich Datenbank-Negativtests erheblich beschleunigen, weil keine Verbindung zu einer Datenbank notwendig ist.
von Timo Rehl

Kommentare

# dotnet-kicks.de said on Freitag, 4. Juni 2010 12:02

Sie wurden gekickt (eine gute Sache) - Trackback von dotnet-kicks.de

Kommentar abgeben

(verpflichtend) 
(verpflichtend) 
(optional)
(verpflichtend)