ایجاد Test Driven Development (TDD) با استفاده از MVC

در این مقاله، ما test method هایی را ایجاد خواهیم کرد و سپس این تست متد ها را در یک برنامه MVC 5 مورد آزمایش قرار خواهیم داد.

ایجاد Test Driven Development (TDD) با استفاده از MVC

معرفی

در این مقاله ما می خواهیم سیستم Rating در یک فروشگاه را با استفاده از متدهای متعدد پیاده سازی کنیم. برای این کار ما نیاز داریم تا متدهای مختلفی را به کار بگیریم تا بتوانیم از اعتبارسنجی در طی تست متدها استفاده کنیم.

بیایید گام به گام شروع کنیم :

-یک پروژه تست ASP.NET ایجاد می کنیم و  نام آن را "TDDWithMVC.Tests" می گذاریم.

- تست متدهای پیشفرض را اضافه و سپس تست خواهیم کرد.

-کلاس Test را اضافه خواهیم کرد.

-با کلاس Test و تست متدها کار خواهیم کرد.

- کلاس های Features را اضافه خواهیم کرد.

- کلاس های Features را فراخوانی خواهیم کرد و تست متدها را بر روی آن تست می کنیم.

-اقداماتی برای بهینه سازی و بهبود ساختار کد انجام خواهیم داد.

-نحوه استفاده از Strategy Design Pattern را نیز بررسی خواهیم کرد.

ایجاد پروژه

بر روی Solution کلیک راست کنید و یک پروژه جدید ایجاد کنید.

در پنجره زیر Web API را انتخاب کنید.

یک پوشه جدید به نام “Features” ایجاد کنید.

حالا یک Test Class به نام “UnitTest1.cs” به پروژه اضافه کنید.

کدهای درون کلاس را با کدهای زیر جایگزین کنید.

    using System;    
    using Microsoft.VisualStudio.TestTools.UnitTesting;    
    using System.Collections.Generic;    
        
    // Calculate Rating using various methods    
    // for this we may need to try diffrent methods over time to have validation againts test method    
    //     
    // 1. Values or reviews for 'n' number of values    
    // 2. Reviews for Weighted Rating    
    // 3. Reviews for top n no of reviews    
    //     
    // 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.    
        
    namespace TDDWithMVC.Tests.Features    
    {    
        [TestClass]    
        public class UnitTest1    
        {    
            [TestMethod]    
            public void TestMethod1()    
            {    
                var data = new CoffeeShop();    
                data.Reviews = new List < ShopReviews > ();    
                data.Reviews.Add(new ShopReviews()     
                {    
                    ratings = 4    
                });    
        
                var rateIndicator = new ShopRater(data);    
                var result = rateIndicator.ComputeRating(10);    
        
                Assert.AreEqual(4, result.Rating);    
            }    
        }    
    }    

حالا نیاز داریم تا کلاس ها و متدهای درون Test class را ایجاد کنیم. همچنین باید Namespace ها را برای کلاس ها در نظر بگیریم.

1-بر روی CoffeeShop کلیک راست کنید و یا از کلید های میانبر Ctrl + . استفاده کنید تا یک کلاس جدید ایجاد کنید.

2-کلاس ShopReviews را مانند زیر ایجاد کنید.

3- Property را نیز مطابق شکل زیر تولید کنید.

5- به همان روش قبل، یک کلاس به نام ShopRater و یک متد جدید به نام  ComputeRating ایجاد کنید. با انجام این امور می توانیم ببینیم که کلاس های زیر به پوشه Features اضافه شده اند.

ما می توانیم سپس کدهای زیر را در کلاس ها پیدا کنیم.

    using System.Collections.Generic;    
        
    namespace TDDWithMVC.Tests.Features    
    {    
        public class CoffeeShop     
        {    
            public List < ShopReviews > Reviews     
            {    
                get;    
                set;    
            }    
        }    
    }    
        
    namespace TDDWithMVC.Tests.Features    
    {    
        public class ShopReviews    
        {    
            public int ratings     
            {    
                get;    
                set;    
            }    
        }    
    }    
        
        
    namespace TDDWithMVC.Tests.Features     
    {    
        class ShopRater     
        {    
            private CoffeeShop data;    
        
            public ShopRater(CoffeeShop data)     
            {    
                // TODO: Complete member initialization    
                this.data = data;    
            }    
        
            public RatingResult ComputeRating(int p)     
            {    
                return new RatingResult();    
            }    
        }    
    }  

6- حالا یک بار پروژه را Build کنید.

7-حالا باید Unit test را اجرا کنید. البته این اجرا با موفقیت نخواهد بود، ولی اولین مرحله برای ساخت TDD است.

8 – بر روی Test method کلیک راست کنید و تست را اجرا کنید.

9-تست با شکست روبرو شده است. می توانید نتایج مربوط به آن را در Test Explorer ببینید.

10-حالا ما login را اضافه خواهیم کرد تا بتوانیم منطق برنامه را به صورت کامل پیاده سازی کنیم.

    public RatingResult ComputeRating(int p)    
    {    
        var result = new RatingResult();    
        result.Rating = 4;    
        return result;    
    }   

11- تست را اجرا کنید.

تست با موفقیت اجرا شده است.

اما این کار برای امتحان یک Unit test چندان نیز مناسب نیست. هر بار ما نمی توانیم برای درخواست متغیر ها، درخواست بدهیم تا متغیر های ورودی را تغییر داده و سپس تست ها و شرایط بیشتری را اضافه کنیم.  برای این کار ما نیاز داریم تا کدهای ComputeRating را تغییر بدهیم تا برنامه به شیوه درست کار کند. در ادامه این کار ما باید ابتدا مطمئن شویم که کدها، شرایط و ویژگی ها را به صورت مناسب اضافه کرده ایم و کد را دچار مشکل و نقص نکرده باشیم. در حقیقت همین موضوع، ارزش حقیقی تست را مشخص می کند.

حالا کد را تغییر بدهید و تغییرات ضروری را اعمال کنید.

    namespace TDDWithMVC.Tests.Features     
    {    
        class ShopRater    
        {    
            private CoffeeShop _coffeeShop;    
        
            public ShopRater(CoffeeShop coffeeShop)     
            {    
                // TODO: Complete member initialization    
                this._coffeeShop = coffeeShop;    
            }    
        
            public RatingResult ComputeRating(int numberOfReviews)    
            {    
                var result = new RatingResult();    
                result.Rating = 4;    
                return result;    
            }    
        }    
    }   

12-یک متد جدید برای بررسی rating ها اضافه کنید.

    [TestMethod]    
    public void Compute_ResultFor_OneReview()     
    {    
        // Arrange    
        var data = new CoffeeShop();    
        data.Reviews = new List < ShopReviews > ();    
        data.Reviews.Add(new ShopReviews()    
        {    
            ratings = 4    
        });    
        
        var rateIndicator = new ShopRater(data);    
        // Act    
        var result = rateIndicator.ComputeRating(10);    
        
        // Assert    
        Assert.AreEqual(4, result.Rating);    
    }    
        
    [TestMethod]    
    public void Compute_ResultFor_TwoReview()    
    {    
        // Arrange    
        var data = new CoffeeShop();    
        data.Reviews = new List < ShopReviews > ();    
        data.Reviews.Add(new ShopReviews()    
        {    
            ratings = 4    
        });    
        data.Reviews.Add(new ShopReviews()    
        {    
            ratings = 8    
        });    
        
        var rateIndicator = new ShopRater(data);    
        // Act    
        var result = rateIndicator.ComputeRating(10);    
        
        // Assert    
        Assert.AreEqual(4, result.Rating);    
    }    

حالا ساختار کد را به صورت زیر تغییر بدهید تا بتواند هر دو تست مربوطه را انجام بدهد.

    public RatingResult ComputeRating(int numberOfReviews)    
    {    
        var result = new RatingResult();    
        result.Rating = (int) _coffeeShop.Reviews.Average(x => x.ratings);    
        return result;    
    }   

فضای نام System.Linq را اضافه کنید.

حالا کمی ساختار کد را بهبود می بخشیم.

    using System;    
    using Microsoft.VisualStudio.TestTools.UnitTesting;    
    using System.Collections.Generic;    
    using System.Linq;    
        
        
    // Calculate Rating using various methods    
    // for this we may need to try diffrent methods over time to have validation againts test method    
    //     
    // 1. Values or reviews for 'n' number of values    
    // 2. reviews for most larger values    
    //     
    // business senariao to discribe approch when to use TDD : i do not know how to design this feature.    
    // So, TDD is grate design tool we can say.    
        
    namespace TDDWithMVC.Tests.Features    
    {    
        [TestClass]    
        public class UnitTest1    
        {    
            [TestMethod]    
            public void Compute_ResultFor_OneReview()    
            {    
                // Arrange     
                //var data = new CoffeeShop();    
                //data.Reviews = new List<ShopReviews>();    
                //data.Reviews.Add(new ShopReviews() { ratings = 4 });    
        
                // Arrange    
                var data = BuildReview(rating: 4);    
        
                var rateIndicator = new ShopRater(data);    
                // Act    
                var result = rateIndicator.ComputeRating(10);    
        
                // Assert    
                Assert.AreEqual(4, result.Rating);    
            }    
        
        
            [TestMethod]    
            public void Compute_ResultFor_TwoReview()    
            {    
                // Arrange    
                //var data = new CoffeeShop();    
                //data.Reviews = new List<ShopReviews>();    
                //data.Reviews.Add(new ShopReviews() { ratings = 4 });    
                //data.Reviews.Add(new ShopReviews() { ratings = 8 });    
        
                // Arrange    
                var data = BuildReview(rating: new []     
                {    
                    4,    
                    8    
                });    
        
                var rateIndicator = new ShopRater(data);    
                // Act    
                var result = rateIndicator.ComputeRating(10);    
        
                // Assert    
                Assert.AreEqual(6, result.Rating);    
            }    
        
            private CoffeeShop BuildReview(params int[] rating)    
            {    
                var coffeeShop = new CoffeeShop();    
                coffeeShop.Reviews = rating.Select(x => new ShopReviews    
                    {    
                        ratings = x    
                    })    
                    .ToList();    
                return coffeeShop;    
            }    
        
        }    
    }    

حالا ما می توانیم تست های بیشتر با موضوعات بیشتری نیز به پروژه اضافه کنیم. اما هدف ما بیشتر ،تمرکز بر روی طراحی است. (و به خصوص بر روی TDD)

حالا یک تست جدید برای محاسبه میانگین وزنی دو مقدار اضافه می کنیم.

یک تست متد مانند زیر اضافه کنید.

    [TestMethod]    
    public void Compute_ResultFor_WeightedReview()     
    {    
        // Arrange    
        var data = BuildReview(3, 9);    
        
        var rateIndicator = new ShopRater(data);    
        // Act    
        var result = rateIndicator.WeightedReviewRating(10);    
        
        // Assert    
        Assert.AreEqual(5, result.Rating)    
    }

کد زیر را به کلاس  “ShopRater” مانند زیر اضافه کنید.

    public RatingResult WeightedReviewRating(int numberOfReviews)    
    {    
        var review = _coffeeShop.Reviews.ToArray();    
        var result = new RatingResult();    
        var counter = 0;    
        var toatl = 0;    
        
        for (int i = 0; i < review.Count(); i++)     
        {    
            if (i < review.Count() / 2)     
            {    
                counter += 2;    
                toatl += review[i].ratings * 2;    
            } else    
            {    
                counter += 1;    
                toatl += review[i].ratings;    
            }    
        }    
        
        result.Rating = toatl / counter;    
        return result;    
    }  

حالا اگر بخواهیم یک الگوریتم جدید به کار بگیریم و تغییرات را پیش بینی و اعمال کنیم، چندان ساده نخواهد بود. به عنوان مثال، اگر ما یک متد جدید به ShopRater اضافه کنیم، دیگر کارها به آسانی قبل انجام نخواهند شد. پس بیایید کد را به قطعات مجزا بشکانیم تا بتوانیم تغییرات دلخواهمان را به آسانی اضافه کنیم.

بنابراین با انجام این کار، تمامی امور مربوط به rating توسط ShopRater انجام خواهند شد.

و ComputeResult نیز به IShopRaterAlgorithm وابسته است که به عنوان یک پارامتر مطابق زیر پاس داده می شود:

    using System;    
    using System.Collections.Generic;    
    using System.Linq;    
        
    namespace TDDWithMVC.Tests.Features    
    {    
        public interface IShopRaterAlgorithm    
        {    
            RatingResult Compute(IList < ShopReviews > shopReviews);    
            //RatingResult ComputeRating(int numberOfReviews);    
            //RatingResult WeightedReviewRating(int numberOfReviews);    
        }    
        
        public class SimpleRatingAlgorithm: IShopRaterAlgorithm     
        {    
            public RatingResult Compute(IList < ShopReviews > reviews)    
            {    
                var result = new RatingResult();    
                result.Rating = (int) reviews.Average(x => x.ratings);    
                return result;    
            }    
        }    
        
        public class WeightedRatingAlgorithm: IShopRaterAlgorithm    
        {    
            public RatingResult Compute(IList < ShopReviews > reviews)    
            {    
                var review = reviews.ToArray();    
                var result = new RatingResult();    
                var counter = 0;    
                var toatl = 0;    
        
                for (int i = 0; i < review.Count(); i++)    
                {    
                    if (i < review.Count() / 2)    
                    {    
                        counter += 2;    
                        toatl += review[i].ratings * 2;    
                    } else    
                    {    
                        counter += 1;    
                        toatl += review[i].ratings;    
                    }    
                }    
        
                result.Rating = toatl / counter;    
                return result;    
            }    
        }    
    }    
    And ShopRater.cs like    
    using System.Collections.Generic;    
    using System.Linq;    
        
    namespace TDDWithMVC.Tests.Features     
    {    
        public class ShopRater     
        {    
            private CoffeeShop _coffeeShop;    
        
            public ShopRater(CoffeeShop coffeeShop)     
            {    
                // TODO: Complete member initialization    
                this._coffeeShop = coffeeShop;    
            }    
        
            public RatingResult ComputeResult(IShopRaterAlgorithm algorithm, int noOfReviewsToUse)    
            {    
                var filterReviews = _coffeeShop.Reviews.Take(noOfReviewsToUse);    
                return algorithm.Compute(filterReviews.ToList());    
            }    
        }    
    }  

حالا متدهای Unit test همواره بر اساس الگوریتمی که استفاده می کند، ComputeResult را فراخوانی می کند.

    var result = rateIndicator.ComputeResult(new SimpleRatingAlgorithm(), 10);  
    Or  
    var result = rateIndicator.ComputeResult(new WeightedRatingAlgorithm(), 10);  

و تعریف Strategy Design Pattern عبارت است از  : یک design pattern نرم افزاری که اجازه می دهد یک الگوریتم و یک رفتار متناسب با آن، در زمان اجرا انتخاب شوند. strategy pattern یک مجموعه از الگوریتم ها را تعریف می کند، هر الگوریتم را کپسوله سازی می کند و امکان جایگزینی به آن ها می دهد.

به صورت خلاصه تر :

1-بنابراین ما کدها را از ShopRater به کلاس های Algorithm انتقال می دهیم که وظایف خاصی را به کلاس های خاصی اختصاص می دهند. بنابراین در این حالت، الگوریتمی داریم که بر روی نتایج محاسباتی تمرکز کرده است.

2- ShopRater مورد نیاز است تا دوباره نتیجه بتواند تولید شود.

3-برای ایجاد کلاس Test نیازی نیست که ما متد مورد نظرمان را فراخوانی کنیم. ما همواره CompteResult را فراخوانی می کنیم و الگوریتم را به آن پاس می دهیم تا بتواند عملیات را انجام دهد. نکته مهم این است که ما می توانیم یک الگوریتم جدید ایجاد کنیم بدون این که کد ما در درون ShopRater تغییری بکند و یا الگوریتم های موجود تحت تاثیر قرار بگیرند.

حالا یک متد اضافه می کنیم که میانگین اولین مقادیر را محاسبه خواهد کرد.

    [TestMethod]    
    public void Compute_ResultFor_Top_n_No_Of_Review()    
    {    
        // Arrange    
        var data = BuildReview(3, 3, 3, 5, 9, 9);    
        var rateIndicator = new ShopRater(data);    
        // Act    
        var result = rateIndicator.ComputeResult(new SimpleRatingAlgorithm(), 3);    
        // Assert    
        Assert.AreEqual(3, result.Rating);    
    }   

حالا به سراغ ShopRater می رویم و متد را مطابق زیر تغییر می دهیم:

public RatingResult ComputeResult(IShopRaterAlgorithm algorithm, int noOfReviewsToUse)     
{    
    var filterReviews = _coffeeShop.Reviews.Take(noOfReviewsToUse);    
    return algorithm.Compute(filterReviews.ToList());    
}

تست ها را اجرا کنید.

ما کد مورد نظرمان را پیاده سازی کردیم و حالا می توانیم آن ها را به هر پروژه دلخواه و با هر ساختاری منتقل کنیم.

آموزش asp.net mvc

فایل های ضمیمه
دانلود نسخه ی PDF این مطلب