Translate

Sunday 23 March 2014

Agile Test Driven Design - Design evolving out of tests

Pre-requisites before you read this - Knowledge of the WCF framework, Web Service Consumption, Unit and Mock Tests and a basic understanding of the difference between a HTTP session context and an examination context (time allotted for tests, questions etc) and a lot of detached reading ability (Nothing directed at anyone personally even though it may seem so) :)

The code and the tests are executable-ready and tested so please do not troll with useless questions or responses.

Agile and the pleasure of Test Driven Design Development

Agile is not so much a pleasure when working with an Agile team as it is a pain when confronted with argumentative, subjective 'technical' experts; when the latter happens, you begin to realize how difficult it is to explain simplicity to these 'expert' project managers to accept their shortcomings and accept that they are not project managers but experts in creating crises and then managing it !

Documentation is 'Tests'

Documentation is the 'D' word - it is like the threat of a nuclear holocaust, the moment the word crops up in a team it is indicative that some reactive measures has crept into the development steps adopted by the team.

The incident of a tester not knowing how to open a VS TS and work with builds but still having the 'confidence' to pull up developers is an indicator of how some believe that having a 'team' writing some nonsense lines on Skype group chat or referring to each other as 'team' is what a team means and that 'testers' have read somewhere that customers write tests in Agile and therefore, being a tester means that the tester is a customer !

There are better PJs doing the rounds than having to encounter such 'poor jokes' within your work environment and that, too, which stinks so much!

Getting back to the D word - the best form of documentation that a team could be blessed with is when a test (Unit, mock or acceptance) 'tells' or 'communicates' how the design of the class, the signature of the method or even the data type of a data member should be.

It is really fortunate that I came across a 'real' example where I could demonstrate how Tests actually works, communicates and evolves a design into a near perfect one - and in far less time than you could conceive of.

I am extracting a simple service out of the whole system below to show how tests should be used to design, right from the beginning of a project.

The requirement

A service model (WCF) that enables a candidate to log in to an online examination application with a service contract as,

using System.ServiceModel;

namespace ABC.LoginService
{
    [ServiceContract]
    public interface ILoginService
    {
        [OperationContract]
        UserInfo DoLogin(string email, string password);

        [OperationContract]
        CandidateSessionInfo GetCandidateSession(UserInfo userInfo);
    }
}

and two data contracts for Candidate's session (not the HTTP session) and user information.

namespace ABC.Service
{
    [DataContract]
    public class CandidateSessionInfo 
    {
        int _id;
        string _title;
        DateTime _startDate;
        DateTime _startTime;
        DateTime _endTime;

        [DataMember]
        public int ID
        {
            get { return _id; }
            set { _id = value; }
        }

        [DataMember]
        public string Title
        {
            get { return _title; }
            set { _title = value; }
        }

        [DataMember]
        public DateTime StartDate
        {
            get { return _startDate; }
            set { _startDate = value; }
        }

        [DataMember]
        public DateTime StartTime
        {
            get { return _startTime; }
            set { _startTime = value; }
        }

        [DataMember]
        public DateTime EndTime
        {
            get { return _endTime; }
            set { _endTime = value; }
        }
    }

    [DataContract]
    public class UserInfo
    {
        int _id;
        string _name;
        string _email;

        [DataMember]
        public int ID
        {
            get { return _id; }
            set { _id = value; }
        }

        [DataMember]
        public string Name
        {
            get { return _name; }
            set { _name = value; }
        }

        [DataMember]
        public string Email
        {
            get { return _email; }
            set { _email = value; }
        }
    }
}

WYTNWYG (What You Think Is Not What You Get) 

This is an example of preconceived notions i.e., WYTNWYG  of Design principles. 

The service contract is simple - it outlines two basic requirements of a login operation but the design begins to go awry in the data contracts that is to provide the login infastructure. 

Let me demonstrate how, through the below tests written in Rhino Mock and NUnit.

using Rhino.Mocks;
using ABC.Service;
using NUnit.Framework;

#if DEBUG
using WebOperationContext = System.ServiceModel.Web.MockedWebOperationContext;
#endif

namespace ABC.MockServices
{
    [TestFixture]
    public class ServicesTests
    {
        WebServiceClient client;
        ILoginService sstudentMock;
        UserInfo studentUnderTest,obj;
        int loginId;
        CandidateSessionInfo sessionObj, sessionUnderTest;

        [SetUp]
        public void init()
        {
            sstudentMock = MockRepository.GenerateMock();
            client = new WebServiceClient(sstudentMock);
        }
        [Test]
        public void testStudentLogin()
        {
            studentUnderTest = new UserInfo();

            studentUnderTest.Email = "abc@gmail.com";
            studentUnderTest.ID = 1;
            studentUnderTest.Name = "abc";
            sstudentMock.Expect(t => t.DoLogin("abc@gmail.com", "pass")).Return(studentUnderTest);
            loginId = client.Login("abc@gmail.com", "pass");
            Assert.AreEqual(studentUnderTest.ID, loginId);
            sstudentMock.VerifyAllExpectations();
        }
        //[Test]
        //public void testSessionForLoginTime()
        //{
        //    sessionUnderTest = new SessionInfo();
        //    sessionUnderTest.ID = 1;
        //    sessionUnderTest.Title = "abcSession";
        //    sessionUnderTest.StartDate = DateTime.Now;
        //    sessionUnderTest.StartTime = DateTime.Now;
        //    sessionUnderTest.EndTime = DateTime.MaxValue;
        //    loginId = client.Login("abc@gmail.com", "pass");
        //    sstudentMock.Expect(t => t.GetSession(obj)).Return(sessionUnderTest);
        //   // sessionObj=student.GetSession(obj);

        //   // Assert.AreEqual(sessionUnderTest.StartTime, sessionObj.StartTime);
        //    //sstudentMock.VerifyAllExpectations();
        //}
        //[Test]
        //public void testSessionForLoginSessionTitle()
        //{
        //    sessionUnderTest = new SessionInfo();
        //    sessionUnderTest.ID = 1;
        //    sessionUnderTest.Title = "AbcSession";
        //    sessionUnderTest.StartDate = DateTime.Now;
        //    sessionUnderTest.StartTime = DateTime.Now;
        //    sessionUnderTest.EndTime = DateTime.MaxValue;
        //    loginId = client.Login("abc@gmail.com", "pass");
        //    sstudentMock.Expect(t => t.GetSession(obj)).Return(sessionUnderTest);
        //    //sessionObj = student.GetSession(obj);           
        //   // Assert.AreEqual(sessionUnderTest.Title, sessionObj.Title);
        //    //sstudentMock.VerifyAllExpectations();
        //}

    }
}

The first test, testStudentLogin, is fine but it is when you move to the next user story - 'getSession...' that the tests, related to the CandidateSessionInfo object tells you, "Hey, as per your design, you need to supply a UserInfo object, where is it?" 

Because 

1. Being on the web platform, you need to send the userinfo object to the getsession...method after the candidate logs in successfully or 
2. Refactor the design.

It is not as simple as choosing between the two - a design, when you arrive or decide it, must have 'testable' artifacts to justify the decision and this is where 'tabled design' fails and test driven design scores!

The test above has communicated (the feedback) that either the contract of the getsession...method is wrong or your programming logic that is not able to maintain the user info object state.

The answer now becomes simple. Your development efforts (and therefore the logic part) has not even started so obviously the choice is clear - refactor the operation contract. (Of course, this explanation of how to make this design decision is only for explanatory purposes - the actual parameters to making this decision could be entirely different based upon the composition of a team or the architect's experience and maturity.)

To  continue...with explanation on why the tests are commented out.

No comments: