in

Dé specialist in .NET trainingen en consultancy

Thomas Huijer

december 2007 - Posts

  • FinalBuilder: mijn favoriete deployment tool

    Voor iedereen die het nog niet kent: FinalBuilder is wat mij betreft zonder enige twijfel de beste deployment tool die er is. Het maakt MSBuild en NAnt hetzelfde als programmeren in Notepad zonder IDE. FinalBuilder doet in concept hetzelfde als MSBuild en NAnt, maar dan op een visuele manier. Een IDE voor je buildprocess. Binnen een paar minuten heb je een Get Latest, Build, Zip en FTP Upload geconfigureerd. Met 1 druk op de knop heb je nu een build. Ik gebruik FinalBuilder al jaren en heb er zelfs al custom actions voor geschreven, zowel in Delphi als in .NET, maar deze week had ik het weer eens nodig voor een hobby-projectje en het gemak waarmee je iets in elkaar sleutelt, maakte me weer erg blij.

    Download de trial als je FinalBuilder nog nooit gezien of gebruikt hebt. Je zal er geen spijt van krijgen.

    Edit: De makers van TypeMock zijn bezig om hun buildprocess ook te implementeren op basis van  FinalBuilder. Hiervoor gebruikten ze MSBuild files.

  • Automated testing en databases

    Eerlijksheidshalve gebied mij te zeggen dat ik dit artikel eerst "Unit testing en databases" wilde noemen. Maar Dennis Vroegop corrigeerde me gelijk toen ik het met hem hier over had. Zodra je een database bij je testen gebruikt, zijn het automatisch integration tests. Ik moet Dennis helemaal gelijk geven. Er worden tegenwoordig veel te veel dingen unit-tests genoemd...

    Automated testing en databases is een verhaal apart. Er zijn verschillende manieren om het te tackelen, maar in feite heb je twee echte keuzes: je gebruikt mock-objects of je gebruikt een echte database. Ik gebruik zelf het liefste een echte database. Mijn argument daarvoor is dat je je triggers, stored procedures, etc., etc. ook goed wilt kunnen testen. Dat is met mock-objects gewoon niet te doen.

    Een echte database gebruiken heeft wel een nadeel. Je moet er voor zorgen dat je testdata constant is. Als je testen runnen, moet het voor elke testrun wel altijd op dezelfde data zijn. En hoe ga je er voor zorgen dat als je bepaalde data nodig hebt voor een test, dat die data ook altijd bij het runnen van je test in je database zit? Als je een lege tabel nodig hebt voor de ene test en een goed gevulde (zelfde) tabel voor een andere test. Hoe ga je daar mee om op een overzichtelijke manier? Hoe weet je welke data in welke test wordt gebruikt?

    De meeste unit-test frameworks hebben daar niet echt ondersteuning voor. Dus voor een hobby projectje voor een Formule1 manager spel, ben ik hier eens mee aan de slag gegaan. Nu gebruik ik zelf eigenlijk altijd de geintegreerde unit-testing van Visual Studio 2005/2008. Ik weet dat er andere, misschien wel betere zijn, maar ik vind de integratie in VS gewoon lekker. Maar hoe breid ik dit framework nou zo uit, dat ik bovenstaande problemen makkelijk kan oplossen?

    De eerste stap was een base-class maken voor mijn testen. Hierin kan ik dan functionaliteit toevoegen die ik nodig heb voor het runnen van mijn database testen.

      [TestClass]

      public class UnitTestBase

      {

        public TestContext TestContext { get; set; }

     

        [TestInitialize()]

        public void TestInitializeInvoker()

        {

          TestInitialize();

        }

     

        protected virtual void TestInitialize()

        {

        }

     

        [TestCleanup()]

        public void TestCleanupInvoker()

        {

          TestCleanup();

        }

     

        protected virtual void TestCleanup()

        {

        }

      }


     

    Volgende stap was om te zorgen dat elke test een connectie naar de database heeft en een transactie start. Door na elke test een Rollback te doen, blijft de database constant qua data. Dus ik maakte een class die tegen de testdatabase kon praten: connecties creeren en openen en scriptjes uitvoeren. De scriptjes zou ik dan opnemen als resource in de test-assembly. Ook koos ik er voor om een DbProviderFactory te gebruiken, zodat ik niet vast hing aan 1 bepaald type database.

      public class TestDatabase

      {

     

        public static IDbConnection GetConnection()

        {

          DbConnection connection = DbProvider.CreateConnection();

          connection.ConnectionString = ConnectionString;

          return connection;

        }

     

        public static IDbConnection OpenConnection()

        {

          IDbConnection connection = DbProvider.CreateConnection();

          connection.ConnectionString = ConnectionString;

          connection.Open();

          return connection;

        }

     

        private static string GetSql( string resourceName )

        {

          string fullResourceName = ResourceAssembly.GetName().Name + ( String.IsNullOrEmpty( ScriptsFolder ) ? "." : "." + ScriptsFolder + "." ) + resourceName;

          Stream resourceStream = ResourceAssembly.GetManifestResourceStream( fullResourceName );

     

          if ( resourceStream == null )

            throw new FileNotFoundException( String.Format( "Resource {0} not found.", resourceName ) );

     

          TextReader textReader = new StreamReader( resourceStream );

          string sql = textReader.ReadToEnd();

          textReader.Close();

          return sql;

        }

     

        private static void ExecuteSql( string sql )

        {

          ExecuteSql( sql, null );

        }

     

        private static void ExecuteSql( string sql, IDbTransaction transaction )

        {

          if ( String.IsNullOrEmpty( sql ) )

            return;

     

          string[] splittedSql = sql.Split( ';' );

          if ( splittedSql.Length > 1 )

            foreach ( string singleSql in splittedSql )

              ExecuteSql( singleSql, transaction );

          else

          {

            IDbCommand command = DbProvider.CreateCommand();

            command.Connection = transaction == null ? GetConnection() : transaction.Connection;

            command.CommandText = sql;

            bool closeConnection = false;

            if ( command.Connection.State == ConnectionState.Closed )

            {

              command.Connection.Open();

              closeConnection = true;

            }

            if (transaction != null )

              command.Transaction = transaction;

            command.ExecuteNonQuery();

            if ( closeConnection )

              command.Connection.Close();

          }

        }

     

        public static void ExecuteScript( string scriptName, IDbTransaction transaction )

        {

          ExecuteSql( GetSql( scriptName ), transaction );

        }

     

        public static void ExecuteScript( string scriptName )

        {

          ExecuteScript( scriptName, null );

        }

     

        public static void ExecuteDefaultScript()

        {

          ExecuteScript( defaultScriptName );

        }

     

        public static void ExecuteDefaultScript( IDbTransaction transaction)

        {

          ExecuteScript( defaultScriptName, transaction );

        }

      }

    }

    Maar wat ik niet wilde, was om mijn base-class te laten bepalen tegen welke database de testen zouden worden gerund. Dus ik had nog een class nodig om dit te bepalen. Ik wilde niet alles in een config file stoppen, maar een aparte class laten bepalen in welke type database we testen. Door middel van Inversion of Control laat ik dat dan een andere class bepalen.

    Om nu een test te laten runnen en automatisch een Rollback te doen na het uitvoeren van een test, maakte ik een Attribute.

      [AttributeUsage( AttributeTargets.Method )]

      public class AutoRollbackAttribute : Attribute

      {

     

      }

    Als ik dat attribute boven een test zet, dan zorgt de UnitTestBase er voor dat de Rollback automatisch wordt uitgevoerd.

        protected virtual void TestInitialize()

        {

          _connection = TestDatabase.OpenConnection();

          StartTransaction();

        }

     

        protected virtual void TestCleanup()

        {

          if ( HasAutoRollbackAttribute() )

            Transaction.Rollback();

        }

     

      private bool HasAutoRollbackAttribute()

      {

        MethodInfo testMethod = this.GetType().GetMetho( TestContext.TestName );

        object[] attributes = testMethod.GetCustomAttributes(
            typeof( AutoRollbackAttribute ), false );

        return attributes.Length > 0;

      }

     

    Een test zou dat alleen (behalve het TestMethod attribute natuurlijk) dan alleen het AutoRollback attribute moeten hebben, om er voor te zorgen dat de test niets veranderd in de database.

     

        [TestMethod]

        [AutoRollback]

        public void SomeTest()

        {

          //test implementation

        }

     

    Hiermee ben ik dus al een heel end. Maar ik wil ook toch testdata kunnen initialiseren voor een bepaalde test. Data die je alleen voor die test nodig hebt. Maar daar blog ik een volgende keer over. Dan zal ik ook de sourcecode beschikbaar stellen van dit mini 'testframework'.