more test-first development and refactoring with classic ASP

In this, the sixth part of a series on unit testing classic ASP, we will continue with our test-first development of the class we created in part five. All the previous parts are available at:

Part one: how to unit test classic ASP
Part two: user stories for classic ASP unit testing example
Part three: beginnings of a classic ASP unit test framework
Part four: automating classic ASP unit tests with NUnit
Part five: using the classic ASP unit test framework

We last left our OrderSettings class and its unit tests in a green pass state, but the CanOrderAt function was still hardcoded. We therefore need to include a new assertion in our unit test ASP file "OrderSettingsTest.asp" to test for a different time:

Dim onePm, twoPm, tenAM
onePm = "10/10/2008 13:00:00"
twoPm = "10/10/2008 14:00:00"
tenAM = "10/10/2008 10:00:00"

tester.AssertIsFalse currentOrderSettings.CanOrderAt(onePm), "Can Not Order At 13:00"
tester.AssertIsTrue currentOrderSettings.CanOrderAt(twoPm), "Can Order At 14:00"
tester.AssertIsTrue currentOrderSettings.CanOrderAt(tenAm), "Can Order At 10:00"

The unit tests now fail on the last assertion so it is time for us to introduce some real code in the CanOrderAt function:

    Public Function CanOrderAt(dateTime)
        
        Dim checkDate, cutOffDate, nextDate
        
        dateTime = CDate(dateTime)
        
        ' first strip the time off the date sent into this function
        checkDate = DateAdd("h",0-DatePart("h",dateTime),dateTime)
        checkDate = DateAdd("n",0-DatePart("n",dateTime),checkDate)
        checkDate = DateAdd("s",0-DatePart("s",dateTime),checkDate)
        
        ' now use this and add the time from the cutOffTime property
        cutOffDate = DateAdd("h",DatePart("h",Me.CutOffTime),checkDate)
        cutOffDate = DateAdd("n",DatePart("n",Me.CutOffTime),cutOffDate)
        cutOffDate = DateAdd("s",DatePart("s",Me.CutOffTime),cutOffDate)
        
        ' add the time from the nextOrderTime property
        nextDate = DateAdd("h",DatePart("h",Me.NextOrderTime),checkDate)
        nextDate = DateAdd("n",DatePart("n",Me.NextOrderTime),nextDate)
        nextDate = DateAdd("s",DatePart("s",Me.NextOrderTime),nextDate)
        
        CanOrderAt = (dateTime < cutOffDate) Or (dateTime >= nextDate)
        
    End Function

This is our first attempt at the code for this method - it strips the time off the original date and stores this in a variable called "checkDate"; we then derive two new variables by adding the time from our properties to this date so that the day remains the same as the one passed in. We can now compare the times and return whether ordering is possible.
When we run our unit tests, they pass.

There is obviously room for refactoring here, so we will now test-first a new class that will handle common date operations for us and enable us to change the code we have just written. We start by creating a new folder in our Unit test area called "Utilitites" and creating a new "DateUtilityFixture.cs" file:

namespace ASPTesting.Tests.UnitTests.Utilities
{
    [TestFixture]
    public class DateUtilityFixture : AspFixture
    {
        [Test]
        public void DateUtilityTest()
        {
            RunAspTest("UnitTests/Utilities/DateUtilityTest.asp");
        }
    }
}

This code is expecting a new DateUtilityTest.asp file - if we run the unit tests now, they fail due to a server error, so let's create the new test file:

<!--#include virtual="Tests/UnitTester.asp"-->
<!--#include virtual="Utilities/DateUtility.asp"-->

<%
dim tester
Set tester = new UnitTester
tester.Init("Testing Date Utility")

Dim dateUtil
Set dateUtil = new DateUtility

%>

Here we have an include for a file called "DateUtility" in our main project that does not yet exist and we set up the unit tester and create an instance of the DateUtility class. If we run the unit test for this it fails because of the missing DateUtility class, but before we write it, we should introduce some tests:

Dim dateUtil
Set dateUtil = new DateUtility

dim aDate, expectedDate, baseDate
aDate = CDate("10/10/2008 21:53:46")
expectedDate = CDate("10/10/2008 00:00:00")

tester.Given("a date of " + CStr(aDate) + " and we are testing StripTime")
tester.AssertAreEqual dateUtil.StripTime(aDate), expectedDate

aDate = CDate("10/10/2008 00:00:00")
tester.Given("a date of " + CStr(aDte) + " and we are testing StripTime")
tester.AssertAreEqual dateUtil.StripTime(aDate), expectedDate

We now have two tests for a StripTime function that we can now write in a new file in our main project - DateUtility. We have the stripping code already inside our OrderSettings class at the moment so we copy that code into our new class and refine it:

<%
Class DateUtility

    Public Function StripTime(inputDate)
    
        dim newDate
        
        inputDate = CDate(inputDate)
    
        newDate = DateAdd("h",0-DatePart("h",inputDate),inputDate)
        newDate = DateAdd("n",0-DatePart("n",inputDate),newDate)
        newDate = DateAdd("s",0-DatePart("s",inputDate),newDate)
        
        StripTime = newDate
    End Function
    
End Class 
%>

We now run our unit tests and they all pass. We could now add some more edge cases in for this function, but for this exercise we will continue onto refactoring our main OrderSettings class.

Because we have unit tests surrounding both the date utility function and the OrderSettings CanOrderAt function we can now refactor with some level of confidence and we see the true value of unit tests with classic ASP - we can confidently change the code! Let's have a look at the new OrderSettings code:

<!--#include virtual="Utilities/DateUtility.asp"-->
<%
Class OrderSettings

    Private m_cutOffTime
    Private m_nextOrderTime

    Public Property Get CutOffTime()
        CutOffTime = m_cutOffTime
    End Property
    Public Property Let CutOffTime(value)
        m_cutOffTime = CDate(value)
    End Property
    
    Public Property Get NextOrderTime()
        NextOrderTime = m_nextOrderTime
    End Property
    Public Property Let NextOrderTime(value)
        m_nextOrderTime = CDate(value)
    End Property
    
    Public Function CanOrderAt(dateTime)
        
        Dim checkDate, cutOffDate, nextDate, dateUtil
        
        Set dateUtil = new DateUtility
        
        dateTime = CDate(dateTime)
        
        checkDate = dateUtil.StripTime(dateTime)
        
        ' now use this and add the time from the cutOffTime property
        cutOffDate = DateAdd("h",DatePart("h",Me.CutOffTime),checkDate)
        cutOffDate = DateAdd("n",DatePart("n",Me.CutOffTime),cutOffDate)
        cutOffDate = DateAdd("s",DatePart("s",Me.CutOffTime),cutOffDate)
        
        ' add the time from the nextOrderTime property
        nextDate = DateAdd("h",DatePart("h",Me.NextOrderTime),checkDate)
        nextDate = DateAdd("n",DatePart("n",Me.NextOrderTime),nextDate)
        nextDate = DateAdd("s",DatePart("s",Me.NextOrderTime),nextDate)
        
        CanOrderAt = (dateTime < cutOffDate) Or (dateTime >= nextDate)
        
    End Function
End Class
%>

We have a new include declaration at the top of the file for the new DateUtility class and now our strip time functionality is one line (and we no longer need the comment as it is self explanatory).
We can now clean the rest of the code by introducing a new AddTime method to the DateUtility class. As always, we start with some new tests in the DateUtilityTest.asp file:

aDate = CDate("10/10/2008 00:00:00")
baseDate = CDate("01/01/2007 23:59:01")
expectedDate = CDate("10/10/2008 23:59:01")
tester.Given("a date of " + CStr(aDate) + " and a baseDate of " + CStr(baseDate) 
         + " and we are testing AddTime")
tester.AssertAreEqual dateUtil.AddTimeFrom(aDate, baseDate), expectedDate

These two new assertions cause our unit tests to fail because the function is not available. We therefore create it and add the code in from our OrderSettings class.
(Note that you could, in true red/green/refactor style, create the function and return a nominal date value first, thus causing the test to fail from an assertion as opposed to a runtime error. This would ensure your test fail for the correct reason - we are not doing so throughout here for brevity).

<%
Class DateUtility

    Public Function StripTime(inputDate)
    
        dim newDate
        
        inputDate = CDate(inputDate)
    
        newDate = DateAdd("h",0-DatePart("h",inputDate),inputDate)
        newDate = DateAdd("n",0-DatePart("n",inputDate),newDate)
        newDate = DateAdd("s",0-DatePart("s",inputDate),newDate)
        
        StripTime = newDate
    End Function
    
    Public Function AddTimeFrom(inputDate, fromDate)
    
        dim newDate
        
        inputDate = CDate(inputDate)
        fromDate = CDate(fromDate)
        
        newDate = DateAdd("h",DatePart("h", fromDate),inputDate)
        newDate = DateAdd("n",DatePart("n", fromDate),newDate)
        newDate = DateAdd("s",DatePart("s", fromDate),newDate)
        
        AddTimeFrom = newDate
    End Function
    
End Class 
%>

We now have our function coded and when we run our unit tests, they now pass. Again, we could add edge cases here and better describe our desired behaviour for this class, but we will continue on in this example.
We now refactor the CanOrderAt function of the OrderSettings class:

    Public Function CanOrderAt(dateTime)
        
        Dim checkDate, cutOffDate, nextDate, dateUtil
        
        Set dateUtil = new DateUtility
        
        dateTime = CDate(dateTime)
        
        checkDate = dateUtil.StripTime(dateTime)
        
        cutOffDate = dateUtil.AddTimeFrom(checkDate,Me.CutOffTime)
        
        nextDate = dateUtil.AddTimeFrom(checkDate,Me.NextOrderTime)
        
        CanOrderAt = (dateTime < cutOffDate) Or (dateTime >= nextDate)
        
    End Function

When we run our unit tests, they all pass!

This part has illustrated how we can do some true test-first development with classic ASP and bring some good practices such as encapsulation and single responsibility principle to our code. We refactored our code with confidence because we had tests surrounding the code that we were adding and modifying.
We have now provided the functionality we need for our first user story that we were exploring:

As a user if I access the system between 13:00 and 14:00, I cannot place an order and the system will provide an appropriate message.

Our order page would use this CanOrderAt function of our new API to evaluate whether ordering was possible for the current time and would display a message accordingly (we are not going to code that page or any other main ASP page in our unit test examples and simply explore our unit testing examples).

In the next part of this series we will explore some more classic ASP unit testing and attempt to deliver more functionality for our user story backlog.



3 comments:

  1. Anonymous October 27, 2009 at 5:18 PM

    Heya Justin,

    You have done a most excellent series here on Co-opting Nunit to run your Classic Asp testing for you.

    A huge kudos to you for opening my eyeballs to look further than AspUnit I was using to test manually.

    again, huge thanks!
    Dave M

    ps - any plans to continue with the next step in the series?

    ""In the next part of this series we will explore some more classic ASP unit testing and attempt to deliver more functionality for our user story backlog......""

     
  2. Anonymous September 19, 2011 at 12:26 PM

    Great work, I just got some classic asp code to work on, and this will my life a million times better. Thanks. Any new parts coming?

     
  3. Justin Davies September 20, 2011 at 10:08 AM

    Thanks for your positive comments!
    Unfortunately it was quite some time ago that I worked with classic ASP and I do not have any plans to come back and add to this series - hopefully it serves as a foundation for you to build on though.
    Good luck in your test first ASP!