Introduction
Application design is one of the most important aspects of creating the application. A design serves as the pillars of the application. An incorrect design can cause delays or even worse destroy the application. In this article, we will take a look at designing application using test driven development.
Evolving the Design with Test Driven Development
People often consider the process of test driven development as testing the application for different inputs. But test driven development is much more than inputs and results. Test driven development allows the developer to design the application and makes sure that the developer only writes the code that is required to produce the desired output.
The purpose of this article is to show you how the application design evolves with the help of test driven development.
Scenario
There is no actual scenario to discuss. We will start by testing two classes, Person
and Address
. A Person
can have many Addresses
. An Address
must belong to a Person
. As we progress, we will find out that the user stories and features keep on growing and unit tests serve as a shield to protect us from those changes.
Person Entity
According to the client, the Person
must have the following attributes:
FirstName
: First name of the person LastName
: Last name of the person MiddleName
: Middle name of the person DOB
: Date of birth DateCreated
: Date and time the person entry was created DateModified
: Date and time the person entry was modified
The Person
entity class as shown below:
public class Person
{
private string _firstName;
private string _lastName;
private DateTime _dob;
private int _id;
public int Id
{
get { return _id; }
internal set { _id = value; }
}
public string FirstName
{
get { return _firstName; }
set {
_firstName = value;
}
}
public string LastName
{
get { return _lastName; }
set
{
_lastName = value;
}
}
public DateTime DOB
{
get { return _dob; }
set {
_dob = value;
}
}
}
What to Test?
One common problem that developers encounter is getting started with test driven development. How should we find out what modules to test? The best way is to study what module is extensively used in the application. Once you find the module, start writing unit tests. A good code coverage tool will also reveal important information about the usage of modules in your application.
Keeping that in mind, we picked up the phone and gave a call to our client to discuss the constraints on the application. The client explained us the following:
- First name cannot be left empty
- Last name cannot be left empty
- Person must be at least 18 years old
This gives us a good starting point to test our application. Let’s start by making sure that the above expectations are met.
Implementing Unit Tests
Here is our first test case that makes sure that the properties are not set if they are in invalid state.
[RowTest]
[Row("","","02/09/2000")]
public void should_not_set_person_properties_if_in_invalid_state(
string firstname, string lastname,DateTime dob)
{
Person person = new Person() { FirstName= firstname, LastName = lastname,
DOB = dob };
Assert.IsTrue(person.FirstName != firstname,"FirstName has been set!");
Assert.IsTrue(person.LastName != lastname,"LastName has been set!");
Assert.IsTrue(person.DOB != dob,"DOB has been set!");
}
The test “should_not_set_person_properties_if_in_invalid_state
” makes sure that the person properties are only set when they are in valid state. We are using the RowTest
attribute provided by the MbUnit framework. The RowTest
attribute enables us to provide multiple test inputs using the Row
attribute. You can learn more about the attributes and the MbUnit framework by visit the Mb.unit site.
When you run the above test, it will fail. This is because we have not made any changes to our entity class Person
and the values will be set. So, we need to add constraints to our Person
class properties. Let’s take a look at the constraints.
public class Person
{
private string _firstName;
private string _lastName;
private DateTime _dob;
public const int LEGAL_AGE = 18;
public string FirstName
{
get { return _firstName; }
set {
if(!String.IsNullOrEmpty(value.Trim()))
{
_firstName = value;
}
}
}
public string LastName
{
get { return _lastName; }
set
{
if (!String.IsNullOrEmpty(value.Trim()))
{
_lastName = value;
}
}
}
public DateTime DOB
{
get { return _dob; }
set {
if ((DateTime.Now.Subtract(value).Days / 365) >= LEGAL_AGE)
{
_dob = value;
}
}
}
}
In the above code, we added several different constraints which will help us to keep the object in the valid state. Now, if you run the test, it will pass.
Now, let’s test that the Person
object is saved successfully. Take a look at the test below:
[Test]
[RollBack]
public void should_check_if_the_person_saved_successfully()
{
Person person = new Person() { FirstName = "Mohammad", LastName = "Azam",
DOB= DateTime.Parse("02/09/1981") };
person.Save();
Assert.IsTrue(person.Id > 0);
Person vPerson = Person.GetById(person.Id);
Assert.AreEqual(person.FirstName, vPerson.FirstName);
Assert.AreEqual(person.LastName, vPerson.LastName);
Assert.AreNotEqual(person.DOB, vPerson.DOB);
}
If you run the above test, it would fail because Person
object does not expose a Save
or GetById
method. Let’s add those two methods:
public void Save()
{
if (this.Id <= 0)
{
SQLDataAccess.SavePerson(this);
}
else
{
SQLDataAccess.UpdatePerson(this);
}
}
public static Person GetById(int id)
{
return SQLDataAccess.GetPersonById(id);
}
The Save
method uses the SQLDataAccess
data access class to insert the Person
in the database. The GetById
method takes “id
” as the parameters and returns the Person
object matching the "id
."
If you are in the initial phases of development and don’t want to work on the data access layer, then you can always mock it out. Mocking means you will fake out the data access layer because it does not exist yet. Once the data access layer is implemented, you can simply plug it in without changing any test code.
Let’s move on to the Address
entity.
public class Adress
{
private int _id;
private string _street;
private Person _person;
public int Id
{
get { return _id; }
internal set { _id = value; }
}
public string Street
{
get { return _street; }
set { _street = value; }
}
public Person Person
{
get { return _person; }
set { _person = value; }
}
public Address(Person person)
{
this.Person = person;
}
The Address
class contains a Person
object since the Address
belongs to a certain Person
. One important thing to note is the Address
object constructor which takes a Person
object.
Public Address(Person person)
{
_this.Person = person;
}
This is to make sure that the Address
must have a Person
object.
This process is also known as the dependency injection. There are two types of dependency injection constructor and property. In the above example, we used the constructor dependency injection.
And here is a test to make sure that the Address
must have a Person
object or Address
must belong to a Person
.
[Test]
public void should_make_sure_that_an_address_must_have_a_person()
{
Address address = new Address(new Person());
Assert.IsNotNull(address.Person);
}
Simple right!
Now, let’s try to add an Address
to a Person
object. Here is the test.
[Test]
public void should_be_able_to_add_an_address_to_the_person()
{
Person person = new Person() { FirstName = "Mohammad", LastName = "Azam" };
Address address = new Address(person);
person.AddAddress(address);
Assert.AreEqual(1, person.Addresses.Count());
}
If you run the above test, it will fail because the Person
object does not have the AddAddress
method and the Addresses
property. So, let’s add those two things.
private IList<Address> _addresses = new List<Address>();
public IList<Address> Addresses
{
get { return new ReadOnlyCollection<Address>(_addresses); }
}
public void AddAddress(Address address)
{
address.Person = this;
_addresses.Add(address);
}
Now if you run the test, it will pass.
Conclusion
In this article, we learned how to get started with test driven development and how the TDD approach helps us to design better applications. In the next article, we are going to see how our design changes when we encounter failing tests.
I hope you liked the article — happy coding!