qileilove

          blog已經轉移至github,大家請訪問 http://qaseven.github.io/

          TDD中的單元測試寫多少才夠?

           測試驅動開發(TDD)已經是耳熟能詳的名詞,既然是測試驅動,那么測試用例代碼就要寫在開發代碼的前面。但是如何寫測試用例?寫多少測試用例才夠?我想大家在實際的操作過程都會產生這樣的疑問。
            3月15日,我參加了thoughtworks組織的“結對編程和TDD Openworkshop”活動,聆聽了tw的資深咨詢專家仝(tong2)鍵的精彩講解,并在講師的帶領下實際參與了一次TDD和結對編程的過程。活動中,仝鍵老師對到底寫多少測試用例才夠的問題,給出了下面一個解釋:
            我們寫單元測試,有一個重要的原因是用來防止自己犯低級錯誤的。我們不能把寫實現代碼的人當作我們的敵人,一定要把全部情況都測到,以防止他們在里面故意留下各種隱蔽的陷阱。測試寫的再多可能也沒有辦法覆蓋全部情況,所以只要能讓自己感到安全即可。怎樣才能讓自己感到安全呢?這是沒有標準答案的,只能是寫多了測試以后慢慢體會。
            另外,寫測試也要花時間的,比如compare這個方法的實現部分,我們只花了一兩分鐘就寫完了,而這些測試代碼,我們花了足足半個多小時,這樣做值得嗎?對于簡單的業務邏輯來說,當然是不值得的,畢竟我們還很多工作等著做,老板花錢是為了我們的產品代碼,而不是測試代碼。
            再考慮一種情況,我要創業,想了一個點子,做了一個網站,我當然是想以最快的速度把它做成型讓別人用。如果我在完全不知道人們會不會喜歡的時候,先花大量時間寫測試,最后發現沒人用只能丟掉,這些測試豈不是白寫了。
            所以還是上面那句話:單元測試是讓你提升自己對代碼的信心的,只要你感覺安全可以繼續開發時就夠了,不是越多越好。
            我相信上面一段解釋對于本文中提出的問題大家都沒有什么異議。但是這里我們不考慮特殊情況,在實際操作中,是否有辦法對單元測試這一工作進行衡量?來判斷是否足夠?
            使用代碼覆蓋率來衡量單元測試是否足夠
            常見的代碼覆蓋率有下面幾種:
            語句覆蓋(Statement Coverage):這是最常用也是最常見的一種覆蓋方式,就是度量被測代碼中每個可執行語句是否被執行到了。
            判定覆蓋(Desicion Coverage):它度量程序中每一個判定的分支是否都被測試到了。
            條件覆蓋(Condition Coverage):它度量判定中的每個子表達式結果true和false是否被測試到了。
            路徑覆蓋(Path Coverage):它度量了是否函數的每一個分支都被執行了。
            前三種覆蓋率大家可以查看下面的引用的第3篇文章,這里就不再多說。我們通過一個例子,來看看路徑覆蓋。比如下面的測試代碼中有兩個判定分支
          int foo(int a, int b)
          {
          int nReturn = 0;
          if (a < 10)
          {// 分支一
          nReturn+= 1;
          }
          if (b < 10)
          {// 分支二
          nReturn+= 10;
          }
          return nReturn;
          }
            我們仔細看看邏輯,nReturn的結果一共有4種可能,我們通過路徑覆蓋的方法設計出來的測試用例:
            Perfect。但是實際中的代碼往往比上面的例子復雜,如果代碼中有5個if-else,那么按照路徑覆蓋的方法,至少需要25=32個測試用例。這樣簡直要瘋掉了。
            沒必要追求代碼覆蓋率,真正要覆蓋的是邏輯
            簡單追求代碼結構上的覆蓋率,容易導致產生大量無意義的測試用例或者無法覆蓋關鍵業務邏輯。我們再看看上面解釋的第一段話。
            我們寫單元測試,有一個重要的原因是用來防止自己犯低級錯誤的。我們不能把寫實現代碼的人當作我們的敵人,一定要把全部情況都測到,以防止他們在里面故意留下各種隱蔽的陷阱。測試寫的再多可能也沒有辦法覆蓋全部情況,所以只要能讓自己感到安全即可。怎樣才能讓自己感到安全呢?這是沒有標準答案的,只能是寫多了測試以后慢慢體會。
           怎么才算讓自己感到安全?覆蓋邏輯,而不是代碼。站在使用者的角度考慮,需要關心的是軟件實現邏輯,而不是覆蓋率。如下面的例子:
          public class UserBusiness
          {
          public string CreateUser(User user)
          {
          string result = "success";
          if (string.IsNullOrEmpty(user.Username))
          {
          result = "usename is null or empty";
          }
          else if (string.IsNullOrEmpty(user.Password))
          {
          result = "password is null or empty";
          }
          else if (user.Password != user.ConfirmPassword)
          {
          result = "password is not equal to confirmPassword";
          }
          else if (string.IsNullOrEmpty(user.Creator))
          {
          result = "creator is null or empty";
          }
          else if (user.CreateDate == new DateTime())
          {
          result = "createdate must be assigned value";
          }
          else if (string.IsNullOrEmpty(user.CreatorIP))
          {
          result = "creatorIP is null or empty";
          }
          if (result != "success")
          {
          return result;
          }
          user.Username = user.Username.Trim();
          user.Password = BitConverter.ToString(MD5.Create().ComputeHash(Encoding.UTF8.GetBytes(user.Password)));
          UserDataAccess dataAccess = new UserDataAccess();
          dataAccess.CreateUser(user);
          return result;
          }
          }
          在寫UserBusiness.CreateUser的測試用例的時候,我們定義了下面幾個單元測試用例:
          [TestClass()]
          public class UserBusinessTest
          {
          private TestContext testContextInstance;
          /// <summary>
          ///Gets or sets the test context which provides
          ///information about and functionality for the current test run.
          ///</summary>
          public TestContext TestContext
          {
          get
          {
          return testContextInstance;
          }
          set
          {
          testContextInstance = value;
          }
          }
          [TestMethod()]
          public void Should_Username_Not_Null_Or_Empty()
          {
          UserBusiness target = new UserBusiness();
          User user = new User();
          string expected = "usename is null or empty";
          string actual = target.CreateUser(user);
          Assert.AreEqual(expected, actual);
          }
          [TestMethod()]
          public void Should_Password_Not_Null_Or_Empty()
          {
          UserBusiness target = new UserBusiness();
          User user = new User()
          {
          Username = "ethan.cai"
          };
          string expected = "password is null or empty";
          string actual = target.CreateUser(user);
          Assert.AreEqual(expected, actual);
          }

          [TestMethod()]
          public void Should_Password_Equal_To_ConfirmPassword()
          {
          UserBusiness target = new UserBusiness();
          User user = new User()
          {
          Username = "ethan.cai",
          Password = "a121ww123",
          ConfirmPassword = "a121ww1231"
          };
          string expected = "password is not equal to confirmPassword";
          string actual = target.CreateUser(user);
          Assert.AreEqual(expected, actual);
          }
          [TestMethod()]
          public void Should_Creator_Not_Null_Or_Empty()
          {
          UserBusiness target = new UserBusiness();
          User user = new User()
          {
          Username = "ethan.cai",
          Password = "a121ww123",
          ConfirmPassword = "a121ww1231"
          };
          string expected = "password is not equal to confirmPassword";
          string actual = target.CreateUser(user);
          Assert.AreEqual(expected, actual);
          }
          [TestMethod()]
          public void Should_CreateDate_Assigned_Value()
          {
          UserBusiness target = new UserBusiness();
          User user = new User()
          {
          Username = "ethan.cai",
          Password = "a121ww123",
          ConfirmPassword = "a121ww123",
          Creator = "ethan.cai"
          };
          string expected = "createdate must be assigned value";
          string actual = target.CreateUser(user);
          Assert.AreEqual(expected, actual);
          }
          [TestMethod()]
          public void Should_CreatorIP_Not_Null_Or_Empty()
          {
          UserBusiness target = new UserBusiness();
          User user = new User()
          {
          Username = "ethan.cai",
          Password = "a121ww123",
          ConfirmPassword = "a121ww123",
          Creator = "ethan.cai",
          CreateDate = DateTime.Now
          };
          string expected = "creatorIP is null or empty";
          string actual = target.CreateUser(user);
          Assert.AreEqual(expected, actual);
          }
          [TestMethod()]
          public void Should_Trim_Username()
          {
          UserBusiness target = new UserBusiness();
          User user = new User()
          {
          Username = "ethan.cai  ",
          Password = "a121ww123",
          ConfirmPassword = "a121ww123",
          Creator = "ethan.cai",
          CreateDate = DateTime.Now,
          CreatorIP = "127.0.0.1"
          };
          string expected = "ethan.cai";
          target.CreateUser(user);
          Assert.AreEqual(expected, user.Username);
          }
          [TestMethod()]
          public void Should_Save_MD5_Hash_Password()
          {
          UserBusiness target = new UserBusiness();
          User user = new User()
          {
          Username = "ethan.cai  ",
          Password = "a121ww123",
          ConfirmPassword = "a121ww123",
          Creator = "ethan.cai",
          CreateDate = DateTime.Now,
          CreatorIP = "127.0.0.1"
          };
          string actual = target.CreateUser(user);
          Assert.IsTrue("success" == actual
          && user.Password == BitConverter.ToString(MD5.Create().ComputeHash(Encoding.UTF8.GetBytes("a121ww123"))));
          }
          [TestMethod()]
          public void Should_Create_User_Successfully_When_User_Is_OK()
          {
          UserBusiness target = new UserBusiness();
          User user = new User()
          {
          Username = "ethan.cai  ",
          Password = "a121ww123",
          ConfirmPassword = "a121ww123",
          Creator = "ethan.cai",
          CreateDate = DateTime.Now,
          CreatorIP = "127.0.0.1"
          };
          string expected = "success";
          string actual = target.CreateUser(user);
          Assert.IsTrue(expected == actual);
          }
          }
            
            如果僅從代碼覆蓋率的角度來看,單元測試Should_Trim_Username、Should_Save_MD5_Hash_Password不會增加覆蓋率,似乎沒有必要,但是從邏輯上看,創建的賬戶的Username頭尾不能包含空白字符,密碼也不能明文存儲,顯然這兩個用例是非常有必要的。
            單元測試寫多少才夠?這個問題沒有確定的答案,但原則是讓你自己覺得安全。代碼覆蓋率高不能保證安全,真正的安全需要用測試用例覆蓋邏輯。

          posted on 2014-04-18 13:25 順其自然EVO 閱讀(186) 評論(0)  編輯  收藏 所屬分類: 測試學習專欄

          <2014年4月>
          303112345
          6789101112
          13141516171819
          20212223242526
          27282930123
          45678910

          導航

          統計

          常用鏈接

          留言簿(55)

          隨筆分類

          隨筆檔案

          文章分類

          文章檔案

          搜索

          最新評論

          閱讀排行榜

          評論排行榜

          主站蜘蛛池模板: 玛纳斯县| 哈密市| 新晃| 南通市| 沙湾县| 资中县| 定兴县| 南丹县| 元朗区| 武川县| 肇庆市| 五指山市| 措美县| 新疆| 黄山市| 安仁县| 临邑县| 万源市| 邻水| 开封县| 兴化市| 武川县| 延川县| 陆良县| 辉县市| 陈巴尔虎旗| 依兰县| 房产| 南乐县| 革吉县| 平度市| 丘北县| 新泰市| 苗栗县| 乐亭县| 山东省| 临澧县| 邛崃市| 宣化县| 顺平县| 岳普湖县|