Introduction to Test Driven Development and Unit Testing

Home / Software Testing / Introduction to Test Driven Development and Unit Testing

Test Driven Development (TDD) is a software development methodology where to deliver a functionality, it is required to write test cases first, then produce the code that can pass those test cases. This method ensures from the very beginning that every single line of code has a corresponding set of unit tests guaranteeing the functionality of the development (at least to some extent). In other words, it is clean code that works when delivered.

The logic of TDD is simple and can be summarized in 4 steps:

  1. Write a test code that fails (initially)
  2. Write the minimum amount of code that passes the test
  3. Refactor the code (if required)
  1. Repeat indefinitely until all requirements are met

Writing Test Cases follow the 3A rule (Arrange, Act, Assert):

  • Arrange all necessary preconditions and inputs
  • Act on the object or method under test
  • Assert that the expected results have occurred

When writing the test cases, it is good practice to divide the test method into these 3 sections separated by comment blocks. Arrange part is variable declaration and initialization for the test case. Act is invoking the code under test. Assert is testing that the expectations were met using some flavor of the Assert.* methods available in many programming languages.

Let’s demonstrate this methodology using a simple example with C# and Visual Studio. We’re constructing a simple Person class and testing proper object initialization and TC Kimlik No (Turkish National Identification Number) related basic functionality such as the same TCKimlikNo for 2 different objects implying the same person.

Simple Example to start development with TDD

Step 1. Start with a Test Case using the TestClass and TestMethod attributes in Visual Studio:

namespace PersonsTest

{

    [TestClass]

    public class Test_ConstructorInitialization

    {

        [TestMethod]

        public void ProperObjectInitialization()

        {

            //Arrange

            //Act

            Person person = new Person(“John Smith”, new DateTime(1999, 9, 9), 1234567890, GenderEnum.MALE);

            //Assert

            Assert.AreEqual(“John Smith”, person.Name);

            Assert.AreEqual(new DateTime(1999, 9, 9), person.BirthDate);

            Assert.AreEqual(1234567890, person.TCKimlikNo);

            Assert.AreEqual(GenderEnum.MALE, person.Gender);

        }

    }

}

Note 1: We’re assuming to use a simple GenderEnum enumeration for the Gender types.

Note 2: A test method must meet the following requirements:

  • The method must be decorated with the [TestMethod] attribute
  • The method must return void
  • The method cannot have parameters

Step 2. Write the code to pass the test case only

public class Person

{

    public Person(string v1, DateTime dateTime, int v2, GenderEnum g)

    {

        Name = v1;

        BirthDate= dateTime;

        TCKimlikNo = v2;

        Gender = g;

    }

    public string Name { get; set; }

    public DateTime BirthDate { get; set; }

    public int TCKimlikNo { get; set; }

    public GenderEnum Gender { get; set; }

}

Step 3. Do some refactoring if necessary (for better readability)

public Person(string name, DateTime birthdate, int tcno, GenderEnum gender)

{

    Name = name;

    BirthDate = birthdate;

    TCKimlikNo = tcno;

    Gender = gender;

}

Step 4. Add another test case

[TestMethod]

[ExpectedException(typeof(ArgumentException)]

public void ThrowsExceptionForNegativeTCKimlikNo()

{

   Person person = new Person(“John Smith”, new DateTime(1999, 9, 9), -1234567890, GenderEnum.MALE);

}

 

At this point, this test case does not pass as there is no code available to make it pass yet:

Step 5. Now produce the code that can make this test case AND the previous test case pass:

public Person(string name, DateTime birthdate, int tcno, GenderEnum gender)

{

     if (tcno < 0)

      {

           throw new ArgumentOutOfRangeException(“TCKimlikNo cannot be negative”);

      }

      Name = name;

      BirthDate = birthdate;

     TCKimlikNo = tcno;

     Gender = gender;

}

Now we should observe that BOTH of our test cases pass:

At this point, we might choose to refactor again for better readability and code organization:

public Person(string name, DateTime birthdate, int tcno, GenderEnum gender)

        {

            Name = name;

            BirthDate = birthdate;

            TCKimlikNo = tcno;

            Gender = gender;

        }

        private int tckimlikno;

        public int TCKimlikNo

        {  

        get { return tckimlikno; }

            set

            {

                if (value < 0)

                {

                    throw new ArgumentOutOfRangeException(“TCKimlikNo cannot be negative”);

                }

                tckimlikno = value;

            }

        }

When a test case is not enough

It is very important that a test cases are properly planned and that they actually test the whole functionality. If one test case is not enough to test one whole functionality, other test cases should be produced to complete the testing of that functionality.

As an example, consider adding a test case and the supporting code to check whether 2 Persons are the same based on their TCKimlikNo:

[TestMethod]

public void SameTCKimlikNoAreSamePersons()

{

    Person person1 = new Person(“John Smith”, new DateTime(1999, 9, 9), 1234567890, GenderEnum.MALE);

    Person person2 = new Person(“John Smith”, new DateTime(1999, 9, 9), 1234567890, GenderEnum.MALE);

    Assert.AreEqual(person1, person2);

}

public override bool Equals(object obj)

{

    return true;

}

Now all of our test cases pass:

But the last test case is meaningless!

To make this example more meaningful, we should also be checking for 2 Persons being NOT equal so that we cover all equality cases. We add the following test case:

[TestMethod]

 public void DifferentTCKimlikNoAreDifferentPersons()

 {

    Person person1 = new Person(“John Smith”, new DateTime(1999, 9, 9), 1234567890, GenderEnum.MALE);

    Person person2 = new Person(“John Smith”, new DateTime(1999, 9, 9), 2345678901, GenderEnum.MALE);

    Assert.AreNotEqual(person1, person2);

}

At this point, our second test case shall fail if we do not change our Equals method:

To make both test cases pass, we rewrite our Equals method:

public override bool Equals(object obj)

{

  return tckimlikno == (obj as Person).tckimlikno;

}

And now all of our test cases pass again:

We repeat this process until all of the required functionality is both tested (first!) and developed fully.

Conclusion

Is TDD a perfect methodology? Obviously not, but it is a good practice to follow especially for validation of detailed specifications in a project. If the specifications are clear, they can be constructed at first as test cases and then the corresponding code can be developed to fulfill the requirements of the specifications.

TDD also allows for writing small code and the frequency of the bugs produced per code unit; however it adds significant additional time to deliver a software project. The decision to use TDD might change from project to project, but it is always better to produce sound code with few bugs to tackle in a slower fashion then producing hasty code with a lot of bugs to fix later on.

References

  1. http://integralpath.blogs.com/thinkingoutloud/2005/09/principles_of_t.html
  2. https://msdn.microsoft.com/en-us/library/ms182532.aspx
  3. https://technologyconversations.com/2013/12/20/test-driven-development-tdd-example-walkthrough/
  4. http://agiledata.org/essays/tdd.html
  5. https://theqalead.com/general/statistics-studies-benefits-test-driven-development/

Leave a Reply

Your email address will not be published.