The Importance of TDD: A simple example

In this post we build a practical example using TDD. If you are interested in playing a little bit with the project or you what to complete the requirement as we suggest at the end of the post the files can be downloaded here

In the previous post on TDD we introduced some of the advantages of having an automated test suite, let’s recap some of them before going a little deeper into some more practical concepts:

  1. First, the computer tells you when you complete your implementation; it can be the most complex pice of requirement, but you don’t have to remember it or try to execute the code in your head to verify if it’s OK, you just run the tests and if it’s green you completed the implementation;
  2. You end up with a set of regression tests that warn you when you introduce a bug while implementing some other requirement. As soon as you run the tests, they verify all the implementation even the one you wrote hundreds of iterations ago;
  3. You gain a lot of time by not having to run tests manually, actually, you stop spending a lot of time in debug mode;

But how do we get all this magic? well, let’s have a look at the implementation of the requirement that you saw in the previous post:

  • All Approved documents can be read by all registered users which are in the Readers group of higher
  • Documents which are in the Draft state can be read by Editors, Administrators or by delegated users
  • Draft documents can be deleted only by Administrators whom cannot Approve them, the only users able to approve the documents are the Editors excluding the Author of the document

This is an actual requirement I got from a client a while ago, it is simple enough to be implemented in a blog post, but it has enough edge cases and can be risky to implement without properly testing the implementation. When we received the requirement, we implemented it in a TDD fashion and after the implementation was finished we did some manual tests. The result was impressive, we couldn’t find any bugs in our implementation  and, while the automated test run in seconds, we spent half a day to test all the requirements manually.

In this post I’ll focus only on the implementation of the method that checks whether one user can read a document or not (the first two bullet points) and I’ll leave to you the last requirement of the feature.

Let’s get started

First let me introduce the domain objects:

The rules state some specification on how every User can access a list of Documents. The User class is defined as following:

public class User
{
public int Id { get; set; }
public string Name { get; set; }
public string Surname { get; set; }
public Role Role { get; set; }
}

And every user is assigned to a specific Role:

public enum Role
{
Guest,
Reader,
Editor,
Master,
Administrator
}

Every document is represented by the class Document and every document can have a list of users assigned as delegates:

public class Document
{
public int Id{get;set;}
public string Name{get;set;}
public DateTime CreatedDate{get;set;}
public DateTime ApprovedDate{get;set;}
public string Content{get;set;}
public List Delegated{get;set;}
public List Attachments{get;set;}
public User Author{get;set;}
public User ModifiedBy{get;set;}
public User Approver{get;set;}
public Status ApprovalState { get; set; }
}

As you can notice, the document can also be in a defined approval state:

public enum Status
{
Draft,
Approved,
Removed,
Pending
}

On with the implementation

First Step

After defining the domain objects needed to implement the requirements we can start with the implementation by writing the first test. We know that a user that is assigned to the Reader role can read a document in the approved state so, let’s write this as a Test:

[Test]
public void ApprovedDocumentCanBeReadByReader()
{
var document = new Document {ApprovalState = Status.Approved};
var user = new User {Role = Role.Reader};
var auth = new AuthorizationChecker();
var canUserReadDocument = auth.CanUserReadDocument(user, document);
Assert.IsTrue(canUserReadDocument);
}

Let’s deconstruct what’s happening:

  1. First I create a Document and set its ApprovalState to Status.Approved
  2. Then I create a User and assign it to the Editor Role
  3. Then I create a new Instance of the class AuthorizationChecker
  4. Then I call the method CanUserReadDocument passing the user and the document created earlier
  5. Later I check if the result is true or not

This code obviously doesn’t compile because the class AuthorizationChecker and it’s method CanUserReadDocument don’t exists; so I proceed to create them. But what should be the return of the method? Well, because I don’t have any other tests than the one I just wrote I’ll make it return the only value that will make the test pass so I can move on with the tests.

public class AuthorizationChecker
{
public bool CanUserReadDocument(User user, Document document)
{
return true;
}
}

Now the test run and PASSES

Second Step

What the heck is this you say? well, this isn’t me being stupid, I know there have to be some complex algorithm to compute the result of the function, but for now I have just translated one little part of the requirement and writing a check like

return user.Role == Role.Reader && document.ApprovalState == Status.Approved;

would be a waste of time;
Let’s see what happens if I write some more tests:

[Test]
public void ApprovedDocumentCanBeReadByEditor()
{
var document = new Document {ApprovalState = Status.Approved};
var user = new User {Role = Role.Editor};
var auth = new AuthorizationChecker();
var canUserReadDocument = auth.CanUserReadDocument(user, document);
Assert.IsTrue(canUserReadDocument);
}

With this tests my simple implementation still passes. And of course with this:

[Test]
public void ApprovedDocumentCanBeReadByMaster()
{
var document = new Document {ApprovalState = Status.Master};
var user = new User {Role = Role.Editor};
var auth = new AuthorizationChecker();
var canUserReadDocument = auth.CanUserReadDocument(user, document);
Assert.IsTrue(canUserReadDocument);
}

And this:

[Test]
public void ApprovedDocumentCanBeReadByAdministrator()
{
var document = new Document {ApprovalState = Status.Approved};
var user = new User {Role = Role.Administrator};
var auth = new AuthorizationChecker();
var canUserReadDocument = auth.CanUserReadDocument(user, document);
Assert.IsTrue(canUserReadDocument);
}

Did you notice any pattern? I have the same test repeated over and over and only some values change; I could use a very useful feature of NUnit to clean the tests a little bit: Parametrized Tests.

[TestCase(Role.Reader)]
[TestCase(Role.Editor)]
[TestCase(Role.Master)]
[TestCase(Role.Administrator)]
public void ApprovedDocumentCanBeReadByReaderOrHigher(Role role)
{
var document = new Document {ApprovalState = Status.Approved};
var user = new User {Role = role};
var auth = new AuthorizationChecker();
var canUserReadDocument = auth.CanUserReadDocument(user, document);
Assert.IsTrue(canUserReadDocument);
}

The TestCase attribute tells the testing framework to run the tests four times, passing every time a different value for role resulting in four different tests.

Third Step

Now I can write another test for the role that cannot read Approved documents: the Guest user.

[Test]
public void ApprovedDocumentsCannotBeReadByGuests()
{
var document = new Document { ApprovalState = Status.Approved };
var user = new User { Role = Role.Guest };
var auth = new AuthorizationChecker();
var canUserReadDocument = auth.CanUserReadDocument(user, document);
Assert.IsFalse(canUserReadDocument);
}

Now this tests obviously fails, And I have finally the need to change my previous implementation of CanUserReadDocument. Again, let me make the simplest change possible:

public bool CanUserReadDocument(User user, Document document)
{
if (user.Role == Role.Guest && document.ApprovalState == Status.Approved)
return false;
return true;
}

The tests are green again and I’m a Happy Coder!!! Without breaking my tests I could rewrite the method like this:

public bool CanUserReadDocument(User user, Document document)
{
return document.ApprovalState == Status.Approved && user.Role != Role.Guest;
}

And then I can be more explicit with what is it I’m try to implement

public bool CanUserReadDocument(User user, Document document)
{
return IsApproved(document) && CanReadApprovedDocuments(user);
}
private static bool IsApproved(Document document)
{
return document.ApprovalState == Status.Approved;
}
private static bool CanReadApprovedDocuments(User user)
{
return user.Role != Role.Guest;
}

My tests still pass, still a Happy Coder!!!

All possible tests for the first bullet point have been written and I’m sure that the implementation is complete.

Fourth Step

The second bullet point states that:

  • Documents which are in the Draft state can be read by Editors, Administrators or by delegated users

So, let’s write a test that specifies this requirement:

[TestCase(Role.Administrator)]
[TestCase(Role.Editor)]
public void DraftCanBeReadByEditorsAndAdministrators(Role role)
{
var document = new Document { ApprovalState = Status.Draft };
var user = new User { Role = role };
var auth = new AuthorizationChecker();
var canUserReadDocument = auth.CanUserReadDocument(user, document);
Assert.IsTrue(canUserReadDocument);
}

As before, this test fails because it’s not implemented yet, so let’s make the simplest implementation possible:

public bool CanUserReadDocument(User user, Document document)
{
if (document.ApprovalState == Status.Draft)
return true;
return IsApproved(document) && CanReadApprovedDocuments(user);
}

This will suffice to make the test pass, but checking that Guest, Reader and Master cannot read Draft document still fails

[TestCase(Role.Guest)]
[TestCase(Role.Reader)]
[TestCase(Role.Master)]
public void DraftCannotBeReadByGuestReaderMaster(Role role)
{
var document = new Document { ApprovalState = Status.Draft };
var user = new User { Role = role };
var auth = new AuthorizationChecker();
var canUserReadDocument = auth.CanUserReadDocument(user, document);
Assert.IsFalse(canUserReadDocument);
}

And to make it pass we can change the implementation like this:

public bool CanUserReadDocument(User user, Document document)
{
if (document.ApprovalState == Status.Draft && (user.Role == Role.Editor || user.Role == Role.Administrator))
return true;
return IsApproved(document) && CanReadApprovedDocuments(user);
}

Pretty simple, right? Now, that the test passes I’ll refactor it a little bit:

public class AuthorizationChecker
{
public bool CanUserReadDocument(User user, Document document)
{
return
IsDraft(document) && CanReadDraftDocument(user) ||
IsApproved(document) && CanReadApprovedDocuments(user);
}
private static bool CanReadDraftDocument(User user)
{
return (user.Role == Role.Editor || user.Role == Role.Administrator);
}
private static bool IsDraft(Document document)
{
return document.ApprovalState == Status.Draft;
}
private static bool IsApproved(Document document)
{
return document.ApprovalState == Status.Approved;
}
private static bool CanReadApprovedDocuments(User user)
{
return user.Role != Role.Guest;
}
}

Fifth Step

Now, let me add a delegated user to a document and verify that it can read the document if the document is draft:

[Test]
public void DraftDocumentsCanBeReadByDelegatedUsers()
{
var user = new User() { Id = 1 };
var document = new Document{ApprovalState = Status.Draft};
document.Delegated = new List<User> {user};
var auth = new AuthorizationChecker();
var canUserReadDocument = auth.CanUserReadDocument(user, document);
Assert.IsTrue(canUserReadDocument);
}

Now we know we can modify the private method CanReadDraftDocument to check whether there’s the user is part of the delegates:

public class AuthorizationChecker
{
public bool CanUserReadDocument(User user, Document document)
{
return
IsDraft(document) && CanReadDraftDocument(user,document) ||
IsApproved(document) && CanReadApprovedDocuments(user);
}
private static bool CanReadDraftDocument(User user,Document document)
{
return
document.Delegated != null && document.Delegated.Any(x => x.Id == user.Id) ||
user.Role == Role.Editor || user.Role == Role.Administrator;
}
private static bool IsDraft(Document document)
{
return document.ApprovalState == Status.Draft;
}
private static bool IsApproved(Document document)
{
return document.ApprovalState == Status.Approved;
}
private static bool CanReadApprovedDocuments(User user)
{
return user.Role != Role.Guest;
}
}

Sixth Step

Relax!

Now that all the requirement is implemented and fully tested. You know it will work in production and you don’t need to run it in debug mode. Now you can go on with your next requirement.

Try to complete the feature implementing the third bullet point

  • Draft documents can be deleted only by Administrators whom cannot Approve them, the only users able to approve the documents are the Editors excluding the Author of the document

Let me know what aha moments did you get while working on it.

 

Author: Daniele Pozzobon

Daniele is an aspiring software craftsman with more that ten years of experience in the software industry. He is currently a consultant in the .Net space for a big insurance company, and previously have worked as a web dev in the manufacturing industry. He has experience with C#, Java, C++, PHP, Javascript, and lately has added some F# to the sauce.
He constantly annoys his friends by talking about software and is passionate about Agile methodologies, which gives him more opportunities to talk annoy his friends even more.
When there are no friends around to annoy, he blogs on CodeCleaners and in his free he time loves go hiking with his wife and two daughters.