Test Driven Development (TDD) Using MVC Web Application

Introduction

Here we going to calculate Rating for Shop using various methods. For this we may need to try different methods over time to have validation against the test method.

Let’s start step by step:

  • Create ASP.NET Test Project - Test Project "TDDWithMVC.Tests".
  • Adding / testing default test methods.
  • Adding a Test class.
  • Working with Test class and test methods.
  • Adding Features classes.
  • Calling the Features classes and test unit test methods.
  • Refactoring Unit test and business class code.
  • Also how to use Strategy Design Pattern.

Create the project

Right click on Solution and add a new project.

new
project

Select a template Web API.

template

solution

Add new folder Name as “Features”.

feature

Now add a Test Class to Test Project “TDDWithMVC.Tests” Names as “UnitTest1.cs”

unit

Replace code with following code

  1. using System;    
  2. using Microsoft.VisualStudio.TestTools.UnitTesting;    
  3. using System.Collections.Generic;    
  4.     
  5. // Calculate Rating using various methods    
  6. // for this we may need to try diffrent methods over time to have validation againts test method    
  7. //     
  8. // 1. Values or reviews for 'n' number of values    
  9. // 2. Reviews for Weighted Rating    
  10. // 3. Reviews for top n no of reviews    
  11. //     
  12. // business senariao to discribe approch when to use TDD : when we do not know how to design any feature for this TDD is grate design tool we can say.    
  13.     
  14. namespace TDDWithMVC.Tests.Features    
  15. {    
  16.     [TestClass]    
  17.     public class UnitTest1    
  18.     {    
  19.         [TestMethod]    
  20.         public void TestMethod1()    
  21.         {    
  22.             var data = new CoffeeShop();    
  23.             data.Reviews = new List < ShopReviews > ();    
  24.             data.Reviews.Add(new ShopReviews()     
  25.             {    
  26.                 ratings = 4    
  27.             });    
  28.     
  29.             var rateIndicator = new ShopRater(data);    
  30.             var result = rateIndicator.ComputeRating(10);    
  31.     
  32.             Assert.AreEqual(4, result.Rating);    
  33.         }    
  34.     }    
  35. }    

 

Now we need to create classes and methods which we are using in Test class. Right now we are considering Namespace for classes,
  1. Right click or Ctrl + . on CoffeeShop and add new class,

    class

  2. Add class ShopReviews as below,

    class

  3. Generate Property as below,

    property

  4. Add property for Rating,

    property

  5. In the same way add class ShopRater and add new method as ComputeRating. Now we can find the following classes  added to Features folder,

    unittest

    Now we will find below code to the classes,
    1. using System.Collections.Generic;    
    2.     
    3. namespace TDDWithMVC.Tests.Features    
    4. {    
    5.     public class CoffeeShop     
    6.     {    
    7.         public List < ShopReviews > Reviews     
    8.         {    
    9.             get;    
    10.             set;    
    11.         }    
    12.     }    
    13. }    
    14.     
    15. namespace TDDWithMVC.Tests.Features    
    16. {    
    17.     public class ShopReviews    
    18.     {    
    19.         public int ratings     
    20.         {    
    21.             get;    
    22.             set;    
    23.         }    
    24.     }    
    25. }    
    26.     
    27.     
    28. namespace TDDWithMVC.Tests.Features     
    29. {    
    30.     class ShopRater     
    31.     {    
    32.         private CoffeeShop data;    
    33.     
    34.         public ShopRater(CoffeeShop data)     
    35.         {    
    36.             // TODO: Complete member initialization    
    37.             this.data = data;    
    38.         }    
    39.     
    40.         public RatingResult ComputeRating(int p)     
    41.         {    
    42.             return new RatingResult();    
    43.         }    
    44.     }    
    45. }  
  6. Now Build the project as it is stable to build the code.

  7. It’s time to Run the Unit test as it is not going to pass but as it is our first step to create TDD .

  8. Right click on Test method and Run test.

    run test

  9. Test has failed;  you can see result into Test Explorer.

    test

  10. Now we will add login to pass the Unit test to fulfill business logic,

    code
    1. public RatingResult ComputeRating(int p)    
    2. {    
    3.     var result = new RatingResult();    
    4.     result.Rating = 4;    
    5.     return result;    
    6. }   
  11. Run the test and here we go,

    test
    Test has passed

    But this is killing me as we have hard coding computation to just pass the Unit test. Every time we cannot ask to change values as input changes As tenant we can add more tests and test conditions and for that we need to change the ComputeRating code to work correctly. Going forward we need to insure that we are adding code, condition, and features and that we are not breaking the code; that is what is the real value of the test.

    Now change the clean code and do the necessary changes to follow coding conventions.
    1. namespace TDDWithMVC.Tests.Features     
    2. {    
    3.     class ShopRater    
    4.     {    
    5.         private CoffeeShop _coffeeShop;    
    6.     
    7.         public ShopRater(CoffeeShop coffeeShop)     
    8.         {    
    9.             // TODO: Complete member initialization    
    10.             this._coffeeShop = coffeeShop;    
    11.         }    
    12.     
    13.         public RatingResult ComputeRating(int numberOfReviews)    
    14.         {    
    15.             var result = new RatingResult();    
    16.             result.Rating = 4;    
    17.             return result;    
    18.         }    
    19.     }    
    20. }   
  12. Add a new method to check multiple rating,
    1. [TestMethod]    
    2. public void Compute_ResultFor_OneReview()     
    3. {    
    4.     // Arrange    
    5.     var data = new CoffeeShop();    
    6.     data.Reviews = new List < ShopReviews > ();    
    7.     data.Reviews.Add(new ShopReviews()    
    8.     {    
    9.         ratings = 4    
    10.     });    
    11.     
    12.     var rateIndicator = new ShopRater(data);    
    13.     // Act    
    14.     var result = rateIndicator.ComputeRating(10);    
    15.     
    16.     // Assert    
    17.     Assert.AreEqual(4, result.Rating);    
    18. }    
    19.     
    20. [TestMethod]    
    21. public void Compute_ResultFor_TwoReview()    
    22. {    
    23.     // Arrange    
    24.     var data = new CoffeeShop();    
    25.     data.Reviews = new List < ShopReviews > ();    
    26.     data.Reviews.Add(new ShopReviews()    
    27.     {    
    28.         ratings = 4    
    29.     });    
    30.     data.Reviews.Add(new ShopReviews()    
    31.     {    
    32.         ratings = 8    
    33.     });    
    34.     
    35.     var rateIndicator = new ShopRater(data);    
    36.     // Act    
    37.     var result = rateIndicator.ComputeRating(10);    
    38.     
    39.     // Assert    
    40.     Assert.AreEqual(4, result.Rating);    
    41. }    
  13. Now refector code the code in ShopRater.cs to pass both the Test,
    1. public RatingResult ComputeRating(int numberOfReviews)    
    2. {    
    3.     var result = new RatingResult();    
    4.     result.Rating = (int) _coffeeShop.Reviews.Average(x => x.ratings);    
    5.     return result;    
    6. }   
    Add Namespace using System.Linq;

    namespace

    Now it’s time to reactor the test code as test code is also as important as business code,
    1. using System;    
    2. using Microsoft.VisualStudio.TestTools.UnitTesting;    
    3. using System.Collections.Generic;    
    4. using System.Linq;    
    5.     
    6.     
    7. // Calculate Rating using various methods    
    8. // for this we may need to try diffrent methods over time to have validation againts test method    
    9. //     
    10. // 1. Values or reviews for 'n' number of values    
    11. // 2. reviews for most larger values    
    12. //     
    13. // business senariao to discribe approch when to use TDD : i do not know how to design this feature.    
    14. // So, TDD is grate design tool we can say.    
    15.     
    16. namespace TDDWithMVC.Tests.Features    
    17. {    
    18.     [TestClass]    
    19.     public class UnitTest1    
    20.     {    
    21.         [TestMethod]    
    22.         public void Compute_ResultFor_OneReview()    
    23.         {    
    24.             // Arrange     
    25.             //var data = new CoffeeShop();    
    26.             //data.Reviews = new List<ShopReviews>();    
    27.             //data.Reviews.Add(new ShopReviews() { ratings = 4 });    
    28.     
    29.             // Arrange    
    30.             var data = BuildReview(rating: 4);    
    31.     
    32.             var rateIndicator = new ShopRater(data);    
    33.             // Act    
    34.             var result = rateIndicator.ComputeRating(10);    
    35.     
    36.             // Assert    
    37.             Assert.AreEqual(4, result.Rating);    
    38.         }    
    39.     
    40.     
    41.         [TestMethod]    
    42.         public void Compute_ResultFor_TwoReview()    
    43.         {    
    44.             // Arrange    
    45.             //var data = new CoffeeShop();    
    46.             //data.Reviews = new List<ShopReviews>();    
    47.             //data.Reviews.Add(new ShopReviews() { ratings = 4 });    
    48.             //data.Reviews.Add(new ShopReviews() { ratings = 8 });    
    49.     
    50.             // Arrange    
    51.             var data = BuildReview(rating: new []     
    52.             {    
    53.                 4,    
    54.                 8    
    55.             });    
    56.     
    57.             var rateIndicator = new ShopRater(data);    
    58.             // Act    
    59.             var result = rateIndicator.ComputeRating(10);    
    60.     
    61.             // Assert    
    62.             Assert.AreEqual(6, result.Rating);    
    63.         }    
    64.     
    65.         private CoffeeShop BuildReview(params int[] rating)    
    66.         {    
    67.             var coffeeShop = new CoffeeShop();    
    68.             coffeeShop.Reviews = rating.Select(x => new ShopReviews    
    69.                 {    
    70.                     ratings = x    
    71.                 })    
    72.                 .ToList();    
    73.             return coffeeShop;    
    74.         }    
    75.     
    76.     }    
    77. }    

Now we can consider more test scenarios for negative reviews or how to deal with odd numbers, But we will mainly focus on Design (TDD is primarily).

Now add a new test to calculate weighted average of two reviews.

Add new test method as follows 

  1. [TestMethod]    
  2. public void Compute_ResultFor_WeightedReview()     
  3. {    
  4.     // Arrange    
  5.     var data = BuildReview(3, 9);    
  6.     
  7.     var rateIndicator = new ShopRater(data);    
  8.     // Act    
  9.     var result = rateIndicator.WeightedReviewRating(10);    
  10.     
  11.     // Assert    
  12.     Assert.AreEqual(5, result.Rating)    

 

Add do implementation to class “ShopRater” so that test passes code as follows,
  1. public RatingResult WeightedReviewRating(int numberOfReviews)    
  2. {    
  3.     var review = _coffeeShop.Reviews.ToArray();    
  4.     var result = new RatingResult();    
  5.     var counter = 0;    
  6.     var toatl = 0;    
  7.     
  8.     for (int i = 0; i < review.Count(); i++)     
  9.     {    
  10.         if (i < review.Count() / 2)     
  11.         {    
  12.             counter += 2;    
  13.             toatl += review[i].ratings * 2;    
  14.         } else    
  15.         {    
  16.             counter += 1;    
  17.             toatl += review[i].ratings;    
  18.         }    
  19.     }    
  20.     
  21.     result.Rating = toatl / counter;    
  22.     return result;    
  23. }  
Now a new business change algorithm and anticipating those changes will make it difficult to do changes; for instance, if I add a new method to ShopRater, So let’s refractor code to make changes easier (it's easy to change algorithm and still I am able test if methods pass).

So now we are going to relay on ShopRater to compute actual rating.

So ComputeResult is relay on IShopRaterAlgorithm which is passed as a parameter which looks as follows,
  1. using System;    
  2. using System.Collections.Generic;    
  3. using System.Linq;    
  4.     
  5. namespace TDDWithMVC.Tests.Features    
  6. {    
  7.     public interface IShopRaterAlgorithm    
  8.     {    
  9.         RatingResult Compute(IList < ShopReviews > shopReviews);    
  10.         //RatingResult ComputeRating(int numberOfReviews);    
  11.         //RatingResult WeightedReviewRating(int numberOfReviews);    
  12.     }    
  13.     
  14.     public class SimpleRatingAlgorithm: IShopRaterAlgorithm     
  15.     {    
  16.         public RatingResult Compute(IList < ShopReviews > reviews)    
  17.         {    
  18.             var result = new RatingResult();    
  19.             result.Rating = (int) reviews.Average(x => x.ratings);    
  20.             return result;    
  21.         }    
  22.     }    
  23.     
  24.     public class WeightedRatingAlgorithm: IShopRaterAlgorithm    
  25.     {    
  26.         public RatingResult Compute(IList < ShopReviews > reviews)    
  27.         {    
  28.             var review = reviews.ToArray();    
  29.             var result = new RatingResult();    
  30.             var counter = 0;    
  31.             var toatl = 0;    
  32.     
  33.             for (int i = 0; i < review.Count(); i++)    
  34.             {    
  35.                 if (i < review.Count() / 2)    
  36.                 {    
  37.                     counter += 2;    
  38.                     toatl += review[i].ratings * 2;    
  39.                 } else    
  40.                 {    
  41.                     counter += 1;    
  42.                     toatl += review[i].ratings;    
  43.                 }    
  44.             }    
  45.     
  46.             result.Rating = toatl / counter;    
  47.             return result;    
  48.         }    
  49.     }    
  50. }    
  51. And ShopRater.cs like    
  52. using System.Collections.Generic;    
  53. using System.Linq;    
  54.     
  55. namespace TDDWithMVC.Tests.Features     
  56. {    
  57.     public class ShopRater     
  58.     {    
  59.         private CoffeeShop _coffeeShop;    
  60.     
  61.         public ShopRater(CoffeeShop coffeeShop)     
  62.         {    
  63.             // TODO: Complete member initialization    
  64.             this._coffeeShop = coffeeShop;    
  65.         }    
  66.     
  67.         public RatingResult ComputeResult(IShopRaterAlgorithm algorithm, int noOfReviewsToUse)    
  68.         {    
  69.             var filterReviews = _coffeeShop.Reviews.Take(noOfReviewsToUse);    
  70.             return algorithm.Compute(filterReviews.ToList());    
  71.         }    
  72.     }    
  73. }  
Now Unit test methods will always call method ComputeResult depending on Algorithm,
  1. var result = rateIndicator.ComputeResult(new SimpleRatingAlgorithm(), 10);  
  2. Or  
  3. var result = rateIndicator.ComputeResult(new WeightedRatingAlgorithm(), 10);  
And this is Strategy Design Pattern: a software design pattern that enables an algorithm's behavior to be selected at runtime. The strategy pattern defines a family of algorithms, encapsulates each algorithm, and makes the algorithms interchangeable within that family.

design

In short:
  1. So I have moved code from ShopRater and to Algorithm classes which is assigning specific responsibility to specific classes. So I have algorithm which focuses on computing result.

  2. ShopRater which has code needed to reproduce that result.

  3. So Test class does not require us to determine which method to call; we always call CompteResult and we pass algorithm which requires to perform computation. Important thing is that we can introduce a new algorithm without changing code inside ShopRater or any of the existing algorithms so that we can extend.

    Now add a method which will compute the average of the first few reviews,
    1. [TestMethod]    
    2. public void Compute_ResultFor_Top_n_No_Of_Review()    
    3. {    
    4.     // Arrange    
    5.     var data = BuildReview(3, 3, 3, 5, 9, 9);    
    6.     var rateIndicator = new ShopRater(data);    
    7.     // Act    
    8.     var result = rateIndicator.ComputeResult(new SimpleRatingAlgorithm(), 3);    
    9.     // Assert    
    10.     Assert.AreEqual(3, result.Rating);    
    11. }   
    Then got to ShopRater and change method as below,
    1. public RatingResult ComputeResult(IShopRaterAlgorithm algorithm, int noOfReviewsToUse)     
    2. {    
    3.     var filterReviews = _coffeeShop.Reviews.Take(noOfReviewsToUse);    
    4.     return algorithm.Compute(filterReviews.ToList());    
    5. }  
    Run the test cases

    test

    We are through and also we can move classes and business code to specific projects and folder structures.
Read more articles on MVC:


Similar Articles