顯示具有 Test 標籤的文章。 顯示所有文章
顯示具有 Test 標籤的文章。 顯示所有文章

2012年6月15日 星期五

Visual Studio 2012 Test Platform

在 VS 2010 中寫單元測試,是一件簡單的事,但是內建的 ms-test 的效能卻是不好。而其他的 test framework 如 xUnit, MbUnit, NUnit 等,又難與 Team Foundation Server 整合。於是,只能一直使用 ms-test。

在 VS 2012 中,Test Framework 不再是單元測試中第一個要選擇的對象了。因為它有了新的概念:Unit Test Platform。

Unit Test Platform

簡單來說,我們現在可以在 VS12 中作許多不同Test Framework 的單元測試平台了。

image

安裝 xUnit Test Runner

在 Extensions and Updates 中,找到 xUnit Test Runner 並安裝。

image

image

接下來,建立一個平常的 Class Library, 並用 NuGet 安裝 xNnit ,以及引用 ms test framework. 完成如下圖的  References

image

再來,就寫測試吧。

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Linq;
   4: using System.Text;
   5: using System.Threading;
   6: using Microsoft.VisualStudio.TestTools.UnitTesting;
   7: using Xunit;
   8:  
   9: namespace ClassLibraryTest
  10: {
  11:     [TestClass]
  12:     public class Class1
  13:     {
  14:         [Fact]
  15:         public async void Test()
  16:         {
  17:             var sut = new AsyncClass();
  18:             await sut.DoAsyncWork();
  19:             Xunit.Assert.Equal("Hello", sut.Property);
  20:         }
  21:  
  22:         [TestMethod]
  23:         public void Test2()
  24:         {
  25:             Microsoft.VisualStudio.TestTools.UnitTesting.Assert.IsTrue(true);
  26:         }
  27:     }
  28: }

執行測試

在左方的 Test Explorer,就可以看兩種不同的 Test Framework 的測試都可以被找到,且測試完畢。

image

結論

VS 2012 中的測試,看來又大幅提升了微軟在測試界的地位了。

2012年5月23日 星期三

Visual Studio 2010 中,MSTest 執行測試時,AppDomain.CurrentDomain.BaseDirectory 的值會隨測試設定不同而改變

今天在寫單元測試時,發現了長久來覺得怪界的現象。這次一定要解開這個謎團

問題:AppDomain.CurrentDomain.BaseDirectory 的值會不同

步驟1:

建立一個測試專案,並在測試中加上

Console.WriteLine(AppDomain.CurrentDomain.BaseDirectory);
結果是
image
當程式需要讀取同目錄的檔案,例如 xml 檔案時,這是合理的。但在 MSTest 中,檔案會複製到 TestResults\xxxxx\Out 目錄下,(xxxxx 是與時間相關的目錄名稱)
步驟2:
將測試設定的選項Deployment 勾選
image
image
再執行測試一次。結果與步驟1相同。

步驟3:

關掉 Visual Studio 2010後再執行 Visual Studio 2010,重新執行該測試。竟然結果不一樣了!!

image

結論

雖然想寫單元測試,但這個結果未免也太不單元了。與環境相關,算不算是整合測試呢?

2011年7月26日 星期二

While trying to generate your tests, the following errors occurred

當我使用 Visual Studio 2010 建立一個 Unit Test 時,發生了下面的錯誤

image

While trying to generate your tests, the following errors occurred:
Value cannot be null.
Parameter name: key

找了一下,原因很簡單。我的 TestProject 已經有相同名稱的檔名了,但在不同的目錄。VS 2010 發現有相同的 class (Full name),就會發生這樣的錯誤。

解決方法呢?就是改檔名或改 namespace 即可。

2010年11月23日 星期二

Visual Studio 2010 做壓力測試 (load test),出現錯誤訊息

使用 Visual Studio 2010 做壓力測試 (load test)好一陣子。有一天做完後,,突然發生了錯誤訊息

Error occured running test. The loadTest completed but an error occurred while collecting the final results. Could not access the load test results repository: Incorrect syntax near 's'.  The label 'http' has already been declared. Label names must be unique within a uqery batch or stored procdure

檢視報表切到 Detail時顯示了下面的訊息

No detail exists at the selected point

使用 「Open and manage results」也找不到剛剛執行的測試結果。

原本想說將 load test 所使用的資料庫清空,重新執行loadtestresultsrepository.sql 就可以解決了。這招也不成功。

看來這是 Visual Studio 的 bug 了。

接著,我想了一下,之前為什麼可以?我修改了什麼後變成這樣了。於是我找到了原因。

我的 load test 中其中一個Web Performance Test 有問題。將該 Web Performance Test 移除,或者在 Test Matrix 中設該test 的比例為0,就可以正常儲存測試結果到資料庫。

為什麼這個 Web Performance Test 會造成 load test 的問題呢?這看起來是 Visual Studio 的 bug

2010年10月19日 星期二

單元測試(7): 使用 Moles 增加可測試性

在多次的文章中討論到可測試性。為了增加 method 的可測試性,往往需要更改 method 的參數,讓不具可測試性的 method,抽離出可測試的部份。

問題

靜態函式往往是最不具測試性的原兇。例如 DateTime.Now, File.Exists 等。為了這些障礙,我們必須一次又一次的拆解我們原本良好的 method 嗎?

舉例

public class MonthHelper
{
    /// <summary>
    /// 今天是否為單數月
    /// </summary>
    /// <returns></returns>
    public bool IsOddNumberMonth()
    {
        return DateTime.Today.Month%2 == 1;
    }
}

DateTime.Today 會隨著電腦時間而變,IsOddNumberMonth 也會因執行時間而回傳不同的結果,因此該 method 不具可測試性。為了可測試性,之前教大家要將此程式拆成兩部份。如下

public class MonthHelper
{
    /// <summary>
    /// 今天是否為單數月
    /// </summary>
    /// <returns></returns>
    public bool IsOddNumberMonth()
    {
        return IsOddNumberMonth(DateTime.Today);
    }

    /// <summary>
    /// 是否為單數月
    /// </summary>
    /// <returns></returns>
    public bool IsOddNumberMonth(DateTime dateTime)
    {
        return dateTime.Month % 2 == 1;
    }
}

這樣,我們就可以對 IsOddNumberMonth(DateTime) 進行測試。而原本的IsOddNumberMonth() 就只好放棄了。

問題在於,如此簡單的 method,卻必須為了可測試性拆成兩個 method,不太合理。萬一有個 method 裡有一堆不可測試的 method,如 DateTime.Now, File.Exists, File.ReadAllLines 等,我們需要拆解多少次才具可測試性?這樣的程式具可讀性嗎?

Moles

Moq 是非常棒的 Isolation framework,但只能對 interface 或 virtual method 進行 mock,對於靜態函式仍莫可奈何。

對此問題,微軟的 Microsoft Research 提出Moles 這一個 Isolation framework。其原理在於對整個 .NET Framework 的 mock,而非單一類別/方法的 mock。

以上例來說,我們可以進行下來步驟進行單元測試撰寫。(記得先下載及安裝 Moles)

1. 建立測試專案

在該 method 上按右鍵,執行「Create Unit Tests」

image

2. 加入 Moles 項目

在剛剛建立的測試專案上增加一個項目,選到 「Moles and Stubs for Testing」。此時項目的名稱必須與要模擬的 assembly 名稱一致。由於 DateTime 這個 type 的assembly 是 mscorlib 中,(見 DateTime 結構),所以名稱必須取作 mscorlib.moles。按 Add鍵到下一步驟。

SNAGHTML30c6ff

3. 建立測試

在測試程式的最上頭,增加 [assembly: MoledType(typeof(System.DateTime))]。這一行說明 Moles 的 runtime 要對 System.DateTime 進行模擬。
image

修改測試如下。[HostType(“Moles”)] 告訴測試環境是跑在 Moles 的環境下。而 MDateTime 則是對 DateTime 的模擬。Moles 對原來的 dotnet framework 的模擬,都使用固定的命名方式。MDateTime 就是對 DateTime 的 mock type. 屬性的 mock也區分 Get 與 Set。MDateTime.TodayGet 是對 DateTime.Today getter 的 mock。

image

4. 執行測試

以下是完整的測試內容。當然,測試結果都是成功的

using System.Moles;
using ConsoleApplication1;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using Microsoft.Moles.Framework;
[assembly: MoledType(typeof(System.DateTime))]

namespace TestProject1
{
    [TestClass()]
    public class MonthHelperTest
    {
        [TestMethod()]
        [HostType("Moles")]
        public void IsOddNumberMonth_Even_ReturnFalse()
        {
            MDateTime.TodayGet = () => new DateTime(2010, 12, 31);
            var target = new MonthHelper();
            bool actual = target.IsOddNumberMonth();
            Assert.AreEqual(false, actual);
        }

        [TestMethod()]
        [HostType("Moles")]
        public void IsOddNumberMonth_Odd_ReturnTrue()
        {
            MDateTime.TodayGet = () => new DateTime(2010, 11, 30);
            var target = new MonthHelper();
            bool actual = target.IsOddNumberMonth();
            Assert.AreEqual(true, actual);
        }
    }
}

結論

怎樣,相當棒吧。Moq 是我最常用的 isolation framework,執行起來會快很多。而當需要模擬 dotnet framework 的靜態方法時,我就會用 Moles。

2010年7月12日 星期一

Performance, Load, Stress Testing

之前我一直把這三者弄混了。這裡做個筆記。

Performance Testing

測試的目的在收集「系統在穩定狀況時的行為」,如Response Time (回應時間),資源的消秏,輸出量等。另一個目的也在於了解「何為系統正常」。

Performance Testing 時,可以只有一個 Process 進行。

Ex:

  1. 同樣的搜尋,在兩台伺服器何者搜尋較快。
  2. 正常情形下,該伺服器平均速度為 200 request per second(RPS)。

Load Testing

主要目的在了解「在正常及尖峰的狀況下系統的行為」,包括可靠度及效能。常常會以遞增的load size (number of concurrent users)來了解這樣的行為。

Load Testing 時,通常有多個 Process 進行。

Ex:

  1. 反應時間在 1 秒內時,可同時搜尋的查詢數。
  2. 該應用程式在該伺服器能承載的最大concurrent users 數為200人。

Stress Testing

對要目的在了解「極端的狀況下系統是如何 Crash 掉的」,進而知道在極端狀況發生時應如何處理。

故系統一定要測到掛了,並了解系統的瓶頸,如 CPU滿載,RAM 不足,硬碟效能不足,大量資料等。

Stress Testing 時,一定會有多個 Process 同時進行測試。

Ex:

  1. 目前這個系統,未來會先面臨 CPU 不足,還是 RAM 不足。發生時應如何補足資源?
  2. 系統在3000 萬筆的訂單,5億筆訂單歷史時,系統還能承受100個線上使用者,且平均網頁還能在4秒內呈現。

2010年7月7日 星期三

Data Driven Test

寫了一個類別

public class Class1
{
    public int Add(int a, int b)
    {
      return a + b;
    }
}
作了一個單元測試 (Unit Test)
[TestMethod]
public void TestMethod1()
{
  var o = new Class1();
  int a = 1;
  int b = 2;
  int c = o.Add(a, b);
  Assert.AreEqual<int>(3, c);
}

當然,這個測試通過了。但可惜的是,只能測一個 1 + 2 = 3 的測試案例。如果要測數個案例,要怎麼做呢?

此時,需要一些步驟。

步驟1:增加測試資料

增加一個文字檔 Data.csv, 並加入 TestProject, 內容如下

Val1, Val2, Result
1,2,3
4,6,10
5,7,12
10,-10,0
儲存時,使用 Chinese Trandional (Big5) 的編碼存檔
image
image

步驟2:設定部署

執行 Test/Edit Test Settings/Local(local.testsettings)

image

選取 Test Settings/Deployment 中的 Enable deploymnet 選項。此選項在 Visual Studio 2008 中預設為選取的,但在 Visual Studio 2010 中預設為不選取。

image

接下來,要在 Test Method 中加上 Deployment 及DataSource屬性,指示進行測試時,需要部署該 Data.csv 檔案。如下

[TestMethod, DataSource("Microsoft.VisualStudio.TestTools.DataSource.CSV", "data.csv", "data#csv", DataAccessMethod.Sequential)]
[DeploymentItem("Data.csv")]
public void TestMethod1()

接著,在 Solution Explorer 中選取 Data.csv,按 F4 打開 Properties Windows,將 Copy to Output Directory 設成 Copy always

image

步驟3:修改測試程式

這個動作將執行測試時,輸入 Class1.Add 的參數改由測試資料來讀取

[TestMethod, DataSource("Microsoft.VisualStudio.TestTools.DataSource.CSV", "data.csv", "data#csv", DataAccessMethod.Sequential)]
[DeploymentItem("Data.csv")]
public void TestMethod1()
{
  var o = new Class1();
  int a = Convert.ToInt32(TestContext.DataRow["Val1"]);
  int b = Convert.ToInt32(TestContext.DataRow["Val2"]);
  int c = o.Add(a, b);
  int checkValue = Convert.ToInt32(TestContext.DataRow["Result"]);
  Assert.AreEqual<int>(checkValue, c);
}

結果

測試結果如下。注意到雖然我們只寫了一個 TestMethod,但資料有4組,使用 Debug 模式追蹤,也的確跑了4次。但為什麼會顯示5個測試呢?我也不清楚。

image

image

 

Sample code download

2010年7月5日 星期一

Coded UI Test 無法以 64 bits 方式執行

在測試 Coded UI Test 的執行過程中,出現了下面會錯誤

Error calling Initialization method for test class MyClass.Tests.CodedUIPermissionsTest: Microsoft.VisualStudio.TestTools.UITest.Extension.UITestException: The Coded UITest cannot be run as a 64 bit process. You must change your test settings in the Hosts tab to run your test in a 32 bit process.

原因

Couded UI Test 無法以 64 bits 方式執行。

解法

在測試設定中,改用 32 bits 方式。如下圖。

image

並且,需要重新啟動 Visual Studio 2010。(我也不知道為什麼,但這樣做重開才會動)

2010年6月30日 星期三

執行Coded UI Test 出現例外:The playback failed to find the control

過程

在本機執行 Coded UI Test 好好的,改到 Test Agent 上執行,卻發生測試的例外:

Microsoft.VisualStudio.TestTools.UITest.Extension.UITestControlNotFoundException: The playback failed to find the control with the given search properties. Additional Details: 
TechnologyName:  'MSAA'
ControlType:  'Button'
Name:  'Close'
 ---> System.Runtime.InteropServices.COMException: Error HRESULT E_FAIL has been returned from a call to a COM component..

原因

錄製測試時,是使用本機 (Windows 7 En + IE 8) 來錄製的。該測試最終時會關閉瀏覽器。

Test Agent 上則是 (Windows 2003 + IE6)。故執行測試時, IE 6 上並沒有 “Close” 的 Control。因為不同文化的關係,中文 IE 上應該是「關閉」而非「Close」。

解法

目前我並不清楚碰到不同語言版本的瀏覽器時,Coded UI Test應該如何處理,但微軟的 Visual Studio Test tools 有關閉瀏覽器的 API 可直接呼叫,就省去以名稱來找 Control 的困擾

var browser = BrowserWindow.Launch(new System.Uri(this.LauchBrowserParams.Url));
browser.Close();

當然,需要手動修改程式。

2010年6月29日 星期二

Asp.Net 4.0 Xml 轉換錯誤

於執行 TFS build  的結果,找到了下面的錯誤

Error    1    The "TransformXml" task failed unexpectedly.
System.UriFormatException: Invalid URI: The URI is empty.
   at System.Uri.CreateThis(String uri, Boolean dontEscape, UriKind uriKind)
   at System.Uri..ctor(String uriString)
   at Microsoft.Web.Publishing.Tasks.TransformXml.Execute()
   at Microsoft.Build.BackEnd.TaskExecutionHost.Microsoft.Build.BackEnd.ITaskExecutionHost.Execute()
   at Microsoft.Build.BackEnd.TaskBuilder.ExecuteInstantiatedTask(ITaskExecutionHost taskExecutionHost, TaskLoggingContext taskLoggingContext, TaskHost taskHost, ItemBucket bucket, TaskExecutionMode howToExecuteTask, Boolean& taskResult)        0    0  

為什麼呢?完全看不懂。

原來,簡單地說是轉換失敗了。我的 Web.config 中,屬性的值裡的大於 > 這個字元,但這是不合法的。Visaul Studio 並不會抱怨。

例如:

<?xml version="1.0"?>
<configuration>
<appSettings>
  <add key="myKey" value="cdcdcdcdjkcjd>" /> <!-- 值裡有大於字元  -->
</appSettings>
</configuration>

而使用 MSDepoly 工具時,會使用 Xml Transform 來轉換 web.config,就會發生錯誤了。

只是,為什麼錯誤訊息是 Invalid URI 呢?

TFS 建置:Coded UI 自動測試

Coded UI test 是 Visual Studio 2010 的一大賣點,可模擬使用者操作程式。而現在我們已經將 Build Server 建置完畢,當然希望也能進行自動測試。單元測試等原本就在 TFS 2008 上即可執行,而 Coded UI test 卻因模擬使用者登入而需要更多的安裝與設定。

安裝

除了 Team Foundation Server 2010 的安裝外,也需要安裝 Build Agent。在建置的過程中,會進行 Source Control 的取得最新版本,compilatin,以及 Test。在Test 的過程,如果有 Coded UI Test,則必須安裝 Test Agent Controller 及 Test Agent。

設定

Build Server

在Build Server 上,我們必須讓 Build Agent 跑在互動的模式。執行 Team Foundation Administration Console, 點擊「Build Configuration」, 找到Build Service 的 Property。

image

先按「Stop to make change」後,開使調整設定。必須選擇「Interative Process」, 並指定適合的 Credentialsi , 按Start鍵。

image

按Start 鍵後,會跑出下面的視窗。

image

Test Controller

Test Controller 用來控制及收集 Test Agent 執行測試的結果。

執行 Microsfot Visual Studio Test Controller 2010 Configuration,出現下面的視窗。一樣地,使用相同的帳號,並按「Apply Settings」。

image

Test Agent

Test Agent 則是安裝在測試機上,用來模擬使用者在電腦上操作。通常這些測試機都會使用虛擬器,以模擬不同的使用者環境,如 Windows 7 + IE8, Windows Vista + IE7,Mac + Safari。

執行 Microsfot Visual Studio Test Controller 2010 Configuration,出現下面的視窗。一樣地,使用相同的帳號,並按「Apply Settings」。

image

需要注意的是,由於這些安裝 Test Agent 的機器需要「模擬」使用者操作 UI,故不能鎖住電腦(螢幕保護程式),本機一開機後也會直接登入。

自動部署

為了讓後續的網站測試能順利進行,我們必須讓 daily build 時自動在 IIS 上建立應用程式。
在 Build Definition 上,加入 MSBuild 的參數

/p:DeployOnBuild=True
/p:DeployTarget=MsDeployPublish
/p:MSDeployPublishMethod=InProc
/p:CreatePackageOnPublish=True
/p:DeployIisAppPath="預設的網站/EInvoice2" 
/p:MsDeployServiceUrl=localhost

image

修改測試

我測試的是一個 Web Form 的應用程式,在專案中加入一個測試專案。在測試專案中再加入一個 Code UI Test。完成錄製後,簽入到TFS後執行。哇!發生錯誤!

Test method xxx.Tests.CodedUIPermissionsTest.AddDeletePageTest threw exception:
Microsoft.VisualStudio.TestTools.UITest.Extension.PlaybackFailureException: Cannot perform 'SetProperty of Password with value "zPjUJLMG7JyDvQw78l9YYBtInZnoHu7P"' on the control. Additional Details:
TechnologyName:  'Web'
ControlType:  'Edit'
Id:  '_txtPassword'
Name:  '_txtPassword'
TagName:  'INPUT'
---> System.Runtime.InteropServices.COMException: Error HRESULT E_FAIL has been returned from a call to a COM component.

怎麼辦呢?原來當初錄製的帳號與執行測試的帳號是不同的。而為了安全,Coded UI 測試時遇到密碼欄位資料會使用原錄製帳號來加密,執行帳號當然不能解密了。

我這時找 Google 大神,也找不到解法。難道這些內容太新了嗎?看了一下測試專案中UIMap.uitest的內容,哈哈!xml 果然容易了解。將有問題的網頁,將 Encoded=”true” 改成 false, 並將內容改成明碼的密碼後,就解決了這次的問題。

<SetValueAction UIObjectName="UIMap.UIGoogleWindowsInterneWindow.UIDocument.UI_txtPasswordEdit">
  <ParameterName />
  <Value Encoded="true">vDeufj/Hr41sx0RsGlhIlhfWB2QAYMfo</Value> <!-- Encoded :是否加密-->
  <Type>String</Type>
</SetValueAction>

 

 

參考

2010年4月9日 星期五

單元測試(6):可測試性2

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

需求

今天在 Review 同事的程式時,發現了不可告人的秘密。程式片段如下。

   1: using System;
   2: using System.IO;
   3:  
   4: namespace ConsoleApplication1
   5: {
   6:   class Class1
   7:   {
   8:     public void Do()
   9:     {
  10:       string[] lines = File.ReadAllLines(@"c:\temp\myfile.txt");
  11:       foreach (var line in lines)
  12:       {
  13:         try
  14:         {
  15:           ParseLineAndSaveToDb(line);
  16:         }
  17:         catch (Exception e)
  18:         {
  19:           SendMail(e.Message);
  20:         }
  21:       }
  22:     }
  23:   }
  24: }

用意大概是:讀取客戶上傳的檔案,解析每一行資料後,儲存到資料庫內。當一行資料有錯誤,仍繼續執行匯入。錯誤的訊息以寄信的方式寄給負責人處理。

問題

這段程式看來完美,也合乎客戶/長官的需求。但詭異的是,當我建立Do() 的單元測試後,該單元測試從未發生例外。
天啊,因此這段程式執行起來永遠不會出錯。永不會出錯的程式一定是好程式??

當然紙包不住火的,當程式執行結果不如預期時,就會回過頭來找 bug。這樣一來就很難找了。單元測試建立起來後無法進行自動測試,因為程式沒有出錯的機會,單元測試 3A pattern 中的 Assert 沒有發揮空間。

為了讓單元測試有機會進行實質的 Assert (斷言),我們必須對程式進行修改,而且必須符合客戶需求。修改後的程式如下

   1: using System;
   2: using System.IO;
   3: using System.Collections.Generic;
   4:  
   5: namespace ConsoleApplication1
   6: {
   7:   class Class1
   8:   {
   9:     public List<string> Do()
  10:     {
  11:       string[] lines = File.ReadAllLines(@"c:\temp\myfile.txt");
  12:       List<string> errors = new List<string>();
  13:       foreach (var line in lines)
  14:       {
  15:         try
  16:         {
  17:           ParseLineAndSaveToDb(line);
  18:         }
  19:         catch (Exception e)
  20:         {
  21:           SendMail(e.Message);
  22:           errors.Add(e.Message);
  23:         }
  24:       }
  25:       return errors;
  26:     }
  27:   }
  28: }

注意到Do() 加上了回傳值 List<string>,用來回傳發生的錯誤訊息。單元測試時,就可根據回傳錯誤長度來判斷是否發生過錯誤。

   1: [TestMethod()]
   2: public void DoTest()
   3: {
   4:   Class1 target = new Class1();
   5:   List<string> errors = target.Do();
   6:   Assert.AreEqual(0, errors.Count);
   7: }

結論

當然解法不一定要回傳 List<string> ,回傳一個錯誤發生次數的整數值也可以。重點是「程式的可測試性」。我們寫出的程式必須具備可被測試的特性,來確保程式的品質。

客戶需求是「錯誤發生時,必須仍繼續匯入」,原始的程式已經滿足了客戶需求。但程式的「可測試性」這個需求尚未滿足啊!因此我們再因應「可測試性」的需求再修改程式。

大略地整理需求的分類

  1. 客戶需求:來自客戶原始的商業需求。這一點無庸置疑地一定要滿足。
  2. 品質需求:例如安全性、效能、可管理性等。這一點也廣泛地被接受,但客戶未必關心。客戶未必關心的事,當然有些時候就偷懶啦。
  3. 測試需求:這裡就是我們強調的事情。程式需要具備「可被測試」的特性,尤其是可被「自動化測試」,以利回歸測試的進行。

2010年4月7日 星期三

Unit Test + Code Coverage + Strong Name = test failed

在執行測試時,發生了如下的錯誤訊息,而測試並未被執行

Strong name verification failed for the instrumented assembly 'Microsoft.Practices.Unity, Version=1.2.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35'. Please ensure that the right key file for re-signing after instrumentation is specified in the test settings.

原因

為了取得 Code Coverage的資訊,執行測試時,事實上是在受測的 assembly 增加了一些程式,以方便取得資訊。

然而,如果 assembly 本身就有 strong name的話,被增加了一些程式,當然會被認為不安全了。因此這個機制就無效,也就無法取得 Code coverage 的資訊了。

解法

可見 Instrumenting and Re-Signing Assemblies。我使用的是比較簡單的Disable Signing Verification方法,也就是不檢查某些 assembly 的簽章啦。

解決我的問題,就是在 cmd.exe 下 sn –Vr Microsoft.Practices.Unity.dll 就解決了。

這樣,只會影響執行測試的機器無法檢查指定的 assembly 了。

進階解法

雖然使用上述的方法可以解決該問題,但在該台機器就不檢查這些指定的 assembly?好像也不太對。最好能在測試前執行 sn –Vr assembly.dll,測試後再回復設定。

因此,就可以使用 sn –Vu assembly.dll 語法了。該語法還原 assembly.dll 成必須檢查 strong name。另外,在執行測試的設定(通常是 LocalTestRun.testrunconfig)上,設定 setup and cleanup scripts,執行這些指令就可以了。

如下圖。setup.cmd 內放 sn –Vr assembly.dll , cleanup.cmd 放sn –Vu assembly.dll

image

2010年1月12日 星期二

Visual Studio 執行 Web Test Record 時,在 IE8 下會出現無法錄製網頁的問題

困擾了一個下午。
同事使用Visual Studio 2008,在錄製 Web Test 時(Windows 7 + IE 8),發現某一些網頁無法被錄製下來。

這個網頁是使用簡易的 window.open 來開啟的。已知 window.showModalDialog 很可能無法錄製的 case 並不是這次的原因。

查了許久,實在找不到原因,而且我的電腦(Win7 + IE8)也有相同的問題。就懷疑是作業系統或 IE8 的問題。因此開了一台 Windows Server 2003 (IE6) 來看,是可以錄製該「消失」的網頁。

原來, IE8為了讓瀏覽器執行「看起來」更穩定,採用了多Process 的運作。也就是不同的網頁,很可能執行在不同的 Process 下。這與 IE7 以前的運作模式大為不同。IE7 的某一個網頁掛了(例如受攻擊),就會連帶的使其他執行在同一 Process 的網頁一起掛掉,而且連救回來的時間也沒有。而IE8 一開始來就使用了2個 processes,其中一個就負責「即時救援」的任務,一旦網頁發現掛掉,立刻救援。使用者只覺得網頁一下子就掛掉,但馬上又被還原了,感覺好多了。

IE 8 這樣的新模式,固然對瀏覽器使用者非常友善,但對於 VS2008 這個在2007年底發佈的開發工具是意想不到的行為。Web Test Recorder 9.0 只會針對同一個 process 進行錄製,因此 window.open 造成新的 process ,是無法錄製的。

解法

這個問題,是要讓新的 tab 開啟時,不要新增一個 process。方式是在使用 regedit。如下步驟

  1. 執行 RegEdit
  2. 瀏覽到 HKEY_CURRENT_USER\SOFTWARE\Microsoft\ Internet Explorer\Main
  3. 新增一個 dword,命名為TabProcGrowth,並設其值為 0
  4. 關閉所有 IE 視窗。下一次執行時就可以讀取到新的設定了。
  5. Visual Studio 2008 執行 Web Test 開始執行錄製。

完工。

     

2010年1月5日 星期二

WebTest 的Form Parameter 與日期有關

以Visual Studio 2008 進行網頁測試(WebTest)錄製後,通常不會有問題。但過一段時間後,再回來執行 WebTest,通常會發生一些錯誤。其中一部份與時間有關。以下是一個例子。

國外旅遊查詢網頁,出發日期只能選最近的10天來查詢。

image

錄製 WebTest後,可看到送出的日期為 2010/01/06 日。

image

過了幾天後,到了 2010/01/07 時,再來跑這個 WebTest,就會發生錯誤。

Invalid postback or callback argument.  Event validation is enabled using <pages enableEventValidation="true"/> in configuration or <%@ Page EnableEventValidation="true" %> in a page.  For security purposes, this feature verifies that arguments to postback or callback events originate from the server control that originally rendered them.  If the data is valid and expected, use the ClientScriptManager.RegisterForEventValidation method in order to register the postback or callback data for validation

原因是到了2010/01/07 時,dropdownlist 只會產生 01/07 ~ 01/16 這10天的資料,並不會有 2010/01/06 的選項。預設,asp.net 2.0 會檢查這個問題,稱為 event validation。

解決方法呢?有兩種,一種是產生 Coded Test, 可使用工具列中的 Generate Code 來產生程式碼,再到該程式碼中,改成 DateTime.Today.ToString(“yyyy/MM/dd”),這樣一來,就解決了問題。

image

image

另外一種較佳的解決方法,則必須安裝 codeplexWeb Test PlugIn 1.1。使用方法,可見說明。於是,只需要修改 Form Parameter 為 <%=DateTime.Today.ToString(“yyyy/MM/dd”)%>即可。

image

結論

兩種方法,當然以第二種較為方便,而且有彈性。第一種方法,在轉成 Code 之後,失去了Web Test 的UI,無法有直覺性地了解測試結果,但也因為轉成code 之後,任何的測試目的都可以達成。

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月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 才寫的出,而且也符合需求。

結論

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

Share with Facebook