Mock Objekt - Wie generiere ich eine SQLException ohne Datenbank?
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.