Introduction
In this article, I will try to explain what is TDD and how it helps during the development process. There is a lot of resources and book that do this, but I will try to introduce with a simple practical example. This is more a "philosophic" overview than the strict definition you can read in a book. Probably purist supporter of this methodology will find this explanation a little bit incomplete (sorry for that..), but I think that this is enough to start learning and understanding the basics. My main purpose is not to write another book on TDD, but just explain what it is in clear and simple words so that beginners can also understand and embrace it.
The full source code is available on github.
What is TDD
Just start with the wikipedia definition:
Quote:
Test-driven development (TDD) is a software development process that relies on the repetition of a very short development cycle: requirements are turned into very specific test cases, then the software is improved to pass the new tests, only. This is opposed to software development that allows software to be added that is not proven to meet requirements.
Clear? The main purpose of TDD is to create a strategy where test will drive the development process in order to make coding more efficient, productive, reducing regression.
The pre-requisite is to decompose a big task in smaller steps and develop using unit test. This allows you to handle a smaller piece of code, make them work, then integrate many working parts together.
Benefits of TDD
Introducing TDD to your coding experience will reach a turning point. Here is a short list of the most important benefits:
- Focus on really important points: You will be asked to decompose the problem, this will help to keep attention on the most important things.
- Handle simpler task: Working with single, smaller, task each time simplifies troubleshooting and speeds up development. You won't fall in a situation where you will write all the code, then something doesn't work, and you don't know why.
- Simplified integration: When multiple working features are completed, putting all together will be a pleasure and an easy task. In case of regression, you will know in advance which part of code is bad.
- Test for free: Once the full task is finished, lot of unit tests remain and can be used as integration\unit test to validate the code and avoid regressions.
What TDD is Not
TDD is a great methodology, but not:
- a replacement of test (unit test, acceptance test, UI test)
- something you can learn in a day
- something that writes code for you
- a holy man that drive away bugs from code
The TDD Lifecycle
TDD is composed mainly of three steps:
- Write the unit test (RED).
- Make it work (GREEN).
- Refactor.
In the example, you can write the unit test, using code inside it to implement the feature until it works, then refactor placing this piece of code where needed.
Steps 1, 2: Make the Test Work
public class StripTest
{
[Fact]
public static void StripHTml()
{
string test="<h1>test</h1>";
string expected="test";
string result=StripHTML(test);
Assert.Equal(expected,result);
}
public static string StripHTML(string input)
{
return Regex.Replace(input, "<.*?>", String.Empty);
}
}
Step 3: Refactoring
public class StripTest
{
[Fact]
public static void StripHTml()
{
string test="<h1>test</h1>";
string expected="test";
string result=HtmlHelper.StripHTML(test);
Assert.Equal(expected,result);
}
}
public static class HtmlHelper
{
public static string StripHTML(string input)
{
return Regex.Replace(input, "<.*?>", String.Empty);
}
}
Limitations
In many cases, it is hard to write unit tests that cover the real code usage. It is easy for fully logical procedures, but when we are going to involve database or UI, the effort of writing will increment and in many cases, could exceed the benefits. There are some best practices and frameworks that help on this, but generally speaking, not all parts of the application will be easy to test using plain unit test.
What is BDD?
BDD is an enhancement of TDD that takes in account situations when unit test is limitative. This extension uses the developer as unit test, keeping the philosophy behind BDD. You can still decompose complex tasks into smaller ones, testing using user behavior, and take same advantages of using TDD on pure backend tasks.
The TDD Prerequisites
When working in teams, all teammates must know and embrace this philosophy, other than have the knowledge of all technologies involved.
First of all, your code must be empowered by a powerful unit test system:
- .NET, .NET Core: built in Visual Studio or xunit (the second one is my personal, preferred choice)
- Java: junit works very well, I didn't need to find another solution
- PHP: PHP unit worked for me in all the cases
Then, important and mandatory: have an architecture that allows to mock or recreate correct behaviour during test. I'm speaking about an ORM that can work in memory or on local database during test, but also to use service or repository pattern. Using a DI framework (the built in .NET core, autofac or whatever else...) helps also.
Last but not the least: a well done build process, integrate in a continuous integration flow, other than the right configuration do define which unit test makes sense to run on it during integration and what are run just locally.
The Example
Let's try to put in practice what we learn about TDD in a real world example. I would like to create a wiki using this methodology. I mean a simple wiki, where user login, write markdown pages and publish.
First of all, I would decompose the "long" task into smaller subsequential activity. Each subpart will be developed using a small unit test. I would focus on wiki page CRUD.
Step 1: Entity to DTO Mapping
- Write the entity.
- Write the wiki page DTO.
- Write the code that maps entity to DTO.
public class WikiPageEntity
{
[DatabaseGenerated(DatabaseGeneratedOption.Identity)]
public Guid Id { get; set; }
public int Version { get; set; }
public string Slug { get; set; }
public string Body { get; set; }
public string Title { get; set; }
}
namespace WikiCore.Lib.DTO
{
public class WikiPageDTO
{
public string Title { get; set; }
public string BodyMarkDown { get; set; }
public string BodyHtml { get; set; }
public int Version { get; set; }
public string Slug { get; set; }
}
}
public void EntityToDTO()
{
WikiPageEntity source = new WikiPageEntity()
{
Title = "title",
Slug = "titleslug",
Version =1
};
var result = Mapper.Map<wikipagedto>(source);
Assert.Equal("title", result.Title);
Assert.Equal(1, result.Version);
}
public MappingProfile()
{
CreateMap<wikipageentity, wikipagedto="">().ReverseMap();
}
Step 2: Markdown to HTML Conversion
- Make a method that converts
markdown
to HTML:
{
[Fact]
public void ConvertMarkDown()
{
var options = new MarkdownOptions
{
AutoHyperlink = true,
AutoNewLines = true,
LinkEmails = true,
QuoteSingleLine = true,
StrictBoldItalic = true
};
Markdown mark = new Markdown(options);
var testo = mark.Transform("#testo");
Assert.Equal("<h1>testo</h1>", testo);
}
[Fact]
public void ConvertMarkDownHelper()
{
Assert.Equal("<h1>testo</h1>", MarkdownHelper.ConvertToHtml("#testo"));
}
public static class MarkdownHelper
{
static MarkdownOptions options;
static Markdown converter;
static MarkdownHelper()
{
options = new MarkdownOptions
{
AutoHyperlink = true,
AutoNewLines = true,
LinkEmails = true,
QuoteSingleLine = true,
StrictBoldItalic = true
};
converter = new Markdown(options);
}
public static string ConvertToHtml(string input)
{
Markdown mark = new Markdown(options);
return mark.Transform(input);
}
}
Step 3: EnHance Mapping With Markdown
- Alter mapping adding HTML field computation:
public class MappingProfile : Profile
{
public MappingProfile()
{
SlugHelper helper = new SlugHelper();
CreateMap<wikipageentity, wikipagedto="">()
.ForMember(dest => dest.BodyMarkDown, (expr) => expr.MapFrom<string>(x => x.Body))
.ForMember(dest => dest.BodyHtml,
(expr) => expr.MapFrom<string>(x => MarkdownHelper.ConvertToHtml(x.Body)))
.ReverseMap();
CreateMap<wikipagebo,wikipageentity>()
.ForMember(dest => dest.Body, (expr) => expr.MapFrom<string>(x => x.BodyMarkDown))
.ForMember(dest => dest.Slug,
(expr) => expr.MapFrom<string>(x => helper.GenerateSlug(x.Title)));
}
}
public void EntityToDTO()
{
WikiPageEntity source = new WikiPageEntity()
{
Body = "# prova h1",
Title = "title",
Slug = "titleslug",
Version =1
};
var result = Mapper.Map<wikipagedto>(source);
Assert.Equal("title", result.Title);
Assert.Equal(1, result.Version);
Assert.Equal("<h1>prova h1</h1>", result.BodyHtml);
}
Step 4: Setup Database Migration
- Run the
Add-Migration
script. - Create an unit test that works in memory to test it.
[Fact]
public void MigrateInMemory()
{
var optionsBuilder = new DbContextOptionsBuilder<DatabaseContext>();
optionsBuilder.UseInMemoryDatabase();
using (var db = new DatabaseContext(optionsBuilder.Options))
{
db.Database.Migrate();
}
}
Step 5: Entity CRUD
- Write a CRUD test.
- Test it.
[Fact]
public void CrudInMemory()
{
var optionsBuilder = new DbContextOptionsBuilder<DatabaseContext>();
optionsBuilder.UseInMemoryDatabase();
using (var db = new DatabaseContext(optionsBuilder.Options))
{
db.Database.Migrate();
db.WikiPages.Add(new Lib.DAL.Model.WikiPageEntity()
{
Title = "title",
Body = "#h1",
Slug = "slug"
});
db.SaveChanges();
var count=db.WikiPages.Where(x => x.Slug == "slug").Count();
Assert.Equal(1, count);
}
}
Step 6: Test the Service
- Create a service with business logic.
- Test it.
[Fact]
public void TestSave()
{
var optionsBuilder = new DbContextOptionsBuilder<DatabaseContext>();
optionsBuilder.UseInMemoryDatabase();
using (var db = new DatabaseContext(optionsBuilder.Options))
{
db.Database.Migrate();
db.SaveChanges();
DatabaseWikiPageService service = new DatabaseWikiPageService(db, Mapper.Instance);
service.Save(new Lib.BLL.BO.WikiPageBO()
{
BodyMarkDown="#h1",
Title="prova prova"
});
var item = service.GetPage("prova-prova");
Assert.NotNull(item);
}
}
Step 7: Continue on the UI
Once testing UI using unit test became complex, I switched to BDD and made multiple steps to complete the UI. So, instead of writing all the code then test it, I decomposed the problem in multiple sub-activity and tested it one by one:
Edit
- Prepare the form, and test it.
- Prepare the model, test what is submitted from form fills the backend model.
- Integrate service to save data, test it.
View
- Prepare model, pass to the view, test it.
- Integrate model with services, to get real data. Test it.
List
- Prepare view model, pass fake data to UI, test it.
- Integrate service, test it.
Conclusion
TDD is a methodology that drives the development process supported by tests. This helps coding in many ways but requires that all the team mates have some basics. Once this step is achieved, you will handle a simpler task and many tests that can be reused. This process will help to avoid regression and reach the goal quicker, also if there is the effort of writing unit test while developing. Moreover, if your application is hard to test because of the complexity, you can keep the same philosophy performing BDD.
History
- 2018-11-17: First version