Click here to Skip to main content
13,797,347 members
Click here to Skip to main content
Add your own
alternative version

Stats

2.2K views
25 downloads
3 bookmarked
Posted 17 Nov 2018
Licenced CPOL

Build an ASP.NET Wiki to Explain TDD

, 17 Nov 2018
Rate this:
Please Sign up or sign in to vote.
TDD & BDD explained with examples

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:

  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 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.
  3. 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.
  4. 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:

  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 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

  1. Write the entity.
  2. Write the wiki page DTO.
  3. Write the code that maps 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

  1. Make a method that converts markdown to 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

  1. Alter mapping adding 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

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

Step 5: Entity CRUD

  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

  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 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

  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.

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

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Daniele Fontani
Chief Technology Officer
Italy Italy
I'm senior developer and architect specialized on portals, intranets, and others business applications. Particularly interested in Agile developing and open source projects, I worked on some of this as project manager and developer.

My programming experience include:

Frameworks \Technlogies: .NET Framework (C# & VB), ASP.NET, Java, php
Client languages:XML, HTML, CSS, JavaScript, angular.js, jQuery
Platforms:Sharepoint,Liferay, Drupal
Databases: MSSQL, ORACLE, MYSQL, Postgres

You may also be interested in...

Pro

Comments and Discussions

 
QuestionBDD Pin
Alex (RSA)19-Nov-18 2:22
professionalAlex (RSA)19-Nov-18 2:22 
AnswerRe: BDD Pin
Daniele Fontani19-Nov-18 9:21
professionalDaniele Fontani19-Nov-18 9:21 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

Permalink | Advertise | Privacy | Cookies | Terms of Use | Mobile
Web06 | 2.8.181207.3 | Last Updated 17 Nov 2018
Article Copyright 2018 by Daniele Fontani
Everything else Copyright © CodeProject, 1999-2018
Layout: fixed | fluid