使用IdleTest進行TDD單元測試驅動開發演練(1)

【前言】

開發工具:Visual Studio 2012html

測試庫:Visual Studio 2012自帶的MSTest前端

DI框架:Unity 框架

數據持久層:Entity Frameworkide

前端UI:ASP.NET MVC 4.0工具

需求:我這裏假設只知足兩個功能,一個用戶註冊,另外一個則是登錄的功能,藉助於一些DDD思想,我將從領域層(或者常說的BLL)開始開發,固然每一層都是採用TDD,按我喜歡的作法就是「接口先行,測試驅動」,不廢話,直奔主題吧。post

有關VS2012的單元測試請參見《VS2012 Unit Test 我的學習彙總(含目錄)單元測試

有關測試中使用的IdleTest庫請參見http://idletest.codeplex.com/學習

 

1、首先來建立解決方案與項目的結構。

 

1. 建立空白解決方案「IdleTest.TDDEntityFramework」,新建解決方案文件夾「Interfaces」,並在文件夾內建立兩個項目 「IdleTest.TDDEntityFramework.IRepositories」 和 「IdleTest.TDDEntityFramework.IServices」。開發工具

2. 直接在解決方案下建立類庫項目 「IdleTest.TDDEntityFramework.Services」、「IdleTest.TDDEntityFramework.Models」 和 「IdleTest.TDDEntityFramework.Repositories」測試

3. 在解決方案下建立MVC4項目"IdleTest.TDDEntityFramework.MvcUI"做爲最終的UI,我這裏選擇空模板,解決方案初始結構初始結構圖以下

4. 把全部類庫項目中自動生成的「Class1.cs」文件刪除。

5. 使用Visio畫出解決方案中各項目的關係(以下圖),這圖畫的是項目關係,實際上這些項目內的類也都遵循這樣的關係。例如本項目只有一個Model,即UserModel,那麼「IdleTest.TDDEntityFramework.IRepositories」下就相應將類命名爲「IUserRepository」,「IdleTest.TDDEntityFramework.IServices」對應「IUserService」,以此類推,非接口則去掉前綴「I」。這是我我的的一些習慣,每一個人可能命名方式可能不太同樣,這很正常,可是若是是超過一我的來共同開發,則應將規範統一,俗話說「約定優於配置」嘛。

6. 這裏只是本身演練TDD的Demo而已,將不使用「UnitOfWork」,其餘也可能會缺乏很多功能,由於不低不在於Entity Framework或MVC等等,而關注的只是單元測試驅動開發罷了。

 

2、測試前的編碼以及其餘方面的準備

 

7. 在「IdleTest.TDDEntityFramework.Models」下添加類「UserModel」。

    public class UserModel
    {
        public string LoginName { get; set; }

        public string Password { get; set; }

        public int Age { get; set; }
    }
UserModel

8. 分別在項目「IdleTest.TDDEntityFramework.IRepositories」和「IdleTest.TDDEntityFramework.IServices」下添加引用「IdleTest.TDDEntityFramework.Models」,並分別添加接口「IUserRepository」、「IRepository」和「IUserService」。 

    public interface IUserRepository : IRepository<UserModel, string>
    {
    }
    public interface IRepository<TEntity, TKey> where TEntity : class
    {
        IEnumerable<TEntity> Get(
            Expression<Func<TEntity, bool>> filter = null,
            Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
            string includeProperties = "");

        TEntity GetSingle(TKey id);

        void Insert(TEntity entity);

        void Update(TEntity entityToUpdate);

        void Delete(TKey id);

        void Delete(TEntity entityToDelete);
    }
IRepository
    public interface IUserService
    {
        bool Login(UserModel model);

        bool Register(UserModel model);

        UserModel GetModel(string loginName);
    }
IUserService

  那麼藉助DDD的一些思想,這裏的IUserService體現着功能需求,Service這層的代碼徹底由業務需求肯定,於是IUserService只編寫了三個方法。而Repository這層則不去關心業務,只是常規性的公開且提供一些方法出來,這在不少項目中幾乎都是肯定,孤兒IRepository也就天然而然具備了增刪改查的功能了。

9. 開始涉及單元測試,建立解決方案文件夾「Tests」,並在該文件夾下建立單元測試項目「IdleTest.TDDEntityFramework.ServiceTest」,添加引
用「IdleTest.TDDEntityFramework.IRepositories」、「IdleTest.TDDEntityFramework.IServices」、「IdleTest.TDDEntityFramework.Services」、「IdleTest.TDDEntityFramework.Models」,緊接着對「IdleTest.TDDEntityFramework.IRepositories」添加「Fakes程序集」(有關Fakes可參照VS2012 Unit Test——Microsoft Fakes入門)。

10. 在解決方案物理路徑下建立文件夾「libs」,並將「IdleTest」中相關dll拷貝進去。接着在項目「IdleTest.TDDEntityFramework.ServiceTest」添加引用,在「引用管理器」中單擊「瀏覽」按鈕,找到剛剛建立的「libs」文件夾,並添加下圖所示引用。有關IdleTest可參照從http://idletest.codeplex.com下載編譯。

 

 

3、編寫單元測試,邊測試邊修改代碼

 

11. 我將在剛添加的測試項目中編寫一個針對「IUserService」的測試基類「BaseUserServiceTest」(關於對接口的測試能夠參照VS2012 Unit Test —— 我對接口進行單元測試使用的技巧)。

using IdleTest;
using IdleTest.MSTest;
using IdleTest.TDDEntityFramework.IServices;
using IdleTest.TDDEntityFramework.IRepositories.Fakes;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks;
using IdleTest.TDDEntityFramework.Models;
using IdleTest.TDDEntityFramework.IRepositories;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace IdleTest.TDDEntityFramework.ServiceTest
{
    public abstract class BaseUserServiceTest
    {
        protected string ExistedLoginName = "zhangsan";

        protected string ExistedPassword = "123456";

        protected string NotExistedLoginName = "zhangsan1";

        protected string NotExistedPassword = "123";

        private IUserRepository userRepository;

        protected IList<UserModel> ExistedUsers;

        protected abstract IUserService UserService
        {
            get;
        }
        
        /// <summary>
        /// IUserRepository模擬對象
        /// </summary>
        public virtual IUserRepository UserRepository
        {
            get
            {
                if (this.userRepository == null)
                {
                    StubIUserRepository stubUserRepository = new StubIUserRepository();
                    //模擬Get方法
                    stubUserRepository.GetExpressionOfFuncOfUserModelBooleanFuncOfIQueryableOfUserModelIOrderedQueryableOfUserModelString
                        = (x, y, z) =>
                        {
                            return this.ExistedUsers.Where<UserModel>(x.Compile());
                        };

                    //模擬GetSingle方法
                    stubUserRepository.GetSingleString = p => this.ExistedUsers.FirstOrDefault<UserModel>(o => o.LoginName == p);

                    //模擬Insert方法
                    stubUserRepository.InsertUserModel = (p) => this.ExistedUsers.Add(p);

                    this.userRepository = stubUserRepository;
                }

                return this.userRepository;
            }
        }

        [TestInitialize]
        public void InitUserList()
        {
            //每次測試前都初始化
            this.ExistedUsers = new List<UserModel> { new UserModel { LoginName = ExistedLoginName, Password = ExistedPassword } };
        }

        public virtual void LoginTest()
        {
            //驗證登錄失敗的場景
            AssertCommon.AssertBoolean<UserModel>(
                new UserModel[] { 
                    null, new UserModel(),
                    new UserModel { LoginName = string.Empty, Password = ExistedPassword }, //帳戶爲空
                    new UserModel { LoginName = ExistedLoginName, Password = string.Empty }, //密碼爲空
                    new UserModel { LoginName = ExistedLoginName, Password = NotExistedPassword }, //密碼錯誤
                    new UserModel { LoginName = NotExistedLoginName, Password = NotExistedPassword },  //帳戶密碼錯誤                    
                    new UserModel { LoginName = NotExistedLoginName, Password = ExistedLoginName }  //帳戶錯誤
                }, false, p => UserService.Login(p));

            //帳戶密碼正確,驗證成功,這裏假設正確的帳戶密碼是"zhangsan"、"123456"
            UserModel model = new UserModel { LoginName = ExistedLoginName, Password = ExistedPassword };
            AssertCommon.AssertEqual<bool>(true, UserService.Login(model));
        }

        public virtual void RegisterTest()
        {
            //驗證註冊失敗的場景
            AssertCommon.AssertBoolean<UserModel>(
                new UserModel[] { 
                    null, new UserModel(),
                    new UserModel { LoginName = string.Empty, Password = NotExistedPassword }, //帳戶爲空
                    new UserModel { LoginName = NotExistedLoginName, Password = string.Empty }, //密碼爲空
                    new UserModel { LoginName = ExistedLoginName, Password = NotExistedPassword }, //帳戶已存在
                }, false, p => UserService.Register(p));

            //驗證註冊成功的場景
            //密碼與他人相同也可註冊
            UserModel register1 = new UserModel { LoginName = "register1", Password = ExistedPassword };
            UserModel register2 = new UserModel { LoginName = "register2", Password = NotExistedPassword };
            UserModel register3 = new UserModel { LoginName = "register3", Password = NotExistedPassword, Age = 18 }; 
            AssertCommon.AssertBoolean<UserModel>(
                new UserModel[] { register1, register2, register3 }, true, p => UserService.Register(p));

            //獲取用戶且應與註冊的信息保持一致
            UserModel actualRegister1 = UserService.GetModel(register1.LoginName);
            AssertCommon.AssertEqual<string>(register1.LoginName, actualRegister1.LoginName);
            AssertCommon.AssertEqual<string>(register1.Password, actualRegister1.Password);
            AssertCommon.AssertEqual<int>(register1.Age, actualRegister1.Age);

            UserModel actualRegister2 = UserService.GetModel(register2.LoginName);
            AssertCommon.AssertEqual<string>(register2.LoginName, actualRegister2.LoginName);
            AssertCommon.AssertEqual<string>(register2.Password, actualRegister2.Password);
            AssertCommon.AssertEqual<int>(register2.Age, actualRegister2.Age);

            UserModel actualRegister3 = UserService.GetModel(register3.LoginName);
            AssertCommon.AssertEqual<string>(register3.LoginName, actualRegister3.LoginName);
            AssertCommon.AssertEqual<string>(register3.Password, actualRegister3.Password);
            AssertCommon.AssertEqual<int>(register3.Age, actualRegister3.Age);
        }

        public virtual void GetModelTest()
        {
            AssertCommon.AssertIsNull<string, UserModel>(TestCommon.GetEmptyStrings(), true, p => UserService.GetModel(p));
            AssertCommon.AssertIsNull(true, UserService.GetModel(NotExistedLoginName));

            UserModel actual = UserService.GetModel(ExistedLoginName);
            AssertCommon.AssertEqual<string>(ExistedLoginName, actual.LoginName);
            AssertCommon.AssertEqual<string>(ExistedPassword, actual.Password);
        }
    }
}
BaseUserServiceTest

 

  BaseUserServiceTest類自己不會具備任何測試,只有子類去繼承它,且實現抽象屬性「UserService」、Override相應的測試方法(LoginTest、RegisterTest、GetModelTest)並聲明「TestMethod」特性後才能進行測試。

12. 在測試項目再編寫類UserServiceTest,繼承BaseUserServiceTest。 

    [TestClass]
    public class UserServiceTest : BaseUserServiceTest
    {     
        protected override IUserService UserService
        {
            get { return new UserService(this.UserRepository); }
        }

        [TestMethod]
        public override void GetModelTest()
        {
            base.GetModelTest();
        }

        [TestMethod]
        public override void LoginTest()
        {
            base.LoginTest();
        }

        [TestMethod]
        public override void RegisterTest()
        {
            base.RegisterTest();
        }
    }
UserServiceTest

  因爲父類已作好了相應的測試代碼,此時編寫UserServiceTest就有點一勞永逸的感受了。

  注意在實現「UserService」屬性時,編寫以下圖所示代碼後按「Alt+Shift+F10」在彈出的小菜單中選中「爲UserService生成類」回車,這時發現它生成在了咱們的測試項目中,我暫時不會去理會這些,如今最要緊的是我須要在最短期最少代碼量上使得個人測試經過。

  接着去修改剛生成的UserService類。 

    public class UserService : IUserService
    {
        private IUserRepository userRepository;

        public UserService(IUserRepository userRepository)
        {
            // TODO: Complete member initialization
            this.userRepository = userRepository;
        }

        public bool Login(UserModel model)
        {
            throw new NotImplementedException();
        }

        public bool Register(UserModel model)
        {
            throw new NotImplementedException();
        }

        public UserModel GetModel(string loginName)
        {
            throw new NotImplementedException();
        }
    }
UserService

13. 生成以後打開「測試資源管理器」稍等幾秒便可發現三個須要測試的方法呈現了。此時測試固然都是所有不經過。繼續往下修改UserService,直至測試經過。

 

    public class UserService : IUserService
    {
        private IUserRepository userRepository;

        public UserService(IUserRepository userRepository)
        {
            // TODO: Complete member initialization
            this.userRepository = userRepository;
        }

        #region IUserService成員
        public bool Login(UserModel model)
        {
            if (!IsValidModel(model))
            {
                return false;
            }

            IList<UserModel> list = 
                userRepository.Get(p => p.LoginName == model.LoginName && p.Password == model.Password).ToList();

            return list != null && list.Count > 0;
        }

        public bool Register(UserModel model)
        {
            if (!IsValidModel(model))
            {
                return false;
            }

            if (GetModel(model.LoginName) != null)
            {
                return false;
            }

            userRepository.Insert(model);
            return true;
        }

        public UserModel GetModel(string loginName)
        {
            if (!string.IsNullOrEmpty(loginName))
                return userRepository.GetSingle(loginName);

            return null;
        }
        #endregion

        private bool IsValidModel(UserModel model)
        {
            return model != null && !string.IsNullOrEmpty(model.LoginName) && !string.IsNullOrEmpty(model.Password);
        }
    }
UserService

 

14. 此時測試已經過,查看代碼覆蓋率,雙擊」UserService「下未達到100%覆蓋率的行(以下圖所示)能夠查看哪些代碼還沒有覆蓋,而後酌情再看是否須要增長或修改代碼以使覆蓋率達到100%,我這裏分析當前未覆蓋的對項目沒有什麼影響,故再也不修改。

15. 最後將UserService類剪切到項目」IdleTest.TDDEntityFramework.Services「,添加引用,修改相應命名空間。

再次運行測試並順利經過,那麼這一階段的開發與單元測試均大功告成。

 

【總結】


  上述過程簡言之,就是先搭建VS解決方案的項目結構,而後編寫Model(此無需測試,也是整個項目傳遞數據的基本),再寫項目須要的接口,接着針對接口編寫單元測試, 最後纔是編寫實現接口的類代碼。


  對於實現接口的類中的一些方法(如「UserService」類的「IsValidModel」方法)我並無針對它編寫測試,首先它是一個私有方法(關於私有方法需不須要測試的爭論貌似如今尚未統一的結論,鄙人能力有限,不敢妄加評價);其次即便它是一個public方法,我也仍然不會去測試它,由於它只是爲「IUserService」接口成員服務的,或者說該方法本來就不須要,只是我寫代碼中重構出來,編寫完UserService我只關心該類中的「IUserService」接口成員,因此…… 其實,這裏也能夠經過代碼覆蓋率看到,即便沒有專門對「IsValidModel」方法編寫相應測試,可是它的覆蓋率仍然是100%,我不能肯定私有方法到底要不要測試,可是在這裏我不測「IsValidModel」方法確定沒有錯。


  測試基類「BaseUserServiceTest」是針對「IUserService」接口編寫的,而它的子類貌似什麼都不作,我之因此這麼寫,只是爲了之後若是有新的類實現「IUserService」接口 時,我仍然只須要簡單的添加「BaseUserServiceTest」的一個子類,就能夠完成測試,文中貌似也提到,有種一勞永逸的感受,除非接口改變,不然對類的修改等等基本都不會影響 到原有測試。這樣就足以保證了之後修改bug、代碼重構或需求變化時對代碼修改後仍能。

 

  因爲使用了依賴注入,故而測試時就能夠隔離依賴,文中Service層本來是依賴Repository,可是我這裏在未具體實現Repository前都不會影響對Service層的開發與測試。


  TDD前期工做量比較大,可是對於後期代碼(例如總體測試修改bug、代碼重構或需求變化時對代碼修改)質量的保證是很是可靠的。

  未完待續。。。。。。

相關文章
相關標籤/搜索