The world’s leading publication for data science, AI, and ML professionals.

TDD & BDD explained with an example

Build an ASP.NET Wiki to learn TDD, the technique for building better software using small test cases

I will try to explain what is TDD and how it helps during the Development process. There is a lot of resources and books that do this, but I will try to introduce it with a simple practical example. This is more a "philosophic" overview than the strict definition you can read in a book. Anyway, I didn’t want to follow a purely theoretical approach, but a more practice way to let you understand what we really need in our daily life. 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.

Maybe you don’t need to read another book on TDD, but just understand what it is in clear and simple words

That’s great for beginners that can intrigue, make a deep exploration, and then embrace it for life.

What is TDD? Test-driven development is a technique to build software using small test cases
What is TDD? Test-driven development is a technique to build software using small test cases

What is TDD

Just start with the Wikipedia definition:

_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 the 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 tests. 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 shortlist of the most important benefits:

  1. Focus on really important points: You will be asked to decompose the problem, this will help to keep attention on the most important things.
  2. Handle simpler task: Working with the single, smaller, tasks each time simplifies troubleshooting and speeds up development. You won’t fall into a situation where you will write all the code, then something doesn’t work, and you don’t know why.
  3. Simplified integration: When multiple working features are completed, putting all together will be a pleasure and an easy task. In the case of regression, you will know in advance which part of the code is bad.
  4. Test for free: Once the full task is finished, a lot of unit tests remain and can be used as integrationunit 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:

  1. Write the unit test (RED).
  2. Make it work (GREEN).
  3. 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);
    }    
}

//somewhere else
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 with this, but generally speaking, not all parts of the application will be easy to test using plain unit tests.

What is BDD?

BDD is an enhancement of TDD that takes into account situations when a unit test is limiting. This extension uses the developer as a unit test, keeping the philosophy behind BDD. You can still decompose complex tasks into smaller ones, Testing using user behavior, and take the 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 behavior during the test. I’m speaking about an ORM that can work in memory or on the 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 into 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 into 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. That sounds complex?

That was very easy. Thanks to the TDD and managing small tasks I complete quickly all the microfeatures and finally I put together already working parts.

First of all, I would decompose the "long" task into smaller subsequential activities. Each subpart will be developed using a small unit test. I would focus on wiki page CRUD.

Step 1: Entity to DTO Mapping

Starting from here sounds not so good. The Entity to DTO mapping is a very raw thing and it’s hard to suppress our coding instincts that want to start coolest parts. Anyway, this is the fist, auto consistent feature. Mapping two classes require just the two classes’ definition, no more. No matter the database connection, network errors and so on. We just need to create the two classes (DTO and Entity), then the mapping. Finally, the test will be a piece of code that will check if the fields from the entity are copied to the DTO. Easy!

Let’s summarize the steps:

  1. Write the entity.
  2. Write the wiki page DTO.
  3. Write the code that maps an entity to DTO.
// Database entity
 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; }
}

// DTO model in BLL
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; }
    }
}

// From unit test, code omitted for brevity
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);
}

// From Mapping configuration, code omitted for brevity
 public MappingProfile()
{
    CreateMap<wikipageentity, wikipagedto="">().ReverseMap();
}

Step 2: Markdown to HTML Conversion

The second step is to make a method that converts markdown to HTML. This will be a very simple method that will take a markdown string and check if its transformation matches the expected HTML.

//Before refactoring public class MarkdownTest
{
[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);
}
// after refactoring ( method moved to helper )
[Fact]
public void ConvertMarkDownHelper()
{
    Assert.Equal("<h1>testo</h1>", MarkdownHelper.ConvertToHtml("#testo"));
}

// From markdown helper
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

Well done! We have the method that generates HTML from markdown and the mapper that translates the entity to the DTP. Next step? Just put all together!

Next piece of code contains the mapping with HTML field computation:

// mapped profile changed
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)));
    }
}

// From unit test, code omitted for brevity
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

Another step is to integrate the database. An important thing to remember is that we have to test only one thing… and database access is a complicated thing. First requirement on the database is the structure. So, the first step to check is to ensure this thought Entity Framework migration.

The steps to perform:

  1. Run the Add-Migration script.
  2. Create a 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();
    }
    // No error assert migration was OK
}

Step 5: Entity CRUD

After we have set up the migration, we can assume all it’s ok with the data structure. Let’s start with a unit test to prove the CRUD feature.

Steps:

  1. Write a CRUD test.
  2. 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);
        // update, delete steps omitted for brevity
    }
}

Step 6: Test the Service

In our architecture, the service layer will provide the abstraction of business logic. In this simple case, our service will wrap the insert-or-update feature, returning a DTO after save.

The steps for this unit test:

  1. Create a service with business logic.
  2. 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();

        //this recreate same behaviour of asp.net MVC usage
        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 the unit test became complex, I switched from the pure TDD approach to a more elastic testing version where I took part in the process. This helps to split all the work into 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

  1. Prepare the form, and test it.
  2. Prepare the model, test what is submitted from form fills the backend model.
  3. Integrate service to save data, test it.

View

  1. Prepare model, pass to the view, test it.
  2. Integrate model with services, to get real data. Test it.

List

  1. Prepare view model, pass fake data to UI, test it.
  2. Integrate service, test it.

Each microfeature was quick to implement and easy to test. This will boost the complete implementation.

Conclusion

TDD is a methodology that drives the development process supported by tests.

This helps to code in many ways but requires that all the teammates 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 tests while developing.

Moreover, if your application is hard to test because of the complexity, you can keep the same philosophy performing some manual steps.


Found this article useful? Follow me (Daniele Fontani) on Medium and check out my most popular articles below! Please 👏 this article to share it!

Resources


Related Articles