Beispiel zu Unit Testing mit NUnit, Spring.Net und ADO .Net

K

Konstantin Denerz

Hallo,


hier ist ein kleines Beispiel zum Einsatz von Spring.Net mit Ado.Net beim Unit-Tests mit NUnit.

Hierbei werden 2 Methoden eins DAO's (Data Access Object) getestet.

hier ist das DAO Interface:
C#:
using System;
using System.Collections.Generic;
using Training.NUnit.SpringWithAdo.Domain;
namespace Training.NUnit.SpringWithAdo.Dao
{

	public interface IBubuDao
	{
		IList<Bubu> GetBubuByName(string name);
		void SaveBubu(Bubu bubu);
	}
}

das Interface IBubuDao hat 2 Methoden:
  • GetBubuByName, die eine Liste von Bubus laden sollte, falls der Name der Bubus gleich dem Wert des Parameters name ist
  • und SaveBubu, die das Geschäftsobjekt Bubu in der Datenbank dauerhaft (persistent) speichern sollte.

das Geschäftsobjekt Bubu:

C#:
using System;

namespace Training.NUnit.SpringWithAdo.Domain
{

	public class Bubu
	{
		private long id;
		private string name;
		
		public long Id {
			get { return id; }
			set { id = value; }
		}
		
		public string Name {
			get { return name; }
			set { name = value; }
		}
		public Bubu(){
			
		}
		public Bubu(long id, string name)
		{
			this.id = id;
			this.name = name;
		}
		
	}
}

Die Implementierung des Interfaces IBubuDao:
C#:
using System;
using System.Collections.Generic;
using Training.NUnit.SpringWithAdo.Domain;
using System.Data;
using Spring.Data;
namespace Training.NUnit.SpringWithAdo.Dao.Internal
{

	public class BubuDao:AdoDaoSupport,IBubuDao
	{
		/// <summary>
		/// Lädt alle Bubus mit einem bestimmten Namen
		/// </summary>
		/// <param name="name">Name der Bubus, die geladen werden sollen</param>
		/// <returns>Liste mit Bubus</returns>
		public IList<Bubu> GetBubuByName(string name)
		{
			return (IList<Bubu>) this.AdoTemplate.QueryWithResultSetExtractor(
                        CommandType.Text,"SELECT * FROM Bubu WHERE name='"+name+"'",
                        new BubuResultSetExtractor());
		}
		
		
		/// <summary>
		/// Speichert Bubu in der DB
		/// </summary>
		/// <param name="bubu">Bubu-Objekt</param>
		public void SaveBubu(Bubu bubu)
		{
			this.AdoTemplate.ExecuteNonQuery(CommandType.Text,
                        "INSERT INTO bubu values ("+string.Join(", ",
                         new string[]{"'"+bubu.Id.ToString()+"'","'"+bubu.Name+"'"})
			                                 	+")");
		}
	}
}
In dieser Implementierung erbt BubuDao von der Spring-Klasse AdoDaoSupport. Diese stellt Klassen und Methoden für vereinfachte Datenbankzugriffe bereit. Eine dieser Klassen ist die Klasse AdoTemplate. In dem Beispiel werden 2 Methoden der Klasse AdoTemplate verwendet:
  • ExecuteNonQuery kapselt die gleichnamige Methode der Schnittstelle IDbCommand im Assembly System.Data
  • QueryWithResultSetExtractor kapselt die Methode ExecuteReader der Schnittstelle IDbCommand im Assembly System.Data und verwendet für das Ergebnis eine Implementierung von IResultSetExtractor, der über Ergebnis (vom Typ IDataReader) iteriert und ein Ergebnisobjekt erstellt.

Die Implementierung des Interfaces IResultSetExtractor
C#:
using System;
using System.Collections.Generic;
using Spring.Data;
using Training.NUnit.SpringWithAdo.Domain;
namespace Training.NUnit.SpringWithAdo.Dao.Internal
{

	public class BubuResultSetExtractor:IResultSetExtractor
	{
	
		public object ExtractData(System.Data.IDataReader reader)
		{
                  
			IList<Bubu> result=new List<Bubu>();
			while(reader.Read()){
				Bubu bubu=new Bubu();
				bubu.Id=long.Parse(reader["id"].ToString());
				bubu.Name=reader["name"].ToString();
				
				result.Add(bubu);
				
			}
			return result;
		}
	}
}

Das App.config File (Anwendungskonfiguration):
XML:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
	<configSections>
		<sectionGroup name="spring">
			<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
			<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
			<section name="parsers" type="Spring.Context.Support.ConfigParsersSectionHandler, Spring.Core" />
		</sectionGroup>
		<section name="DatabaseConfiguration" type="System.Configuration.NameValueSectionHandler" />
	</configSections>
	<spring>
		<parsers>
			<parser type="Spring.Remoting.RemotingConfigParser, Spring.Services" />
			<parser namespace="http://www.springframework.net/database" type="Spring.Data.DatabaseConfigParser, Spring.Data" schemaLocation="assembly://Spring.Data/Spring.Data/spring-database.xsd" />
		</parsers>
		<context caseSensitive="false" name="spring.root">
			<resource uri="~/Config/Spring/dao.xml" />
		</context>
	</spring>
	<DatabaseConfiguration>
		<add key="db.connectionstring" value="DRIVER={MaxDB};Datasource Name=meinDatasourcename;Server=localhost;UID=meinName;Pwd=meinPasswort;Database=meineDatenbank" />
	</DatabaseConfiguration>
</configuration>

Die Springkonfiguration:
XML:
<?xml version="1.0" encoding="utf-8"?>
<objects xmlns="http://www.springframework.net" xmlns:d="http://www.springframework.net/database">
	<d:dbProvider id="DbProvider" provider="System.Data.Odbc" connectionString="${db.connectionstring}" />
	<object id="adoTemplate" type="Spring.Data.AdoTemplate, Spring.Data">
		<property name="DbProvider" ref="DbProvider" />
	</object>
	<object name="appConfigPropertyHolder" type="Spring.Objects.Factory.Config.PropertyPlaceholderConfigurer, Spring.Core">
		<property name="configSections">
			<value>DatabaseConfiguration</value>
		</property>
	</object>
	<object name="BubuDao" type="Training.NUnit.SpringWithAdo.Dao.Internal.BubuDao, Training.NUnit.SpringWithAdo">
		<property name="adoTemplate" ref="adoTemplate">
		</property>
	</object>
</objects>

Und zuletzt der Unit-Test:
C#:
#if TEST 

using NUnit.Framework;
using System;
using System.Data;
using System.Collections.Generic;
using Training.NUnit.SpringWithAdo.Domain;
using Training.NUnit.SpringWithAdo.Dao;
using Spring.Data;
using Spring.Testing.NUnit;
namespace Training.NUnit.SpringWithAdo
{
	[TestFixture]
	public class BubuTest:AbstractDependencyInjectionSpringContextTests
	{
		private IBubuDao bubuDao;
		public IBubuDao BubuDao {
			get { return bubuDao; }
			set { bubuDao = value; }
		}
		
		private AdoTemplate template;
		public AdoTemplate Template {
			get { return template; }
			set { template = value; }
		}
				
		protected override string[] ConfigLocations {
			get { return new string[]{
				@"Config\Spring\dao.xml"
				}; }
		}
		
		[Test]
		public void GetBubus()
		{
			//speichert Bubu für den Test
			this.Template.ExecuteNonQuery(CommandType.Text,"insert into bubu values (5,'FooBar')");
			//prüft, ob ein Bubu mit dem Namen FooBar angelegt wurde
			IList<Bubu> list = this.BubuDao.GetBubuByName("FooBar");
			Assert.IsTrue(list.Count>0,"Liste der Bubus ist 0. Es wurden keine Bubus geladen/angelegt.");
		}
		
		[Test]
		public void SaveBubu(){
			this.BubuDao.SaveBubu(new Bubu(1,"BarFoo"));
			//prüft, ob ein Bubu mit der ID = 1 angelegt wurde
			DataTable dataTable = this.Template.DataTableCreate(CommandType.Text,"select * from bubu where id = 1");
			Assert.IsTrue(dataTable.Rows.Count!=0,"Anzahl der Bubus ist 0. Es wurden keine Bubus gespeichert.");
		}

		protected override void OnTearDown()
		{
			base.OnTearDown();
			//räumt auf
			this.Template.ExecuteNonQuery(CommandType.Text,"delete from bubu where id = 1 or id = 5");
		}		
	}
}
#endif

Die Klasse BubuTest erbt von der Spring-Klasse AbstractDependencyInjectionSpringContextTests aus dem Assembly Spring.Testing.NUnit, die die 2 Methoden SetUp und TearDown schon implementiert. Nach dem die Eigenschaft ConfigLocations überschrieben wird und man im Getter die Pfade zu den Springkonfigurationsdateien einträgt, wird beim Ausführen des Tests in der SetUp-Methode ein ApplicationContext erstellt.
Weiterhin werden die Abhängigkeiten durch Dependency Injection (DI) automatisch aufgelöst (Autowiring). (falls es nicht deaktiviert wird) So bekommen die 2 Attribute, bubuDao und template, Objekte gleichen Typs zugewiesen, die in der Springkonfiguration definiert sind. Beim DI bekommt template über den Typ das entsprechende Objekt zugewiesen, da der Name ungleich dem ist, der in der Springkonfiguration definiert ist.

In der Springkonfiguration ist der DBProvider System.Data.Odbc definiert. Wer diesen benutzen will muss folgende Springkonfiguration in die Datei dbproviders.xml in dem Projekt Spring.Data im Verzeichnis Data/Common einfügen:
XML:
<object id="Odbc-2.0" type="Spring.Data.Common.DbProvider, Spring.Data" singleton="false">
    <constructor-arg name="dbMetaData">
      <object type="Spring.Data.Common.DbMetadata">
        <constructor-arg name="productName" value="Odbc"/>
        <constructor-arg name="assemblyName" value="System.Data" />
        <constructor-arg name="connectionType" value="System.Data.Odbc.OdbcConnection, System.Data"/>        
        <constructor-arg name="commandType" value="System.Data.Odbc.OdbcCommand, System.Data" />
        <constructor-arg name="parameterType" value="System.Data.Odbc.OdbcParameter, System.Data" />
        <constructor-arg name="dataAdapterType" value="System.Data.Odbc.OdbcDataAdapter, System.Data" />
        <constructor-arg name="commandBuilderType" value="System.Data.Odbc.OdbcCommandBuilder, System.Data" />
        <constructor-arg name="commandBuilderDeriveParametersMethod" value="DeriveParameters"/>
        <constructor-arg name="parameterDbType" value="System.Data.Odbc.OdbcType, System.Data" />
        <constructor-arg name="parameterDbTypeProperty" value="OdbcType"/>
        <constructor-arg name="parameterIsNullableProperty" value="IsNullable"/>
        <constructor-arg name="parameterNamePrefix" value="?"/>
        <constructor-arg name="exceptionType" value="System.Data.Odbc.OdbcException, System.Data"/>
        <constructor-arg name="useParameterNamePrefixInParameterCollection" value="false"/>
        <constructor-arg name="bindByName" value="false"/>
        <constructor-arg name="errorCodeExceptionExpression" value="Errors[0].NativeError.ToString()"/>
        <property name="ErrorCodes.BadSqlGrammarCodes">
          <value>156,170,207,208</value>
        </property>
        <property name="ErrorCodes.PermissionDeniedCodes">
          <value>229</value>
        </property>
        <property name="ErrorCodes.DataIntegrityViolationCodes">
          <value>2627,8114,8115</value>
        </property>
        <property name="ErrorCodes.DeadlockLoserCodes">
          <value>1205</value>
        </property>
      </object>
    </constructor-arg>

  </object>
<alias name="Odbc-2.0" alias="System.Data.Odbc"/>

Hier findet man die vorhanden DBProvider aufgelistet: DB-Provider




Gruß Konstantin
 
Zuletzt bearbeitet von einem Moderator:
Hallo,

erstmal cooles Beispiel :)
Hier meine Verbesserungsvorschläge:

DAO Interface: hier würde ich ein generisches DAO Interface Vorschlagen
Beispielsweise so:
C#:
using System;
using System.Collections.Generic;
using System.Text;

namespace De.Tutorials.Training
{
    public interface IGenericDAO<TEntity,TKey>
    {
        TKey MakePersistent(TEntity entity);
        void MakeTransient(TEntity entity);
        TEntity GetByKey(TKey key);
        List<TEntity> FindByAttribute(string attributeName, object attributeValue);
        List<TEntity> FindByAttributes(string[] attributeNames, object[] attributeValues);
        List<TEntity> FindAll();
    }
}

Business Objects sollte man immer mit einer entsprechenden ToString() Methode ausstatten.
Neben dem technischen/künstlichen Schlüssel/surrogate key (id) auch entsprechende business keys
(im Sinne der Anwendungsdomäne Eindeutigekennung eines Business Objects -> Sozialversicherungsnummer,
AccountName, etc.) definieren.

Equals/GetHashCode so definieren, dass (nur) die Felder des BusinessKey's verwendet werden,
nicht der technische Schlüssel. Equals/ GetHashCode so implementieren, dass wenn zwei Entitäten
den gleichen Hashcode haben sie auch Equals sind. Equals Transitiv definieren:
a.Equals(b); -> b.Equals(c); -> a.Equals(c); -> b.Equals(a) ... etc.

SQL Statements in konfigurationsdatei auslagern. Parameterisierte SQL Statements benutzen:
INSERT INTO bubu VALUES :)name,:dateOfBirth, :homeTown);

...

Gruß Tom
 
Zurück