Test First Development Walk Through with Visual Studio 2011 Preview

Introduction

Many of us know we should be using test first development, however it is hard to break old habits.  I have to admit, I started to solve this particular problem I’m going to use an an example first without test first, then realized what a pickle I was going to be in proving to myself it worked.  So, I thought, why not blog my experience as I do it.  Using Visual Studio unit testing makes this pretty easy.

 

The Problem

I’m currently building a multithreaded email processor and part of that process is I have to figure out, for any given use whether they are supposed to have there email server checked.  So, the way I look at it, I need a method that takes in the following parameter.

    1. Now – This is actually todays date and time in UTC, but since this method need to be tested, I’m going to pass in now rather just use it directly.
    2. LastActivityOfUserDateTime – This is the last time we saw any activity for this user.
    3. LastEmailSession – This is when the email session was last run (server check).
    4. DefaultSecondsForLastActivityTreshold – This is used to determine weather the user last activity makes that user considered active or inactive.  That is, say this is 60 seconds.  Then, if the user has been active in the last 60 seconds, then this user is considered active.
    5. DefaultSecondsBetweenEmailRetryOutsideThreshold – If the user is not active (as defined by DefaultSecondsForLastActivityTreshold) then this is the number of seconds we should wait before we should try and contact the users email server again.
    6. DefaultSecondsBetweenEmailRetryInsideThreshold – If the user is active (as defined by DefaultSecondsForLastActivityTreshold) then this is the number of seconds we should wait before we should try and contact the users email server again.

Given these 4 parameters, we should write a method that returns a Boolean indicating weather the mail server needs to be rechecked for a given user.

 

The Method To Test

So, let’s write a method signature.  We will use this in our real project, then write some tests to see if it works correctly.  We have not written the actual method yet, we are just defining what it does, then first, very important, writing the tests to prove it will work.  This way, as we add all the corner cases, we can keep making sure the original cases we programmed have not broken.

So, here is how I see the class definition:

public bool IsUserReadyToContactEmailServer(DateTime currentDateTime,
            DateTime lastActivityOfUserDateTime,
            DateTime lastEmailSession,
            int defaultSecondsForLastActivityTreshold,
            int defaultSecondsBetweenEmailRetryOutsideThreshold,
            int defaultSecondsBetweenEmailRetryInsideThreshold)
        {
            bool activeUser = IsUserCurrentlyActive(currentDateTime, lastActivityOfUserDateTime,
                                                      defaultSecondsForLastActivityTreshold);
            int thresholdForRetrySeconds =
                activeUser
                    ? defaultSecondsBetweenEmailRetryInsideThreshold
                    : defaultSecondsBetweenEmailRetryOutsideThreshold;

            // this has to be a double because could be huge number if last email send very long ago
            double timeSinceLastEmailSessionSeconds =
                currentDateTime.Subtract(lastEmailSession).TotalSeconds;

           return timeSinceLastEmailSessionSeconds > thresholdForRetrySeconds;
        }

Creating a Test Project In Visual Studio 2011 Preview

Now that we have our method we want to test, let’s create a test project.  This is very straight forward.  We first just say “File/New Project” then choose “Unit Test Project”.

 

image

 

I’m going to simply create a method called TestMethodIsUserReadyToContactEmailServer().

using System;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace AEWeb.Tests
{
    [TestClass]
    public class UnitTest1
    {
        [TestMethod]
        public void TestMethodIsUserReadyToContactEmailServer()
        {
        }
    }
}

Now, we need to add some guts.  There is an automated way to create the actual test stubs, but at the moment I can’t seem to find it.  In this case, we really just need to test a bunch of corner cases so let’s code the tests up. Below are the tests I’ve come up with including comments that make them self explanatory.

So, as I’m creating my tests, I realize I need to create an extra class to help this one.  That is specifically, I need to add a method that simply determines if the user is currently active.  I’m going to call that IsCurrentlyActive and have it take three paramaters.  currentDateTime, DefaultSecondsForLastActivityThreshold and usersLastActivityDate.  I realized this because solving the full problem of figuring out if the user is ready to sync email is to complex in one step.

If I were developing this without unit tests, I would have figured the same thing out, but then as I built it, I would not be verifying it’s correctness with testing.

Creating Stub Classes

So, my new class I want to test will be this:

 /// <summary>
        /// This really just a supporting method for the above IsUserReadyToContactEmailServer call
        /// </summary>
        /// <param name="currentDateTime">Current datetime (for testing can be anything)</param>
        /// <param name="usersLastActivityDate">time user was last seen </param>
        /// <param name="defaultSecondsForLastActivityTreshold">our definition of what makes a user currently active</param>
        /// <returns></returns>
        public bool IsUserCurrentlyActive(DateTime currentDateTime,
            DateTime usersLastActivityDate,
            int defaultSecondsForLastActivityTreshold
            )
        {

            return true;
        }

The Unit Tests Themselves

So, now is time to actually write the tests.  I’m not going to explain here all the mechanics of testing.  I assume you understand the basics are you right calls to methods, then make assertions for correctness.  If the assertion is wrong, then print that message and of course the test fails.

So, Here are my test cases for just the helper method (IsUserCurrentlyActive).

using System;
using AELib;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace AEWeb.Tests
{
    [TestClass]
    public class UnitTestEmailReadyCheck
    {

        [TestMethod]
        public void UnitTestRecentActivity()
        {
            {
                // a long time ago
                var currentDateTime = new DateTime(2012, 3, 1, 12, 0, 0); // assume current time is 3/1/2012 at noon
                var lastActivityOfUserDateTime = new DateTime(2012, 2, 1, 12, 0, 0);
                // last activity was a month ago, 2/1/2012 at noon
                // threshhold for calling a user active
                const int defaultSecondsForLastActivityTreshold = 60*15; // let's call this 15 minute threshhold
                bool isActive = new MailServerReadyCheck().
                    IsUserCurrentlyActive(currentDateTime, lastActivityOfUserDateTime,
                                          defaultSecondsForLastActivityTreshold);
                Assert.IsFalse(isActive,
                               "User Should Be Inactive because last activity was 1 month ago and threshhold is 15 minutes");
            }
        }

        [TestMethod]
        public void UnitTestNoRecentActivity()
        {
            // a just inside threshold
            var currentDateTime = new DateTime(2012, 3, 1, 12, 0, 0); // assume current time is 3/1/2012 at noon
            var lastActivityOfUserDateTime = new DateTime(2012, 3, 1, 11, 55, 0);
            // last activity was 5 minutes before noon on 2/1/2012
            // threshhold for calling a user active
            const int defaultSecondsForLastActivityTreshold = 60*15; // let's call this 15 minute threshhold
            bool isActive = new MailServerReadyCheck().
                IsUserCurrentlyActive(currentDateTime, lastActivityOfUserDateTime,
                                      defaultSecondsForLastActivityTreshold);
            Assert.IsTrue(isActive,
                          "User Should Be active because last activity was 5 minutes ago and threshhold is 15 minutes");
        }

        [TestMethod]
        public void UnitTestCurrentDateBehindActivity()
        {
            // impossible cause of last activity being after current time. just need to
            // make sure it returns true and does not crash
            var currentDateTime = new DateTime(2012, 3, 1, 12, 0, 0); // assume current time is 3/1/2012 at noon
            var lastActivityOfUserDateTime = new DateTime(2012, 3, 1, 18, 0, 0);
            // last activity was 5 minutes after noon on 2/1/2012
            // threshhold for calling a user active
            const int defaultSecondsForLastActivityTreshold = 60*15; // let's call this 15 minute threshhold
            bool isActive = new MailServerReadyCheck().
                IsUserCurrentlyActive(currentDateTime, lastActivityOfUserDateTime,
                                      defaultSecondsForLastActivityTreshold);
            Assert.IsTrue(isActive,
                          "User Should Be active because last activity was 5 minutes after current time");
        }
    }
}

 

Then, I write the actual method:

 

       /// <summary>
        /// This really just a supporting method for the above IsUserReadyToContactEmailServer call
        /// </summary>
        /// <param name="currentDateTime">Current datetime (for testing can be anything)</param>
        /// <param name="usersLastActivityDate">time user was last seen </param>
        /// <param name="defaultSecondsForLastActivityTreshold">our definition of what makes a user currently active</param>
        /// <returns></returns>
        public bool IsUserCurrentlyActive(DateTime currentDateTime,
            DateTime usersLastActivityDate,
            int defaultSecondsForLastActivityTreshold
            )
        {
            int secondsSinceLastActivity = Convert.ToInt32(currentDateTime.Subtract(usersLastActivityDate).TotalSeconds);
            bool activeStatus = defaultSecondsForLastActivityTreshold > secondsSinceLastActivity;
            return activeStatus;
        }

And finally, run the test (skipping a little debugging to make them all work)

Presto!

 

image

 

For completeness, I’m pasting the full unit test for the other main class I’m interested in testing below (as well as the class itself), however I won’t go into all the details.  Everything is basically the same, just more of it

 

Conclusions and Observations

Turns out, this was quite a bit of work to generate all these tests.  At first pass, I can hear someone saying it seems like a lot to do just for two fairly simple methods.  My answer is that I struggled trying to get it right until I finally decided to write the tests.  Now, I’m confident it works correctly.  What is more, if there is a bug in my algorithm, or something else comes up I need to include (which is very likely) I now have a great way to prove to myself (and others) that my class works.  Also, this serves as great documentation for what expectations are for this very important function.

 

Appendex – Full Unit Test And Class Source

 

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace AELib
{
    public class MailServerReadyCheck
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="currentDateTime">This is actually todays date and time in UTC, but since 
        ///    this method need to be tested, I’m going to pass in now rather just use it directly</param>
        /// <param name="lastActivityOfUserDateTime"> This is used to determine weather the user last activity
        ///      makes that user considered active or inactive.  That is, say this is 60 seconds.
        ///      Then, if the user has been active in the last 60 seconds, then this user is considered active.</param>
        /// <param name="lastEmailSession">When the last email completed for this user</param>
        /// <param name="defaultSecondsForLastActivityTreshold">This is used to determine weather the user last activity makes that 
        ///      user considered active or inactive.  That is, say this is 60 seconds.  Then, if the user has 
        ///      been active in the last 60 seconds, then this user is considered active.</param>
        /// <param name="defaultSecondsBetweenEmailRetryOutsideThreshold"> If the user is not active (as defined by 
        ///      DefaultSecondsForLastActivityTreshold) then this is the number of seconds we should wait before 
        ///      we should try and contact the users email server again.</param>
        /// <param name="defaultSecondsBetweenEmailRetryInsideThreshold">If the user is active (as defined by 
        ///      DefaultSecondsForLastActivityTreshold) then this is the number of seconds we should wait 
        ///      before we should try and contact the users email server again.</param>
        /// <returns>returns a boolean indicating weather the mail server needs to be rechecked for a given user.</returns>
        public bool IsUserReadyToContactEmailServer(DateTime currentDateTime,
            DateTime lastActivityOfUserDateTime,
            DateTime lastEmailSession,
            int defaultSecondsForLastActivityTreshold,
            int defaultSecondsBetweenEmailRetryOutsideThreshold,
            int defaultSecondsBetweenEmailRetryInsideThreshold)
        {
            bool activeUser = IsUserCurrentlyActive(currentDateTime, lastActivityOfUserDateTime,
                                                      defaultSecondsForLastActivityTreshold);
            int thresholdForRetrySeconds =
                activeUser
                    ? defaultSecondsBetweenEmailRetryInsideThreshold
                    : defaultSecondsBetweenEmailRetryOutsideThreshold;

            // this has to be a double because could be huge number if last email send very long ago
            double timeSinceLastEmailSessionSeconds =
                currentDateTime.Subtract(lastEmailSession).TotalSeconds;

           return timeSinceLastEmailSessionSeconds > thresholdForRetrySeconds;
        }

        /// <summary>
        /// This really just a supporting method for the above IsUserReadyToContactEmailServer call
        /// </summary>
        /// <param name="currentDateTime">Current datetime (for testing can be anything)</param>
        /// <param name="usersLastActivityDate">time user was last seen </param>
        /// <param name="defaultSecondsForLastActivityTreshold">our definition of what makes a user currently active</param>
        /// <returns></returns>
        public bool IsUserCurrentlyActive(DateTime currentDateTime,
            DateTime usersLastActivityDate,
            int defaultSecondsForLastActivityTreshold
            )
        {
            int secondsSinceLastActivity = Convert.ToInt32(currentDateTime.Subtract(usersLastActivityDate).TotalSeconds);
            bool activeStatus = defaultSecondsForLastActivityTreshold > secondsSinceLastActivity;
            return activeStatus;
        }
    }
}

And the tests…

 

using System;
using AELib;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace AEWeb.Tests
{
    [TestClass]
    public class UnitTestEmailReadyCheck
    {

        ///////////////////// IsUserCurrentlyActive follows


        [TestMethod]
        public void UnitTestRecentActivity()
        {
            {
                // a long time ago
                var currentDateTime = new DateTime(2012, 3, 1, 12, 0, 0); // assume current time is 3/1/2012 at noon
                var lastActivityOfUserDateTime = new DateTime(2012, 2, 1, 12, 0, 0);
                // last activity was a month ago, 2/1/2012 at noon
                // threshhold for calling a user active
                const int defaultSecondsForLastActivityTreshold = 60*15; // let's call this 15 minute threshhold
                bool isActive = new MailServerReadyCheck().
                    IsUserCurrentlyActive(currentDateTime, lastActivityOfUserDateTime,
                                          defaultSecondsForLastActivityTreshold);
                Assert.IsFalse(isActive,
                               "User Should Be Inactive because last activity was 1 month ago and threshhold is 15 minutes");
            }
        }

        [TestMethod]
        public void UnitTestNoRecentActivity()
        {
            // a just inside threshold
            var currentDateTime = new DateTime(2012, 3, 1, 12, 0, 0); // assume current time is 3/1/2012 at noon
            var lastActivityOfUserDateTime = new DateTime(2012, 3, 1, 11, 55, 0);
            // last activity was 5 minutes before noon on 2/1/2012
            // threshhold for calling a user active
            const int defaultSecondsForLastActivityTreshold = 60*15; // let's call this 15 minute threshhold
            bool isActive = new MailServerReadyCheck().
                IsUserCurrentlyActive(currentDateTime, lastActivityOfUserDateTime,
                                      defaultSecondsForLastActivityTreshold);
            Assert.IsTrue(isActive,
                          "User Should Be active because last activity was 5 minutes ago and threshhold is 15 minutes");
        }

        [TestMethod]
        public void UnitTestCurrentDateBehindActivity()
        {
            // impossible cause of last activity being after current time. just need to
            // make sure it returns true and does not crash
            var currentDateTime = new DateTime(2012, 3, 1, 12, 0, 0); // assume current time is 3/1/2012 at noon
            var lastActivityOfUserDateTime = new DateTime(2012, 3, 1, 18, 0, 0);
            // last activity was 5 minutes after noon on 2/1/2012
            // threshhold for calling a user active
            const int defaultSecondsForLastActivityTreshold = 60*15; // let's call this 15 minute threshhold
            bool isActive = new MailServerReadyCheck().
                IsUserCurrentlyActive(currentDateTime, lastActivityOfUserDateTime,
                                      defaultSecondsForLastActivityTreshold);
            Assert.IsTrue(isActive,
                          "User Should Be active because last activity was 5 minutes after current time");
        }

        ///////////////////// IsUserReadyToContactEmailServer follows

        [TestMethod]
        public void UnitTestIsUserReadyToContactEmailServerLongTimeAgoUserInActive()
        {
            // let's use these parameters for every test

            // let's call this 30 minute threshhold. no contact within 30 minutes, than user is inactive
            const int defaultSecondsForLastActivityTreshold = 60 * 30;

            // if active user, check email every 2 minutes
            const int defaultSecondsBetweenEmailRetryInsideThreshold = 60 * 2;

            // if inactive user, check email every 20 minutes
            const int defaultSecondsBetweenEmailRetryOutsideThreshold = 60 * 20; 

            // user checked email 5 hours ago and email has not been run for 3 hours
            var currentDateTime = new DateTime(2012, 3, 1, 12, 0, 0); // assume current time is 3/1/2012 at noon
            var lastEmailSessionDateTime = new DateTime(2011,3,1,2, 0, 0); // email session 2am of 1 year ago (very long ago)
            var lastActivityOfUserDateTime = new DateTime(2011, 3, 1, 7, 0, 0); // user checked email at 7am a year ago
           
            bool emailSessionNeeded = new MailServerReadyCheck().IsUserReadyToContactEmailServer(currentDateTime,
                lastActivityOfUserDateTime,lastEmailSessionDateTime,defaultSecondsForLastActivityTreshold,
                defaultSecondsBetweenEmailRetryOutsideThreshold,
                defaultSecondsBetweenEmailRetryInsideThreshold);


            Assert.IsTrue(emailSessionNeeded,
                          "user checked email 5 hours ago and email has not been run for 3 hours. Should have needed email session");
        }

        [TestMethod]
        public void UnitTestIsUserReadyToContactEmailServerLongTimeAgoUserActive()
        {
            // let's use these parameters for every test
            // let's call this 30 minute threshhold. no contact within 30 minutes, than user is inactive
            const int defaultSecondsForLastActivityTreshold = 60 * 30;

            // if active user, check email every 2 minutes
            const int defaultSecondsBetweenEmailRetryInsideThreshold = 60 * 2;

            // if inactive user, check email every 20 minutes
            const int defaultSecondsBetweenEmailRetryOutsideThreshold = 60 * 20;

            // user checked email 1 minute ago and email has not been run for 3 hours
            var currentDateTime = new DateTime(2012, 3, 1, 12, 0, 0); // assume current time is 3/1/2012 at noon
            var lastEmailSessionDateTime = new DateTime(2011, 3, 1, 2, 0, 0); // email session 2am of 1 year ago (very long ago)
            var lastActivityOfUserDateTime = new DateTime(2012, 3, 1, 11, 59, 0); // user checked email 1 minute ago

            bool emailSessionNeeded = new MailServerReadyCheck().IsUserReadyToContactEmailServer(currentDateTime,
                lastActivityOfUserDateTime, lastEmailSessionDateTime, defaultSecondsForLastActivityTreshold,
                defaultSecondsBetweenEmailRetryOutsideThreshold,
                defaultSecondsBetweenEmailRetryInsideThreshold);


            Assert.IsTrue(emailSessionNeeded,
                          "user checked email  1 minute ago and email not been run for 1 year. should have run email again");
        }

        [TestMethod]
        public void UnitTestIsUserReadyToContactEmailServerRecentlyUserActive()
        {
            // let's use these parameters for every test
            // let's call this 30 minute threshhold. no contact within 30 minutes, than user is inactive
            const int defaultSecondsForLastActivityTreshold = 60 * 30;

            // if active user, check email every 2 minutes
            const int defaultSecondsBetweenEmailRetryInsideThreshold = 60 * 2;

            // if inactive user, check email every 20 minutes
            const int defaultSecondsBetweenEmailRetryOutsideThreshold = 60 * 20;

            // user checked email 1 minute ago and email has not been run for 3 hours
            var currentDateTime = new DateTime(2012, 3, 1, 12, 0, 0); // assume current time is 3/1/2012 at noon
            var lastEmailSessionDateTime = new DateTime(2012, 3, 1, 11, 50, 0); // email session 10 minutes ago
            var lastActivityOfUserDateTime = new DateTime(2012, 3, 1, 11, 59, 0); // user checked email 1 minute ago

            bool emailSessionNeeded = new MailServerReadyCheck().IsUserReadyToContactEmailServer(currentDateTime,
                lastActivityOfUserDateTime, lastEmailSessionDateTime, defaultSecondsForLastActivityTreshold,
                defaultSecondsBetweenEmailRetryOutsideThreshold,
                defaultSecondsBetweenEmailRetryInsideThreshold);


            Assert.IsTrue(emailSessionNeeded,
                          "active user, email checked 10 minutes ago but since active should check again");
        }


        [TestMethod]
        public void UnitTestIsUserReadyToContactEmailServerRecentlyUserInActive()
        {
            // let's use these parameters for every test
            // let's call this 30 minute threshhold. no contact within 30 minutes, than user is inactive
            const int defaultSecondsForLastActivityTreshold = 60 * 30;

            // if active user, check email every 2 minutes
            const int defaultSecondsBetweenEmailRetryInsideThreshold = 60 * 2;

            // if inactive user, check email every 20 minutes
            const int defaultSecondsBetweenEmailRetryOutsideThreshold = 60 * 20;

            // user checked email 1 minute ago and email has not been run for 3 hours
            var currentDateTime = new DateTime(2012, 3, 1, 12, 0, 0); // assume current time is 3/1/2012 at noon
            var lastEmailSessionDateTime = new DateTime(2012, 3, 1, 11, 50, 0); // email session 10 minutes ago
            var lastActivityOfUserDateTime = new DateTime(2011, 3, 1, 11, 59, 0); // user checked email 1 year ago

            bool emailSessionNeeded = new MailServerReadyCheck().IsUserReadyToContactEmailServer(currentDateTime,
                lastActivityOfUserDateTime, lastEmailSessionDateTime, defaultSecondsForLastActivityTreshold,
                defaultSecondsBetweenEmailRetryOutsideThreshold,
                defaultSecondsBetweenEmailRetryInsideThreshold);


            Assert.IsFalse(emailSessionNeeded,
                          "inactive user, but email checked very recently so should not be checking again");
        }


        [TestMethod]
        public void UnitTestIsUserReadyToContactEmailServerEmailLastSession10MinutesagoActive()
        {
            // let's use these parameters for every test
            // let's call this 30 minute threshhold. no contact within 30 minutes, than user is inactive
            const int defaultSecondsForLastActivityTreshold = 60 * 30;

            // if active user, check email every 2 minutes
            const int defaultSecondsBetweenEmailRetryInsideThreshold = 60 * 2;

            // if inactive user, check email every 20 minutes
            const int defaultSecondsBetweenEmailRetryOutsideThreshold = 60 * 20;

            // user checked email 1 minute ago and email has not been run for 3 hours
            var currentDateTime = new DateTime(2012, 3, 1, 12, 0, 0); // assume current time is 3/1/2012 at noon
            var lastEmailSessionDateTime = new DateTime(2012, 3, 1, 1, 11, 50); // email session 10 minutes ago
            var lastActivityOfUserDateTime = new DateTime(2011, 3, 1, 11, 59, 0); // user checked email 5 minutes ago

            bool emailSessionNeeded = new MailServerReadyCheck().IsUserReadyToContactEmailServer(currentDateTime,
                lastActivityOfUserDateTime, lastEmailSessionDateTime, defaultSecondsForLastActivityTreshold,
                defaultSecondsBetweenEmailRetryOutsideThreshold,
                defaultSecondsBetweenEmailRetryInsideThreshold);


            Assert.IsTrue(emailSessionNeeded,
                          "active user, but email checked very recently so should be checking again");
        }






    }
}

 

And the results of all tests running Smile

image

 

And, if you are still reading, I’ve got 100% of these two methods covered from these unit tests using Code Coverage.

 

image

 

 

About Peter Kellner

Peter is a software professional specializing in mobile and web technologies. He has also been a Microsoft MVP for the past 7 years. To read more about Peter Kellner and his experience click here. For information about how Peter Kellner might be able to help you with your project click here.

Follow me:


Comments

  1. Nice work! Warning though, unit testing is addictive :).

Your Comments

*

Protected with IP Blacklist CloudIP Blacklist Cloud

Follow

Get every new post delivered to your Inbox

Join other followers: