November 23, 2014

使用Unity實作AOP Logging

目前專案某個函式庫裡有支base class使用RestSharp來發送HTTP request,並使用Common.Logging掛上log4net來實作log機制。按照現行設計,我們是透過constructor injection的方式把RestSharp的IRestClient介面inject到base class。如
protected ApiProxyBase(IRestClient restClient)
{
    Client = restClient;
}

protected IRestClient Client { get; private set; }

protected ILog Log = LogManager.GetLogger("Request");
Common.Logging則是直接在base class使用,並未採用constructor injection,主要因為這麼做不會對unit test造成太大影響,目前團隊也未對log機制來做unit test。log的內容則是hardcode在這支base class裡,所以當log的內容需要修改時,這支base class就得修改重新發佈新版本。log的部份主要是在發送HTTP request和接收到HTTP response時使用,如
protected IRestResponse<T> GetRestResponse<T>(IRestRequest restRequest)
{
    var sb = new StringBuilder();
   
    try
    {
        AppendRequestLog(restRequest, sb);
        IRestResponse<T> response = Client.Execute<T>(restRequest);
        AppendResponseLog(response, sb);
        Log.Info(sb);
        return response;
    }
    catch (Exception ex)
    {
        Log.Error(sb, ex);
        throw new BaseClassException("Failed to get information.", ex);
    }
}
現行的log設計有幾個主要問題
  1. log內容若修改,base class就得修改重新發佈新版本函式庫
  2. log機制侷限於使用Common.Logging,函式庫打包時得多加入Common.Logging
  3. 函式庫使用者相依於Common.Logging,且需使用特定Common.Logging adapter,如log4net/NLog,或是自行實作出一個adapter
  4. log內容無法由函式庫使用者來自訂擴充或修改
要讓函式庫使用者能自訂log內容,我們可以用delegate藉由屬性或方法傳入base class,但如此一來base class就得新增屬性或方法參數,只為了傳遞log用的delegate,有點違反SRP的味道在。如果能讓函式庫使用者自訂log內容,並且把Common.Logging從函式庫抽掉,這樣的設計豈不乾淨且具彈性許多。

文章開始所提到的IRestClient介面目前是透過IoC container經由constructor injection把實體類別帶入base class裡。目前團隊使用的IoC container為Unity,如果Unity提供AOP的機制讓我們在具現化RestClient類別時,把log邏輯塞到Execute<T>方法(signature為public virtual IRestResponse Execute(IRestRequest request) where T : new();)裡,一切就搞定了。

目前參考base class函式庫的專案使用的Unity nuget版本為3.0.1304,可由https://www.nuget.org/packages/Unity/3.0.1304安裝。Unity本身預設沒有支援AOP的機制,需額外安裝Unity Interception Extension。安裝成功的話,專案會多出兩個參考,Microsoft.Practices.Unity.InterceptionMicrosoft.Practices.Unity.Interception.Configuration

接下來在宣告container時把這個extension加進來
var container = new UnityContainer();
container.AddNewExtension<Interception>();

並建立一類別實作IInterceptionBehavior介面,如
internal class LoggingInterceptionBehavior : IInterceptionBehavior
{
    protected ILog Log = LogManager.GetLogger("Base");

    public IMethodReturn Invoke(IMethodInvocation input,
                                GetNextInterceptionBehaviorDelegate getNext)
    {
        if (input.MethodBase.Name != "Execute")
        {
            return getNext()(input, getNext);
        }

        var sb = new StringBuilder();

        try
        {
            var request = input.Arguments["request"] as IRestRequest;

            if (request == null)
            {
                return getNext()(input, getNext);
            }

            AppendRequestLog(request, sb);

            IMethodReturn result = getNext()(input, getNext);

            var response = result.ReturnValue as IRestResponse;

            AppendResponseLog(response, sb);

            Log.Info(sb);

            return result;
        }
        catch (Exception ex)
        {
            Log.Error(sb, ex);
            throw;
        }
    }

    private void AppendRequestLog(IRestRequest restRequest, StringBuilder sb)
    {
        // Log request
    }

    private void AppendResponseLog(IRestResponse response, StringBuilder sb)
    {
        // Log response
    }

    public IEnumerable<Type> GetRequiredInterfaces()
    {
        return Type.EmptyTypes;
    }

    public bool WillExecute
    {
        get { return true; }
    }
}

最後用Unity具現化RestClient
container.RegisterType<IRestClient, RestClient>(new Interceptor<InterfaceInterceptor>(),
    new InterceptionBehavior<LoggingInterceptionBehavior>());

到這裡我們已經把log邏輯從base class內拉出來,接下來便可以把base class裡原本使用到的log邏輯移除,base class顯得乾淨許多,函式庫也不用再相依Common.Logging。日後如果要更換logging framework或是採用自己撰寫的log元件,只需要修改LoggingInterceptionBehavior這支類別即可。

November 9, 2014

簡報分享 - Web Automation Testing Using Selenium

星期六公司在台中臨時辦公室舉辦了.NET meetup活動,有三場技術分享,我負責了其中一場,有與趣的朋友可以看看簡報內容。



簡報內使用到的範例可由http://goo.gl/KtyF6r下載。

June 29, 2014

AWS EC2 Load Balancer上設定SSL憑證

前一篇文章中提到了在AWS EC2上建立load balancer (以下簡稱LB)的方法,當時是以HTTP做分流為範例。我的開發環境中,SSL憑證是直接安裝在web server (IIS)上的,而EC2提供的LB具有SSL offloading的機制,可以直接把SSL憑證安裝在LB上,減輕web server加解密資料的負擔。

接續前面的設定,新增一筆listener。


Load Balancer Protocol選取HTTPS (Secure HTTP),點選SSL Certificate欄位裡的Change


可以看到頁面要求輸入憑證的相關資訊,如Private Key和Public Key Certificate,而且是*.pem格式。由於目前已經有SSL憑證安裝在IIS上,所以我必須先把憑證匯出成*.pfx再轉換成*.pem,並取出Private Key和Public Key Certificate。


將*.pfx轉換成*.pem需要借助OpenSSL,可至http://slproweb.com/products/Win32OpenSSL.html下載Windows專用的OpenSSL工具,在此下載Win64 OpenSSL v1.0.1h Light安裝。安裝完後在C:\OpenSSL-Win64\bin下會看到openssl.exe檔案。


準備好匯出的憑證,如G:\test.pfx。以管理者身份開啟Command Prompt,輸入C:\OpenSSL-Win64\bin\openssl.exe pkcs12 -in G:\test.pfx -out G:\test.pem -nodes及憑證匯出時所設定的密碼將*.pfx轉換成*.pem格式。


接下來輸入C:\OpenSSL-Win64\bin\openssl.exe x509 -in G:\test.pem -out G:\test_public.txt產生Public Key Certificate


接下來輸入C:\OpenSSL-Win64\bin\openssl.exe rsa -in G:\test.pem -out G:\test_private.txt產生Private Key


最後將G:\test_private.txt內容複製到Private Key欄位中,而G:\test_public.txt內容複製到Public Key Certificate欄位中。這裡要特別注意的是,檔案內容中如-----BEGIN RSA PRIVATE KEY-----和-----END RSA PRIVATE KEY-----皆需放至欄位中,否則無法設定成功。


輸入完後按下Save完成所有設定




參考
  1. Converting a .pfx/.p12 ("PKCS #12") file to certificate and key
  2. Install Wildcard Certificate onto AWS EC2 Load Balancer

June 15, 2014

AWS EC2上設定Load Balancer

目前團隊的開發和測試環境是放在AWS (Amazon Web Services) EC2上,最近因為專案需要在load balance的環境下運行,所以直接在EC2上設定環境,設定的過程相當簡單且快速。

先到EC2 Dash board,點選左邊選單的Load Balancers


點選Create Load Balancer


輸入Load Balancer name,預設會有HTTP加入設定,也可新增其它設定如HTTPS (Secure HTTP、TCP和SSL (Secure TCP)。點選Continue


預設load balancer透過監聽HTTP port 80下的某支檔案來判斷instance是否還有效。


不過我改以TCP監聽port 80,主要是之前使用過Rackspace的load balancer (Broadcade)經驗,當時以檔案來監聽,load balancer會以HTTP status code是否為200來判別檔案是否存在,所以load balancer在第一次存取檔案時會取得200。但因為cache的關係,load balancer第二次存取檔案時會取得304而判定instance失效。設定完成後點選Continue


選擇要加入要使用load balance的instance,點選Continue


點選Create開始建立load balancer




建立完成後會顯示成功訊息,點選Close


完成設定後AWS會分配3組domain name給load balancer,接下來只要到你的domain name provider或是Route 53內將原先的domain name設定CName對應到load balancer提供的domain name即可。



June 14, 2014

使用Fluent Assertions提昇測試程式可讀性

目前團隊所使用的unit testing framework為NUnit。在NUnit裡如果要做assertion的話可以這樣寫
[Test]
public void Can_Add_Integer()
{
    int total = Calculator.Add(1, 2);
    Assert.AreEqual(3, total);
}

上面的Assertion閱讀起來不難懂,可以理解是在測試total與3是否相等。然而對開發人員在撰寫程式碼時卻有個小問題,Assert.AreEqual(3, total)Assert.AreEqual(total, 3)意義相同嗎?看一下NUnit提供的說明可以知道第一個參數要代入的是預期該得到的數值,而第二個參數是實際運算完後的數值。


當assertion永遠成功時,Assert.AreEqual(3, total)Assert.AreEqual(total, 3)看起來沒什麼差別。但在assertion失敗時,意義上就不一樣了,因為NUnit提供的失敗訊息會有很大差別。如

Assert.AreEqual(3, total)


Assert.AreEqual(total, 3)


以上是NUnit提供的Classic Assert Model。事實上NUnit還有提供另一種assertion方式稱為Constraint Model,如
[Test]
public void Can_Add_Integer_Using_Constraints()
{
    int total = Calculator.Add(1, 2);
    Assert.That(total, Is.EqualTo(3));
}

用以上方式撰寫assertion程式碼在閱讀上易讀許多。

雖然Constraint Model已提供可讀性不錯的撰寫方式,但每次要使用時還是得先Assert.That,而且有時NUnit提供的除錯訊息不是那麼清楚。基於以上因素,便出現Fluent Assertions這套函式庫來強化NUnit(或其它framework)在assertion上的不足。

將上述的assertion改以Fluent Assertions重新撰寫可以得到更簡潔的assertion
[Test]
public void Can_Add_Integer_Using_FluentAssertions()
{
    int total = Calculator.Add(1, 2);
    total.Should().Be(3);
}

寫成一行閱讀起來也很清楚,就如同說話一般
[Test]
public void Can_Add_Integer_Using_FluentAssertions()
{
    Calculator.Add(1, 2).Should().Be(3);
}
有時我們需要驗證功能是否有正確拋出例外,也可以Fluent Assertions來撰寫如
[Test]
public void Cannot_Subtract_And_Throw_NotSupportedException()
{
    Action action = () => Calculator.Subtract(5, 3);
    action.ShouldThrow<NotSupportedException>().WithMessage("Not yet implemented");
}

Fluent Assertions提供的assertion很豐富也簡潔易懂,目前我所在團隊已將原本NUnit的assertion全部換成使用Fluent Assertions,效果還不錯,在撰寫assertion程式碼時速度快了許多也沒遇到太大的問題。

May 11, 2014

多方案檔環境下共用Nuget Package Restore資料夾位置

NuGet提供的Package Restore功能可以讓開發人員在編譯程式碼時才將所參考的third-party函式庫下載回來,而不需放進source code repository。在大部份情況下,開發的系統可能只有一個方案檔,建置方案後nuget預設會將函式庫放在packages資料夾,且與方案檔位在相同路徑下。

前陣子在幫公司系統做架構重整,利用git submodules功能將幾個核心函式庫專案獨立出來放到專屬的repository且有自己的方案檔可以開啟這些專案進行修改。引用到核心函式庫的客制化專案則放到另一個專屬的repository也同樣擁用自己的方案檔。

在如此架構下,如果將客制化專案的repository拉回本機,資料夾的結構就像下方,SubmoduelsDemo-Sub是核心函式庫,而SubmoduelsDemo-Main為客製化專案。



如果編譯SubmoduelsDemo-Sub方案檔,其packages資料夾便會建立在SubmoduelsDemo-Sub資料夾內,這點毫無疑問。

但問題來了,如果事先不開啟SubmoduelsDemo-Sub方案檔編譯,而是直接開啟SubmoduelsDemo-Main方案檔編譯,SubmoduelsDemo-Sub資料夾內並不會有packages資料夾產生,因為nuget預設會以當下被編譯的方案檔(SubmoduelsDemo-Main)所在路徑來建立packages資料夾。如此,核心函式庫在編譯時就會因為找不到third-party參考而建置失敗,因為它是參考到SubmoduelsDemo-Sub資料夾內的packages資料夾。

為了解決這個問題,nuget提供了透過修改設定檔(NuGet.Config)的方式,讓我們變更packages資料夾的預設路徑,如下加入repositoryPath設定
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <config>
    <add key="repositorypath" value="..\SubmodulesDemo-Sub\packages" />
  </config>
  <solution>
    <add key="disableSourceControlIntegration" value="true" />
  </solution>
</configuration>

要注意的是,這個路徑是相對於NuGet.Config檔案的路徑。此外,在能使用這項設定前,請先將nuget更新到最新版本。設定完成後建置SubmoduelsDemo-Main方案,就可以看到packages資料夾建立在SubmoduelsDemo-Sub資料夾內。



完整範例可參考SubmodulesDemo-MainSubmodulesDemo-Sub

April 28, 2014

使用Git Extensions建立submodules

目前開發的某個系統A其核心模組未來需要被不同的客製化專案所共用,A在GitHub上使用了一個repository。但因為上述需求,未來的客製化專案一定會使用到A的核心模組。最簡單的方式就是開一個repository給新專案,再把核心模組也複製一份過去使用。但如此一來就會有兩份核心模組需要維護,未來若有數十個客製化專案,維護成本不小且容易出錯。

Git為了解決這類問題,提供了submodules的概念,讓repository具有類似參考的功能。以上面的例子來看,我可以將模心模組切割出來放到新的repository R,接著在原本系統所使用的repository中建立submodules並將參考位址指向R。往後如果有新的客製化專案,也是依照一樣的方式,建立新的repository並設定submodules位址。如此開發人員只要維護一份核心模組即可,另外還有好處就是可以對核心模組的repository做權限控管。

這裡並不打算介紹以Git指令來建立submodules,而是使用Git Extensions來說明,因為Git Extensions是目前我的團隊中所使用的Git圖形化工具。

在說明前,我已先在GitHub上準備好兩個repository,分別是SubmodulesDemo-MainSubmodulesDemo-Sub,且各放了一個文字檔。



接下來在本機的SubmodulesDemo-Main中,右鍵撰擇GitExt Browse


點選Repository-> Submodules


點選Add submodulePath to submodule輸入SubmodulesDemo-Sub的位址https://github.com/petekcchen/SubmodulesDemo-Sub.git後點選Add完成設定。




回到SubmodulesDemo-Main資料夾,可以發現多了.gitmodules檔案和SubmodulesDemo-Sub資料夾,且資料已被pull到本機。


最後右鍵選擇GitExt Commit,將submodules設定push回SubmodulesDemo-Main repository,在GitHub上即能看到對SubmodulesDemo-Sub repository的參考。



submodules建立完成後,有一點要注意的是,當SubmodulesDemo-Sub repository有更新時,需要到本機的SubmodulesDemo-Sub資料夾做pull才能取到最新的commit,在SubmodulesDemo-Main中做pull是無法同時取得SubmodulesDemo-Main repository和SubmodulesDemo-Sub repository內的資料。在取得SubmodulesDemo-Sub repository最新的commit後,也記得要再push回SubmodulesDemo-Main repository以便更新最新的submodules參考。

April 20, 2014

以Page Factory撰寫Selenium測試程式

Page Factory是Selenium提供的另一個library,用來輔助以Page Object Pattern所撰寫的測試程式。要使用這個library,可以透過nuget指令Install-Package Selenium.Support安裝。

在之前的文章中有提到,在頁面上抓取某個HTML物件,可以透過以下方式
IWebElement userNameField = this._driver.FindElement(By.Id("UserName"));

以Page Factory的方式來撰寫的話,則改為宣告私有變數加上annotation方式取得頁面上物件,如
[FindsBy(How = How.Id, Using = "UserName")]
private IWebElement _usernameField;

並在建構式中初始化整個page類別
public RegisterPageUsingPageFactory(IWebDriver driver)
{
    this._driver = driver;
    PageFactory.InitElements(driver, this);
}
接下來前述宣告的私有變數即可使用,如
public RegisterPageUsingPageFactory EnterUsername(string username)
{
    this._username = username;
    this._usernameField.SendKeys(username);
    return this;
}

以page factory加annotation的方式撰寫程式,是不是優雅且程式碼乾淨許多?以下為以page factory改寫過後的page物件。


從以上的例子也許還看不出page factory撰寫方式的優點,但隨著HTML物件的重覆使用率變高,就越能顯示出page factory的好處。但有一點需要注意的是,由於宣告的私有變數是在run time時才由PageFactory類別初始化,所以在compile time時Visual Studio就會出現警告訊息。


若要移除警告訊息,可加入#pragma warning指示詞,如
#pragma warning disable 649

        [FindsBy(How = How.Id, Using = "UserName")]
        private IWebElement _usernameField;

        [FindsBy(How = How.Id, Using = "Password")]
        private IWebElement _passwordField;

        [FindsBy(How = How.Id, Using = "ConfirmPassword")]
        private IWebElement _confirmPassword;

        [FindsBy(How = How.ClassName, Using = "btn")]
        private IWebElement _registerButton;

        [FindsBy(How = How.XPath, Using = "//div[@class='validation-summary-errors']/ul/li")]
        private IList<IWebElement> _errorMessages;

#pragma warning restore 649


完整程式碼可由https://github.com/petekcchen/blog/tree/master/SeleniumDemo下載


參考

April 13, 2014

以Page Object Pattern撰寫Selenium測試程式

在解釋Page Object Pattern之前,我們來看以下測試程式

以上兩支測試程式分別用來測試以下案例
  1. Can_Register_User:使用者可以註冊帳號
  2. Cannot_Register_User_With_Empty_Username:註冊帳號時若沒有輸入帳號並送出,畫面將會出現正確的錯誤訊息

測試程式內容本身其實並不難理解,但存在code smell
  1. 過多的實作細節。對於非測試程式撰寫者來說,過多的資訊會造成閱讀困難,如果想快速地了解某個測試案例做了哪些事,程式碼閱讀者需要逐行研究
  2. 重覆的程式碼。以註冊帳號來說,不難想像還會有其它的測試案例存在,也因此會有重覆的程式碼會散亂在各個測試案例程式碼中
接下來先看重整過後的程式碼

測試程式是不是變得乾淨許多?Page Object Pattern是撰寫UI測試程式時會用到的一個設計模式,它其實是利用物件導向封裝的特性,將實作細節隱藏起來,增加程式的可讀性和複用性。以上面的例子來看,測試程式的閱讀者只需要了解RegisterPage類別及HomePage類別的抽象涵義即可了解該測試案例的用意,而無需了解到實作細節。下面為RegisterPage類別的實作細節,為了符合Page Object Pattern的特性,重整過後的程式碼也顯得整齊許多,且RegisterPage類別也符合Single Responsibility Principle

完整程式碼可由https://github.com/petekcchen/blog/tree/master/SeleniumDemo下載

參考

SlowCheetah無法轉換組態設定檔

在build server上佈署網站至staging server後,網站無法正常運作。檢查了一下設定檔(*.config)發現裡面的設定不是為staging server所用,懷疑Staging組態下設定檔未被成功轉換輸出。在本機切換至Staging組態並重新建置網站,檢查bin\Staging下所轉換出的設定檔也發現並未轉換成功。

查看了一下專案檔內容,發現SlowCheetah所用到的PropertyGroup
<PropertyGroup Label="SlowCheetah">
  <SlowCheetahToolsPath>$([System.IO.Path]::GetFullPath( $(MSBuildProjectDirectory)\..\packages\SlowCheetah.2.5.10.3\tools\))</SlowCheetahToolsPath>
  <SlowCheetah_EnableImportFromNuGet Condition=" '$(SC_EnableImportFromNuGet)'=='' ">true</SlowCheetah_EnableImportFromNuGet>
  <SlowCheetah_NuGetImportPath Condition=" '$(SlowCheetah_NuGetImportPath)'=='' ">$([System.IO.Path]::GetFullPath( $(MSBuildProjectDirectory)\Properties\SlowCheetah\SlowCheetah.Transforms.targets ))</SlowCheetah_NuGetImportPath>
  <SlowCheetahTargets Condition=" '$(SlowCheetah_EnableImportFromNuGet)'=='true' and Exists('$(SlowCheetah_NuGetImportPath)') ">$(SlowCheetah_NuGetImportPath)</SlowCheetahTargets>
</PropertyGroup>
被移動到
<Import Project="$(SlowCheetahTargets)" Condition="Exists('$(SlowCheetahTargets)')" Label="SlowCheetah" />

之後

查了一下版控的歷史紀錄,PropertyGroup原是放在Import之前,在把順序重新移動再建置後,已可順利地轉換設定檔。至於為何PropertyGroup被移動了,目前還無法確認實際原因,似乎是在我加入了其它新的組態設定檔後,Visual Stuio把它移動了。有國外的開發人員也提出這個問題,不過是在將SlowCheetah由2.5.10.2升級到2.5.10.3後發生,最後也是以手動方式修改專案檔來解決。

April 12, 2014

使用SlowCheetah套件新增及轉換組態設定檔內容

在開發專案的過程中,常會遇到需要因應不同的開發或佈署環境而有不同的組態設定。在Visual Studio裡,預設的兩種組態分別為Debug及Release。當我們需要除了這兩種以外的組態時,可以到BUILD-> Configuration Manager裡新增。



在Console Application專案中,預設會有一個App.config設定檔,按照前述需求,不同的開發或佈署環境會有不同的組態設定,我們可以透過SlowCheetah套件新增不同組態下相對應的App.config,如App.Debug.config或App.Release.config。除此之外,SlowCheetah還可以協助應用程式於程式碼建置時輸出正確的設定檔內容。

先建立一個名為SlowCheetahDemo的Console Application專案,並透過nuget指令安裝SlowCheetah,Install-Package SlowCheetah

右鍵選取App.config,點選Add transform,App.config下會新增App.Debug.config及App.Release.config檔案



以下為App.config內容
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
  <connectionStrings>
    <add name="Default"
         connectionString="Server=localhost;Database=Demo;Persist Security Info=True;integrated security=false; user id=demo; password=1234"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
  <startup>
    <supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
  </startup>
</configuration>


現在我們希望在Release組態下,設定檔內容可以切換到不同的連線字串,App.Release.config設定如下
<?xml version="1.0" encoding="utf-8" ?>
<!-- For more information on using transformations
     see the web.config examples at http://go.microsoft.com/fwlink/?LinkId=214134. -->
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <connectionStrings>
    <add name="Default" xdt:Transform="SetAttributes" xdt:Locator="Match(name)"
         connectionString="Server=ReleaseServer;Database=Release;Persist Security Info=True;integrated security=false; user id=release; password=5678"
         providerName="System.Data.SqlClient" />
  </connectionStrings>
</configuration>

設定完成後,將專案組態模式切到Debug並建置程式碼,可以看到在bin\Debug\SlowCheetahDemo.exe.config內容為一開始設定在App.config的內容。接下來切換到Release組態再次建置程式碼,可以看到bin\Release\SlowCheetahDemo.exe.config裡的連線字串已設定為在App.Release.config中所設定的連線字串。

組態設定檔的轉換除了對開發上有幫助外,對於有導入持續整合及自動化佈署的專案,也增加了不少彈性。目前我所在團隊開發的一個專案中,就有Dev、QA、Staging和Production不同的組態設定,在build server建置程式碼時便會根據當下的組態設定去轉換出對應的資料庫連線字串,佈署位址及網站位址等。

完整範例檔可至https://github.com/petekcchen/blog/tree/master/SlowCheetahDemo下載

參考

March 30, 2014

Git Extensions無法push程式碼至GitHub

Git Extensions無法將程式碼push至GitHub上,出現syntax error near unexpected token錯誤訊息


檢查路徑%USERPROFILE%\.gitconfig (以我本機來說路徑為C:\Users\Pete),發現git-credential-winstore.exe路徑有問題。將原本的!\\\"C:/Program Files (x86)/GitExtensions/GitCredentialWinStore/git-credential-winstore.exe\\\"改為!\"C:/Program Files (x86)/GitExtensions/GitCredentialWinStore/git-credential-winstore.exe\"後再重新push便不再出現相同問題。至於為何多出額外的反斜線,原因不明。


March 23, 2014

使用Selenium WebDriver進行網頁自動化測試

幾年前剛加入某個團隊時,因為對當時團隊所開發的系統還不熟悉,所以先被指派協助做UAT,順便了解一下系統中的主要功能,該系統是以ASP.NET Web Forms所開發。一開始還傻傻地照著測試案例文件做測試,在幾天之後開始覺得每次要手動做重覆的事情,雖然可以藉此了解一下系統功能,但似乎有點不太科學,也有點浪費時間,心想這種事應該是要自動化處理吧?

當時腦海先是閃過利用Sikuli來幫我開網頁點按鈕輸入資料做這些苦工,卻意外發現了Selenium IDE,讓我眼睛為之一亮。在接下來的幾天我就改用Selenium IDE錄製好測試案例,輕鬆地完成自動化UAT。然而Selenium IDE只支援FireFox,當時開發的系統主要還是以IE為主,所以又開始尋找可以以IE進行網頁自動化測試的方式。當時使用了WatiN,也成功地寫了些測試程式。

以上是題外話,目前的團隊相當重視測試,團隊成員不僅需要撰寫單元/整合測試,也需要知道如何撰寫Selenium測試,以便讓build server進行自動化測試,也因此發現原來Selenium也已經提供WebDriver機制讓.NET開發人員可以在VS裡針對不同的瀏覽器撰寫相同的測試程式。以下介紹如何使用Selenium WebDriver來進行網頁自動化測試,所使用的瀏覽器為Chrome。

首先建立兩個demo用的專案,一個為ASP.NET MVC 5專案SeleniumDemo,另一個為Class Library專案SeleniumDemo.Tests,並加入了Microsoft.VisualStudio.QualityTools.UnitTestFramework參考


在SeleniumDemo.Tests專案中透過NuGet指令Install-Package Selenium.WebDriver加入Selenium WebDriver參考。


http://chromedriver.storage.googleapis.com/index.html下載Chrome DriverChrome Driver為Selenium WebDriver啟動Chrome會使用到的執行檔(chromedriver.exe),並存放在tools資料夾裡。


確認ASP.NET MVC 5專案預設的網站可以正常執行後,我們要模擬使用者點選Register連結,看看使用者是否有被導向註冊頁面。

using System.IO;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using OpenQA.Selenium;
using OpenQA.Selenium.Chrome;

namespace SeleniumDemo.Tests
{
    [TestClass]
    public class AccountTests
    {
        [TestMethod]
        public void Can_Display_Register_Page()
        {
            string chromeDriverDirectory = string.Format(@"{0}\..\..\..\tools", Directory.GetCurrentDirectory());

            IWebDriver driver = new ChromeDriver(chromeDriverDirectory);
            driver.Navigate().GoToUrl("http://localhost:64872/");

            IWebElement registerLink = driver.FindElement(By.Id("registerLink"));

            registerLink.Click();

            Assert.AreEqual("http://localhost:64872/Account/Register", driver.Url);

            driver.Quit();
        }
    }
}

在以上程式碼中,我們設定了Chrome Driver(chromedriver.exe)的實體位置給WebDriver後,便透過WebDriver開啟瀏覽器導向網站首頁,找出Register連結並點選它,最後驗證導向後的網址是否為註冊頁面的網址,成功地話瀏覽器就會被關閉。這個測試案例很簡單,雖然有個小問題,例如測試失敗的話,瀏覽器就不會被關閉。但做為demo,我想這樣已經足夠。完整的範例專案可由https://github.com/petekcchen/blog/tree/master/SeleniumDemo下載。

March 16, 2014

HTTP Error 500.19 - Internal Server Error

協助同事佈署ASP.NET MVC 5至Windows Server 2008時出現以下錯誤。


詳細的錯誤訊息為The requested page cannot be accessed because the related configuration data for the page is invalid.。從訊息看起來應該是應用程式沒有足夠的權限可以讀取web.config檔案。依照以往的經驗,試著在應用程式所在的資料夾中加入IIS_IUSRSNETWORK SERVICE兩個使用者權限。重新瀏覽應用程式,還是得到相同的錯誤訊息。試著檢查web.config的內容也未發現不正確的區段或設定。

再檢查了一下應用程式資料夾裡的檔案,發現檔案名稱顯示為綠色,覺得有點好奇。

研究了一下發現有國外的網友遇到類似的問題。當檔名呈現為綠色時表示檔案被加密過,該網友將被加密過的圖檔放到網站上卻無法顯示出來。解決的方式便是將加密取消,在應用程式資料夾右鍵選取Properties


點選Advanced,將Encrypt contents to secure data取消勾選。



設定完成後可以看到應用程式資料夾內的檔案名稱恢復成黑色。重新執行ASP.NET MVC 5應用程式已可正常運作。


參考