2009年9月30日 星期三

單元測試(5): 可測試性

這個系列,請見
單元測試(1): 什麼是單元測試
單元測試(2): 單元測試的好與壞
單元測試(3): 單元測試的好與壞2
單元測試(4): 單元測試的好與壞3

由前面的文章可知,單元測試的好壞,相當依賴於原程式的可測試性。為了增加可測試性,我們可以增加/修改原程式的架構,以便進行測試。

舉例來說:
1  原為 private 的方法,為了可測試性,改成 internal
2  方法設成 virtual,以利 mock
3  原為 static 的方法,改成 instance 的方法,以利 mock

有人會質疑,這些步驟,是真的需要嗎?
最有名的例子,就是 asp.net 了。原來有個 System.Web 的 namespace,其下的類別雖然相當好用,但卻難以進行單元測試。到了asp.net MVC後,增加了 System.Web.Abstractions.dll 的類別庫,其下的類別都與 System.Web 的類別相似,甚至可以找到對應的類別。但 System.Web.Abstractions.dll 下的類別都進行了抽象,以利進行單元測試。

System.Web,可看到其下有相當多的類別是以 Base結尾,這些就是為了可測試性所增加的類別。例如HttpApplicationState不容易進行單元測試,就增加了 HttpApplicationStateBase 這個類別,利於進行單元測試。所以我們可以觀察到,兩者的成員(見 HttpApplicationState MembersHttpApplicationStateBase Members)幾乎是一樣的,只是 Base 的成員幾乎都是可被 override 。

2009年9月16日 星期三

單元測試(4): 單元測試的好與壞3

這個系列,請見
單元測試(1): 什麼是單元測試
單元測試(2): 單元測試的好與壞
單元測試(3): 單元測試的好與壞2

上次首次提到了「可測試性」的重要,這一次舉另外一個例子。
這段程式,是一個將一段xml寫到一個特定目錄的檔案。

 
using System;
using System.IO;
using System.Xml.Linq;

namespace ClassLibrary2
{
  public class Class1
  {
    public void WriteXmlToFile(DateTime date)
    {
      XElement el = new XElement("Order", new XElement("OrderDate", date));
      File.WriteAllText(@"c:\temp\result.xml", el.ToString());
    }
  }
}

相同的,如下的測試程式,要如何聲明(Assert)是否正確呢?

using System;
using ClassLibrary2;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace TestProject1
{
  [TestClass()]
  public class Class1Test
  {
    [TestMethod()]
    public void WriteXmlToFileTest()
    {
      var target = new Class1();
      target.WriteXmlToFile(new DateTime(2009, 1, 1));
    }
  }
} 

真的沒辦法嗎?我們也可以將輸出的結果檔案讀出來。並與預期的結果比對。當然,這樣是不好的。

BAD

using System;
using ClassLibrary2;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.IO;

namespace TestProject1
{
  [TestClass()]
  public class Class1Test
  {
    [TestMethod()]
    public void WriteXmlToFileTest()
    {
      var target = new Class1();
      target.WriteXmlToFile(new DateTime(2009, 1, 1));
      string result = File.ReadAllText(@"c:\temp\result.xml");
      Assert.AreEqual(@"
  2009-01-01T00:00:00
", result);
    }
  }
}

這樣有什麼不好呢?除了前次提到的與檔案系統相關外,又有什麼不好呢?目錄指定的路徑是客戶指定的,除可以改設定到 config 檔上,其餘沒有什麼好談的。

其實,這一段的確是可以測試的。但需要強調的事情是:可測試性不佳。
如果客戶要求的是將 xml 資料以 email 或 web service 方式傳出去呢?那要如何測試該段 xml 是正確的呢?難道要去讀對方 web service 是否收了了訊息,或 email server 是否收了 email?

我的解答如下:改變 method 的 signature

using System;
using System.IO;
using System.Xml.Linq;

namespace ClassLibrary2
{
  public class Class1
  {
    public string WriteXmlToFile(DateTime date)
    {
      XElement el = new XElement("Order", new XElement("OrderDate", date));
      string result = el.ToString();
      File.WriteAllText(@"c:\temp\result.xml", result);
      return result;
    }
  }
}
測試程式碼如下:
using System;
using ClassLibrary2;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.IO;

namespace TestProject1
{
  [TestClass()]
  public class Class1Test
  {
    [TestMethod()]
    public void WriteXmlToFileTest()
    {
      var target = new Class1();
      string result = target.WriteXmlToFile(new DateTime(2009, 1, 1));
      Assert.AreEqual(@"
  2009-01-01T00:00:00
", result);
    }
  }
}

只有一點小改變:該method 回傳 xml 的結果。
質疑聲:「什麼?為什麼要回傳 xml 呢?又不需要!只會浪費時間,效能變差」

這一點,真的不好回答。回到一個問題,什麼是需求?什麼樣的事情才是需求?
我們常將需求分成「功能需求」與「非功能需求」。功能需求是指客戶真的需要的功能。非功能需求又可再細分安全性、效能、可維護性、…等。

為了安全性,會加上一堆的 System.Security 的類別與物件。
為了效能,會使用多執行緒的方式加快程式執行速度、加上效能物件以量測正式機上的效能。
為了可維護性,更會以物件導向、再搞出Multi-layer, SOA, MVC 等架構。

那「可測試性」不也是一種「非功能需求」嗎?為了「可測試性」,當然也可以將某一 method 由 void 改成回傳 string 啊!

結論

為了可測試性,可以改變程式的 signature。

2009年9月11日 星期五

Run Code Metrics 出現錯誤

在使用 VSTS 的 run code metrics 功能時,發生了無法分析的錯誤。

An error occurred while calculating code metrics for target file ‘xxx.dll’  The following error was encountered while reading module 'Microsoft.SharePoint': Could not resolve type: T ObjectModel

這裡有個特殊符號,可不是我隨便打的。整個錯誤訊息 copy 下來就有的。

這該怎麼解呢?還好之前的 SandCastle 的安裝及使用 的經驗,知道這類的程式,一定程度上需要了解 dll 的關聯。我的例子,是我reference了 Sharpoint 的 元件 Microsoft.SharePoint.dll。在我的 project 中,是可以被 compile 並執行的。但是 Microsoft.SharePoint.dll 在執行時期,需要引用 Microsoft.SharePoint.Security.dll。
Microsoft.SharePoint.Security.dll 並不在我引用的範圍中。因此分析出錯了。

要解決這個 issue,只需要手動地再加入 reference Microsoft.SharePoint.Security.dll 即可。

2009年9月10日 星期四

HttpModule 中使用置換 Response.Filter 為何無效

最近,為了asp.net 的網站安全,被迫加強檢查使用者自行修改 Url 的問題。
例如 http://server/ap/Order/View/1 是目前使用者可以看的訂單1。但若使用者以此類推,修改 Url 為 http://server/ap/Order/View/2 就可以看到訂單2的資料,即使原來該使用者是不能看到的。

為了檢查這方面的問題,特別寫了 httpModule來防止自行修改。原理是加上 hash value. 也就是將 http://server/ap/Order/View/1 的值作 hash,然後加到 Url 上。如 http://server/ap/Order/View/1?h=887766AABB2

這樣應該很完美了。但是,有些網頁就是不能動。原來,我的方法是使用 httpModule 中,將 Respose.Filter 置換成自訂的 Filter。但這個方法在 Server.Tranfer(), Response.End(), Response.Redirect() 時是無效的。

參考文件,見 http://aspnetresources.com/articles/HttpFilters.aspx 

這是相當特殊的狀況,必須記錄下來,以免忘記了

2009年9月9日 星期三

單元測試(3): 單元測試的好與壞2

這個系列,請見
單元測試(1): 什麼是單元測試
單元測試(2): 單元測試的好與壞

另一種常見不好的單元測試,是測試與時間相關的程式。

舉例來說,有個 IsTimeMath() 的 method,要傳回當天的時間是否已過了當年的四分之三。程式大概如下:

 
  public class Class1
  {
    public bool IsMatch()
    {
      DateTime today = DateTime.Today;
      DateTime lastDayInYear = new DateTime(today.Year, 12, 31);
      return ((float)today.DayOfYear) / lastDayInYear.DayOfYear > 0.75f;
    }
  }
不好的測試程式如下
    /// 
    ///A test for IsMatch
    ///
    [TestMethod()]
    public void IsMatchTest()
    {
      Class1 target = new Class1();
      bool expected = false;
      bool actual = target.IsMatch();
      Assert.AreEqual(expected, actual);
    }

當在寫測試程式時,您會發現測試邏輯是想不出來的。原因呢?沒有任何參數需要輸入,卻需要聲明測試是否正確?很怪吧!
既然測試邏輯想不出來,代表原來的需求是錯的嗎?也不是,因為需求的確是「回傳當天的時間是否已過了當年的四分之三」。

那到底是怎麼樣?不是需求的問題,測試也寫不出來,到底該怎麼辦?
原來,追究原因,是 IsMatch 這個方法是難以測試的 (not testable),原因是與時間相關,而時間一直變動。
解決的方法,是讓 method 改成與時間無關,才是可測試的 method。改 IsMatch() 如下

public class Class1
  {
    public bool IsMatch()
    {
      return IsMatch(DateTime.Today);
    }

    public virtual bool IsMatch(DateTime today)
    {
      DateTime lastDayInYear = new DateTime(today.Year, 12, 31);
      return ((float)today.DayOfYear) / lastDayInYear.DayOfYear > 0.75f;
    }
  }

我做了什麼改變呢?我將程式改寫為兩個。第一個 IsMatch() 呼叫 第二個 IsMatch(DateTime today)。主要的邏輯放在第二個 IsMatch(DateTime today)
改變的動機,是
1. 第二個 IsMatch(DateTime today) 才具可測試性,才寫得出測試邏輯來
2. 第一個 IsMatch() 符合原需求。

這樣一來,IsMatch(DateTime currentDate) 才是可測試的,Unit test 才寫的出,而且也符合需求。

結論

我們所撰寫的程式,必須是符合「可測試性」,才有辦法寫出好的單元測試。

2009年9月7日 星期一

OpenXml Sdk 2.0

之前提到過 不建議在 asp.net 上使用 office Interop 的原因,那在伺服器端需要生成 Office 文件時,究竟用什麼方法較適當呢?我建議使用 xml 的方法。

Office 2003 有自己的 xml格式。到了 Office 2007 時代, Microsoft 提交了 Open Xml Format 。建立 xml 的資料是純文字的,任何平台都能建立文件。

但是,了解 xml 的格式是需要相當的時間,尤其這些 Word, Excel 等格式,充滿著許許多多的物件及相對應的 xml element。為了讓開發 Open Xml 更為方式,微軟推出了 OpenXml Sdk,目前推到了 2.0 , (仍為 CTP 版)。

我初步使用的心得:

  1. 還是在操作 xml:當初以為是像 Office automation 的物件方式在生成文件,其實這個想法是錯的。
  2. 已經相當成熟了:雖然是操作 xml,但配合多個 Tool,反而更能深入了解該 xml format 的用意。
  3. 是格式,而非應用程式:例如製作 doc的目錄 (Table of Content),這是一個 Word 的功能,因此只能由 Word 應用程式來生成。在 xml 中是沒有這個功能的。當然,我們可以寫一個類似的功能,直接生成文件目錄的 xml。但,蠻花時間的哦!

2009年9月5日 星期六

MVC (3): Controller

接收使用者的 Request ,經由 Url routing 後,第一關來到的是 Controller。
Controller 的責任可參考 MVC (1): Asp.NET MVC 概念說明

Controller 的責任是:處理資料 (Model) 並挑選一個 View 回應給 Client。由 Routing ,到 Controller 處理Model,最後到 View,一氣喝成。一個關節沒弄好,就會出錯。

舉例來說,預設的 routing table 如下:

routes.MapRoute( 
"Default",                                              // Route name 
"{controller}/{action}/{id}",                           // URL with parameters 
new { controller = "Home", action = "Index", id = "" }  // Parameter defaults 
); 
http://localhost/Home 則會找到 HomeController 的 Index Action。請參考下例
[HandleError]
    public class HomeController : Controller
    {
        //http://localhost:4041/Home/
        public ActionResult Index()
        {
            return View();
        }

        //http://localhost:4041/About/
        public ActionResult About()
        {
            return View();
        }

        //http://localhost:4041/About/3
        public ActionResult About(int id)
        {
            ViewData["id"] = id;
            return View();
        }
    }
如果執行 http://localhost:4041/Home的要求,但 HomeController 沒有 Index() 這個 Action的話,會出現什麼狀況呢?答案是 The resource cannot be found.

controller要如何「挑選」View 呢?答案是 View() 這個 Method
當我們使用 return View() 時,會使用同 Action 的 View。

舉例來說,
[HandleError]
    public class HomeController : Controller
    {
        //http://localhost:4041/Home/
        public ActionResult Index()
        {
            return View();
		//與 return View("Index") 相同
        }
    }
如果,硬要挑選其他名稱的 View,就要指定 View 的名稱。如下例

[HandleError]
    public class HomeController : Controller
    {
        //http://localhost:4041/Home/
        public ActionResult Index()
        {
            return View("List"); //雖然是 HomeController下的 Index Action,但指名挑選名為 List 的 View
        }
    }

2009年9月3日 星期四

The SessionStateTempDataProvider requires SessionState to be enabled

'/EPWeb' 應用程式中發生伺服器錯誤。
--------------------------------------------------------------------------------

The SessionStateTempDataProvider requires SessionState to be enabled.

面對這樣的錯誤時,實在不知如何下手。當然,Google 大神還是要招喚一下。

原來,asp.net mvc 是需要使用 session來作為暫存資料用。(oh! my god)
而我的應用程式的虛擬目錄,是建立在 moss 的網站下,會受到 moss 的 web.config 影響。

參考 http://www.flyvergrillen.dk/2009/03/26/being-trapped-in-iis/ 後,發現雖然我是使用 Windows 2003 ,但發現了我仍是缺少 SessionStateModule ,因此,加上 SessionStateModule,並啟用 SessionState 即可。

<httpModules>
      <add name="ScriptModule" type="System.Web.Handlers.ScriptModule, System.Web.Extensions, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
      <add name="UrlRoutingModule" type="System.Web.Routing.UrlRoutingModule, System.Web.Routing, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35"/>
      <add name="SessionStateModule" type="System.Web.SessionState.SessionStateModule" />
    </httpModules>
    <sessionState mode="InProc" />

MVC (2): Url Routing 的測試

以往,在 Web form 甚至在 asp 的久遠年代,網頁執行的 url 是與作業系統相關的。
舉例來說,http://localhost/app/Bulletin/Add.aspx 這個 url ,我們大概可以猜 add.aspx 是位於 web server 的 c:\wwwroot\inetpub\app\Bulletin\Add.aspx 下。雖然IIS 的網站主目錄會變動,但 Bulletin\Add.aspx 的目錄結構幾乎是定律了。

但,誰說這是一成不變的?在 asp.net 上,也有 HttpContext.Current.RewritePath 方法可以改變執行的網頁。因此上述的定律開始被打破。

到了 MVC 的世界,Url 與執行的網頁是不對應的。Request 必須經由解析後,找到對應的 Controller來處理。由 Request 找到對應 Controller 的過程,就是 url routing。

Url routing 如何配置呢?目前 asp.net mvc 1.0 的做法是在 global.asax.cs 上以 coding 的方法處理。這一點相當不彈性,應該可以在 config file 上設定即可。目前我尚未研究是否有 config file 的方法。

程式如下:

public static void RegisterRoutes(RouteCollection routes)
    {
      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
      routes.MapRoute(
          "Default",                                              // Route name
          "{controller}/{action}/{id}",                           // URL with parameters
          new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
      );
    }

以上是預設的,也就是 asp.net mcv project template 一開始就建好的範本。

我們也可以寫兩個以上的 routing rule。當 request 進入時,就會由上而下開始對應,直到第一個配對成功。

public static void RegisterRoutes(RouteCollection routes)
    {
      routes.IgnoreRoute("{resource}.axd/{*pathInfo}");
      routes.MapRoute(
    "R1",                                              // Route name
    "Order/{action}/{date}",                           // URL with parameters
    new { controller = "Order", action = "Index", date = "" }  // Parameter defaults
        );

      routes.MapRoute(
          "Default",                                              // Route name
          "{controller}/{action}/{id}",                           // URL with parameters
          new { controller = "Home", action = "Index", id = "" }  // Parameter defaults
      );
    }

此時,http://localhost/app/Order 會match 到 R1 的規則,而其他如 http://localhost/app 的則會被 Default 的規則所補獲。

以上是人工的預測。但這個 routing table 是有些複雜的。到底 match 到哪一個 route卻是相當重要的。有沒有一個工具可以幫我們測試 routing 呢?有的。請參考 http://haacked.com/archive/2008/03/13/url-routing-debugger.aspx  的說明

image

Share with Facebook