Archive

Archive for October, 2009

Using Machine.Specifications with nant

October 20, 2009 1 comment

Wasted at least an hour today trying to track down an issue running my specs with NAnt. Turns out the problem was using <arg value=”{assembly1} {assembly 2}”  instead of <arg line=”{assembly1} {assembly2}” />.

The subtle difference between value and line is that value allows spaces and presents this to the exe as one argument by quoting the content of the attribute, whereas line passes the content of the attribute as it appears ( so the console runner sees 1 arg in the form “{assembly1} {assembly2}” instead of 2 separate args).  This problem presents itself as an error containing the following text :

Missing Assembly: {assembly1} {assembly2}

The xml for running machine.specifications with NAnt is:

    <target name="test.mspec" unless="${test.skip}">
        <mkdir dir="${test.specreportdir}" />
        <exec program="${test.mspecrunner}"
            basedir="${test.mspecdir}"
            workingdir="${root.dir}">
            <arg value="--silent" if="${teamcity.build}" />
            <arg value="--teamcity" if="${teamcity.build}" />
            <arg value="--html" /><arg dir="${test.specreportdir}" />
            <arg line="${test.assemblies}"/>
        </exec>
    </target>

where:

  • ${test.specreportdir} is the directory for the reports to go into
  • ${test.mspecrunner} is machine.specifications.consolerunner.exe
  • ${test.mspecdir} is the directory containing the mspec console runner
  • ${root.dir} is the root project directory
  • ${teamcity.build} is set from a team city specific build that is used for TC builds
  • ${test.assemblies} is a list of the assemblies containing specs separated by spaces

Simplify IUserType Testing

October 16, 2009 4 comments

Background

The application I am currently working on makes ( will make ) heavy use of the .NET TimeZoneInfo class to allow us to display times in the current users local time. To achieve this we need to capture and store the timezone from the user.

Capturing the timezone is relatively simple, I use a slightly modified version of this technique used by Nate Kohari’s most excellent Zen.

Storing the timezone provides an interesting choice of options;

I could store the raw offset in hours ( a double ) or in minutes ( an int ) and recreate the TimeZoneInfo each time I want to display a date or time in the users local time. This has the benefit of being incredibly simple and, with a little work to create an extension method, generates almost no duplication except the act of converting the double or int into a TimeZoneInfo.

The second alternative was to let my trusty ORM do the work for me. I use NHibernate and after a little research I found the following articles about creating an IUserType that would allow me to map a TimeZoneInfo into a database field. If you want all the gory details of how to do it go and read those articles, otherwise the code for my TimeZoneInfoUserType is shown at the bottom of the post ( see the section titled The Implementation ).

The Useful Stuff

Normally I use a BDD style approach for building something like this, but as I wasn’t sure how to solve the problem, I spiked out the approach, then added the tests and refined the implementation as required.

Part of spiking out the solution was figuring out how to test it and this initially tripped me up as I was considering mocking IDbCommand and IDataReader ( which gets ugly fast ).

After a little bit of thought, I figured I could use a SqlCommand for the IDbCommand. The following snippet shows what you need to do to use a SqlCommand for testing your NullSafeSet ( note where parameter index comes from ).

_command = new SqlCommand();
_parameterIndex = _command.Parameters.Add( _command.CreateParameter() );

// ... this is then used in the NullSafeSet test ...
_userType.NullSafeSet( _command, null, _parameterIndex );

To find an acceptable IDataReader stand-in, I went poking around with Reflector and found an IDataReader implementation that wraps a DataTable. This gave me a means of quickly and easily controlling the data being passed to the code under test. The following snippet shows how you can set up an IDataReader from a DataTable in as little as 5 lines of code for testing your NullSafeGet ( note the data reader must be “initialised” by calling Read() before using it ):

var dataTable = new DataTable();
dataTable.Columns.Add( ColumnName, typeof( int ) );
dataTable.LoadDataRow( new object[] {TimezoneOffset * 60}, true );
_dataReader = new DataTableReader( dataTable );
_dataReader.Read();

// ... this is then used in the NullSafeSet test ...
_timezoneInfo = (TimeZoneInfo) _userType.NullSafeGet( _dataReader, new[] {ColumnName}, null );

The full source for the TimeZoneInfoUserType and the associated tests is shown below:

The Implementation

    public class TimezoneInfoUserType : IUserType
    {
        public new bool Equals(object x, object y)
        {
            if( ReferenceEquals( x, y ) )
            {
                return true;
            }
            if( x == null || y == null )
            {
                return false;
            }

            return x.Equals( y );
        }

        public int GetHashCode( object x )
        {
            return x.GetHashCode();
        }

        public object NullSafeGet( IDataReader rs, string[] names, object owner )
        {
            var rawValue = NHibernateUtil.Int32.NullSafeGet( rs, names[ 0 ] );
            if( rawValue == null )
            {
                return null;
            }
            var timezoneOffsetMinutes = (int) rawValue;
            var timezoneOffset = new TimeSpan( 0, timezoneOffsetMinutes, 0 );
            return TimeZoneInfoFactory.Create( timezoneOffset.TotalHours );
        }

        public void NullSafeSet( IDbCommand cmd, object value, int index )
        {
            if(value == null)
            {
                NHibernateUtil.Int32.NullSafeSet( cmd, null, index );
                return;
            }
            var timezoneInfo = (TimeZoneInfo) value;
            var offsetInMinutes = timezoneInfo.BaseUtcOffset.TotalMinutes;
            NHibernateUtil.Int32.NullSafeSet( cmd, Convert.ToInt32( offsetInMinutes ) , index );
        }

        public object DeepCopy( object value )
        {
            return value;
        }

        public object Replace( object original, object target, object owner )
        {
            return original;
        }

        public object Assemble( object cached, object owner )
        {
            return cached;
        }

        public object Disassemble( object value )
        {
            return value;
        }

        public SqlType[] SqlTypes
        {
            get { return new[] {SqlTypeFactory.Int32}; }
        }

        public Type ReturnedType
        {
            get { return typeof( TimeZoneInfo ); }
        }

        public bool IsMutable
        {
            get { return false; }
        }
    }

The Tests
Note: If you are wondering about the =()=> sequence, I use MSpec for my testing.

    [Subject(typeof(TimezoneInfoUserType))]
    public class When_saving_a_null_TimezoneInfo
    {
        static int _parameterIndex;
        static TimezoneInfoUserType _userType;
        static IDbCommand _command;

        Establish context = () =>
        {
            _userType = new TimezoneInfoUserType();
            _command = new SqlCommand();
            _parameterIndex = _command.Parameters.Add( _command.CreateParameter() );
        };

        Because of = () =>
            _userType.NullSafeSet( _command, null, _parameterIndex );

        It the_offset_is_stored_as_null = () =>
            ((IDataParameter)_command.Parameters[ _parameterIndex ]).Value.ShouldEqual(DBNull.Value);
    }

    [Subject(typeof(TimezoneInfoUserType))]
    public class When_saving_a_TimezoneInfo_with_a_zero_offset
    {
        static int _parameterIndex;
        static TimezoneInfoUserType _userType;
        static IDbCommand _command;
        static TimeZoneInfo _timezoneInfo;

        Establish context = () =>
        {
            _timezoneInfo = TimeZoneInfoFactory.Create( 0 );
            _userType = new TimezoneInfoUserType();
            _command = new SqlCommand();
            _parameterIndex = _command.Parameters.Add( _command.CreateParameter() );
        };

        Because of = () =>
            _userType.NullSafeSet(_command, _timezoneInfo, _parameterIndex);

        It the_offset_is_stored_as_zero = () =>
            ( (IDataParameter) _command.Parameters[ _parameterIndex ] ).Value.ShouldEqual( 0 );
    }

    [Subject(typeof(TimezoneInfoUserType))]
    public class When_saving_a_TimezoneInfo_with_a_positive_offset
    {
        const int TimezoneOffset = 12;
        static int _parameterIndex;
        static TimezoneInfoUserType _userType;
        static IDbCommand _command;
        static TimeZoneInfo _timezoneInfo;

        Establish context = () =>
        {
            _timezoneInfo = TimeZoneInfoFactory.Create( TimezoneOffset );
            _userType = new TimezoneInfoUserType();
            _command = new SqlCommand();
            _parameterIndex = _command.Parameters.Add( _command.CreateParameter() );
        };

        Because of = () =>
            _userType.NullSafeSet( _command, _timezoneInfo, _parameterIndex );

        It the_offset_is_stored_in_minutes = () =>
            ( (IDataParameter) _command.Parameters[ _parameterIndex ] ).Value.ShouldEqual( TimezoneOffset * 60 );
    }

    [Subject(typeof(TimezoneInfoUserType))]
    public class When_saving_a_TimezoneInfo_with_a_negative_offset
    {
        const int TimezoneOffset = -12;
        static int _parameterIndex;
        static TimezoneInfoUserType _userType;
        static IDbCommand _command;
        static TimeZoneInfo _timezoneInfo;

        Establish context = () =>
        {
            _timezoneInfo = TimeZoneInfoFactory.Create( TimezoneOffset );
            _userType = new TimezoneInfoUserType();
            _command = new SqlCommand();
            _parameterIndex = _command.Parameters.Add( _command.CreateParameter() );
        };

        Because of = () =>
            _userType.NullSafeSet( _command, _timezoneInfo, _parameterIndex );

        It the_offset_is_stored_in_minutes = () =>
            ( (IDataParameter) _command.Parameters[ _parameterIndex ] ).Value.ShouldEqual( TimezoneOffset * 60 );
    }

    [Subject(typeof(TimezoneInfoUserType))]
    public class When_retrieving_a_null_TimezoneInfo
    {
        const string ColumnName = "Timezone";
        static TimeZoneInfo _timezoneInfo;
        static TimezoneInfoUserType _userType;
        static IDataReader _dataReader;

        Establish context = () =>
        {
            _userType = new TimezoneInfoUserType();
            var dataTable = new DataTable();
            dataTable.Columns.Add( ColumnName, typeof( int ) );
            dataTable.LoadDataRow( new object[] {null}, true );
            _dataReader = new DataTableReader( dataTable );
            _dataReader.Read();
        };

        Because of = () =>
            _timezoneInfo = (TimeZoneInfo) _userType.NullSafeGet( _dataReader, new[] {ColumnName}, null );

        It returns_null = () =>
            _timezoneInfo.ShouldBeNull();
    }

    [Subject(typeof(TimezoneInfoUserType))]
    public class When_retrieving_a_TimezoneInfo_with_a_zero_offset
    {
        const string ColumnName = "Timezone";
        static TimeZoneInfo _timezoneInfo;
        static TimezoneInfoUserType _userType;
        static IDataReader _dataReader;

        Establish context = () =>
        {
            _userType = new TimezoneInfoUserType();
            var dataTable = new DataTable();
            dataTable.Columns.Add( ColumnName, typeof( int ) );
            dataTable.LoadDataRow( new object[] {0}, true );
            _dataReader = new DataTableReader( dataTable );
            _dataReader.Read();
        };

        Because of = () =>
            _timezoneInfo = (TimeZoneInfo) _userType.NullSafeGet( _dataReader, new[] {ColumnName}, null );

        It the_BaseUtcOffset_is_zero = () =>
            _timezoneInfo.BaseUtcOffset.TotalHours.ShouldEqual( 0d );

        It the_displayname_is_GMT = () =>
            _timezoneInfo.DisplayName.ShouldEqual( "GMT" );
    }

    [Subject(typeof(TimezoneInfoUserType))]
    public class When_retrieving_a_TimezoneInfo_with_a_positive_offset
    {
        const string ColumnName = "Timezone";
        const int TimezoneOffset = 12;
        static TimeZoneInfo _timezoneInfo;
        static TimezoneInfoUserType _userType;
        static IDataReader _dataReader;

        Establish context = () =>
        {
        _userType = new TimezoneInfoUserType();
        var dataTable = new DataTable();
        dataTable.Columns.Add(ColumnName, typeof(int));
        dataTable.LoadDataRow(new object[] { TimezoneOffset * 60 }, true);
        _dataReader = new DataTableReader(dataTable);
        _dataReader.Read();
        };

        Because of = () =>
            _timezoneInfo = (TimeZoneInfo)_userType.NullSafeGet(_dataReader, new[] { ColumnName }, null);

        It the_BaseUtcOffset_is_generated_from_the_number_of_minutes_retrieved = () =>
            _timezoneInfo.BaseUtcOffset.TotalHours.ShouldEqual( TimezoneOffset );

        It the_displayname_follows_the_pattern_GMT_plus_timezone_offset_ = () =>
            _timezoneInfo.DisplayName.ShouldEqual( "GMT+" + TimezoneOffset );
    }

    [Subject(typeof(TimezoneInfoUserType))]
    public class When_retrieving_a_TimezoneInfo_with_a_negative_offset
    {
        const string ColumnName = "Timezone";
        const int TimezoneOffset = -12;
        static TimeZoneInfo _timezoneInfo;
        static TimezoneInfoUserType _userType;
        static IDataReader _dataReader;

        Establish context = () =>
        {
            _userType = new TimezoneInfoUserType();
            var dataTable = new DataTable();
            dataTable.Columns.Add( ColumnName, typeof( int ) );
            dataTable.LoadDataRow( new object[] {TimezoneOffset * 60}, true );
            _dataReader = new DataTableReader( dataTable );
            _dataReader.Read();
        };

        Because of = () =>
            _timezoneInfo = (TimeZoneInfo) _userType.NullSafeGet( _dataReader, new[] {ColumnName}, null );

        It the_BaseUtcOffset_is_generated_from_the_number_of_minutes_retrieved = () =>
            _timezoneInfo.BaseUtcOffset.TotalHours.ShouldEqual( TimezoneOffset );

        It the_displayname_follows_the_pattern_GMT_minus_timezone_offset_ = () =>
            _timezoneInfo.DisplayName.ShouldEqual( "GMT" + TimezoneOffset );
    }

Categories: NHibernate, TDD, Unit Testing
Follow

Get every new post delivered to your Inbox.