<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title><![CDATA[網站製作學習誌]]></title>
  <subtitle><![CDATA[記錄在開發網站時所學的一切]]></subtitle>
  <link href="/atom.xml" rel="self"/>
  <link href="http://jaceju.net/"/>
  <updated>2015-11-09T17:28:54.000Z</updated>
  <id>http://jaceju.net/</id>
  
  <author>
    <name><![CDATA[Jace Ju]]></name>
    
  </author>
  
  <generator uri="http://hexo.io/">Hexo</generator>
  
  <entry>
    <title><![CDATA[在 PHPUnit 中測試需要 closure 的函式]]></title>
    <link href="http://jaceju.net/2015/11/09/php-closure-testing/"/>
    <id>http://jaceju.net/2015/11/09/php-closure-testing/</id>
    <published>2015-11-09T11:11:39.000Z</published>
    <updated>2015-11-09T17:28:54.000Z</updated>
    <content type="html"><![CDATA[<p>不知道你有沒有在開發 PHP 程式的過程中，測試過需要使用 anonymous function 或 closure 的函式或類別方法？我在開發自己的函式庫時，就遇到了需要測試 closure 是否被正確調用的問題。</p>
<p>在解決幾個問題後，我發現其實做法並不難，所以接下來我就來介紹幾個測試 closure 的方式。</p>
<a id="more"></a>
<h2 id="範例">範例</h2><p>先來看看一個簡單的 closure 使用範例：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Example</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">runClosure</span><span class="params">(Closure <span class="variable">$closure</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="variable">$closure</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>在 <code>Example::runClosure</code> 方法中接受了一個 <code>$closure</code> 參數，而它的型別屬於 <code>Closure</code> 類別，使我們可以直接在程式裡用 <code>$closure()</code> 的方式來執行它的內容。</p>
<p>測試則是這樣寫的：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">ExampleTest</span> <span class="keyword">extends</span> <span class="title">PHPUnit_Framework_TestCase</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">testRunClosure</span><span class="params">()</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="variable">$example</span> = <span class="keyword">new</span> Example();</span><br><span class="line"></span><br><span class="line">        <span class="variable">$closure</span> = <span class="function"><span class="keyword">function</span> <span class="params">()</span> </span>&#123;&#125;;</span><br><span class="line">        <span class="variable">$example</span>-&gt;runClosure(<span class="variable">$closure</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>在測試中，我們傳入一個 anonymous function 給目標物件的 <code>runClosure</code> 方法使用。在 PHP 中， closure 和 anonymous function 其實是一樣的，它們最後都會轉化成 <code>Closure</code> 物件；這點和 JavaScript 不同，要特別注意。</p>
<p>問題來了，我們怎麼驗證 <code>$closure</code> 被呼叫了呢？</p>
<h2 id="遇到的問題">遇到的問題</h2><p>我第一個想法是使用 <a href="http://docs.mockery.io" target="_blank" rel="external">Mockery</a> 來將 anonymous function 包起來，看看 PHP 底層會呼叫 closure 的哪個函式，我再做 <code>shouldReceive</code> 驗證：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="variable">$closure</span> = Mockery::mock(<span class="function"><span class="keyword">function</span> <span class="params">()</span> </span>&#123;&#125;);</span><br><span class="line"><span class="variable">$example</span>-&gt;runClosure(<span class="variable">$closure</span>);</span><br></pre></td></tr></table></figure>
<p>結果執行測試時，出現了以下錯誤訊息：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Argument 1 passed to Example::runClosure() must be an instance of Closure, instance of Mockery_0_Closure_Closure given</span><br></pre></td></tr></table></figure>
<p>這就奇怪了， Mockery 所 mock 出來的物件，類型應該是 Closure 的子類別呀？為什麼會被 type hint 打槍呢？</p>
<p>帶著疑惑，我試著直接 mock <code>Closure</code> 類別：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="variable">$closure</span> = Mockery::mock(Closure::class);</span><br><span class="line"><span class="variable">$example</span>-&gt;runClosure(<span class="variable">$closure</span>);</span><br></pre></td></tr></table></figure>
<p>錯誤訊息變成了：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Mockery\Exception: The class \Closure is marked final and its methods cannot be replaced. Classes marked final can be passed in to \Mockery::mock() as instantiated objects to create a partial mock, but only if the mock is not subject to type hinting checks.</span><br></pre></td></tr></table></figure>
<p>原來問題就出在於 <code>Closure</code> 類別在 PHP 中是被宣告為 <code>final</code> ，也就是無法再被繼承。而 Mockery 遇到這樣的類別，<a href="http://docs.mockery.io/en/latest/reference/final_methods_classes.html" target="_blank" rel="external">官方的建議</a>是：</p>
<blockquote>
<p>The simplest solution is not to mark classes or methods as final!</p>
</blockquote>
<p>就是不要用 <code>final</code> 啦！可是 Closure 是 PHP 的內建類別，沒辦法把 <code>final</code> 拿掉，這樣一來不就無解了？</p>
<h2 id="注入_spy_物件來驗證">注入 spy 物件來驗證</h2><p>其實轉個念頭，因為傳入待測程式的 closure 內容是我可以控制的，所以我不一定要去 mock closure ，而是讓它實際跑跑看，然後驗證裡面的程式碼是否有被執行就可以了。而最簡單的方法，就是插入一個 spy 物件，透過它來得知 closure 是否有被執行。</p>
<p>我在測試案例裡 mock 了 <code>stdClass</code> 這個標準類別，然後放在 <code>$spy</code> 這個變數裡；然後告訴它應該要接收到 <code>detected</code> 這個方法被執行一次的資訊。最後把這個 <code>$spy</code> 變數注入 closure 裡，在裡面執行 <code>detected</code> 方法：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">testRunClosure</span><span class="params">()</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="variable">$spy</span> = Mockery::mock(stdClass::class);</span><br><span class="line">    <span class="variable">$example</span> = <span class="keyword">new</span> Example();</span><br><span class="line">    </span><br><span class="line">    <span class="variable">$spy</span>-&gt;shouldReceive(<span class="string">'detected'</span>)-&gt;once();</span><br><span class="line">    </span><br><span class="line">    <span class="variable">$example</span>-&gt;runClosure(<span class="function"><span class="keyword">function</span> <span class="params">()</span> <span class="title">use</span> <span class="params">(<span class="variable">$spy</span>)</span> </span>&#123;</span><br><span class="line">        <span class="variable">$spy</span>-&gt;detected();</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>這樣一來就可以透過 Spy 物件來驗證 closure 是否有被執行了。</p>
<h2 id="驗證注入目標物件的_closure">驗證注入目標物件的 closure</h2><p>不過有時候我們會希望在 closure 裡使用目標物件，例如：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Example</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">runClosure</span><span class="params">(Closure <span class="variable">$closure</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="variable">$closure</span>(<span class="variable">$this</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>這時 closure 就可以將目標物件當做參數注入，然後再執行它的方法。例如：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="variable">$example</span> = <span class="keyword">new</span> Example();</span><br><span class="line">    </span><br><span class="line"><span class="variable">$example</span>-&gt;runClosure(<span class="function"><span class="keyword">function</span> <span class="params">(<span class="variable">$target</span>)</span> </span>&#123;</span><br><span class="line">   <span class="variable">$target</span>-&gt;otherMethod();</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>但我只是要確認目標物件有被正確傳入 closure 中，所以應該要驗證目標物件的類別是 <code>Example</code> 就可以了。我們可以直接在 closure 中使用 <code>$this</code> 來呼叫驗證方法，因為這時的 <code>$this</code> 是指向測試案例的物件。所以測試就可以寫成：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">testRunClosure</span><span class="params">()</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="variable">$example</span> = <span class="keyword">new</span> Example();</span><br><span class="line"></span><br><span class="line">    <span class="variable">$example</span>-&gt;runClosure(<span class="function"><span class="keyword">function</span> <span class="params">(<span class="variable">$target</span>)</span> </span>&#123;</span><br><span class="line">        <span class="variable">$this</span>-&gt;assertInstanceOf(Example::class, <span class="variable">$target</span>);</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>像這樣的場合就不需要使用 spy 物件了。</p>
<h2 id="驗證使用_bindTo_的_closure">驗證使用 bindTo 的 closure</h2><p>如果在待測目標物件的方法裡，使用 <code>Closure::bindTo</code> 這個方法來重新定義 <code>$this</code> 時，該怎麼測試呢？例如：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">runClosure</span><span class="params">(Closure <span class="variable">$closure</span>)</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="variable">$cb</span> = <span class="variable">$closure</span>-&gt;bindTo(<span class="variable">$this</span>);</span><br><span class="line">    <span class="variable">$cb</span>();</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>注意，這時候 <code>$cb</code> 並不是用注入的參數，而是使用執行時期的 context (也就是 <code>$this</code> ) 來指向目標物件；這也使得我們不能在測試中直接用 <code>$this</code> 來呼叫驗證方法，必須另尋出路。</p>
<p>所幸 PHP 的 closure 還提供了一個 <code>use</code> 的語法，讓我們可以把外部變數帶入 closure 中。但它不能直接帶入 <code>$this</code> ，所以必須換個名字。最後測試就可以改成：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">testRunClosure</span><span class="params">()</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="variable">$assert</span> = <span class="variable">$this</span>;</span><br><span class="line">    <span class="variable">$example</span> = <span class="keyword">new</span> Example();</span><br><span class="line"></span><br><span class="line">    <span class="variable">$example</span>-&gt;runClosure(<span class="function"><span class="keyword">function</span> <span class="params">()</span> <span class="title">use</span> <span class="params">(<span class="variable">$assert</span>)</span> </span>&#123;</span><br><span class="line">        <span class="variable">$assert</span>-&gt;assertInstanceOf(Example::class, <span class="variable">$this</span>);</span><br><span class="line">    &#125;);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="總結">總結</h2><p>closure 是在 PHP 5.3 中就引入的特性，現在越來越多函式庫與框架都已經將它納入設計時的考量了。當你有需要自己設計使用 closure 的方法時，就可以嘗試這些方法來測試 closure ：</p>
<ol>
<li>使用 anonymous function 時，使用 spy 物件來觀察。</li>
<li>當 closure 會注入目標物件時，直接驗證目標物件的類別。</li>
<li>當 closure 是透過 <code>bindTo</code> 來繫結目標物件時，用 <code>use</code> 來另外傳遞測試案例物件，以便呼叫 assertion 方法驗證。</li>
</ol>
<p>如果有更好的方法，也歡迎大家建議。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>不知道你有沒有在開發 PHP 程式的過程中，測試過需要使用 anonymous function 或 closure 的函式或類別方法？我在開發自己的函式庫時，就遇到了需要測試 closure 是否被正確調用的問題。</p>
<p>在解決幾個問題後，我發現其實做法並不難，所以接下來我就來介紹幾個測試 closure 的方式。</p>]]>
    
    </summary>
    
      <category term="PHP" scheme="http://jaceju.net/tags/PHP/"/>
    
      <category term="測試" scheme="http://jaceju.net/tags/%E6%B8%AC%E8%A9%A6/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[利用 PHPUnit 與 Mink 來做 Web 測試]]></title>
    <link href="http://jaceju.net/2015/10/27/web-testing-with-phpunit-mink/"/>
    <id>http://jaceju.net/2015/10/27/web-testing-with-phpunit-mink/</id>
    <published>2015-10-27T10:36:05.000Z</published>
    <updated>2015-11-09T10:22:37.000Z</updated>
    <content type="html"><![CDATA[<p>如果你面對的是以前舊有的 PHP 程式，是時候負起一些責任了。</p>
<p>我知道它改起來很痛苦，一堆不良的 PHP 程式習慣都阻礙你的修正；使得每次調整功能時，到底改得對不對，得要等到上線才知道。想要重寫一個新版本，但太多的實作細節你不清楚；也沒有最新的規格文件，讓你無法為新版本做出功能無誤的保證。</p>
<p>現在你唯一擁有的，就是已經在線上運作的程式邏輯；雖然它可能還有 bug ，但至少大多數的功能是通過使用者驗證的。那麼先為它買個保險吧！確保之後的修改不會影響到其他功能的正常運作；而最直接的方式，就是把目前程式邏輯所呈現的結果或是使用者的操作，寫成自動化 Web 測試。</p>
<p>建立 Web 測試的方法有很多，這裡我將介紹我在實務上使用 <a href="https://phpunit.de/" target="_blank" rel="external">PHPUnit</a> 加上 <a href="http://mink.behat.org/en/latest/" target="_blank" rel="external">Mink</a> 搭配 <a href="http://phantomjs.org/" target="_blank" rel="external">PhantomJS</a> 的方法。</p>
<a id="more"></a>
<h2 id="所需工具與原理">所需工具與原理</h2><p>在 Web 測試中，主要分成三個部份：</p>
<ul>
<li>自動化測試框架：負責執行測試案例及驗證</li>
<li>瀏覽器控制器或模擬器：透過腳本來操作或模擬瀏覽器的行為</li>
<li>目標瀏覽器：就是我們常用的網頁瀏覽器</li>
</ul>
<p>PHPUnit 是 PHP 中最常見的自動化測試框架，要應用在舊專案中也非常輕鬆。</p>
<p>Mink 扮演的就是控制瀏覽器的角色，它可以透過不同的 <a href="http://mink.behat.org/en/latest/guides/drivers.html" target="_blank" rel="external">Driver</a> 來控制或模擬瀏覽器。</p>
<p>而 PhantomJS 則是一個透過程式來操作的 Headless WebKit 瀏覽器；也因為它沒有視窗介面，所以啟動速度非常快，非常適合用來測試。另外它還內建 <a href="GhostDriver">GhostDriver</a> ，讓我們可以透過 <a href="https://code.google.com/p/selenium/wiki/JsonWireProtocol" target="_blank" rel="external">WebDriver Wire Protocol</a>  來操作它。</p>
<p>所以整個 Web 測試的基礎，就是在 PHPUnit 的測試案例中，透過 Mink 的 Selenium2 Driver 來操作 PhamtomJS 。</p>
<p>接下來就進入實作吧。</p>
<h2 id="工具的安裝">工具的安裝</h2><p>以下介紹的安裝方式，都是在 Mac OS X 環境下完成；其他作業系統的安裝方式也差不多，這裡就不再贅述。</p>
<h3 id="安裝_PHPUnit_與_Mink">安裝 PHPUnit 與 Mink</h3><p>先建立一個專案目錄，然後切換到專案目錄下，執行：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">composer require phpunit/phpunit behat/mink-selenium2-driver</span><br></pre></td></tr></table></figure>
<p>這樣 Composer 會將 PHPUnit 、 Mink 及 Mink Selenium 2 Driver 安裝在 <code>vendor</code> 目錄下，並自動建立 <code>composer.json</code> 及 <code>composer.lock</code> 兩個檔案。</p>
<p>註：這裡我假設你的環境可以執行 <code>composer</code> 指令，所以也不再贅述 Composer 的安裝流程。</p>
<h3 id="安裝_PhantomJS">安裝 PhantomJS</h3><p>接著到 <a href="http://phantomjs.org/download.html" target="_blank" rel="external">PhantomJS 官網</a>下載 Mac OS X 專用的 ZIP 檔。然後執行：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">unzip phantomjs-2.0.0-macosx.zip&#10;sudo mv phantomjs /usr/local/bin/</span><br></pre></td></tr></table></figure>
<p>用以下指令確認有安裝完成：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">phantomjs --version</span><br></pre></td></tr></table></figure>
<p>沒問題的話，應該會出現 <code>2.0.0</code> 。</p>
<h2 id="設定專案的_PHPUnit">設定專案的 PHPUnit</h2><p>在專案目錄下新增 <code>phpunit.xml</code> 檔，內容為：</p>
<figure class="highlight xml"><table><tr><td class="code"><pre><span class="line"><span class="pi">&lt;?xml version="1.0" encoding="UTF-8"?&gt;</span></span><br><span class="line"><span class="tag">&lt;<span class="title">phpunit</span> <span class="attribute">backupGlobals</span>=<span class="value">"false"</span></span><br><span class="line">         <span class="attribute">backupStaticAttributes</span>=<span class="value">"false"</span></span><br><span class="line">         <span class="attribute">bootstrap</span>=<span class="value">"vendor/autoload.php"</span></span><br><span class="line">         <span class="attribute">colors</span>=<span class="value">"true"</span></span><br><span class="line">         <span class="attribute">convertErrorsToExceptions</span>=<span class="value">"true"</span></span><br><span class="line">         <span class="attribute">convertNoticesToExceptions</span>=<span class="value">"true"</span></span><br><span class="line">         <span class="attribute">convertWarningsToExceptions</span>=<span class="value">"true"</span></span><br><span class="line">         <span class="attribute">processIsolation</span>=<span class="value">"false"</span></span><br><span class="line">         <span class="attribute">stopOnFailure</span>=<span class="value">"false"</span></span><br><span class="line">         <span class="attribute">syntaxCheck</span>=<span class="value">"false"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="title">testsuites</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="title">testsuite</span> <span class="attribute">name</span>=<span class="value">"Application Test Suite"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="title">directory</span>&gt;</span>./tests/<span class="tag">&lt;/<span class="title">directory</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="title">testsuite</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="title">testsuites</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="title">phpunit</span>&gt;</span></span><br></pre></td></tr></table></figure>
<p>執行 <code>./vendor/bin/phpunit</code> ，確認有使用這個設定檔：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">PHPUnit 5.0.8 by Sebastian Bergmann and contributors.&#10;&#10;Time: 13 ms, Memory: 1.75Mb&#10;&#10;No tests executed!</span><br></pre></td></tr></table></figure>
<h2 id="測試實例">測試實例</h2><p>簡單介紹撰寫測試案例的步驟：</p>
<ol>
<li>建立一個 driver 物件，這裡是使用 <code>Selenium2Driver</code> 。</li>
<li>建立一個 session 物件，並透過上面的 driver 物件來操作瀏覽器。</li>
<li>將 session 物件連上指定網址。</li>
<li>從 session 取出 page 物件來操作頁面。</li>
<li>取出 page 物件的狀態或內容來驗證。</li>
</ol>
<p>詳細的測試寫法可以參考 <a href="http://mink.behat.org/en/latest/index.html" target="_blank" rel="external">Mink 官方文件</a>。</p>
<p>以下我示範如何用 Google 來搜尋關鍵字，並驗證搜尋結果有包含我所預期的文字。</p>
<p>先建立 <code>tests</code> 目錄，然後新增一個 <code>tests/GoogleSearchTest.php</code> 檔，內容如下：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="preprocessor">&lt;?php</span></span><br><span class="line"><span class="keyword">use</span> <span class="title">Behat</span>\<span class="title">Mink</span>\<span class="title">Driver</span>\<span class="title">Selenium2Driver</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Behat</span>\<span class="title">Mink</span>\<span class="title">Session</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">GoogleSearchTest</span> <span class="keyword">extends</span> <span class="title">PHPUnit_Framework_TestCase</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">testSearchWithKeyword</span><span class="params">()</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="comment">// 使用 Selenium2Driver 來操作 PhantomJS</span></span><br><span class="line">        <span class="variable">$driver</span> = <span class="keyword">new</span> Selenium2Driver(<span class="string">'phantomjs'</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 建立一個 Session 物件來控制瀏覧器</span></span><br><span class="line">        <span class="variable">$session</span> = <span class="keyword">new</span> Session(<span class="variable">$driver</span>);</span><br><span class="line">        <span class="variable">$session</span>-&gt;start();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 瀏覽 Google 首頁</span></span><br><span class="line">        <span class="variable">$session</span>-&gt;visit(<span class="string">'https://www.google.com'</span>);</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 操作頁面物件來搜尋關鍵字</span></span><br><span class="line">        <span class="variable">$page</span> = <span class="variable">$session</span>-&gt;getPage();</span><br><span class="line">        <span class="variable">$page</span>-&gt;fillField(<span class="string">'q'</span>, <span class="string">'Jace Ju'</span>);</span><br><span class="line">        <span class="variable">$page</span>-&gt;find(<span class="string">'css'</span>, <span class="string">'form'</span>)-&gt;submit();</span><br><span class="line"></span><br><span class="line">        <span class="comment">// 得到搜尋結果後驗證是否包含預期中的文字</span></span><br><span class="line">        <span class="variable">$text</span> = <span class="variable">$page</span>-&gt;getText();</span><br><span class="line">        <span class="variable">$this</span>-&gt;assertContains(<span class="string">'網站製作學習誌'</span>, <span class="variable">$text</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="執行測試">執行測試</h2><p>在執行測試之前，需要先啟動 PhantomJS 。 PhantomJS 提供一個 <code>--webdriver</code> 的選項讓它可以啟用遠端 WebDriver 模式，接收測試程式透過 WebDriver API 傳來的要求。另外因為有時測試的網址會包含 SSL ，所以要用 <code>--ssl-protocol=tlsv1</code> 及 <code>--ignore-ssl-errors=true</code> 來確保 SSL 的操作正常。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">phantomjs --webdriver=<span class="number">4444</span> --ssl-protocol=tlsv1 --ignore-ssl-errors=<span class="literal">true</span></span><br></pre></td></tr></table></figure>
<p>PhantomJS 順利啟動後，就可以另開一個 terminal 視窗來進行測試了：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">./vendor/bin/phpunit</span><br></pre></td></tr></table></figure>
<p>測試無誤的話會出現以下結果：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">PHPUnit 5.0.8 by Sebastian Bergmann and contributors.&#10;&#10;.                                                                   1 / 1 (100%)&#10;&#10;Time: 2.38 seconds, Memory: 4.50Mb&#10;&#10;OK (1 test, 1 assertion)</span><br></pre></td></tr></table></figure>
<h2 id="Page_Objects_模式">Page Objects 模式</h2><p>上面的例子中有個問題：當頁面功能沒有更動，但是 UI 改變時 (例如 DOM 元素或 id/class 名稱) ，我們就必須去更改測試案例的程式碼；而如果同樣的功能在多個測試案例中出現，那麼要改的地方就更多了。所以在實務中，我們會將頁面的功能行為與 UI 細節分離開來，以解決 UI 細節重複的問題；為了這個目標，我們引入 <a href="https://code.google.com/p/selenium/wiki/PageObjects" target="_blank" rel="external">Page Objects</a> 這個模式。</p>
<p>要特別注意的是， Page Objects 模式和 Mink 的 page 物件是兩件事。 Page Objects 模式主要是透過 API 描述頁面的行為，並封裝 UI 細節；而 Mink 的 page 物件則實際上是一個 <code>DocumentElement</code> 物件，主要是用來操作頁面上的元素。換句話說，在 Page Objects 模式中，頁面類別所封裝的 UI 細節，就是用 <code>DocumentElement</code> 物件來操作的。</p>
<h3 id="實作_Page_Objects_模式">實作 Page Objects 模式</h3><p>雖然 Page Objects 模式可以自行實作，但為了省下一些自行撰寫的時間，我特地寫了一個 <a href="https://github.com/jaceju/mink-page-objects" target="_blank" rel="external">goez/mink-page-objects</a> 供大家使用。</p>
<p>首先在專案目錄下執行：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ composer require goez/mink-page-objects --dev</span><br></pre></td></tr></table></figure>
<p>建立一個 <code>tests/bootstrap.php</code> ，內容如下：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="preprocessor">&lt;?php</span></span><br><span class="line"><span class="comment">/** <span class="doctag">@var</span> Composer\Autoload\ClassLoader $loader */</span></span><br><span class="line"><span class="variable">$loader</span> = <span class="keyword">require</span> <span class="keyword">__DIR__</span> . <span class="string">'/../vendor/autoload.php'</span>;</span><br><span class="line"><span class="variable">$loader</span>-&gt;addPsr4(<span class="string">'Google\\'</span>, <span class="keyword">__DIR__</span> . <span class="string">'/Google/'</span>);</span><br></pre></td></tr></table></figure>
<p>將 <code>phpunit.xml</code> 中的 <code>vendor/autoload.php</code> ，改為 <code>tests/bootstrap.php</code> 。</p>
<h3 id="將頁面細節封裝在頁面行為功能裡">將頁面細節封裝在頁面行為功能裡</h3><p>接下來先建立 <code>tests/Google/Home.php</code> 檔；這是 Google 首頁類別，它繼承抽象的 <code>Page</code> 類別，並提供一個 <code>search</code> 方法：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="preprocessor">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">Google</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">use</span> <span class="title">Goez</span>\<span class="title">PageObjects</span>\<span class="title">Page</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Home</span> <span class="keyword">extends</span> <span class="title">Page</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">protected</span> <span class="variable">$parts</span> = [</span><br><span class="line">        <span class="string">'SearchForm'</span> =&gt; [<span class="string">'css'</span> =&gt; <span class="string">'form'</span>],</span><br><span class="line">    ];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">search</span><span class="params">(<span class="variable">$keyword</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> <span class="variable">$this</span>-&gt;getPart(SearchForm::class)</span><br><span class="line">            -&gt;search(<span class="variable">$keyword</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>接下來我建立一個 <code>tests/Google/SearchForm.php</code> ，它主要是封裝搜尋的操作細節：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="preprocessor">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">Google</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">use</span> <span class="title">Goez</span>\<span class="title">PageObjects</span>\<span class="title">Part</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SearchForm</span> <span class="keyword">extends</span> <span class="title">Part</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="comment">/**</span><br><span class="line">     * <span class="doctag">@param</span> $keyword</span><br><span class="line">     * <span class="doctag">@return</span> SearchResult</span><br><span class="line">     * <span class="doctag">@throws</span> \Behat\Mink\Exception\ElementNotFoundException</span><br><span class="line">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">search</span><span class="params">(<span class="variable">$keyword</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="variable">$this</span>-&gt;element-&gt;fillField(<span class="string">'q'</span>, <span class="variable">$keyword</span>);</span><br><span class="line">        <span class="variable">$this</span>-&gt;element-&gt;submit();</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="variable">$this</span>-&gt;createPage(SearchResult::class);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>這裡，我把原來輸入關鍵字並送出表單的 UI 操作，封裝在 <code>search</code> 方法中，並回傳一個搜尋結果頁面物件。</p>
<p>再建立 <code>tests/Google/SearchResult.php</code> 檔，它主要是封裝搜尋結果頁。</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="preprocessor">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">Google</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">use</span> <span class="title">Goez</span>\<span class="title">PageObjects</span>\<span class="title">Page</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">SearchResult</span> <span class="keyword">extends</span> <span class="title">Page</span></span><br><span class="line"></span>&#123;</span><br><span class="line"></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>最後就可以把原來的測試案例改用新的頁面類別來重寫了：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="preprocessor">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">use</span> <span class="title">Behat</span>\<span class="title">Mink</span>\<span class="title">Driver</span>\<span class="title">Selenium2Driver</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Behat</span>\<span class="title">Mink</span>\<span class="title">Session</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Goez</span>\<span class="title">PageObjects</span>\<span class="title">Context</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Goez</span>\<span class="title">PageObjects</span>\<span class="title">Helper</span>\<span class="title">PhantomJSRunner</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Google</span>\<span class="title">Home</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">GoogleSearchTest</span> <span class="keyword">extends</span> <span class="title">PHPUnit_Framework_TestCase</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="comment">// 自動啟動 phantomjs</span></span><br><span class="line">    <span class="keyword">use</span> <span class="title">PhantomJSRunner</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">testSearchWithKeyword</span><span class="params">()</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="variable">$driver</span> = <span class="keyword">new</span> Selenium2Driver(<span class="string">'phantomjs'</span>);</span><br><span class="line"></span><br><span class="line">        <span class="variable">$session</span> = <span class="keyword">new</span> Session(<span class="variable">$driver</span>);</span><br><span class="line">        <span class="variable">$session</span>-&gt;start();</span><br><span class="line"></span><br><span class="line">        <span class="variable">$context</span> = <span class="keyword">new</span> Context(<span class="variable">$session</span>, [</span><br><span class="line">            <span class="string">'baseUrl'</span> =&gt; <span class="string">'https://www.google.com'</span>,</span><br><span class="line">        ]);</span><br><span class="line"></span><br><span class="line">        <span class="variable">$context</span>-&gt;createPage(Home::class)</span><br><span class="line">            -&gt;open()</span><br><span class="line">            -&gt;search(<span class="string">'Jace Ju'</span>)</span><br><span class="line">            -&gt;shouldContainText(<span class="string">'網站製作學習誌'</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>這麼一來，在測試案例中就可以清楚地用頁面物件的行為去描述實際的需求，而不是落在操作 UI 的思維裡。讓外部的測試案例可以用更語意化的方式來使用這個類別，是一種 <code>Tell Don&#39;t Ask</code> 的實現。</p>
<h2 id="總結">總結</h2><p>雖然舊專案可能難以做到單元測試，但我們可以先利用 Web 測試來驗證它已經存在的行為；而在 Web 測試中可以透過程式來控制瀏覽器，達到自動化測試的目的。在撰寫測試案例時，最重要的是對需求的描述，而不是 UI 操作的細節；因此可以用 Page Objects 模式來封裝 UI 細節，讓頁面物件提供有語意化的行為操作方式。</p>
<p>希望這個介紹能幫助大家對 Web 測試有基本的瞭解，當然在實務上可能會遇到的問題會更複雜；有機會的話我會另文分享自己在實務上遇到的問題，也歡迎大家提供不同的見解。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>如果你面對的是以前舊有的 PHP 程式，是時候負起一些責任了。</p>
<p>我知道它改起來很痛苦，一堆不良的 PHP 程式習慣都阻礙你的修正；使得每次調整功能時，到底改得對不對，得要等到上線才知道。想要重寫一個新版本，但太多的實作細節你不清楚；也沒有最新的規格文件，讓你無法為新版本做出功能無誤的保證。</p>
<p>現在你唯一擁有的，就是已經在線上運作的程式邏輯；雖然它可能還有 bug ，但至少大多數的功能是通過使用者驗證的。那麼先為它買個保險吧！確保之後的修改不會影響到其他功能的正常運作；而最直接的方式，就是把目前程式邏輯所呈現的結果或是使用者的操作，寫成自動化 Web 測試。</p>
<p>建立 Web 測試的方法有很多，這裡我將介紹我在實務上使用 <a href="https://phpunit.de/">PHPUnit</a> 加上 <a href="http://mink.behat.org/en/latest/">Mink</a> 搭配 <a href="http://phantomjs.org/">PhantomJS</a> 的方法。</p>]]>
    
    </summary>
    
      <category term="Selenium" scheme="http://jaceju.net/tags/Selenium/"/>
    
      <category term="Web 開發" scheme="http://jaceju.net/tags/Web-%E9%96%8B%E7%99%BC/"/>
    
      <category term="測試" scheme="http://jaceju.net/tags/%E6%B8%AC%E8%A9%A6/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[這樣寫測試錯了嗎？]]></title>
    <link href="http://jaceju.net/2015/10/14/what-is-test/"/>
    <id>http://jaceju.net/2015/10/14/what-is-test/</id>
    <published>2015-10-14T06:01:34.000Z</published>
    <updated>2015-10-14T06:30:17.000Z</updated>
    <content type="html"><![CDATA[<p>這篇文章的出現，主要是因為尤川豪的 <a href="http://blog.turn.tw/?p=2741" target="_blank" rel="external">PHP與撰寫測試入門</a> 一文，然後在 Facebook 上也有相關討論：<a href="https://www.facebook.com/groups/199493136812961/permalink/871965886232346/" target="_blank" rel="external">連結一</a>、<a href="https://www.facebook.com/groups/199493136812961/permalink/872137862881815/" target="_blank" rel="external">連結二</a>。</p>
<p>只是當我提出測試不該有邏輯時，討論就往該不該有 assertion library 偏過去了。我發現我太執著在這個點上，沒有正確地自己的想法傳達清楚。</p>
<p>所以接下來我會從幾個層面來討論測試該怎寫，將我想表達的觀念重新整理一下。</p>
<a id="more"></a>
<h2 id="為什麼這樣寫測試有問題">為什麼這樣寫測試有問題</h2><p>起因就是原文的這個範例，這是它的待測目標：</p>
<figure class="highlight php"><figcaption><span>// simple_add_test.php</span></figcaption><table><tr><td class="code"><pre><span class="line"><span class="preprocessor">&lt;?php</span></span><br><span class="line"><span class="variable">$arg1</span> = <span class="variable">$_GET</span>[<span class="string">'arg1'</span>];</span><br><span class="line"><span class="variable">$arg2</span> = <span class="variable">$_GET</span>[<span class="string">'arg2'</span>];</span><br><span class="line"><span class="variable">$return</span> = (int)<span class="variable">$arg1</span> + (int)<span class="variable">$arg2</span>;</span><br><span class="line"><span class="keyword">echo</span> <span class="variable">$return</span>;</span><br></pre></td></tr></table></figure>
<p>測試程式是這樣寫的：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="preprocessor">&lt;?php</span></span><br><span class="line"><span class="comment">// 把整個模擬測試流程寫成一個function，方便重用</span></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">execute</span><span class="params">(<span class="variable">$arg1</span>, <span class="variable">$arg2</span>)</span></span>&#123;</span><br><span class="line">    <span class="variable">$_GET</span>[<span class="string">'arg1'</span>] = <span class="variable">$arg1</span>;</span><br><span class="line">    <span class="variable">$_GET</span>[<span class="string">'arg2'</span>] = <span class="variable">$arg2</span>;</span><br><span class="line">    ob_start();</span><br><span class="line">    <span class="keyword">include</span> <span class="string">'simple_add.php'</span>;</span><br><span class="line">    <span class="keyword">return</span> ob_get_clean();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="variable">$result</span> = execute(<span class="number">1</span>, <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span>(<span class="variable">$result</span>==<span class="number">2</span>)&#123;</span><br><span class="line">    <span class="keyword">echo</span> <span class="string">"simple_add_test passed\n"</span>;</span><br><span class="line">&#125;<span class="keyword">else</span>&#123;</span><br><span class="line">    <span class="keyword">echo</span> <span class="string">"simple_add_test failed\n"</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="variable">$result</span> = execute(<span class="number">1</span>, <span class="number">2</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span>(<span class="variable">$result</span>==<span class="number">3</span>)&#123;</span><br><span class="line">    <span class="keyword">echo</span> <span class="string">"simple_add_test passed\n"</span>;</span><br><span class="line">&#125;<span class="keyword">else</span>&#123;</span><br><span class="line">    <span class="keyword">echo</span> <span class="string">"simple_add_test failed\n"</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="variable">$result</span> = execute(<span class="number">1</span>, -<span class="number">1</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span>(<span class="variable">$result</span>==<span class="number">0</span>)&#123;</span><br><span class="line">    <span class="keyword">echo</span> <span class="string">"simple_add_test passed\n"</span>;</span><br><span class="line">&#125;<span class="keyword">else</span>&#123;</span><br><span class="line">    <span class="keyword">echo</span> <span class="string">"simple_add_test failed\n"</span>;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="variable">$result</span> = execute(<span class="number">0</span>, -<span class="number">1</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span>(<span class="variable">$result</span>==-<span class="number">1</span>)&#123;</span><br><span class="line">    <span class="keyword">echo</span> <span class="string">"simple_add_test passed\n"</span>;</span><br><span class="line">&#125;<span class="keyword">else</span>&#123;</span><br><span class="line">    <span class="keyword">echo</span> <span class="string">"simple_add_test failed\n"</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// 注意：後面的一大串if/else以及echo敘述，其實可以再包成function來簡化</span></span><br></pre></td></tr></table></figure>
<p>如果這篇文章會繼續提到更嚴謹的做法，也許就不會有後續的討論了。事實上以往我在教測試時，也差不多是這樣開始的。只是整篇讀完後，我發現其實沒有任何後續 (雖然後來作者有提到會再寫) ，這才讓我開始擔心起來。</p>
<p>原本我想提的主要有兩點：</p>
<ol>
<li><p>這只是測試的初步觀念，但不是嚴謹的作法，更不是一種創意。</p>
<p>當然在原文的範例裡，用 <code>if ==</code> 來驗證其實是沒有問題的；只是當你信任這個驗證方式，測試也通過了，天下就太平了嗎？如同作者自己提到的，我們有機會在自己寫的 assertion 中犯錯；而當你發現錯誤其實是在自己的驗證程式時，我想後續的除錯成本就不在話下了。這也是為什麼我一直強調就算是要用自己的驗證機制，也應該對它進行驗證。</p>
<p>至於測試中的 assertion 寫錯，跟該不該用第三方 assertion library 其實是兩回事，這點後面我會說為什麼。</p>
</li>
<li><p>測試案例不該寫在一起，這樣會讓初學者覺得寫測試反而導致更多問題。</p>
<p>再次強調，這個範例沒有問題不表示可以用這個做法通吃所有的狀況。測試案例彼此應該是獨立的，也就是我們要讓每個測試案例在執行時都不受到前一個測試案例的影響，讓我們能單純驗證目標對象的邏輯在受控環境的結果。</p>
<p>一個好的測試案例應該只測一件事，而一件事通常是只會測試目標邏輯一次。如果今天 <code>execute()</code> 的邏輯中會有可能影響下一次執行結果的狀況，那麼這樣的寫法就有可能發生連續的錯誤，而你就很難確定錯誤是在哪個測試案例中開始的。</p>
</li>
</ol>
<h2 id="測試裡不該有邏輯">測試裡不該有邏輯</h2><p>有朋友說，所以這些驗證工具裡面也是有 <code>if</code> 及 <code>throw exception</code> 的邏輯呀！怎麼會說測試裡不該有邏輯？</p>
<p>這裡確實是我表達得不夠好的點，也是討論上我過於執著的地方。事實上在 assertion 中的這些邏輯，是被封裝起來的，對我們來說就是不需要在意的邏輯，我們只要知道它是具有可靠性的黑盒子即可。所以最前面原文的那個例子，在 assertion 沒有被封裝的狀況下， <code>if</code> 嚴格來說就必須算是測試案例裡的邏輯。</p>
<p>我瞭解這解釋有點過於嚴格了，但其實我有強力阻擋的理由：當初學者認為這裡可以用 <code>if</code> 來判斷時，通常就會毫無限制地使用了。這也就為什麼我們常說：雖然你可以這麼做，但你不該這麼做。</p>
<p>還是要再次強調，因為範例中的判斷邏輯只是簡單的 <code>==</code> ，所以看不出會有什麼影響；但這只是為了說明測試的原理，不代表實務上可以這麼做。</p>
<p>只是到底什麼是測試裡的邏輯？舉例來說 <code>try...catch</code> 就不該出現在測試案例裡，接收異常的機制應該是由測試框架來處理，我們要確保的是待測試邏輯「一定會拋出預期的異常」，而 <code>try...catch</code> 就有不確定的意味存在。</p>
<p>測試的目的就是確保該發生的一定要發生，而不是去判斷某件事會不會發生。如果測試案例中包含了不確定性的邏輯，那就不叫測試了。</p>
<h2 id="測試只是輔助">測試只是輔助</h2><p>有朋友提到 TDD 循環，其實我認為是在這個討論中有些失焦了。但不是說提出 TDD 是不對的，事實上我非常認同；只是我在這裡想討論的是「測試的寫法」，換句話說，就是在紅燈之前你怎麼寫測試案例。</p>
<p>TDD 有一個很重要的觀念：確保我們想清楚規格了才去動手。</p>
<p>我們寫測試通常是為了有測試而寫，只是想要驗證到底我們寫出來的程式碼對不對。但真正好的測試案例應該是以測試程式來描述規格。</p>
<p>回頭看一下原文範例的寫法：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="variable">$result</span> = execute(<span class="number">1</span>, <span class="number">1</span>);</span><br><span class="line"></span><br><span class="line"><span class="keyword">if</span>(<span class="variable">$result</span>==<span class="number">2</span>)&#123;</span><br><span class="line">    <span class="keyword">echo</span> <span class="string">"simple_add_test passed\n"</span>;</span><br><span class="line">&#125;<span class="keyword">else</span>&#123;</span><br><span class="line">    <span class="keyword">echo</span> <span class="string">"simple_add_test failed\n"</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>當看到這個測試案例時，你知道原來的邏輯是做什麼的嗎？我想除了程式原作者之外，應該是沒人知道待測邏輯要做什麼吧？</p>
<p>「一個測試案例應該清楚描述待測對象的用途」，我們可以把測試案例想像成是 production code 的說明書，讓看到測試案例的人一眼就能明白該怎麼去使用待測對象。</p>
<p>之所以 assertion library 會封裝驗證的邏輯，除了提供可靠性之外，另一個目的就是提供較為語意化的 API 。例如 PHPUnit 的 <code>assertEquals</code> 、 <code>assertContains</code> 等，這些語義化的 API 很清楚地告訴我們要驗證的是什麼；如果用錯了，很有可能就是我們其實對需求理解得不夠明確。</p>
<p>那麼測試怎麼去描述需求呢？像是 JavaScript 的 mocha 框架就可以讓我們這樣寫：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line">describe(<span class="string">'Array'</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  describe(<span class="string">'#indexOf()'</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">    it(<span class="string">'should return -1 when the value is not present'</span>, <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">      [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>].indexOf(<span class="number">5</span>).should.equal(-<span class="number">1</span>);</span><br><span class="line">      [<span class="number">1</span>,<span class="number">2</span>,<span class="number">3</span>].indexOf(<span class="number">0</span>).should.equal(-<span class="number">1</span>);</span><br><span class="line">    &#125;);</span><br><span class="line">  &#125;);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>像這樣就能清楚地表達陣列的 <code>indexOf</code> 方法可以怎麼用。你可以發現裡面其實沒有邏輯，因為「需求的 what 是不會有邏輯的，只有 how 才有邏輯。」</p>
<p>如果是原來的例子，如果萬不得已真的要自己寫驗證工具，可以寫成：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="preprocessor">&lt;?php</span> <span class="comment">// my_test_tools.php</span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">runScriptWithQuery</span><span class="params">(<span class="variable">$script</span>, <span class="variable">$queryParams</span> = [])</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="variable">$_GET</span> = <span class="variable">$queryParams</span>;</span><br><span class="line">    ob_start();</span><br><span class="line">    <span class="keyword">require</span> <span class="variable">$script</span>;</span><br><span class="line">    <span class="keyword">return</span> ob_get_clean();</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">assertEquals</span><span class="params">(<span class="variable">$expected</span>, <span class="variable">$actual</span>)</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable">$expected</span> == <span class="variable">$actual</span>) &#123;</span><br><span class="line">        <span class="keyword">echo</span> <span class="string">"Passed\n"</span>;</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="keyword">echo</span> <span class="string">"Failed asserting that $actual matches expected $expected.\n"</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>用法會是這樣：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="preprocessor">&lt;?php</span> <span class="comment">// simple_add_test_cases.php</span></span><br><span class="line"><span class="keyword">require</span> <span class="string">'my_test_tools.php'</span>;</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">function</span> <span class="title">it_should_passed_with_1_add_2_equals_3</span><span class="params">()</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="comment">// Arrange</span></span><br><span class="line">    <span class="variable">$queryParams</span> = [<span class="string">'arg1'</span> =&gt; <span class="number">1</span>, <span class="string">'arg2'</span> =&gt; <span class="number">2</span>];</span><br><span class="line">    <span class="variable">$expected</span> = <span class="number">3</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Act</span></span><br><span class="line">    <span class="variable">$actual</span> = runScriptWithQuery(<span class="string">'simple_add_test.php'</span>, <span class="variable">$queryParams</span>);</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Assert</span></span><br><span class="line">    assertEquals(<span class="variable">$expected</span>, <span class="variable">$actual</span>);</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">it_should_passed_with_1_add_2_equals_3();</span><br></pre></td></tr></table></figure>
<p>註：避免跟原來的 <code>simple_add_test.php</code> 混淆，測試程式我改名為 <code>simple_add_test_cases.php</code> 。</p>
<p>這樣就可以很清楚的知道我們要測試的是 <code>simple_add_test.php</code> ，它的用途也是一清二楚。對看測試程式的人來說，它就是一種「對需求的描述」而不是「對程式邏輯的測試」。</p>
<h2 id="該不該用自動化測試框架">該不該用自動化測試框架</h2><p>在討論中 Ricky 提到沒有 PHPUnit 是不是就不能做測試了？這樣在沒有 PHPUnit 之前的程式碼不就不合格了？當然不是，還是強調一點：用自己寫的驗證機制是沒問題的，只要你能確保它驗證出來的結果是可信任的。</p>
<p>例如 c9s 提到 PHP 原始碼裡的一支<a href="https://github.com/php/php-src/blob/master/tests/lang/004.phpt" target="_blank" rel="external">測試</a>：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">--TEST--&#10;Simple If/Else Test&#10;--FILE--&#10;&#60;?php&#10;$a=1;&#10;if($a==0) &#123;&#10;    echo &#34;bad&#34;;&#10;&#125; else &#123;&#10;    echo &#34;good&#34;;&#10;&#125;&#10;?&#62;&#10;--EXPECT--&#10;good</span><br></pre></td></tr></table></figure>
<p>事實上就是用到了 PHP 自帶的測試程式 (<a href="https://github.com/php/php-src/blob/master/run-tests.php" target="_blank" rel="external"><code>run-tests.php</code></a>) ，因為在測試 PHP 語法這個需求當初並沒有適合的自動化測試框架，所以 PHP 需要自己建立一個。</p>
<p>註：要注意這個測試的對象是 <code>if..else</code> 這個語法，並不是指 <code>if..else</code> 就是用來做驗證的機制。</p>
<p>那為什麼我們還是需要 PHPUnit 或其他自動化測試框架？除了公認的可靠性之外，主要是它提供了我們在測試上很多重要的機制：</p>
<ol>
<li>自動化驗證：你可以專注在寫測試，該批次執行的事情框架都會幫你處理好。</li>
<li>隔離測試：模擬待測試邏輯中被隔離出去的相依物件。</li>
<li>程式碼測試涵蓋率：確保測試有測到待測邏輯。</li>
<li>與其他開發工具的整合：例如 IDE 、 CI Service 等，以造就完整的開發生態圈。</li>
</ol>
<p>這些都是真正實務上經過千錘百鍊後的特色，也適用在絕大多數的 PHP 專案上。如果專案沒有特別的需求，當然是優先使用別人寫好自動化測試框架。</p>
<h2 id="結論">結論</h2><p>我一直很強調測試的重要性，但並不是要讓它變成我們開發上的負擔，而是用來輔助我們很快地去驗證我們的程式碼是否符合了需求。也就是說我們不要為了只是要驗證程式的正確性而去寫測試，而是應該讓測試來協助我們去理解程式想要滿足什麼需求。</p>
<p>工具是手段沒錯，但它的目的就是要幫我們很快地去把一件事做好。今天如果自己寫的工具可以做到這件事，那當然是很好的。但如果已經有更好的工具，為什麼不直接使用呢？當然工具會出錯，但 open source 的好處就是讓我們有機會去修補它們。當工具的公正性已經接受過時間的考驗以及許多案例的檢驗，我想那都會比你花時間自己弄一套來得嚴謹很多。</p>
<p>當然每個人心中對測試有自己的定見，我很難說服每一個人。但還是希望讓大家可以重新思考一下測試的本質，在正確的地方轉彎。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>這篇文章的出現，主要是因為尤川豪的 <a href="http://blog.turn.tw/?p=2741">PHP與撰寫測試入門</a> 一文，然後在 Facebook 上也有相關討論：<a href="https://www.facebook.com/groups/199493136812961/permalink/871965886232346/">連結一</a>、<a href="https://www.facebook.com/groups/199493136812961/permalink/872137862881815/">連結二</a>。</p>
<p>只是當我提出測試不該有邏輯時，討論就往該不該有 assertion library 偏過去了。我發現我太執著在這個點上，沒有正確地自己的想法傳達清楚。</p>
<p>所以接下來我會從幾個層面來討論測試該怎寫，將我想表達的觀念重新整理一下。</p>]]>
    
    </summary>
    
      <category term="PHP" scheme="http://jaceju.net/tags/PHP/"/>
    
      <category term="測試" scheme="http://jaceju.net/tags/%E6%B8%AC%E8%A9%A6/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[邁向 PHP 重構之路 - 以 Laravel 程式碼片段為例]]></title>
    <link href="http://jaceju.net/2015/10/05/simple-refatoring-example-01/"/>
    <id>http://jaceju.net/2015/10/05/simple-refatoring-example-01/</id>
    <published>2015-10-05T04:53:48.000Z</published>
    <updated>2015-10-05T05:01:16.000Z</updated>
    <content type="html"><![CDATA[<p>來上 TDD 課的學員問到一個 Laravel 程式碼重構的問題，這裡簡單地做分享。未來如果有好的實戰範例，這系列就會延續下去。</p>
<a id="more"></a>
<h2 id="開始重構">開始重構</h2><p>當然重構前，我們必須先有測試做保障。在每個步驟完成後，我們都應該確保修改後的程式碼能通過測試的驗證。</p>
<p>接下來開始重構，這是原本的程式碼：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Step 0</span></span><br><span class="line"><span class="keyword">if</span> (<span class="variable">$errorRedirectViewType</span> == <span class="string">'create'</span>) &#123;</span><br><span class="line">    <span class="keyword">return</span> Redirect::route(<span class="keyword">self</span>::__module . <span class="string">'.'</span> . <span class="keyword">self</span>::__function . <span class="string">'.'</span> . <span class="variable">$errorRedirectViewType</span>)</span><br><span class="line">        -&gt;with(<span class="string">'message'</span>, <span class="string">'一樣的 message'</span>)</span><br><span class="line">        -&gt;withInput(<span class="variable">$allInput</span>);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="keyword">return</span> Redirect::route(<span class="keyword">self</span>::__module . <span class="string">'.'</span> . <span class="keyword">self</span>::__function . <span class="string">'.'</span> . <span class="variable">$errorRedirectViewType</span>, [<span class="string">'id'</span> =&gt; <span class="variable">$allInput</span>[<span class="string">'id'</span>]])</span><br><span class="line">        -&gt;with(<span class="string">'message'</span>, <span class="string">'一樣的 message'</span>)</span><br><span class="line">        -&gt;withInput(<span class="variable">$allInput</span>);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>第一步我們引入一個 <code>$redirect</code> 變數：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Step 1</span></span><br><span class="line"><span class="variable">$redirect</span> = <span class="keyword">null</span>;</span><br><span class="line"><span class="keyword">if</span> (<span class="variable">$errorRedirectViewType</span> == <span class="string">'create'</span>) &#123;</span><br><span class="line">    <span class="variable">$redirect</span> = Redirect::route(<span class="keyword">self</span>::__module . <span class="string">'.'</span> . <span class="keyword">self</span>::__function . <span class="string">'.'</span> . <span class="variable">$errorRedirectViewType</span>)</span><br><span class="line">        -&gt;with(<span class="string">'message'</span>, <span class="string">'一樣的 message'</span>)</span><br><span class="line">        -&gt;withInput(<span class="variable">$allInput</span>);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="variable">$redirect</span> = Redirect::route(<span class="keyword">self</span>::__module . <span class="string">'.'</span> . <span class="keyword">self</span>::__function . <span class="string">'.'</span> . <span class="variable">$errorRedirectViewType</span>, [<span class="string">'id'</span> =&gt; <span class="variable">$allInput</span>[<span class="string">'id'</span>]])</span><br><span class="line">        -&gt;with(<span class="string">'message'</span>, <span class="string">'一樣的 message'</span>)</span><br><span class="line">        -&gt;withInput(<span class="variable">$allInput</span>);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="variable">$redirect</span>;</span><br></pre></td></tr></table></figure>
<p>第二步我們把共用的部份移出 <code>if...else</code> 外：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Step 2</span></span><br><span class="line"><span class="variable">$redirect</span> = <span class="keyword">null</span>;</span><br><span class="line"><span class="keyword">if</span> (<span class="variable">$errorRedirectViewType</span> == <span class="string">'create'</span>) &#123;</span><br><span class="line">    <span class="variable">$redirect</span> = Redirect::route(<span class="keyword">self</span>::__module . <span class="string">'.'</span> . <span class="keyword">self</span>::__function . <span class="string">'.'</span> . <span class="variable">$errorRedirectViewType</span>);</span><br><span class="line">&#125; <span class="keyword">else</span> &#123;</span><br><span class="line">    <span class="variable">$redirect</span> = Redirect::route(<span class="keyword">self</span>::__module . <span class="string">'.'</span> . <span class="keyword">self</span>::__function . <span class="string">'.'</span> . <span class="variable">$errorRedirectViewType</span>, [<span class="string">'id'</span> =&gt; <span class="variable">$allInput</span>[<span class="string">'id'</span>]]);</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">return</span> <span class="variable">$redirect</span></span><br><span class="line">        -&gt;with(<span class="string">'message'</span>, <span class="string">'一樣的 message'</span>)</span><br><span class="line">        -&gt;withInput(<span class="variable">$allInput</span>);</span><br></pre></td></tr></table></figure>
<p>第三步把 <code>if...else</code> 提煉成 <code>redirectByViewType</code> 方法：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Step 3</span></span><br><span class="line"><span class="comment">// Extracted method</span></span><br><span class="line"><span class="keyword">protected</span> <span class="function"><span class="keyword">function</span> <span class="title">redirectByViewType</span><span class="params">(<span class="variable">$errorRedirectViewType</span>, <span class="variable">$id</span>)</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="variable">$redirect</span> = <span class="keyword">null</span>;</span><br><span class="line">    <span class="keyword">if</span> (<span class="variable">$errorRedirectViewType</span> == <span class="string">'create'</span>) &#123;</span><br><span class="line">        <span class="variable">$redirect</span> = Redirect::route(<span class="keyword">self</span>::__module . <span class="string">'.'</span> . <span class="keyword">self</span>::__function . <span class="string">'.'</span> . <span class="variable">$errorRedirectViewType</span>);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="variable">$redirect</span> = Redirect::route(<span class="keyword">self</span>::__module . <span class="string">'.'</span> . <span class="keyword">self</span>::__function . <span class="string">'.'</span> . <span class="variable">$errorRedirectViewType</span>, [<span class="string">'id'</span> =&gt; <span class="variable">$id</span>]);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable">$redirect</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>然後改用新的 <code>redirectByViewType</code> 方法：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Step 3</span></span><br><span class="line"><span class="variable">$redirect</span> = <span class="variable">$this</span>-&gt;redirectByViewType(<span class="variable">$errorRedirectViewType</span>, <span class="variable">$allInput</span>[<span class="string">'id'</span>]);</span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="variable">$redirect</span></span><br><span class="line">        -&gt;with(<span class="string">'message'</span>, <span class="string">'一樣的 message'</span>)</span><br><span class="line">        -&gt;withInput(<span class="variable">$allInput</span>);</span><br></pre></td></tr></table></figure>
<p>至於第二步到第三步要不要做，就看我們有沒有 reuse 這段邏輯的需求；但一般我會做，因為程式碼看起來好讀，後面也可以再做其他重構。</p>
<p>第四步就可以把原來的 <code>$redirect</code> 拿掉，因為不需要了。</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Step 4</span></span><br><span class="line"><span class="keyword">return</span> <span class="variable">$this</span>-&gt;redirectByViewType(<span class="variable">$errorRedirectViewType</span>, <span class="variable">$allInput</span>[<span class="string">'id'</span>])</span><br><span class="line">        -&gt;with(<span class="string">'message'</span>, <span class="string">'一樣的 message'</span>)</span><br><span class="line">        -&gt;withInput(<span class="variable">$allInput</span>);</span><br></pre></td></tr></table></figure>
<p>這種先引入一個臨時變數讓重構好進行的方式，是很常見的。而什麼時候應該需要使用這個技巧？這就要多累積經驗。通常你可以想像一下重構後的程式碼，大致與重構前會有什麼樣的差異，再判斷是否需要引用一個臨時變數。</p>
<p>第五步，我們把 <code>redirectByViewType</code> 重複的程式碼再引用一個解釋用的變數 <code>$routeName</code> ：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="comment">// Step 5</span></span><br><span class="line"><span class="keyword">protected</span> <span class="function"><span class="keyword">function</span> <span class="title">redirectByViewType</span><span class="params">(<span class="variable">$errorRedirectViewType</span>, <span class="variable">$id</span>)</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="variable">$redirect</span> = <span class="keyword">null</span>;</span><br><span class="line">    <span class="variable">$routeName</span> = <span class="keyword">self</span>::__module . <span class="string">'.'</span> .</span><br><span class="line">               <span class="keyword">self</span>::__function . <span class="string">'.'</span> .</span><br><span class="line">               <span class="variable">$errorRedirectViewType</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (<span class="variable">$errorRedirectViewType</span> == <span class="string">'create'</span>) &#123;</span><br><span class="line">        <span class="variable">$redirect</span> = Redirect::route(<span class="variable">$routeName</span>);</span><br><span class="line">    &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">        <span class="variable">$redirect</span> = Redirect::route(<span class="variable">$routeName</span>, [<span class="string">'id'</span> =&gt; <span class="variable">$id</span>]);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="variable">$redirect</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>這樣程式碼就更容易被理解了。</p>
<h2 id="後記">後記</h2><p>希望這個小例子可以讓大家瞭解到，實戰中的重構其實是很簡單的。它就是在不更改原有邏輯的狀態下，一步一步讓你的程式碼變得更易讀也更易維護。</p>
<p>當初因為是臨時示範給學員看，所以並沒有特別加上測試，也沒有用 PhpStorm 來協助重構；結果後來我發現在 extract method 時，忘了把 <code>$allInput[&#39;id&#39;]</code> 帶到 <code>redirectByViewType</code> 裡面。</p>
<p>這就是一種工程師很容易忽略的盲點，就是太容易相信自己的想法，而不是真正去驗證它。在沒有測試和工具的輔助下，千萬要特別小心這種小錯誤。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>來上 TDD 課的學員問到一個 Laravel 程式碼重構的問題，這裡簡單地做分享。未來如果有好的實戰範例，這系列就會延續下去。</p>]]>
    
    </summary>
    
      <category term="Laravel" scheme="http://jaceju.net/tags/Laravel/"/>
    
      <category term="Refatoring" scheme="http://jaceju.net/tags/Refatoring/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[在 Laravel 上用 MailCatcher 發送測試信件]]></title>
    <link href="http://jaceju.net/2015/07/29/laravel-mailcatcher/"/>
    <id>http://jaceju.net/2015/07/29/laravel-mailcatcher/</id>
    <published>2015-07-29T07:22:29.000Z</published>
    <updated>2015-07-29T09:03:09.000Z</updated>
    <content type="html"><![CDATA[<p>雖然 Laravel 在寄送測試信件上提供了 <a href="http://www.mailgun.com/" target="_blank" rel="external">Mailgun</a> 這個服務的串接方式，不過如果能夠在 Homestead 就可以直接測試是更棒的選擇；而 <a href="http://mailcatcher.me/" target="_blank" rel="external">MailCatcher</a> 剛好就提供這樣的功能，它能啟動一個 SMTP 模擬服務，並且讓我們透過 Web 介面來查看信件是否有被發送出來。</p>
<blockquote>
<p>註：類似的工類還有用 Go 寫的 <a href="https://github.com/mailhog/MailHog" target="_blank" rel="external">MailHog</a> ，據說速度更快；雖然我還沒試過，但我想用法應該是相同的。</p>
</blockquote>
<p>以下就簡單介紹一下如何在 Laravel 5.1 上使用 MailCatcher 。</p>
<a id="more"></a>
<h2 id="安裝_MailCatcher">安裝 MailCatcher</h2><p>這裡我們用 Homestead 示範，先把 Homestead 的 port 1080/1025 導到本機的 port 1080/1025 ，方便稍後在本機操作。執行：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ homestead edit</span><br></pre></td></tr></table></figure>
<blockquote>
<p>註： <code>$</code>  為提示字元，不需要輸入。</p>
</blockquote>
<p>然後加入：</p>
<figure class="highlight"><table><tr><td class="code"><pre><span class="line">ports:&#10;    - send: 1080&#10;      to: 1080&#10;    - send: 1025&#10;      to: 1025</span><br></pre></td></tr></table></figure>
<p>接著進入 Homestead ：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ homestead up</span><br><span class="line">$ homestead ssh</span><br></pre></td></tr></table></figure>
<p>確認 Ruby 環境是安裝好的：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ ruby -v</span><br><span class="line">ruby <span class="number">2.1</span>.<span class="number">2</span>p95 (<span class="number">2014</span>-<span class="number">05</span>-<span class="number">08</span>) [x86_64-linux-gnu]</span><br><span class="line"></span><br><span class="line">$ gem -v</span><br><span class="line"><span class="number">2.2</span>.<span class="number">2</span></span><br></pre></td></tr></table></figure>
<p>然後安裝 MailCatcher ：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ sudo gem install mailcatcher --no-ri --no-rdoc</span><br></pre></td></tr></table></figure>
<p>安裝完成後，直接執行 <code>mailcatcher</code> ：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ mailcatcher --ip <span class="number">0.0</span>.<span class="number">0.0</span></span><br><span class="line">Starting MailCatcher</span><br><span class="line">==&gt; smtp://<span class="number">0.0</span>.<span class="number">0.0</span>:<span class="number">1025</span></span><br><span class="line">==&gt; http://<span class="number">0.0</span>.<span class="number">0.0</span>:<span class="number">1080</span></span><br><span class="line">*** MailCatcher runs as a daemon by default. Go to the web interface to quit.</span><br></pre></td></tr></table></figure>
<p>這樣 MailCatcher 的 port 1025 就會監聽 SMTP 請求，而 port 1080 就會是它的 Web 管理介面。</p>
<p>打開本機的瀏覽器，瀏覽 <code>http://127.0.0.1:1080</code> ，應該就會看到 MailCatcher 的 Web 管理介面：</p>
<p><img src="/resources/laravel-mailcatcher/mailcatcher-web-ui.png" alt="MailCatcher Web UI"></p>
<h2 id="修改_Laravel_Mail_設定">修改 Laravel Mail 設定</h2><p>這裡我假設你已經建立一個 Laravel 5.1 專案了，所以修改 <code>.env</code> 中的 <code>MAIL_HOST</code> 與 <code>MAIL_PORT</code> 即可：</p>
<figure class="highlight ini"><table><tr><td class="code"><pre><span class="line"><span class="setting">MAIL_HOST=<span class="value"><span class="number">127.0</span>.<span class="number">0.1</span></span></span></span><br><span class="line"><span class="setting">MAIL_PORT=<span class="value"><span class="number">1025</span></span></span></span><br></pre></td></tr></table></figure>
<p>其他一切都可以不需要改動。</p>
<h2 id="測試發送信件">測試發送信件</h2><p>我們可以直接用 tinker 來測試是否能夠發送信件，執行：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ php artisan tinker</span><br><span class="line">Psy Shell v0.<span class="number">5.2</span> (PHP <span class="number">5.6</span>.<span class="number">8</span> — cli) by Justin Hileman</span><br><span class="line">&gt;&gt;&gt;</span><br></pre></td></tr></table></figure>
<p>然後輸入：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Mail::raw(&#39;This is a test mail&#39;, function ($message) &#123;&#10;$message-&#62;subject(&#39;Test&#39;);&#10;$message-&#62;from(&#39;laravel@example.com&#39;, &#39;Laravel&#39;);&#10;$message-&#62;to(&#39;user@example.com&#39;);&#10;&#125;);</span><br></pre></td></tr></table></figure>
<p>結果應該會回傳 <code>1</code> ，然後你可以回到瀏覽器查看 MailCatcher 是否有收到這封信，結果應該會如下圖所示：</p>
<p><img src="/resources/laravel-mailcatcher/mailcatcher-result.png" alt="MailCatcher Result"></p>
<h2 id="與其他測試框架的整合">與其他測試框架的整合</h2><p>MailCatcher 可以跟 PHP 的自動化測試框架做很好的整合，詳情可以參考以下文章：</p>
<ul>
<li>PHPUnit - <a href="http://codeception.com/12-15-2013/testing-emails-in-php.html" target="_blank" rel="external">Testing Emails in PHP. Part 1: PHPUnit</a></li>
<li>Behat - <a href="https://github.com/kibao/behat-mailcatcher-extension" target="_blank" rel="external">MailCatcher extension for Behat</a></li>
<li>Codeception - <a href="https://github.com/captbaritone/codeception-mailcatcher-module" target="_blank" rel="external">Test emails in your Codeception acceptance tests</a></li>
</ul>
]]></content>
    <summary type="html">
    <![CDATA[<p>雖然 Laravel 在寄送測試信件上提供了 <a href="http://www.mailgun.com/">Mailgun</a> 這個服務的串接方式，不過如果能夠在 Homestead 就可以直接測試是更棒的選擇；而 <a href="http://mailcatcher.me/">MailCatcher</a> 剛好就提供這樣的功能，它能啟動一個 SMTP 模擬服務，並且讓我們透過 Web 介面來查看信件是否有被發送出來。</p>
<blockquote>
<p>註：類似的工類還有用 Go 寫的 <a href="https://github.com/mailhog/MailHog">MailHog</a> ，據說速度更快；雖然我還沒試過，但我想用法應該是相同的。</p>
</blockquote>
<p>以下就簡單介紹一下如何在 Laravel 5.1 上使用 MailCatcher 。</p>]]>
    
    </summary>
    
      <category term="Laravel" scheme="http://jaceju.net/tags/Laravel/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[Laravel 5.1 Events Broadcasting 實務練習]]></title>
    <link href="http://jaceju.net/2015/07/26/laravel-events-broadcasting/"/>
    <id>http://jaceju.net/2015/07/26/laravel-events-broadcasting/</id>
    <published>2015-07-25T16:38:37.000Z</published>
    <updated>2015-07-25T17:13:17.000Z</updated>
    <content type="html"><![CDATA[<p>Laravel 5.1 提供了一個非常棒的 Events Broadcasting 特色，它能讓開發者建立一個 RealTime Web App 。作者 Taylor 也錄製了一個 Events Broadcasting 的<a href="https://laracasts.com/lessons/broadcasting-events-in-laravel-5-1" target="_blank" rel="external">教學影片</a>，讓開發者可以更快瞭解這個新功能。</p>
<p>教學影片中雖然是使用 <a href="https://pusher.com/" target="_blank" rel="external">Pusher</a> 服務來做事件推送，不過 Laravel 也可以搭配 <a href="http://redis.io/" target="_blank" rel="external">Redis</a> 來做到同樣的事情。考量到未來的系統發展，我打算採用 Redis 來當做事件推送伺服器，所以本文也會在此基礎進行說明。</p>
<p>以下就來介紹如何用 Laravel 的 Events Broadcasting 來實作一個簡單的聊天室。</p>
<a id="more"></a>
<h2 id="原理">原理</h2><p>簡單說明一下本文的實作原理：</p>
<ol>
<li>啟動 Redis 伺服器來監聽 Laravel 發送出來的事件。</li>
<li>透過 Node Express 建立一個 Socket.IO Server ，並且接收 Redis Server 推送過來的事件，然後將它廣播到 WebSocket 上。</li>
<li>瀏覽器上建立與 Socket.IO Server 的連結，透過 WebSocket 接收事件來完成即時互動。</li>
</ol>
<h2 id="開發環境">開發環境</h2><p>Laravel 官方推薦開發者使用 Homestead ，原因是它已經幫我們安裝好所有 Laravel 需要的執行環境，例如 Redis 與 Node.js 。在繼續下去之前，請先依照<a href="http://laravel.com/docs/5.1/homestead" target="_blank" rel="external">官方說明</a> (<a href="http://laravel.tw/docs/5.1/homestead" target="_blank" rel="external">中文版</a>) 將 Homestead 安裝好。</p>
<p>然後利用 <code>homestead edit</code> 打開 Homestead 的設定檔：</p>
<blockquote>
<p>註：以下指令中，開頭的 <code>$</code> 為系統提示符號，不用輸入。</p>
</blockquote>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ homestead edit</span><br></pre></td></tr></table></figure>
<p>確認本機路徑 <code>~/Projects</code> 有正確 mount 到 Homestead 的 <code>/home/vagrant/Projects</code> 上。</p>
<figure class="highlight"><table><tr><td class="code"><pre><span class="line">folders:&#10;    - map: ~/Projects&#10;      to: /home/vagrant/Projects</span><br></pre></td></tr></table></figure>
<p>要連上 Homestead 裡的虛擬站台 (Virtual Host) ，必須要讓本機認得專案對應的 hostname 。所以要編輯本機的 <code>/etc/hosts</code> ，加入 IP 與 hostname 的對應：</p>
<figure class="highlight"><table><tr><td class="code"><pre><span class="line">192.168.10.10 chat-room.app</span><br></pre></td></tr></table></figure>
<p><code>192.168.10.10</code> 是在 Homestead.yml 中設定的 IP 。</p>
<p>啟動 Homestead ，並用 ssh 連入 Homestead ：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ homestead up</span><br><span class="line">$ homestead ssh</span><br></pre></td></tr></table></figure>
<p>檢查 node.js 版本：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ node -v</span><br><span class="line">v1.<span class="number">8.1</span></span><br></pre></td></tr></table></figure>
<blockquote>
<p>註： Homestead 是透過 nvm 安裝 io.js ，所以可以自行升級 io.js 到最新版。</p>
</blockquote>
<p>檢查 redis 是否啟動：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ redis-cli ping</span><br><span class="line">PONG</span><br></pre></td></tr></table></figure>
<p>新增一個虛擬站台：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ serve chat-room.app ~/Projects/chat-room/public</span><br></pre></td></tr></table></figure>
<h2 id="建立應用程式">建立應用程式</h2><p>在 Homestead 中，利用 composer 來下載已經設定好的 Laravel 5.1 Boilerplate ，執行：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ <span class="built_in">cd</span> ~/Projects</span><br><span class="line">$ composer create-project jaceju/b5 chat-room <span class="operator">-s</span> dev</span><br></pre></td></tr></table></figure>
<p>程式就會開始下載並進行安裝。完成後進入專案資料夾，以便進行後續操作。</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ <span class="built_in">cd</span> chat-room</span><br></pre></td></tr></table></figure>
<h3 id="調整_gulpfile-js">調整 gulpfile.js</h3><p>在 Homestead 上開發時，不需要啟動 Web Server ，因此要調整 <code>gulpfile.js</code> 。</p>
<p>編輯 <code>gulpfile.js</code> ，把 <code>port</code> 變數與 <code>serve</code> task 移除，並將 <code>proxy</code> 改到 <code>chat-room.app</code> ，完成後如下：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="comment">// ... (略)</span></span><br><span class="line"></span><br><span class="line">elixir(<span class="function"><span class="keyword">function</span> (<span class="params">mix</span>) </span>&#123;</span><br><span class="line">    mix.clean()</span><br><span class="line">        .sass(<span class="string">'*.scss'</span>)</span><br><span class="line">        .wiredep()</span><br><span class="line">        .jshint()</span><br><span class="line">        .sync(<span class="string">'resources/assets/js/**/*.js'</span>, <span class="string">'public/js'</span>);</span><br><span class="line"></span><br><span class="line">    <span class="keyword">if</span> (elixir.config.production) &#123;</span><br><span class="line">        mix.useref(&#123; src: <span class="literal">false</span> &#125;)</span><br><span class="line">            .version([<span class="string">'js/*.js'</span>, <span class="string">'css/*.css'</span>])</span><br><span class="line">    &#125;</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<h2 id="安裝必要套件">安裝必要套件</h2><p>Laravel 操作 Redis 是透過 <a href="https://github.com/nrk/predis" target="_blank" rel="external">Predis</a> 套件，所以我們要透過 composer 安裝：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ composer require predis/predis</span><br></pre></td></tr></table></figure>
<p>接下來要透過 Npm 與 Bower 安裝 Socket.IO Server 相關套件，包含前後端：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ npm install express ioredis socket.io --save</span><br><span class="line">$ bower install socket.io-client --save</span><br></pre></td></tr></table></figure>
<h2 id="建立_Socket-IO_Server">建立 Socket.IO Server</h2><p>接下來要利用 Node.js 的 express 和 http 模組建立一個 Web Server ，然後讓 Socket.IO 透過這個 WebServer 來廣播從 Redis 接收到的事件。先建立 <code>socket.js</code> ，內容為：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="keyword">var</span> app = <span class="built_in">require</span>(<span class="string">'express'</span>)();</span><br><span class="line"><span class="keyword">var</span> http = <span class="built_in">require</span>(<span class="string">'http'</span>).Server(app);</span><br><span class="line"><span class="keyword">var</span> io = <span class="built_in">require</span>(<span class="string">'socket.io'</span>)(http);</span><br><span class="line"><span class="keyword">var</span> Redis = <span class="built_in">require</span>(<span class="string">'ioredis'</span>);</span><br><span class="line"><span class="keyword">var</span> redis = <span class="keyword">new</span> Redis();</span><br><span class="line"></span><br><span class="line"><span class="comment">// Redis 訂閱 `chat-channel` 頻道</span></span><br><span class="line">redis.subscribe(<span class="string">'chat-channel'</span>, <span class="function"><span class="keyword">function</span> (<span class="params">err, count</span>) </span>&#123;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 當 Redis 有事件發生時，透過 Socket.IO Server 發送事件</span></span><br><span class="line">redis.on(<span class="string">'message'</span>, <span class="function"><span class="keyword">function</span> (<span class="params">channel, message</span>) </span>&#123;</span><br><span class="line">    message = <span class="built_in">JSON</span>.parse(message);</span><br><span class="line">    io.emit(channel + <span class="string">':'</span> + message.event, message.data);</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 讓用戶端可以透過 Port 3000 連接 Socket.IO Server</span></span><br><span class="line">http.listen(<span class="number">3000</span>, <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">'Listening on Port 3000'</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>然後在 Homestead 上啟動 Socket.IO Server ：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ node socket.js &amp;</span><br><span class="line">Listening on Port <span class="number">3000</span></span><br></pre></td></tr></table></figure>
<h2 id="修改設定">修改設定</h2><p>Laravel 5.1 預設是使用 Pusher 做為事件推送伺服器，可以在 <code>config/broadcasting.php</code> 裡看到這個設定：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="string">'default'</span> =&gt; env(<span class="string">'BROADCAST_DRIVER'</span>, <span class="string">'pusher'</span>),</span><br></pre></td></tr></table></figure>
<p>因為這裡使用了 <code>env</code> 函式，所以我們可以編輯 <code>.env</code> ，加入以下設定來改用 Redis 伺服器：</p>
<figure class="highlight ini"><table><tr><td class="code"><pre><span class="line"><span class="setting">BROADCAST_DRIVER=<span class="value">redis</span></span></span><br></pre></td></tr></table></figure>
<h2 id="建立_Event_類別">建立 Event 類別</h2><p>接下來就要讓 Laravel 能夠發送事件了，在 Laravel 5.0 以後的版本提供了 <code>make:event</code> 這個指令可以協助我們建立 Event 類別。首先我們的聊天室需要一個「訊息被建立」的事件，所以執行：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ php artisan make:event MessageCreated</span><br></pre></td></tr></table></figure>
<p>這樣就會建立 <code>app/Events/MessageCreated.php</code> 。</p>
<p>接著我們要讓 <code>MessageCreated</code> 類別能夠被 Redis 推送，所以編輯 <code>app/Events/MessageCreated.php</code> ，讓它實作 <code>Illuminate\Contracts\Broadcasting\ShouldBroadcast</code> 介面。</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Contracts</span>\<span class="title">Broadcasting</span>\<span class="title">ShouldBroadcast</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MessageCreated</span> <span class="keyword">extends</span> <span class="title">Event</span> <span class="keyword">implements</span> <span class="title">ShouldBroadcast</span></span></span><br></pre></td></tr></table></figure>
<p>在推送事件時，我們可以附帶一組要傳送的資料，稱為 payload 。通常我們會在 Event 類別的建構子中帶入 payload 。修改 <code>MessageCreated</code> 類別，加入 <code>$username</code> 與 <code>$message</code> 屬性，並在建構子中注入：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="variable">$username</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">private</span> <span class="variable">$message</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span><span class="params">(<span class="variable">$username</span>, <span class="variable">$message</span>)</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="variable">$this</span>-&gt;username = <span class="variable">$username</span>;</span><br><span class="line">    <span class="variable">$this</span>-&gt;message = <span class="variable">$message</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>接著我們要讓它能在推送事件時，把這兩個屬性一起傳送出去；主要是透過 <code>broadcastWith</code> 這個方法：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">broadcastWith</span><span class="params">()</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">return</span> [</span><br><span class="line">        <span class="string">'username'</span> =&gt; <span class="variable">$this</span>-&gt;username,</span><br><span class="line">        <span class="string">'message'</span> =&gt; <span class="variable">$this</span>-&gt;message,</span><br><span class="line">    ];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>最後我們需要一個頻道來廣播事件，這是因為要讓 Redis 可以識別要廣播的對象。我們可以在 <code>broadcastOn</code> 方法回傳 Redis 訂閱的頻道名稱，即為前面指定的 <code>chat-channel</code> ：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">broadcastOn</span><span class="params">()</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">return</span> [<span class="string">'chat-channel'</span>];</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<blockquote>
<p>註：一個事件可以廣播給數個頻道，所以這裡要回傳一個陣列。</p>
</blockquote>
<p>完成後的程式碼如下：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="preprocessor">&lt;?php</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">namespace</span> <span class="title">App</span>\<span class="title">Events</span>;</span><br><span class="line"></span><br><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Contracts</span>\<span class="title">Broadcasting</span>\<span class="title">ShouldBroadcast</span>;</span><br><span class="line"><span class="keyword">use</span> <span class="title">Illuminate</span>\<span class="title">Queue</span>\<span class="title">SerializesModels</span>;</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">MessageCreated</span> <span class="keyword">extends</span> <span class="title">Event</span> <span class="keyword">implements</span> <span class="title">ShouldBroadcast</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">use</span> <span class="title">SerializesModels</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="variable">$username</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">private</span> <span class="variable">$message</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span><span class="params">(<span class="variable">$username</span>, <span class="variable">$message</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="variable">$this</span>-&gt;username = <span class="variable">$username</span>;</span><br><span class="line">        <span class="variable">$this</span>-&gt;message = <span class="variable">$message</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">broadcastWith</span><span class="params">()</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> [</span><br><span class="line">            <span class="string">'username'</span> =&gt; <span class="variable">$this</span>-&gt;username,</span><br><span class="line">            <span class="string">'message'</span> =&gt; <span class="variable">$this</span>-&gt;message,</span><br><span class="line">        ];</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">broadcastOn</span><span class="params">()</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">return</span> [<span class="string">'chat-channel'</span>];</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="建立_Routes_與_Controller">建立 Routes 與 Controller</h2><p>這裡我們只需要兩個 route ：聊天室頁面，以及發送訊息。改寫 <code>app/Http/routes.php</code> ，內容如下：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="preprocessor">&lt;?php</span></span><br><span class="line">get(<span class="string">'/'</span>, <span class="string">'ChatController@index'</span>); <span class="comment">// 聊天室頁面</span></span><br><span class="line">post(<span class="string">'send-message'</span>, <span class="string">'ChatController@sendMessage'</span>); <span class="comment">// 發送訊息</span></span><br></pre></td></tr></table></figure>
<p>然後要建立 <code>ChatController</code> 來處理程式流程，在 Terminal 中執行：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ php artisan make:controller ChatController --plain</span><br></pre></td></tr></table></figure>
<p>編輯新建立的 <code>app/Http/Controllers/ChatController.php</code> ，先加入 <code>index</code> 方法：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">index</span><span class="params">()</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    srand(time()); <span class="comment">// 亂數種子</span></span><br><span class="line">    <span class="variable">$username</span> = sprintf(<span class="string">'user%06d'</span>, rand(<span class="number">1</span>, <span class="number">100000</span>)); <span class="comment">// 決定 user 名稱 (註)</span></span><br><span class="line">    <span class="keyword">return</span> view(<span class="string">'chat'</span>, compact(<span class="string">'username'</span>));</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>在 <code>index</code> 方法中，主要是產生一個隨機的使用者名稱，並顯示在首頁樣版裡。</p>
<blockquote>
<p>註：這裡產生 <code>username</code> 的方法並不嚴謹，沒有考慮到名稱重複的問題，但現階段先暫時這樣。</p>
</blockquote>
<p>接下來我們要接收使用者建立的訊息，然後發送一個「訊息被建立」的事件，所以新增 <code>sendMessage</code> 方法，內容為：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">sendMessage</span><span class="params">(Request <span class="variable">$request</span>)</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="variable">$username</span> = <span class="variable">$request</span>-&gt;get(<span class="string">'username'</span>);</span><br><span class="line">    <span class="variable">$message</span> = <span class="variable">$request</span>-&gt;get(<span class="string">'message'</span>);</span><br><span class="line">    event(<span class="keyword">new</span> MessageCreated(<span class="variable">$username</span>, <span class="variable">$message</span>));</span><br><span class="line">    <span class="keyword">return</span> <span class="string">'message sent'</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<h2 id="建立樣版頁面">建立樣版頁面</h2><p>切換到前端開發模式，我們要修改一下介面的呈現。</p>
<p>先處理 HTML 的部份，將原來的 <code>resources/views/welcome.blade.php</code> 重新命名為 <code>resources/views/chat.blade.php</code> ，將 <code>div.container</code> 的內容修改如下：</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="title">div</span> <span class="attribute">class</span>=<span class="value">"container"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="title">div</span> <span class="attribute">class</span>=<span class="value">"row"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="title">div</span> <span class="attribute">class</span>=<span class="value">"col-md-6 col-md-offset-3"</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="title">h1</span>&gt;</span>Chat Room Demo<span class="tag">&lt;/<span class="title">h1</span>&gt;</span></span><br><span class="line"></span><br><span class="line">            <span class="comment">&lt;!-- 訊息列表框 --&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="title">div</span> <span class="attribute">id</span>=<span class="value">"chat-room"</span>&gt;</span></span><br><span class="line"></span><br><span class="line">            <span class="tag">&lt;/<span class="title">div</span>&gt;</span></span><br><span class="line"></span><br><span class="line">            <span class="comment">&lt;!-- 輸入訊息的表單 --&gt;</span></span><br><span class="line">            <span class="tag">&lt;<span class="title">form</span> <span class="attribute">id</span>=<span class="value">"send-message"</span> <span class="attribute">method</span>=<span class="value">"post"</span> <span class="attribute">action</span>=<span class="value">"/send-message"</span>&gt;</span></span><br><span class="line">                &#123;!! csrf_field() !!&#125;</span><br><span class="line">                <span class="tag">&lt;<span class="title">input</span> <span class="attribute">type</span>=<span class="value">"hidden"</span> <span class="attribute">name</span>=<span class="value">"username"</span> <span class="attribute">value</span>=<span class="value">"&#123;&#123; $username &#125;&#125;"</span> /&gt;</span></span><br><span class="line">                <span class="tag">&lt;<span class="title">div</span> <span class="attribute">class</span>=<span class="value">"input-group"</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="title">label</span> <span class="attribute">class</span>=<span class="value">"input-group-addon"</span>&gt;</span>&#123;&#123; $username &#125;&#125;<span class="tag">&lt;/<span class="title">label</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="title">input</span> <span class="attribute">id</span>=<span class="value">"message"</span> <span class="attribute">type</span>=<span class="value">"text"</span> <span class="attribute">value</span>=<span class="value">""</span> <span class="attribute">class</span>=<span class="value">"form-control"</span> /&gt;</span></span><br><span class="line">                    <span class="tag">&lt;<span class="title">span</span> <span class="attribute">class</span>=<span class="value">"input-group-btn"</span>&gt;</span></span><br><span class="line">                        <span class="tag">&lt;<span class="title">button</span> <span class="attribute">class</span>=<span class="value">"btn btn-success"</span> <span class="attribute">id</span>=<span class="value">"send"</span>&gt;</span>Send<span class="tag">&lt;/<span class="title">button</span>&gt;</span></span><br><span class="line">                    <span class="tag">&lt;/<span class="title">span</span>&gt;</span></span><br><span class="line">                <span class="tag">&lt;/<span class="title">div</span>&gt;</span></span><br><span class="line">            <span class="tag">&lt;/<span class="title">form</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;/<span class="title">div</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="title">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="title">div</span>&gt;</span></span><br></pre></td></tr></table></figure>
<p>這樣會讓畫面上有一個訊息列表框以及一個輸入訊息的表單，如下圖所示。</p>
<p><img src="/resources/chat-room-demo/interface.png" alt="介面"></p>
<p>接下來稍微調整介面的樣式，編輯 <code>resources/assets/sass/app.scss</code> ，將內容修改如下：</p>
<figure class="highlight scss"><table><tr><td class="code"><pre><span class="line"><span class="at_rule">@<span class="keyword">import</span> <span class="string">"../../../public/bower_components/bootstrap-sass/assets/stylesheets/bootstrap"</span>;</span></span><br><span class="line"></span><br><span class="line"><span class="tag">html</span>, <span class="tag">body</span> &#123;</span><br><span class="line">  <span class="attribute">height</span><span class="value">: <span class="number">100%</span>;</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 訊息列表框</span></span><br><span class="line"><span class="id">#chat-room</span> &#123;</span><br><span class="line">  <span class="attribute">border</span><span class="value">: <span class="number">1px</span> solid <span class="hexcolor">#ccc</span>;</span></span><br><span class="line">  <span class="attribute">height</span><span class="value">: <span class="number">20rem</span>;</span></span><br><span class="line">  <span class="attribute">padding</span><span class="value">: <span class="number">1rem</span>;</span></span><br><span class="line">  <span class="attribute">overflow-x</span><span class="value">: hidden;</span></span><br><span class="line">  <span class="attribute">overflow-y</span><span class="value">: auto;</span></span><br><span class="line"></span><br><span class="line">  <span class="comment">// 單則訊息</span></span><br><span class="line">  <span class="class">.message</span> &#123;</span><br><span class="line">    <span class="attribute">padding</span><span class="value">: <span class="number">1rem</span>;</span></span><br><span class="line">    <span class="attribute">margin-bottom</span><span class="value">: <span class="number">1rem</span>;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="comment">// 輸入訊息的表單</span></span><br><span class="line"><span class="id">#send-message</span> &#123;</span><br><span class="line">  <span class="attribute">margin-top</span><span class="value">: -<span class="number">1px</span>;</span></span><br><span class="line"></span><br><span class="line">  <span class="class">.input-group-addon</span> &#123;</span><br><span class="line">    <span class="attribute">border-top-left-radius</span><span class="value">: <span class="number">0</span>;</span></span><br><span class="line">  &#125;</span><br><span class="line"></span><br><span class="line">  <span class="class">.input-group-btn</span> &gt; <span class="class">.btn</span> &#123;</span><br><span class="line">    <span class="attribute">border-top-right-radius</span><span class="value">: <span class="number">0</span>;</span></span><br><span class="line">  &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>最後透過 JavaScript 讓所有東西串在一起，</p>
<p>編輯 <code>resources/assets/js/app.js</code> ，內容如下：</p>
<figure class="highlight js"><table><tr><td class="code"><pre><span class="line"><span class="pi">'use strict'</span>;</span><br><span class="line"><span class="keyword">var</span> $chatRoom = $(<span class="string">'#chat-room'</span>);</span><br><span class="line"><span class="keyword">var</span> $sendMessage = $(<span class="string">'#send-message'</span>);</span><br><span class="line"><span class="keyword">var</span> $messageInput = $sendMessage.find(<span class="string">'input[name=message]'</span>);</span><br><span class="line"><span class="keyword">var</span> io = <span class="built_in">window</span>.io;</span><br><span class="line"><span class="keyword">var</span> socket = io(<span class="string">'http://chat-room.app:3000'</span>);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 當送出表單時，改用 Ajax 傳送，並清空輸入框。</span></span><br><span class="line">$sendMessage.on(<span class="string">'submit'</span>, <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">    $.post(<span class="keyword">this</span>.action, $sendMessage.serialize());</span><br><span class="line">    $messageInput.val(<span class="string">''</span>);</span><br><span class="line">    <span class="keyword">return</span> <span class="literal">false</span>;</span><br><span class="line">&#125;);</span><br><span class="line"></span><br><span class="line"><span class="comment">// 當接收到訊息建立的事件時，將接收到的 payload</span></span><br><span class="line">socket.on(<span class="string">'chat-channel:App\\Events\\MessageCreated'</span>, <span class="function"><span class="keyword">function</span> (<span class="params">payload</span>) </span>&#123;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> html = <span class="string">'&lt;div class="message alert-info" style="display: none;"&gt;'</span>;</span><br><span class="line">    html += payload.username + <span class="string">': '</span>;</span><br><span class="line">    html += payload.message;</span><br><span class="line">    html += <span class="string">'&lt;/div&gt;'</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">var</span> $message = $(html);</span><br><span class="line">    $chatRoom.append($message);</span><br><span class="line">    $message.fadeIn(<span class="string">'fast'</span>);</span><br><span class="line">    $chatRoom.animate(&#123;scrollTop: $chatRoom[<span class="number">0</span>].scrollHeight&#125;, <span class="number">1000</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<h2 id="執行測試">執行測試</h2><p>在 Homestead 上執行：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">$ gulp</span><br></pre></td></tr></table></figure>
<p>然後在本機分別開啟兩個瀏覽器視窗瀏覽 <code>http://chat-room.app</code> ，然後在輸出框上輸入文字後按 <code>Send</code> ，應該就會讓兩個瀏覽器同時出現相同的文字。</p>
<p><img src="/resources/chat-room-demo/in-use.png" alt="展示"></p>
<p>完成的範例可以在我的 <a href="https://github.com/jaceju/example-laravel-chat-room" target="_blank" rel="external">GitHub</a> 上找到。</p>
<h2 id="結論">結論</h2><p>這個 Demo 如果真的要在實務上使用，還有很多地方要考慮，例如訊息歷史、使用者登入等等。不過這已經足夠讓我們瞭解 Laravel 5.1 在實作 Broadcasting 時有多麼輕鬆，使得我們更容易在專案前期就先實現很多想法。</p>
<p>希望這個簡單的教學，能對大家使用 Laravel 來開發即時系統時有所幫助。</p>
<h2 id="參考">參考</h2><ul>
<li><a href="https://laracasts.com/discuss/channels/general-discussion/step-by-step-guide-to-installing-socketio-and-broadcasting-events-with-laravel-51" target="_blank" rel="external">Step by Step Guide to Installing Socket.io and Broadcasting Events with Laravel 5.1 </a></li>
</ul>
]]></content>
    <summary type="html">
    <![CDATA[<p>Laravel 5.1 提供了一個非常棒的 Events Broadcasting 特色，它能讓開發者建立一個 RealTime Web App 。作者 Taylor 也錄製了一個 Events Broadcasting 的<a href="https://laracasts.com/lessons/broadcasting-events-in-laravel-5-1">教學影片</a>，讓開發者可以更快瞭解這個新功能。</p>
<p>教學影片中雖然是使用 <a href="https://pusher.com/">Pusher</a> 服務來做事件推送，不過 Laravel 也可以搭配 <a href="http://redis.io/">Redis</a> 來做到同樣的事情。考量到未來的系統發展，我打算採用 Redis 來當做事件推送伺服器，所以本文也會在此基礎進行說明。</p>
<p>以下就來介紹如何用 Laravel 的 Events Broadcasting 來實作一個簡單的聊天室。</p>]]>
    
    </summary>
    
      <category term="Laravel" scheme="http://jaceju.net/tags/Laravel/"/>
    
      <category term="PHP" scheme="http://jaceju.net/tags/PHP/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[幾個有興趣的前端框架]]></title>
    <link href="http://jaceju.net/2015/07/08/front-end-framework-summary/"/>
    <id>http://jaceju.net/2015/07/08/front-end-framework-summary/</id>
    <published>2015-07-08T02:04:54.000Z</published>
    <updated>2015-07-27T12:34:07.000Z</updated>
    <content type="html"><![CDATA[<p>如果你像我一樣是一個對 UI 開發不那麼在行，或是無法花太多時間在 UI 設計的 Web 開發者，但卻想能夠快速建立一個可用的 UI 介面，那麼最快的方法就是借重別人寫好的前端框架。</p>
<p>這裡我對前端框架的定義是包含了頁面排版、風格樣式、 UI 元件等，不限純 CSS ，可以包含 JavaScript 。</p>
<p>本文從以前我所使用及目前研究的幾個前端框架，依照研究的深入程度，由深至淺來簡單分享我個人的一些分析心得。至於優缺點，完全是個人主觀意見，大家參考看看即可。</p>
<a id="more"></a>
<h2 id="Twitter_Bootstrap"><a href="http://getbootstrap.com/" target="_blank" rel="external">Twitter Bootstrap</a></h2><p>Twitter 所推出的 Bootstrap 大概是使用者最多，資源也最豐富的框架了。因為推出時間較早， Bootstrap 的社群也相當龐大；不少開發者也在它的基礎上建立了很多自訂的元件，延續了它在前端的戰場生命。</p>
<p>一開始 Boostrap 是以 <a href="http://lesscss.org/" target="_blank" rel="external">LESS</a> 開發的，不過後來官方也提供了 <a href="http://sass-lang.com/" target="_blank" rel="external">SASS</a> 版本；對我來說，在開發流程裡就不會再有 LESS 和 SASS 混搭的問題了。</p>
<h3 id="優點">優點</h3><p>Boostrap 包含的樣式與元件都已經涵蓋了多數應用場合，而且文件非常完整。有許多第三方 UI 元件都是基於它來開發的。另外也有很多開發者在 Boostrap 的基礎上建立出多種 Theme 讓開發者套用，讓 Bootstrap 能跟上目前時下流行的網站設計風格。</p>
<p>在 Boostrap 的 LESS 與 SASS 版本中，都提供了大量的變數讓使用者可以做出自己的 Boostrap 客製化版本；而 Boostrap 也提供了<a href="http://getbootstrap.com/customize/" target="_blank" rel="external">線上客製功能</a>，讓官方網站直接幫你組合出適合你的 Boostrap 。</p>
<h3 id="缺點">缺點</h3><p>就我個人的使用經驗來看，稍微複雜的 Grid 系統常常需要多層巢狀 HTML 標籤來組合，大概就是 Bootstrap 的罩門了。另外表單的排版也是不夠簡明，一個小 UI 可能需要好幾層 HTML 來組合。</p>
<p>相依在 jQuery 上或許是另一個小缺點，兩者的 JavaScript 檔案大小相加後超過 100KB ，在行動裝置上使用時要特別注意載入的效率。</p>
<h3 id="相關資源">相關資源</h3><ul>
<li><a href="http://bootsnipp.com/" target="_blank" rel="external">Bootsnipp</a> - 讓使用者分享他們的 Bootstrap 元件或頁面範例。</li>
<li><a href="https://bootswatch.com/" target="_blank" rel="external">Bootswatch</a> - 提供多種 Theme 讓使用者套用。</li>
<li><a href="http://builtwithbootstrap.com/" target="_blank" rel="external">Built with Bootstrap</a> - 收集許多用 Boostrap 製作的網站。</li>
<li><a href="http://pingendo.com/" target="_blank" rel="external">Pingendo</a> - 使用 Bootstrap 來做 prototype 的 Mac App 。</li>
<li><a href="https://github.com/search?utf8=%E2%9C%93&amp;q=bootstrap" target="_blank" rel="external">GitHub 上的相關專案</a></li>
</ul>
<h2 id="Semantic_UI"><a href="http://semantic-ui.com/" target="_blank" rel="external">Semantic UI</a></h2><p>一套強調語意化的前端框架，主要是在 HTML 標籤的 class 屬性去組合語意。例如兩欄等高的排版會長這樣：</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line"><span class="tag">&lt;<span class="title">main</span> <span class="attribute">class</span>=<span class="value">"ui two column equal height page grid"</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;<span class="title">div</span> <span class="attribute">class</span>=<span class="value">"stretched divided row"</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="title">section</span> <span class="attribute">class</span>=<span class="value">"twelve wide column"</span>&gt;</span> ... <span class="tag">&lt;/<span class="title">section</span>&gt;</span></span><br><span class="line">        <span class="tag">&lt;<span class="title">aside</span> <span class="attribute">class</span>=<span class="value">"four wide column"</span>&gt;</span> ... <span class="tag">&lt;/<span class="title">aside</span>&gt;</span></span><br><span class="line">    <span class="tag">&lt;/<span class="title">div</span>&gt;</span></span><br><span class="line"><span class="tag">&lt;/<span class="title">main</span>&gt;</span></span><br></pre></td></tr></table></figure>
<p>從 class 就可以看到跟 Boostrap 非常不一樣的設計理念，像 <code>column</code> 在不同的位置會有不一樣的解釋。第一層的 <code>two column ... page grid</code> 指這個 grid 裡會包含兩欄，而後的 <code>twelve wide column</code> 與 <code>four wide column</code> 則定義了欄的寬度。</p>
<p>另外 Semantic UI 2.0 有個特別之處，就是它在 Layout 和 UI 元件上，全部採用了 CSS Flexbox 設計。當然這樣會有瀏覽器支援的問題，但如果不必考慮這個問題的話，這樣的設計實在是讓我覺得很舒服。</p>
<p>目前 Semantic UI 是採用 LESS 開發，還沒有官方的 SASS 版本。</p>
<h3 id="優點-1">優點</h3><p>Semantic UI 透過 class 的語意化組合使得頁面的 HTML 變得很簡潔，讓開發者不會像 Bootstrap 一樣迷失在 HTML 標籤海裡。另外搭配 HTML5 本身的語意化標籤，讓開發者可以更輕鬆地定位到他想套用程式的位置。</p>
<p>另一個優點是它的 UI 預設風格非常精緻，個人還滿喜歡的。當然它也跟 Boostrap 一樣可以更換 Theme ，而且在設計上是採用三層繼承架構。</p>
<h3 id="缺點-1">缺點</h3><p>雖然說語意化排版是 Semantic UI 的強項，但卻讓人有不知道該怎麼組合的挫敗感；網路上有關 Semantic UI 的 Layout 範例都停留在舊版，著實讓我在用 2.0 版時頭痛了一陣子。所幸最近官方的文件正逐漸改進中，應該能解決掉這個問題。</p>
<p>另外社群針對 Semantic UI 所寫的第三方 UI 元件實在不多，內建的元件也不如 Boostrap 多樣化；如果你不打算自己開發 UI 元件的話，這相當於限制你只能在這個框架中尋找替代做法。</p>
<p>Semantic UI 在 Responsive 的部份也必須因為有不同的排版策略而有不同的 HTML 區塊，這點反而不如 Boostrap 的設計 (但搭配 <a href="http://www.lukew.com/ff/entry.asp?1392" target="_blank" rel="external">RESS</a> 後反而變成優點了) 。</p>
<h3 id="相關資源-1">相關資源</h3><ul>
<li><a href="http://learnsemantic.com/" target="_blank" rel="external">Learn Semantic</a></li>
</ul>
<h2 id="UIKit"><a href="http://getuikit.com" target="_blank" rel="external">UIKit</a></h2><p>UIKit 是 <a href="http://yootheme.com/" target="_blank" rel="external">YOOTheme</a> 開發的一套前端框架，除了 class 名稱不太一樣之外，整體設計理念很接近 Boostrap 3 ；尤其是 Responsive Grid 的設計，跟 Boostrap 如出一轍。</p>
<p>UIKit 的所有元件都做了 Responsive 化，同時把所有資訊都寫在 HTML 標籤上，所以它的元件不需要像 Boostrap 一樣寫額外的 JavaScript ；只要在元件 HTML 標籤上定義相關屬性，接下來的事情會由 UIKit 的 JavaScript 程式庫處理掉。不過它和 Boostrap 一樣，也需要 jQuery 的支援。</p>
<p>目前 UIKit 是採用 LESS 開發，還沒有官方的 SASS 版本。</p>
<h3 id="優點-2">優點</h3><p>UIKit 的官方文件相當齊全，完全不輸 Bootstrap ；部份常用的 Web 元件甚至是 Bootstrap 所沒有提供的，像是 Date Picker 與 HTML Editor ，讓開發者不需要引用太多第三方套件。</p>
<p>除了傳統 Grid 排版， UIKit 也提供了 Flex 排版機制；讓開發者在不需考慮瀏覽器支援度的狀況下，可以更靈活地安排版面。</p>
<h3 id="缺點-2">缺點</h3><p>也因為 UIKit 跟 Boostrap 很像，所以在排版上也有多層巢狀 HTML 標籤的問題，不過與 Bootstrap 相比，已經改善不少。</p>
<p>另外也許是因為 UIKit 非大公司出品，在社群上的討論度不那麼高，使得它在第三方的支援上就不如 Boostrap 這麼廣泛。不過我想 UIKit 本身所提供的這些元件，就應該足以應付常見的需求了。</p>
<h3 id="相關資源-2">相關資源</h3><ul>
<li><a href="http://www.getuikit.net/" target="_blank" rel="external">UIKit 中文網</a></li>
</ul>
<h2 id="Materialize"><a href="http://materializecss.com" target="_blank" rel="external">Materialize</a></h2><p>Materialize 是一群 Carnegie Mellon University 的學生，依照 Google 的 Material Design 所開發出來的框架。從它的 Grid 範例來看，應該也參考不少 Bootstrap 的設計。</p>
<p>目前 Materialize 是採用 SASS 開發，還沒有官方的 LESS 版本。</p>
<h3 id="優點-3">優點</h3><p>Materialize 是目前實作較完整的 Material Design 框架，大多數 Material Design 的模式都很容易在這個框架中實現。而除了 class 名稱不同外，大多數的元件在使用上和 Bootstrap 是差不多的。它的文件非常清楚簡潔，大多數元件只要看範例就知道如何使用了。</p>
<p>如果你非常想在新專案實現 Material Design ，那麼這個框架絕對是首選。而如果你熟悉的是 Bootstrap ，但又想套用 Material Design 的話，可以參考看看 <a href="http://fezvrasta.github.io/bootstrap-material-design/bootstrap-elements.html" target="_blank" rel="external">Bootstrap Material Design</a> 。</p>
<h3 id="缺點-3">缺點</h3><p>因為 Materialize 發展還不到一年，所以社群支援程度還不夠高，相關應用發展也是不如 Bootstrap ；不過我想這個問題會因為 Material Design 被高度關注的狀況下，逐漸得到解決。</p>
<h3 id="相關資源-3">相關資源</h3><ul>
<li><a href="http://blog.codeply.com/2015/04/09/5-material-design-examples-using-materializecss/" target="_blank" rel="external">5 Material Design Examples using MaterializeCSS</a></li>
<li><a href="http://materialdesignblog.com/" target="_blank" rel="external">Material Design Blog</a></li>
</ul>
<h2 id="Material_Design_Lite"><a href="http://www.getmdl.io/" target="_blank" rel="external">Material Design Lite</a></h2><p>Material Design Lite 是 Google 自己推出的 Material Design 框架，它的出現造成了社群中高度的討論。我個人是認為 Google 推出 Material Design Lite 其實帶有宣示的意味，它要讓開發者知道 Google 對 Material Design 真正想表達的就實現在這裡了 (當然這是我自己猜想的，沒有什麼根據) 。</p>
<p>目前 Material Design Lite 是採用 SASS 開發，還沒有官方的 LESS 版本。</p>
<h3 id="優點-4">優點</h3><p>Material Design Lite 是 Google 推出的，未來發展性也許不錯 (雖然有可能夭折) 。</p>
<h3 id="缺點-4">缺點</h3><p>因為 Material Design Lite 的 class 是採用 BEM ，所以就有了 class 爆炸的問題，一個元件的定義可能就會包含了十來個 class 。</p>
<p>另外因為剛推出不久，它的元件也不夠豐富，文件的流暢性也不像其他框架這麼好。總之這個框架還需要時間熟成，有興趣的朋友不妨耐心等候。</p>
<h2 id="結論">結論</h2><p>好的前端框架如果用在正確的地方，絕對可以節省很多開發時間。當然如果你的設計不是那麼中規中矩時，套用這些框架反而容易綁手綁腳，比不用還糟糕。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>如果你像我一樣是一個對 UI 開發不那麼在行，或是無法花太多時間在 UI 設計的 Web 開發者，但卻想能夠快速建立一個可用的 UI 介面，那麼最快的方法就是借重別人寫好的前端框架。</p>
<p>這裡我對前端框架的定義是包含了頁面排版、風格樣式、 UI 元件等，不限純 CSS ，可以包含 JavaScript 。</p>
<p>本文從以前我所使用及目前研究的幾個前端框架，依照研究的深入程度，由深至淺來簡單分享我個人的一些分析心得。至於優缺點，完全是個人主觀意見，大家參考看看即可。</p>]]>
    
    </summary>
    
      <category term="前端開發" scheme="http://jaceju.net/tags/%E5%89%8D%E7%AB%AF%E9%96%8B%E7%99%BC/"/>
    
      <category term="開發工具" scheme="http://jaceju.net/tags/%E9%96%8B%E7%99%BC%E5%B7%A5%E5%85%B7/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[Laravel 5.1 正式釋出]]></title>
    <link href="http://jaceju.net/2015/06/10/laravel-5-1/"/>
    <id>http://jaceju.net/2015/06/10/laravel-5-1/</id>
    <published>2015-06-09T16:34:37.000Z</published>
    <updated>2015-06-09T17:48:54.000Z</updated>
    <content type="html"><![CDATA[<p>經過了幾個月的等待， Laravel 5.1 終於在美國時間 6/9 <a href="https://laravel-news.com/2015/06/laravel-5-1-released/" target="_blank" rel="external">正式釋出</a>了。同時 Laracasts 也推出了一系列的 <a href="https://laracasts.com/series/whats-new-in-laravel-5-1" target="_blank" rel="external">Laravel 5.1 新功能介紹</a>，絕對是每位 Artisan 必看的影片。</p>
<p>官方也介紹了 5.1 有哪些<a href="http://laravel.com/docs/5.1/releases" target="_blank" rel="external">新特色</a> ，以下我會簡單介紹它們。</p>
<a id="more"></a>
<h2 id="不再支援_PHP_5-5-9_以前的版本">不再支援 PHP 5.5.9 以前的版本</h2><p>從推出以來，我個人認為 Laravel 的作者似乎都一直在依循著 PHP 的新特色來開發 Laravel ，讓整個 Framework 不論在程式架構或效能上，都能保持在一定的水準。這次 5.1 版也是如此，直接就告訴大家：該把 PHP 升級到 5.5.9 以上的版本了，這樣才能使用新版的 Laravel 所帶來的好處。</p>
<p>很難想像這樣先進的 PHP Framework 的開發作者，原來是寫 .Net 的；而在開發 Laravel 之前完全沒學過 PHP ，真是令我汗顏。</p>
<h2 id="第一個_LTS_版本">第一個 LTS 版本</h2><p>當然隨著 PHP 持續演進的過程中，勢必就得有所取捨；這樣的特色讓許多依賴舊的 Laravel 版本所開發的專案，在穩定與升級之間面臨很大的抉擇。到了 Laravel 5.1 ，作者終於正式宣佈它是一個 LTS (Long Term Support) 版本，將會讓企業安心用它來開發需要穩定的長期專案。</p>
<p>也就是說現在用 Laravel 5.1 開發的專案，在未來的兩、三年內可以不用再怕沒人修核心的 bug 了。</p>
<h2 id="新的文件">新的文件</h2><p>在 5.1 將要發佈之前，作者花了很多心力在完善文件。例如 <a href="http://laravel.com/docs/5.1/authentication" target="_blank" rel="external">Authentication</a> 的部份就做了很完整的 Quickstart 範例，讓開發者瞭解如何去實作自己的認證機制。另外即時搜尋也是這次文件系統的新特色，讓開發者可以很快地用關鍵字來查閱相關說明。</p>
<p>不過我還是覺得字太小，而且每次要看其他章節都得捲回去頁首。</p>
<h2 id="PSR-2">PSR-2</h2><p>在 Laravel 5.1 之前的版本，讓我最困擾的就是作者自己弄了一套 Coding Style ，而不是照著 PSR 標準走。終於在眾開發者的要求下，作者從善如流，讓核心代碼以及產生器所產生的程式碼，都遵守了 PSR-2 標準。</p>
<p>這個結果，我只能拍手叫好了；因為即便 PSR-2 再怎麼不如己意，但它終歸是大家討論出來的標準。 (公司的 coding style 也不正是如此嗎？)</p>
<ul>
<li>相關影片： <a href="https://laracasts.com/series/whats-new-in-laravel-5-1/episodes/1" target="_blank" rel="external">Adopting PSR-2</a></li>
</ul>
<h2 id="在樣版中注入服務">在樣版中注入服務</h2><p>在 Blade 引擎中也新增了一個 <code>@inject</code> 指令，讓開發者可以即時地注入一個物件。</p>
<figure class="highlight html"><table><tr><td class="code"><pre><span class="line">@inject('metrics', 'App\Services\MetricsService')</span><br><span class="line"></span><br><span class="line"><span class="tag">&lt;<span class="title">div</span>&gt;</span></span><br><span class="line">    Monthly Revenue: &#123;&#123; $metrics-&gt;monthlyRevenue() &#125;&#125;.</span><br><span class="line"><span class="tag">&lt;/<span class="title">div</span>&gt;</span></span><br></pre></td></tr></table></figure>
<p>這樣一來應該就可以不必在 Controller 或 View Composer 中引入變數了。</p>
<ul>
<li>相關文件： <a href="http://laravel.com/docs/5.1/blade#service-injection" target="_blank" rel="external">Blade Templates - Service Injection</a></li>
<li>相關影片： <a href="https://laracasts.com/series/whats-new-in-laravel-5-1/episodes/2" target="_blank" rel="external">Injecting Services With Blade</a></li>
</ul>
<h2 id="Middleware_參數">Middleware 參數</h2><p>原本我們只能從傳入 request 物件給 middleware ，再從 request 物件中取得參數給 middleware 使用；在新版本終於可以自訂 middleware 的參數，再從 route 的定義裡傳入參數給 middleware 。</p>
<p>例如在製作權限系統時，就可以在 route 傳入指定的角色，讓管理權限的 middleware 判斷使用者是否符合這個角色。</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">RoleMiddleware</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">handle</span><span class="params">(<span class="variable">$request</span>, Closure <span class="variable">$next</span>, <span class="variable">$role</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (! <span class="variable">$request</span>-&gt;user()-&gt;hasRole(<span class="variable">$role</span>)) &#123;</span><br><span class="line">            <span class="comment">// Redirect...</span></span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="variable">$next</span>(<span class="variable">$request</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">Route::put(<span class="string">'post/&#123;id&#125;'</span>, [<span class="string">'middleware'</span> =&gt; <span class="string">'role:editor'</span>, <span class="function"><span class="keyword">function</span> <span class="params">(<span class="variable">$id</span>)</span> </span>&#123;</span><br><span class="line">    <span class="comment">//</span></span><br><span class="line">&#125;]);</span><br></pre></td></tr></table></figure>
<p>另外 Middleware 的設計也因為底層的 Symfony 採用了剛定案的 PSR-7 標準，所以也符合 PSR-7 的標準。</p>
<ul>
<li>相關文件： <a href="http://laravel.com/docs/5.1/middleware#middleware-parameters" target="_blank" rel="external">HTTP Middleware - Middleware Parameters</a></li>
</ul>
<h2 id="事件廣播">事件廣播</h2><p>Laravel 原本已經包含了一個強大的事件系統，現在加上實作更簡單的事件廣播功能，能讓開發者搭配特定的服務 (例如 Redis) 以廣播 WebSocket 事件，便可以更有效率地做出即時應用程式。</p>
<p>原本我打算用 <a href="http://reactphp.org/" target="_blank" rel="external">ReactPHP</a> 的說，看來又要改變主意了。</p>
<ul>
<li>相關文件： <a href="http://laravel.com/docs/5.1/events#broadcasting-events" target="_blank" rel="external">Events - Broadcasting Events</a></li>
<li>相關影片： <a href="https://laracasts.com/series/intermediate-laravel/episodes/3" target="_blank" rel="external">The Power of Eventing</a> (需付費)</li>
</ul>
<h2 id="更完整的測試框架">更完整的測試框架</h2><p>先前的版本所提供的測試工具提供的功能有限，僅能做到部份的整合測試。現在 Laravel 5.1 引入了 laracasts 的測試套件，讓整合測試變得更易讀。</p>
<p>我才剛把 <a href="http://codeception.com/" target="_blank" rel="external">Codeception</a> 引入而已，這套要不要用可能要再研究一下。</p>
<ul>
<li>相關文件： <a href="http://laravel.com/docs/5.1/testing" target="_blank" rel="external">Testing</a></li>
</ul>
<h2 id="小結">小結</h2><p>據作者說， Laravel 5.1 是他目前最滿意的版本；當然這或許有些老王賣瓜的感覺，但從 Laravel 3 追到現在，我也覺得 Laravel 真的是與時俱進，不斷地結合許多先進的開發觀念。</p>
<p>接下來我會把一些目前正在執行的專案都換成 5.1 ，再來看看它是不是真的有如作者所說的這麼強大。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>經過了幾個月的等待， Laravel 5.1 終於在美國時間 6/9 <a href="https://laravel-news.com/2015/06/laravel-5-1-released/">正式釋出</a>了。同時 Laracasts 也推出了一系列的 <a href="https://laracasts.com/series/whats-new-in-laravel-5-1">Laravel 5.1 新功能介紹</a>，絕對是每位 Artisan 必看的影片。</p>
<p>官方也介紹了 5.1 有哪些<a href="http://laravel.com/docs/5.1/releases">新特色</a> ，以下我會簡單介紹它們。</p>]]>
    
    </summary>
    
      <category term="Laravel" scheme="http://jaceju.net/tags/Laravel/"/>
    
      <category term="PHP" scheme="http://jaceju.net/tags/PHP/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[在 Mac OS X 上搭建 Selenium 測試環境]]></title>
    <link href="http://jaceju.net/2015/06/03/selenium-on-mac/"/>
    <id>http://jaceju.net/2015/06/03/selenium-on-mac/</id>
    <published>2015-06-03T02:21:45.000Z</published>
    <updated>2015-06-27T13:46:52.000Z</updated>
    <content type="html"><![CDATA[<p>在開發網站的過程中，因為需要測試介面以模擬使用者的操作，最理想的工具就是 <a href="http://www.seleniumhq.org/" target="_blank" rel="external">Selenium</a> 了。</p>
<p>現在我是用 Mac 來當做開發環境，所以簡單記錄一下如何在 OS X 上建立 Selenium 測試環境。</p>
<p>Selenium 的簡單原理與應用可以參考：<a href="http://jaceju.net/2015/05/23/skilltree-tdd-2/">自動測試與 TDD 實務開發 - 上課心得 (中)</a></p>
<a id="more"></a>
<h2 id="準備工作">準備工作</h2><ol>
<li>確認可以執行 Java ，因為 Selenium Server 需要用 Java 執行；如果沒有 Java 的話，可以去 Oracle 官方網站<a href="http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html" target="_blank" rel="external">下載</a> 後安裝。</li>
<li>確認可以執行 <a href="http://brew.sh/" target="_blank" rel="external">homebrew</a> ，因為稍後有幾個套件會使用它來安裝。</li>
<li>在適合的位置建立一個新資料夾，例如 <code>~/Selenium</code> ，接下來的工作都會在這裡進行。</li>
<li>最後在 Terminal 中下載我寫好的 script 並執行它，它會下載 selenium server 並安裝 ChromeDriver 及建立 Firefox Profile。</li>
</ol>
<p>完整的指令如下：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">mkdir -p ~/Selenium &amp;&amp; <span class="built_in">cd</span> <span class="variable">$_</span></span><br><span class="line">curl -S <span class="operator">-s</span> -L https://goo.gl/s519kT &gt; run-selenium</span><br><span class="line">chmod +x run-selenium &amp;&amp; mv run-selenium /usr/<span class="built_in">local</span>/bin</span><br><span class="line">run-selenium init</span><br></pre></td></tr></table></figure>
<h2 id="設定瀏覽器">設定瀏覽器</h2><p>先確認好已經安裝 <a href="http://mozilla.com.tw/" target="_blank" rel="external">Firefox</a> 、 <a href="https://www.google.com.tw/chrome/" target="_blank" rel="external">Google Chrome</a> 等瀏覽器， Safari 則已內建。</p>
<h3 id="Google_Chrome">Google Chrome</h3><p><a href="https://sites.google.com/a/chromium.org/chromedriver/" target="_blank" rel="external">ChromeDriver</a> 可以讓 Selenium Server 呼叫 Google Chrome 執行；如果前面已經執行過 <code>./run-selenium init</code> 的話，就已經安裝好了。可以在 Terminal 執行 <code>chromedriver -v</code> 來驗證是否正確安裝。</p>
<h3 id="Safari">Safari</h3><p><a href="https://github.com/SeleniumHQ/selenium/wiki/SafariDriver" target="_blank" rel="external">SafariDriver</a> 則可以讓 Selenium Server 呼叫 Safari 執行，它是一個 Safari Extension ，必須手動安裝。</p>
<ol>
<li>在 Selenium 官網<a href="http://www.seleniumhq.org/download/" target="_blank" rel="external">下載頁</a>找到 <code>SafariDriver</code> ，下載 <code>Latest release</code> 連結的 <code>SafariDriver.safariextz</code> 檔。</li>
<li>用滑鼠雙擊 <code>SafariDriver.safariextz</code> 檔， Safari 會提示是否安裝 <code>WebDriver</code> ，選「安裝」。</li>
<li>開啟 Safari ，在「偏好設定」裡面切換到「延伸功能」頁籤。</li>
<li>在「延伸功能」頁籤畫面上，應該就會有 <code>WebDriver</code> ，確認它有被啟用。</li>
<li><strong>最後在「安全性」頁籤畫面上，將「阻擋彈出式視窗」取消勾選，避免阻擋測試程式執行。</strong></li>
</ol>
<h2 id="啟用並停用_Selenium_Server">啟用並停用 Selenium Server</h2><p>在要測試之前，啟用 Selenium Server ：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">run-selenium start</span><br></pre></td></tr></table></figure>
<p>要結束 Selenium Server 則是：</p>
<figure class="highlight bash"><table><tr><td class="code"><pre><span class="line">run-selenium stop</span><br></pre></td></tr></table></figure>
<h2 id="測試用範例">測試用範例</h2><p>這邊已經有個我寫好的<a href="https://github.com/jaceju/selenium-demo" target="_blank" rel="external">範例</a>，它整合了以下幾個 Selenium Server 的用法：</p>
<ul>
<li><a href="https://github.com/giorgiosironi/phpunit-selenium" target="_blank" rel="external">PHPUnit Selenium</a></li>
<li><a href="https://github.com/instaclick/php-webdriver" target="_blank" rel="external">PHP WebDriver</a></li>
<li><a href="http://codeception.com/docs/modules/WebDriver" target="_blank" rel="external">Codeception WebDriver Module</a></li>
</ul>
<p>用法請參考 <a href="https://github.com/jaceju/selenium-demo/blob/master/README.md" target="_blank" rel="external">README</a> 說明。</p>
<h2 id="參考">參考</h2><ul>
<li><a href="http://www.hashbangcode.com/blog/automating-headless-selenium-phpunit-tests" target="_blank" rel="external">Automating Headless Selenium PHPUnit Tests</a></li>
<li><a href="http://elementalselenium.com/tips/69-safari" target="_blank" rel="external">Elemental Selenium - How To Use Safari</a></li>
</ul>
]]></content>
    <summary type="html">
    <![CDATA[<p>在開發網站的過程中，因為需要測試介面以模擬使用者的操作，最理想的工具就是 <a href="http://www.seleniumhq.org/">Selenium</a> 了。</p>
<p>現在我是用 Mac 來當做開發環境，所以簡單記錄一下如何在 OS X 上建立 Selenium 測試環境。</p>
<p>Selenium 的簡單原理與應用可以參考：<a href="http://jaceju.net/2015/05/23/skilltree-tdd-2/">自動測試與 TDD 實務開發 - 上課心得 (中)</a></p>]]>
    
    </summary>
    
      <category term="Selenium" scheme="http://jaceju.net/tags/Selenium/"/>
    
      <category term="測試" scheme="http://jaceju.net/tags/%E6%B8%AC%E8%A9%A6/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[自動測試與 TDD 實務開發 - 上課心得 (下)]]></title>
    <link href="http://jaceju.net/2015/05/31/skilltree-tdd-3/"/>
    <id>http://jaceju.net/2015/05/31/skilltree-tdd-3/</id>
    <published>2015-05-31T02:10:10.000Z</published>
    <updated>2015-06-03T02:44:08.000Z</updated>
    <content type="html"><![CDATA[<p>第三週是這門課程的最後一堂課，上完課的我心裡其實有很大的衝擊，一直不知道該怎麼整理這最後的心得；就好像美妙的音樂感動了你的心靈，但自己一時之間卻很難重現那樣的弦律。</p>
<p>前兩週講師介紹了<a href="http://jaceju.net/2015/05/17/skilltree-tdd/">單元測試基礎</a>以及<a href="http://jaceju.net/2015/05/23/skilltree-tdd-2/">如何重構舊有程式</a>之後，但我們依舊面臨了一個很大的問題，那就是：「我們寫出來的程式，不見得就是我們所想的；而就算符合我們想的，也不見得是需求要的。」</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#12300;&#22079;&#65281;&#20320;&#27794;&#30561;&#22909;&#21966;&#65311;&#19978;&#26143;&#26399;&#25552;&#30340;&#25628;&#23563;&#21151;&#33021;&#20320;&#25033;&#35442;&#23436;&#25104;&#20102;&#21543;&#65311;&#12301;&#10;&#12300;&#21999;&#65292;&#25105;&#26152;&#22825;&#21152;&#29677;&#23436;&#25104;&#20102;&#65292;&#36996;&#21152;&#19978;&#20102;&#28204;&#35430;&#65292;&#25033;&#35442;&#24456;&#23436;&#32654;&#65292;&#20320;&#35430;&#35430;&#12290;&#12301;&#10;&#10;&#19981;&#20037;&#24460;...&#10;&#10;&#12300;&#24590;&#40636;&#25105;&#25628;&#23563;&#12302;&#29305;&#20729;&#12303;&#36889;&#20491;&#23383;&#65292;&#27794;&#26377;&#20986;&#29694;&#29305;&#20729;&#21830;&#21697;&#30340;&#39006;&#21029;&#38913;&#65311;&#12301;&#10;&#12300;&#19981;&#26159;&#21543;&#65311;&#25628;&#23563;&#32080;&#26524;&#25033;&#35442;&#26159;&#29544;&#31435;&#30340;&#25628;&#23563;&#21015;&#34920;&#21568;&#65281;&#36889;&#19981;&#26159; common sense &#21966;&#65311;&#12301;&#10;&#12300;&#19981;&#23565;&#19981;&#23565;&#65292;&#20320;&#25033;&#35442;&#22312;&#25105;&#25628;&#23563;&#12302;&#29305;&#20729;&#12303;&#30340;&#26178;&#20505;&#65292;&#30452;&#25509;&#23566;&#21040;&#29305;&#20729;&#21830;&#21697;&#38913;&#23601;&#22909;&#20102;&#12290;&#12301;&#10;&#12300;&#38752;&#65281;&#36889;&#20841;&#22238;&#20107;&#21543;&#65311;&#32780;&#19988;&#20320;&#32102;&#30340;&#30340;&#38656;&#27714;&#21482;&#26377;&#25628;&#23563;&#21830;&#21697;&#32780;&#24050;&#65292;&#21097;&#19979;&#30340;&#35201;&#25105;&#33258;&#24049;&#33126;&#35036;&#21908;&#65311;&#12301;</span><br></pre></td></tr></table></figure>
<p>如果這種戲碼常常在辦公室上演的話，浪費的可不只是開發人員的時間呀！面對不清不楚的需求，或是雙方對需求的認知上有所誤解，甚至開發人員「善意地」加入根本不在需求裡的程式碼，這些問題都將可能拖垮整個專案的進度！</p>
<p>所以最後一週，講師為我們帶來了整套課程的高潮：不僅僅是以規格來完成程式碼，更能<strong>用規格來自動驗證你的程式碼！</strong></p>
<a id="more"></a>
<h2 id="測試驅動了開發，那什麼驅動了測試？">測試驅動了開發，那什麼驅動了測試？</h2><p>自從大師們對 TDD 進行一場論戰後，很多人發現自己誤解了 TDD 的真義，那就是：所謂的測試驅動從來不是為了「讓系統因為有寫測試而顯得專業」或是「儘可能寫出完美而獨立的測試程式」，而是「從有目標的測試中來完成程式碼」。</p>
<p>TDD 雖然能驅動著開發者專注在測試所在乎的目標上，讓程式碼不會有過度設計的問題；但當目標錯誤的話，即便測試寫得再完整也是白搭。然而測試的目標到底是什麼呢？想當然爾就是我們的需求規格；不過這兩者之間還是存在著巨大的門檻，所以我們要面對的問題是：到底要怎麼讓需求轉換成測試的目標？而且隨著需求不斷地成長，也要能驅動我們的測試持續演進？</p>
<h2 id="需求該如何描述？">需求該如何描述？</h2><p>「網站這麼大，使用者很難一下子就找到他們要的商品，我們是不是應該有個搜尋商品的功能？」當客戶提出這樣的需求時，他們通常不明白也不需要明白程式是怎麼做的 (他們只知道這樣的描述可以讓工程師累垮) 。為了避免客戶與工程師之間有認知上的落差，專案負責人 (PO, Product Owner) 可能就會想辦法生出一大堆文件，來描述詳細的系統規格。</p>
<p>然而我們都知道在大環境的變化下，需求可能會被改得面目全非，先前寫好的文件已然成為一堆廢紙。為了不要大家做白工，敏捷開發提出了 User Stories 這個方式，讓真正有價值的需求能被先簡約地描述出來，而不是一開始就陷在文件地獄裡。</p>
<p>通常一個 User Story 可以用以下的格式來描述：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">As a &#60;role&#62;, I want to &#60;action&#62; because of &#60;business value&#62;.&#10;(&#12300;&#26576;&#20491;&#35282;&#33394;&#12301;&#21487;&#20197;&#12300;&#20570;&#26576;&#20214;&#20107;&#12301;&#20358;&#24471;&#21040;&#12300;&#26377;&#21830;&#26989;&#20729;&#20540;&#30340;&#32080;&#26524;&#12301;)</span><br></pre></td></tr></table></figure>
<p>例如：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#12300;&#20351;&#29992;&#32773;&#12301;&#21487;&#20197;&#12300;&#36664;&#20837;&#38364;&#37749;&#23383;&#12301;&#20358;&#12300;&#25214;&#20986;&#31449;&#20839;&#30340;&#21830;&#21697;&#12301;</span><br></pre></td></tr></table></figure>
<p>這樣就是一個 User Story ，它明白地指出需求的目標是什麼，而不會包含技術細節。基本上，我們可以把 User Story 想成是用來確認產品功能的大綱，實作細節可以在之後補上，就不會因為需求的變化而浪費大家的時間。更詳細的 User Story 介紹可以參考以下文章：</p>
<ul>
<li><a href="https://ihower.tw/blog/archives/2090" target="_blank" rel="external">User Stories (1) 什麼是 User Story? by ihower</a> (結果這系列好像富奸了…)</li>
<li><a href="http://kojenchieh.pixnet.net/blog/post/386322818" target="_blank" rel="external">撰寫使用者故事常見的問題 by David Ko</a></li>
</ul>
<p>當然只靠 User Story 的描述對開發者來說是不友善的，所以我們需要靠一些方法讓 User Story 和我們的程式有所繫結；也就是說我們要從 User Story 中找出更詳細的規格，進而整合到前面 TDD 的測試目標裡。當通過了以規格所建立出來的測試，我們的程式碼也才真正地符合需求。</p>
<p>為了能達到開發、測試與需求三位一體的目標，所以 <a href="http://goo.gl/gyuAWY" target="_blank" rel="external">BDD - Behavior Driven Development (行為驅動開發)</a> 就誕生了。</p>
<h2 id="告訴我，這個功能會怎麼被使用">告訴我，這個功能會怎麼被使用</h2><p>BDD 定義了需求方如何撰寫 User Story ，以及開發人員如何把 User Story 轉換成測試；所以 BDD 的重點並不是測試，而是在定義需求的規格。 BDD 在不同的程式語言中都有實作，例如：</p>
<ul>
<li><a href="http://jbehave.org/" target="_blank" rel="external">JBehave</a> - Java 上的 BDD 框架，同時也是最早的 BDD 框架。</li>
<li><a href="http://rspec.info/" target="_blank" rel="external">RSpec</a> - Ruby 上的 BDD 框架，使用 ruby 來直接描述規格。</li>
<li><a href="http://www.phpspec.net/" target="_blank" rel="external">PHPSpec</a> - PHP 上的 BDD 框架，使用 php 來直接描述規格。</li>
<li><a href="https://cucumber.io/" target="_blank" rel="external">Cucumber</a> - 一個用 Ruby 寫的 BDD 框架，後來因為推出 Cucumber-JVM 後，讓其他 JVM-based 語言也可以使用。它的特色是用文字格式的規格檔案來執行測試，後來就變成了一個業界非成文的標準，後來有<a href="https://cucumber.io/docs#cucumber-implementations" target="_blank" rel="external">很多 BDD 框架</a>就參考它的運作方式來實作了。</li>
<li><a href="http://www.specflow.org/" target="_blank" rel="external">SpecFlow</a> - .Net 上的 Cucumber 實作，同時也是這次上課所使用的框架。</li>
<li><a href="http://behat.org/" target="_blank" rel="external">Behat</a> - PHP 上的 Cucumber 實作。</li>
</ul>
<p>Cucumber 利用 <a href="https://github.com/cucumber/cucumber/wiki/Gherkin" target="_blank" rel="external">Gherkin</a> 語法來描述需求規格，所以前面的 User Story 就會變成一個 feature 檔：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># features/products_searching&#10;&#10;Feature: &#20351;&#29992;&#32773;&#21487;&#20197;&#36664;&#20837;&#38364;&#37749;&#23383;&#20358;&#25214;&#20986;&#31449;&#20839;&#30340;&#21830;&#21697;&#10;    In order to &#25214;&#20986;&#25351;&#23450;&#30340;&#21830;&#21697;&#10;    As a &#20351;&#29992;&#32773;&#10;    I want to &#36664;&#20837;&#38364;&#37749;&#23383;</span><br></pre></td></tr></table></figure>
<ul>
<li>註：這裡我不會用課程裡的範例，一是我希望自己是真的理解了 BDD ，所以自己試著如何去應用；二是課程裡的範例更加有挑戰性，我不想破了講師的哏。</li>
</ul>
<p>接著要定義出這個需求的使用場景，也就是更明確地說明這個需求的功能「要怎麼被使用」，我們稱為 Scenario 。</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">Scenario: &#25628;&#23563; &#34;iphone&#34; &#65292;&#25214;&#20986;&#30340;&#21830;&#21697;&#21253;&#21547;&#20102; 5 &#20491;&#21517;&#31281;&#20013;&#31526;&#21512; &#34;iphone&#34; &#30340;&#21830;&#21697;&#10;    Given &#22312;&#25628;&#23563;&#36664;&#20837;&#26694;&#20013;&#36664;&#20837; &#34;iphone&#34;&#10;    When &#25105;&#25353;&#19979;&#25628;&#23563;&#25353;&#37397;&#10;    Then &#22312;&#25628;&#23563;&#38913;&#24471;&#21040; 5 &#20491;&#21517;&#31281;&#20013;&#31526;&#21512; &#34;iphone&#34; &#30340;&#21830;&#21697;&#10;&#10;Scenario: &#25628;&#23563; &#34;&#29305;&#20729;&#34; &#65292;&#26371;&#23566;&#21521;&#29305;&#20729;&#21830;&#21697;&#27963;&#21205;&#38913;&#10;    Given &#22312;&#25628;&#23563;&#36664;&#20837;&#26694;&#20013;&#36664;&#20837; &#34;&#29305;&#20729;&#34;&#10;    When &#25105;&#25353;&#19979;&#25628;&#23563;&#25353;&#37397;&#10;    Then &#23566;&#21521;&#29305;&#20729;&#21830;&#21697;&#27963;&#21205;&#38913;</span><br></pre></td></tr></table></figure>
<p>每個 Scenario 都是一條完整的功能執行路徑，只要有路徑分叉 (例如 <code>if</code> 判斷) 的話，就要有不同的 Scenario 。其中 Given-When-Then 就是用來描述一個 Scenario 的三要角，它們組合起來的意思是：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#20551;&#23450; (Given) &#22312;&#26576;&#20491;&#26781;&#20214;&#19979;&#65292;&#30070; (When) &#25105;&#20570;&#20102;&#26576;&#20491;&#21205;&#20316;&#65292;&#28982;&#24460; (Then) &#23601;&#26371;&#30332;&#29983;&#20160;&#40636;&#32080;&#26524;&#12290;</span><br></pre></td></tr></table></figure>
<p>是不是有種既視感？沒錯！ Given-When-Then 剛好對應到 3A 原則的： Arrange-Act-Assert ，所以 Scenario 可以被轉換成測試！</p>
<h3 id="我想用中文">我想用中文</h3><p>前面在 feature 檔案保留英文單字，是為了讓 Gherkin 的語法解釋器能夠辨別；如果不喜歡這種寫法，也可以用中文，只要在檔案開頭加上 <code># language: zh-TW</code> 就可以。</p>
<p>然後原來的 feature 檔就可以改成：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line"># features/product_searching.feature&#10;# language: zh-TW&#10;&#10;&#21151;&#33021;: &#20351;&#29992;&#32773;&#21487;&#20197;&#36664;&#20837;&#38364;&#37749;&#23383;&#20358;&#25214;&#20986;&#31449;&#20839;&#30340;&#21830;&#21697;&#10;    &#28858;&#20102; &#25214;&#20986;&#25351;&#23450;&#30340;&#21830;&#21697;&#10;    &#36523;&#28858; &#34;&#20351;&#29992;&#32773;&#34;  # &#35387;&#65306;&#36889;&#35041;&#20197;&#12300;&#32773;&#12301;&#32080;&#23614;&#30340;&#35441;&#22312; behat &#35041;&#26371;&#35722;&#20098;&#30908;&#65292;&#25152;&#20197;&#29305;&#21029;&#34389;&#29702;&#10;    &#25105;&#35201; &#36664;&#20837;&#38364;&#37749;&#23383;&#10;&#10;&#22580;&#26223;: &#25628;&#23563; &#34;iphone&#34; &#65292;&#25214;&#20986;&#30340;&#21830;&#21697;&#21253;&#21547;&#20102; 5 &#20491;&#21517;&#31281;&#20013;&#31526;&#21512; &#34;iphone&#34; &#30340;&#21830;&#21697;&#10;    &#20551;&#23450; &#22312;&#25628;&#23563;&#36664;&#20837;&#26694;&#20013;&#36664;&#20837; &#34;iphone&#34;&#10;    &#30070; &#25105;&#25353;&#19979;&#25628;&#23563;&#25353;&#37397;&#10;    &#37027;&#40636; &#22312;&#25628;&#23563;&#38913;&#24471;&#21040; 5 &#20491;&#21517;&#31281;&#20013;&#31526;&#21512; &#34;iphone&#34; &#30340;&#21830;&#21697;&#10;&#10;&#22580;&#26223;: &#25628;&#23563; &#34;&#29305;&#20729;&#34; &#65292;&#26371;&#23566;&#21521;&#29305;&#20729;&#21830;&#21697;&#27963;&#21205;&#38913;&#10;    &#20551;&#23450; &#22312;&#25628;&#23563;&#36664;&#20837;&#26694;&#20013;&#36664;&#20837; &#34;&#29305;&#20729;&#34;&#10;    &#30070; &#25105;&#25353;&#19979;&#25628;&#23563;&#25353;&#37397;&#10;    &#37027;&#40636; &#23566;&#21521;&#29305;&#20729;&#21830;&#21697;&#27963;&#21205;&#38913;</span><br></pre></td></tr></table></figure>
<p>註：這裡因為我是用 Behat ，所以關鍵字的部份是採用 Behat 的語法 (可以用 <code>behat --story-syntax --lang zh-TW</code> 看到範例) 。</p>
<h2 id="讓測試程式跟著需求跑">讓測試程式跟著需求跑</h2><p>課程裡是使用 SpecFlow ，這裡我改用 Behat 來練習。先在專案裡面初始化 Behat 的執行環境：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">behat --init</span><br></pre></td></tr></table></figure>
<p>它會產生一個 <code>features/bootstrap/FeatureContext.php</code> ，而它就是連繫 feature 和測試的關鍵。</p>
<p>建立一個 <code>features/products_searching.feature</code> 檔，然後把前面定義的 featrue 內容貼上去，接著再執行 <code>behat</code> 指令就會得到以下輸出內容：</p>
<p><img src="/resources/skilltree-tdd/behat-01.png" alt="Behat 執行結果"></p>
<p>可以看到 Behat 把 feature 檔裡的 Given-When-Then 都變成了方法，它們稱為 Step Definition 。 Step Definition 是可以被重複使用的，所以可以看到兩個 Scenario 共用了其中兩個方法。不過因為 PHP 不支援用中文當做方法名稱，因此 Behat 幫我們用很奇怪的拼音組成方法名稱。</p>
<p>把 Behat 產生的方法複製到 <code>FeatureContext.php</code> 裡，然後把方法名稱改成易懂的英文，例如：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="comment">// ... 略 ...</span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">FeatureContext</span> <span class="keyword">implements</span> <span class="title">Context</span>, <span class="title">SnippetAcceptingContext</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="comment">/**</span><br><span class="line">     * <span class="doctag">@Given</span> 在搜尋輸入框中輸入 :arg1</span><br><span class="line">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">typingInSearchField</span><span class="params">(<span class="variable">$arg1</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> PendingException();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span><br><span class="line">     * <span class="doctag">@When</span> 我按下搜尋按鈕</span><br><span class="line">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">iEnterSearchButton</span><span class="params">()</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> PendingException();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span><br><span class="line">     * <span class="doctag">@Then</span> 在搜尋頁得到 :arg2 個名稱中符合 :arg1 的商品</span><br><span class="line">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">getProductsThatIncludeInNameOnResultPage</span><span class="params">(<span class="variable">$arg1</span>, <span class="variable">$arg2</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> PendingException();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">/**</span><br><span class="line">     * <span class="doctag">@Then</span> 導向特價商品活動頁</span><br><span class="line">     */</span></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">redirectToPageOfSpecialOffer</span><span class="params">()</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">throw</span> <span class="keyword">new</span> PendingException();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>接下來再執行一次 <code>behat</code> ，就會看到 Behat 要我們一步一步完成測試碼：</p>
<p><img src="/resources/skilltree-tdd/behat-02.png" alt="Behat 執行結果"></p>
<p>這實在是太酷了！現在我們已經讓測試跟規格文件繫結在一起，想要增加需求或修改需求，就是更改測試步驟。而且 Scenario 之間通常只是一兩個步驟的變化，所有已經測試過的步驟都可以重覆利用；而我們要做的就是隨著增長的規格，補上新增的 Step Definition 就可以！</p>
<p>至於怎麼寫測試呢？其實就和單元測試很像，只不過換了一種寫法，前兩週學的都可以應用在這裡面！這樣的開發方式真叫人欲罷不能！</p>
<p>註：心得不打算介紹太多實作，有機會我再補充 Behat 的做法。</p>
<h2 id="不再是對立，而是能一起驗收需求">不再是對立，而是能一起驗收需求</h2><p>定義好了 feature 檔，我們只要把一個個的 Scenario 完成；就像雕刻一樣，一刀一刀切，最後就會看到完整的成品。而 PO 驗收專案時只需要對照 feature 所對應的測試是否為綠燈，只要通過了，也就是符合需求！這樣一來辦公室就再也不會有劍拔弩張的氣氛了！</p>
<p>當然一開始不見得會這麼順利，身為 BDD 的導入者，或許我們需要花更多耐心來引導 PO 協助我們去撰寫 Scenario ；當他漸漸瞭解這樣做的好處時，就會知道用這個方式能讓需求更快被滿足。未來只要整個團隊認同並遵循這樣的做法，我相信 BDD 一定能為專案帶來莫大的益處。</p>
<h2 id="讓文件活下去">讓文件活下去</h2><p>基本上， 多數的 Developer 通常很討厭：</p>
<ul>
<li>寫文件</li>
<li>寫註解</li>
<li>寫測試</li>
</ul>
<p>但是更討厭：</p>
<ul>
<li>別的 Developer 都不寫文件</li>
<li>別的 Developer 都不寫註解</li>
<li>別的 Developer 都不寫測試</li>
</ul>
<p>現在有了 feature 檔，也就同時有了文件和測試，更酷的是這兩者是隨著程式碼一起成長的！</p>
<p>而這門課最有價值的一節，就是教你如何把 feature 執行的結果轉換成 HTML 及 Word 文件，這樣一來只要搭配 CI 就可以讓所有作業一氣呵成！這樣的快感，是工程師夢寐以求的呀！</p>
<p>詳細方法當然還是上課才有的福利囉，至於 PHP 的方法我還在找尋中，這時就不得不說學 .Net 的朋友真是幸福。</p>
<h2 id="總結">總結</h2><p>不要一直想著要寫出完美的測試，因為那通常已經偏離了真正的需求。讓程式符合需求才是開發者的最終目標，只是完整而詳細的需求卻不會自己長腳跑來。</p>
<p>讓真正瞭解需求的人協助你一起完成規格，讓這些規格協助你建立測試，而且執行測試。最後我們唯一要關注的，就是如何去滿足這些測試的目標。對整個團隊來說，真的能改善不少浪費的問題。</p>
<p>如果有機會再開課的話，我強烈推薦大家報名參加呀！趕快關注「<a href="http://skilltree.my/events/ebg" target="_blank" rel="external">自動測試與 TDD 實務開發</a>」的下一梯課程吧！</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>第三週是這門課程的最後一堂課，上完課的我心裡其實有很大的衝擊，一直不知道該怎麼整理這最後的心得；就好像美妙的音樂感動了你的心靈，但自己一時之間卻很難重現那樣的弦律。</p>
<p>前兩週講師介紹了<a href="http://jaceju.net/2015/05/17/skilltree-tdd/">單元測試基礎</a>以及<a href="http://jaceju.net/2015/05/23/skilltree-tdd-2/">如何重構舊有程式</a>之後，但我們依舊面臨了一個很大的問題，那就是：「我們寫出來的程式，不見得就是我們所想的；而就算符合我們想的，也不見得是需求要的。」</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#12300;&#22079;&#65281;&#20320;&#27794;&#30561;&#22909;&#21966;&#65311;&#19978;&#26143;&#26399;&#25552;&#30340;&#25628;&#23563;&#21151;&#33021;&#20320;&#25033;&#35442;&#23436;&#25104;&#20102;&#21543;&#65311;&#12301;&#10;&#12300;&#21999;&#65292;&#25105;&#26152;&#22825;&#21152;&#29677;&#23436;&#25104;&#20102;&#65292;&#36996;&#21152;&#19978;&#20102;&#28204;&#35430;&#65292;&#25033;&#35442;&#24456;&#23436;&#32654;&#65292;&#20320;&#35430;&#35430;&#12290;&#12301;&#10;&#10;&#19981;&#20037;&#24460;...&#10;&#10;&#12300;&#24590;&#40636;&#25105;&#25628;&#23563;&#12302;&#29305;&#20729;&#12303;&#36889;&#20491;&#23383;&#65292;&#27794;&#26377;&#20986;&#29694;&#29305;&#20729;&#21830;&#21697;&#30340;&#39006;&#21029;&#38913;&#65311;&#12301;&#10;&#12300;&#19981;&#26159;&#21543;&#65311;&#25628;&#23563;&#32080;&#26524;&#25033;&#35442;&#26159;&#29544;&#31435;&#30340;&#25628;&#23563;&#21015;&#34920;&#21568;&#65281;&#36889;&#19981;&#26159; common sense &#21966;&#65311;&#12301;&#10;&#12300;&#19981;&#23565;&#19981;&#23565;&#65292;&#20320;&#25033;&#35442;&#22312;&#25105;&#25628;&#23563;&#12302;&#29305;&#20729;&#12303;&#30340;&#26178;&#20505;&#65292;&#30452;&#25509;&#23566;&#21040;&#29305;&#20729;&#21830;&#21697;&#38913;&#23601;&#22909;&#20102;&#12290;&#12301;&#10;&#12300;&#38752;&#65281;&#36889;&#20841;&#22238;&#20107;&#21543;&#65311;&#32780;&#19988;&#20320;&#32102;&#30340;&#30340;&#38656;&#27714;&#21482;&#26377;&#25628;&#23563;&#21830;&#21697;&#32780;&#24050;&#65292;&#21097;&#19979;&#30340;&#35201;&#25105;&#33258;&#24049;&#33126;&#35036;&#21908;&#65311;&#12301;</span><br></pre></td></tr></table></figure>
<p>如果這種戲碼常常在辦公室上演的話，浪費的可不只是開發人員的時間呀！面對不清不楚的需求，或是雙方對需求的認知上有所誤解，甚至開發人員「善意地」加入根本不在需求裡的程式碼，這些問題都將可能拖垮整個專案的進度！</p>
<p>所以最後一週，講師為我們帶來了整套課程的高潮：不僅僅是以規格來完成程式碼，更能<strong>用規格來自動驗證你的程式碼！</strong></p>]]>
    
    </summary>
    
      <category term="BDD" scheme="http://jaceju.net/tags/BDD/"/>
    
      <category term="TDD" scheme="http://jaceju.net/tags/TDD/"/>
    
      <category term="測試" scheme="http://jaceju.net/tags/%E6%B8%AC%E8%A9%A6/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[自動測試與 TDD 實務開發 - 上課心得 (中)]]></title>
    <link href="http://jaceju.net/2015/05/23/skilltree-tdd-2/"/>
    <id>http://jaceju.net/2015/05/23/skilltree-tdd-2/</id>
    <published>2015-05-23T08:19:43.000Z</published>
    <updated>2015-06-03T02:44:06.000Z</updated>
    <content type="html"><![CDATA[<p>曾經有個工程師對著已經上線的網站說：「別說使用者不曉得這個系統是怎麼運作的，其實已經接手那麼久的我也不知道。」</p>
<p>如果你對這句話心有戚戚焉的話，那你真的不孤單。其實有很多維護維護前人程式碼的工程師在在接到新的需求而去修改程式碼時，常常是很戰戰競競的，然後辦公室裡就會響起這樣的聲音：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#12300;&#25105;&#35352;&#24471;&#21069;&#19968;&#20491;&#24037;&#31243;&#24107;&#35498;&#36889;&#37002;&#21152;&#24190;&#34892;&#31243;&#24335;&#30908;&#23601;&#21487;&#20197;&#20102;&#65292;&#20320;&#35258;&#24471;&#21602;&#65311;&#12301;&#10;&#12300;&#25105;&#30452;&#35258;&#36889;&#27171;&#25913;&#26371;&#20986;&#21839;&#38988;...&#26377;&#29872;&#22659;&#35731;&#25105;&#20808;&#28204;&#35430;&#30475;&#30475;&#21966;&#65311;&#12301;&#10;&#12300;&#20808;&#19981;&#31649;&#65292;&#24460;&#22825;&#27963;&#21205;&#35201;&#19978;&#20102;&#65292;&#20320;&#20808;&#25913;&#23436;&#35731;&#23427;&#19978;&#32218;&#24460;&#20877;&#35498;&#12290;&#12301;&#10;&#12300;&#27794;&#26377;&#28204;&#35430;&#30340;&#35441;...&#12301;&#10;&#12300;&#38281;&#22068;&#65281;&#24555;&#21435;&#25913;&#12290;&#12301;&#10;&#19981;&#20037;&#24460;...&#10;&#12300;&#24920;&#20102;&#65281;&#35330;&#21934;&#20986;&#22823;&#21839;&#38988;&#20102;&#65281;&#20320;&#21487;&#20197;&#20808;&#36996;&#21407;&#21407;&#20358;&#30340;&#31243;&#24335;&#30908;&#21966;&#65311;&#12301;&#10;&#12300;&#19981;&#23565;&#21568;&#65281;&#25105;&#25913;&#36889;&#37002;&#24590;&#40636;&#26371;&#35731;&#37027;&#35041;&#20986;&#37679;&#65311;&#12301;&#10;&#21448;&#36942;&#20102;&#19968;&#26371;&#20818;...&#10;&#12300;&#20320;&#26377;&#36996;&#21407;&#21966;&#65311;&#65281;&#25972;&#20491;&#35330;&#21934;&#36039;&#26009;&#22823;&#20098;&#20102;&#21568;&#65281;&#20320;&#19968;&#23450;&#26377;&#21205;&#21040;&#20160;&#40636;&#26481;&#35199;&#20102;&#65281;&#12301;&#10;&#12300;&#25105;&#20840;&#36996;&#21407;&#20102;&#65281;&#29694;&#22312;&#36305;&#30340;&#26159;&#25105;&#27794;&#25913;&#36942;&#21069;&#30340;&#29256;&#26412;&#65281;&#12301;&#10;&#12300;&#19981;&#31649;&#65281;&#21152;&#29677;&#20462;&#22909;&#23427;&#65281;&#12301;&#10;&#12300;&#25105;&#19981;&#23601;&#35498;&#27794;&#28204;&#35430;&#30340;&#35441;&#26371;&#26377;&#21839;&#38988;&#21966;&#65281;&#65311;&#28858;&#20160;&#40636;&#19981;&#32893;&#65281;&#65311;&#12301;&#10;&#12300;&#20320;&#32769;&#38342;&#25105;&#32769;&#38342;&#65311;&#20877;&#38752;&#26479;&#35430;&#35430;&#30475;&#65281;&#12301;&#10;&#19977;&#22825;&#24460;...&#10;&#12300;&#22816;&#20102;&#65281;&#25294;&#26479;&#36208;&#65281;&#25105;&#19981;&#24819;&#20877;&#25913;&#36889;&#20491;&#31995;&#32113;&#20102;&#65281; (&#25684;&#26479;&#23376;) &#12301;</span><br></pre></td></tr></table></figure>
<p>假設今天有時間讓你調整這個系統，在不知道線上系統整體如何運作的狀況下，你會怎麼讓它容易被維護與增添新功能呢？</p>
<p>上週所介紹的<a href="http://jaceju.net/2015/05/17/skilltree-tdd/">單元測試</a>其實還沒辦法可以讓我們立刻套用在這樣的系統上，所以本週課程的重點就在：如何為已經上線的 legacy code 加上測試。</p>
<p>為了達成這個目標，講師介紹了兩個招式： Web Testing 與 Refactoring 。以下我就以我的方式來介紹我所學到的心得。</p>
<a id="more"></a>
<h2 id="Web_Testing">Web Testing</h2><p>看過聖鬥士星矢的話，應該都知道雅典娜說過：「你不是還有生命嗎？」</p>
<p><img src="/images/athena.jpg" alt="無良老闆雅典娜"></p>
<p>那麼回到問題點：現行的系統沒有測試怎麼辦？</p>
<p>對工程師來說，雅典娜的話就變成了：「你不是還有線上功能嗎？」</p>
<p>是呀！當系統是黑箱作業卻正常運作的時候，我們是從頁面 (介面) 上來確認的。換句話說，既然我們操作的方式是對的，而且畫面所得到結果也是我們所預期的，那麼對外部來說，這個已知功能就應該是正確的。所以我們可以先「對所有正確執行的功能建立測試」！只要在修改程式碼後，再跑相同的測試而沒有發生錯誤，就能證明我們的修改沒有影響到舊有程式碼。這實在是太酷了！</p>
<p>就網站來說，這樣的外部測試方式就稱為 Web Testing 。而如果所有的測試是以自動化腳本的形式存在，讓我們每次修改完程式碼後，可以方便地一次全部執行的話就太好了。最重要的，就是要有工具能幫我們產生這樣的腳本！而這種神一般的工具，就非 Selenium 莫屬了。</p>
<h3 id="Selenium">Selenium</h3><p>Selenium 是一個讓瀏覽器自動化執行使用者操作流程的工具，通常用來當做測試的用途。它主要原理是利用 Selenium IDE 將使用者操作的過程錄製下來，並透過 Selenium IDE 或 Selenium WebDriver 執行該腳本，讓瀏覽器自動重現使用者操作過程。而它除了讓使用者操作的流程自動化之外，最厲害的地方是能夠把錄製下來的腳本轉換成不同的程式語言格式，融入到各語言的自動測試框架裡。</p>
<p>簡單介紹 Selenium 幾個要角：</p>
<ol>
<li>Selenium IDE ： Firefox 的 Add-on ，用來錄製使用者在操作頁面時所有的動作與輸入，並且驗證輸出結果是否符合預期。而它的重播功能除了讓開發者快速驗證功能外，用來製作說明文件裡的操作範本也相當適合。</li>
<li>Selenium WebDriver ：告訴使用者的 IDE (例如 Visual Studio) 或 Selenium Server 如何啟動不同瀏覽器的中介層。常用的 WebDriver 包含了 IE 、 Firefox 、 Chrome 、 PhantomJS 等。 (原名 Selenium Remote Control)</li>
<li>Selenium Server (選備) ：用 Java 寫的 Daemon 服務；如果你的開發環境和測試用的瀏覽器不在同一台機器上，或是某些無法直接啟動瀏覽器的狀況下，你可能會需要透過 Selenium Server 來協助 WebDriver 啟動測試用的瀏覽器。 (原名 Selenium RC Server)</li>
</ol>
<p>註：由於課程是使用 Visual Studio 來做本機開發與測試，所以只要用 Nuget 安裝 Selenium WebDriver 套件即可，不需要 Selenium Server 。</p>
<p>整個 Selenium 的使用流程如下：</p>
<ol>
<li>在 Firefox 上安裝 <a href="http://release.seleniumhq.org/selenium-ide/2.9.0/selenium-ide-2.9.0.xpi" target="_blank" rel="external">Selenium IDE</a> 。</li>
<li>視開發環境決定是否要啟動 Selenium Server ，或使用整合式的 Selenium WebDriver 。如果是 PHP 的話，不論是不是在本機執行，都一定要啟動 Selenium Server 。</li>
<li>啟動 Web 服務，讓 Firefox 連上測試網站。</li>
<li>針對所有功能錄製測試腳本 (包含操作流程與結果驗證) 。</li>
<li>重跑所有腳本，確認沒有操作上的問題。</li>
<li>利用 Selenium 的 Formatter 外掛，將 Selenium IDE 腳本轉換成對應的程式語言自動化測試框架的程式碼。</li>
<li>讓自動化測試框架透過 Selenium WebDriver 來完整執行所有的 Selenium 測試腳本，並確認各瀏覽器有被正常開啟。</li>
<li>接下來就可以繼續後續程式的新增功能或重構了。</li>
</ol>
<p>而 Selenium WebDriver 詳細的使用方式在不同的語言有不同的作法，請分別參考我兩位好友的介紹：</p>
<ul>
<li>Vistual Studio 走這邊： <a href="http://goo.gl/uIFwD" target="_blank" rel="external">[30天快速上手TDD]Integration Testing &amp; Web UI Testing by Joey Chen</a> (作者名字很熟對不對？)</li>
<li>PHP 走這邊：<a href="https://www.youtube.com/watch?v=CtsH1n5-Xcc&amp;hd=1" target="_blank" rel="external">PHP 也有Day #13 - 如何使用 Selenium 搞定前端測試 by Ricky Su</a></li>
</ul>
<p>註：之前我也寫過 <a href="http://jaceju.net/2010/07/12/1293/">Web UI 測試的好幫手 - Selenium</a> ，雖然內容有點舊，但原理沒差太多。</p>
<h3 id="常見的問題">常見的問題</h3><p>像 Web Testing 這種由外部建立測試的方法，雖然在遇到大多數沒有撰寫測試的 legacy 系統時非常有用，但也不能說毫無缺點。當系統對環境依賴度相當高時，想重建一個供測試用的系統會變得非常困難，而這通常也是必須先克服的問題之一。</p>
<p>然後規模稍大，功能點較多的網站就會面臨第二個問題：「誰來錄操作流程？」答案很簡單：當然不是工程師。通常 legacy 系統一定會有瞭解它是如何使用的人，這時候必須借重老闆的影響力，想辦法讓這些人能夠協助列出系統所有的功能流程。</p>
<p>由於 Selenium IDE 非常易用，可以先花點時間教會這些人學習怎麼錄製操作流程。接下來請他們在邊列功能時，順手將流程錄製起來；這樣一來當功能列完的同時，就有完整的測試可以使用，還順便帶有操作示範呢。</p>
<p>另外若是錯誤的操作會讓程式出現非預期的結果的話，就應該歸類在原本就有的 bug 。但這也表示我們知道有這個操作流程應該要被避免，因此也要特別為它建立一組測試；一來可以在修改程式後透過這組測試確認程式的修改是正確的，二來也確保未來不會再讓這個錯誤發生。就像「同樣的招式不能對聖鬥士使用第二次」，同樣的 bug 也不該在系統上發生第二次。</p>
<h3 id="FluentAutomation_從愛開始">FluentAutomation 從愛開始</h3><p>Seleinum WebDriver 程式碼好難看懂怎麼辦？如果未來我加新功能而沒辦法錄製腳本時，要照著這樣寫測試嗎？當然不！我們需要讓測試腳本變得抽象化一些，看起來就像是用人話在描述規格一樣。</p>
<p>又是借重好用工具的時刻，在微軟體系上有個好用的 <a href="http://fluent.stirno.com/" target="_blank" rel="external">FluentAutomation</a> 第三方套件，就是能將 Selenium 腳本語法包裝起來的語義化框架。</p>
<p>例如我們有這樣的規格：</p>
<figure class="highlight csharp"><table><tr><td class="code"><pre><span class="line"><span class="comment">// 登入成功</span></span><br><span class="line">    <span class="comment">// 我打開 Login 的網頁</span></span><br><span class="line">    <span class="comment">// 在 id 裡面輸入 user</span></span><br><span class="line">    <span class="comment">// 在 password 裡面輸入 pass</span></span><br><span class="line">    <span class="comment">// 按下登入</span></span><br><span class="line">    <span class="comment">// 期望應該導到首頁</span></span><br></pre></td></tr></table></figure>
<p>用 FluentAutomation 寫出來的話，就會是：</p>
<figure class="highlight csharp"><table><tr><td class="code"><pre><span class="line"><span class="keyword">private</span> <span class="keyword">string</span> baseUrl = <span class="string">@"http://localhost:29021/"</span>;</span><br><span class="line"></span><br><span class="line">[TestMethod]</span><br><span class="line"><span class="function"><span class="keyword">public</span> <span class="keyword">void</span> <span class="title">TestLoginSuccess</span>(<span class="params"></span>)</span><br><span class="line"></span>&#123;</span><br><span class="line">    I.Open(baseUrl + <span class="string">"login"</span>)</span><br><span class="line">        .Enter(<span class="string">"user"</span>).In(<span class="string">"#id"</span>)</span><br><span class="line">        .Enter(<span class="string">"pass"</span>).In(<span class="string">"#password"</span>)</span><br><span class="line">        .Click(<span class="string">"input[type=\"submit\"]"</span>)</span><br><span class="line">        .Assert.Url(baseUrl);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>因為測試的主角是「我」，所以一切從 <code>I</code> 開始；而測試的對象是「網頁」，所以我們會有 <code>Open</code> (開啟網址) / <code>Enter</code> (輸入) / <code>Click</code> (點擊) 等操作方式；最後再使用 <code>Assert</code> 來驗證畫面輸出是否如我們所預期。整段程式碼是不是看起來更語意化呢？</p>
<p>除了語法更為抽象之外， FluentAutomation 執行的結果其實和 Selenium WebDriver 是一樣的。 FluentAutomation 能讓開發者融入測試的情境中，從使用者行為的角度出發去看待系統功能。一切就像用人話在說明功能，看起來就是這麼自然。</p>
<p>註： FluentAutomation 也<a href="http://fluent.stirno.com/docs/#multi-browser" target="_blank" rel="external">支援多種不同瀏覽器</a>同時測試，相關的說明請參考<a href="http://fluent.stirno.com/docs/" target="_blank" rel="external">官方文件</a>。</p>
<p>註： PHP 也有類似的框架，是包含在 <a href="http://codeception.com/" target="_blank" rel="external">Codeception</a> 的 <a href="http://codeception.com/docs/modules/WebDriver" target="_blank" rel="external">WebDriver</a> 模組中。</p>
<h3 id="Page_Objects_Pattern">Page Objects Pattern</h3><p>課程中介紹另一個我覺得很棒的觀念是，雖然功能是一樣的，但如果頁面結構調整了怎麼辦？這時候就要用到物件導向最重要的觀念：封裝變化。</p>
<p>Page Objects Patterns 把頁面當成是一個物件，只讓它露出必要的行為，讓我們的主要流程只描述如何跟這個頁面互動，而不在乎它頁面上的細節。這樣一來不管頁面結構怎麼變化，只要主要功能不變，我們都只需要修改這個 Page Object 就好。</p>
<p>Page Objects 在 FluentAutomation 的實作範例可以參考官方說明的 <a href="http://fluent.stirno.com/docs/#pageobjects" target="_blank" rel="external">PageObjects</a> 一節。</p>
<p>同樣地在 PHP 的 Codeception 也支援 Page Objects Patterns ，請參考 <a href="http://codeception.com/docs/07-AdvancedUsage#PageObjects" target="_blank" rel="external">PageObjects</a> 一節。</p>
<h2 id="Refactoring">Refactoring</h2><p>假設我們已經有了 Web Testing 來確保我們的功能都經過了外部測試，那麼我們就有機會對它進行重構了。</p>
<p>課程中的範例可以說是這兩週課程的精華呀，從一個很難撼動的程式碼，一步一步重構成包含了 Web Testing 和單元測試的可維護可擴充架構。</p>
<p>這裡很難把它用三言兩言完整描述出來，恕我偷懶一下，這裡請直接參考講師以前寫的 30 天快速上手 TDD 之重構系列文章：</p>
<ul>
<li><a href="http://goo.gl/59rTl" target="_blank" rel="external">[30天快速上手TDD][Day 9]Refactoring legacy code 簡介</a></li>
<li><a href="http://goo.gl/4zdts" target="_blank" rel="external">[30天快速上手TDD][Day 10]Refactoring 起手式 - 建立測試</a></li>
<li><a href="http://goo.gl/LWUDp" target="_blank" rel="external">[30天快速上手TDD][Day 11]Refactoring - 讓程式碼說話</a></li>
<li><a href="http://goo.gl/U3nvF" target="_blank" rel="external">[30天快速上手TDD][Day 12]Refactoring - 職責分離</a></li>
<li><a href="http://goo.gl/zRgyy" target="_blank" rel="external">[30天快速上手TDD][Day 13]Refactoring - 告訴我，你要什麼</a></li>
<li><a href="http://goo.gl/A7EnM" target="_blank" rel="external">[30天快速上手TDD][Day 14]Refactoring - 驗貨</a></li>
<li><a href="http://goo.gl/xMt7p" target="_blank" rel="external">[30天快速上手TDD][Day 15]Refactoring - 食神歸位</a></li>
<li><a href="http://goo.gl/gcylXL" target="_blank" rel="external">[30天快速上手TDD][Day 16]Refactoring - 介面導向</a></li>
<li><a href="http://goo.gl/2THME" target="_blank" rel="external">[30天快速上手TDD][Day 17]Refactoring - Strategy Pattern</a></li>
<li><a href="http://goo.gl/vyHtI" target="_blank" rel="external">[30天快速上手TDD][Day 18]Refactoring - Factory Pattern</a></li>
<li><a href="http://goo.gl/yzsQc" target="_blank" rel="external">[30天快速上手TDD][Day 19]Refactoring - The End is the Beginning</a></li>
</ul>
<p>簡單說明一下整套重構流程的重點：</p>
<ol>
<li>用 Selenium 錄下你的 Web 測試，越完整越好，而且讓它可以隨時執行。</li>
<li>理解你要重構的 legacy 程式碼意圖，把註解補上去。不用管細節，只要知道某段程式打算做什麼就好。</li>
<li>重構：想辦法把邏輯和 UI 分離開來，這個可能會因為語言或框架的不同而有不同的做法。</li>
<li>重構：應用常用的 Extract Method 技巧來將程式碼拆分出易懂的方法。</li>
<li>重構：依照職責分離的原則，把剛剛分離出來的方法引到新的類別中。</li>
<li>獨立出類別後，就可以把單元測試補上去了。接著所有測試都跑跑看吧。</li>
<li>重構：為各相似的類別抽出介面 (Interface) ，讓主程式去依賴介面，而不要依賴剛剛的類別。</li>
<li>重構：在抽出介面後，利用 Factory Method 將生成物件 (<code>new</code>) 的職責獨立出去。</li>
</ol>
<p>要特別記住：</p>
<ul>
<li>每一步重構後都要測試，而且只要測試是綠燈，都是應該是能夠上線的狀態。</li>
<li>通過測試後，就把程式碼 commit 到 VCS 裡，別偷懶。</li>
<li>重構在搬移程式碼時，只修改 context ，而不要修改流程結構。</li>
<li>修改流程結構 (例如 <code>if</code> 換成 <code>switch</code> ) 要自成一個重構。</li>
</ul>
<h3 id="避免增加中介層的獨立測試">避免增加中介層的獨立測試</h3><p>這段是講師的壓箱寶，據說是這次課程才加入的，有聽有賺到。</p>
<p>這邊我打算用一句話來帶過：懶爸爸有個相依耦合的類別，把它抽出來放到可覆寫的方法中回傳；用個笨兒子來繼承懶爸爸，接著笨兒子用 DI 來注入相依物件的 stub object ；最後改測試笨兒子，結束。</p>
<p>如果看得懂上面這句，我想就能得到講師這段課程的精髓了。至於這個技巧多有用？如果發現類別中相依耦合度很高，卻又不確定這些類別被哪些程式用到，更擔心修改方法簽名就會影響整個系統時，你就會知道這個技巧的威力了。</p>
<h2 id="心得">心得</h2><p>我必須老實說，不論是補測試或是重構，技術上絕對不是什麼大問題；像這次的課程所介紹的技巧，都沒有什麼非常困難的部份。那麼為什麼很多人不做呢？我想是因為大家都拿「新功能都做不完了，哪來時間做重構？」做為藉口了。但回到最文章最開頭的情境，其實真的花你時間的，反而不是這些測試或重構，而是你跟同事因為系統出問題的爭執。</p>
<p>如果老闆有心解決舊有系統的問題，就想辦法說服他加上測試並重構吧。</p>
<p>想瞭解更多的話，請報名「<a href="http://skilltree.my/events/ebg" target="_blank" rel="external">自動測試與 TDD 實務開發</a> 」。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>曾經有個工程師對著已經上線的網站說：「別說使用者不曉得這個系統是怎麼運作的，其實已經接手那麼久的我也不知道。」</p>
<p>如果你對這句話心有戚戚焉的話，那你真的不孤單。其實有很多維護維護前人程式碼的工程師在在接到新的需求而去修改程式碼時，常常是很戰戰競競的，然後辦公室裡就會響起這樣的聲音：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">&#12300;&#25105;&#35352;&#24471;&#21069;&#19968;&#20491;&#24037;&#31243;&#24107;&#35498;&#36889;&#37002;&#21152;&#24190;&#34892;&#31243;&#24335;&#30908;&#23601;&#21487;&#20197;&#20102;&#65292;&#20320;&#35258;&#24471;&#21602;&#65311;&#12301;&#10;&#12300;&#25105;&#30452;&#35258;&#36889;&#27171;&#25913;&#26371;&#20986;&#21839;&#38988;...&#26377;&#29872;&#22659;&#35731;&#25105;&#20808;&#28204;&#35430;&#30475;&#30475;&#21966;&#65311;&#12301;&#10;&#12300;&#20808;&#19981;&#31649;&#65292;&#24460;&#22825;&#27963;&#21205;&#35201;&#19978;&#20102;&#65292;&#20320;&#20808;&#25913;&#23436;&#35731;&#23427;&#19978;&#32218;&#24460;&#20877;&#35498;&#12290;&#12301;&#10;&#12300;&#27794;&#26377;&#28204;&#35430;&#30340;&#35441;...&#12301;&#10;&#12300;&#38281;&#22068;&#65281;&#24555;&#21435;&#25913;&#12290;&#12301;&#10;&#19981;&#20037;&#24460;...&#10;&#12300;&#24920;&#20102;&#65281;&#35330;&#21934;&#20986;&#22823;&#21839;&#38988;&#20102;&#65281;&#20320;&#21487;&#20197;&#20808;&#36996;&#21407;&#21407;&#20358;&#30340;&#31243;&#24335;&#30908;&#21966;&#65311;&#12301;&#10;&#12300;&#19981;&#23565;&#21568;&#65281;&#25105;&#25913;&#36889;&#37002;&#24590;&#40636;&#26371;&#35731;&#37027;&#35041;&#20986;&#37679;&#65311;&#12301;&#10;&#21448;&#36942;&#20102;&#19968;&#26371;&#20818;...&#10;&#12300;&#20320;&#26377;&#36996;&#21407;&#21966;&#65311;&#65281;&#25972;&#20491;&#35330;&#21934;&#36039;&#26009;&#22823;&#20098;&#20102;&#21568;&#65281;&#20320;&#19968;&#23450;&#26377;&#21205;&#21040;&#20160;&#40636;&#26481;&#35199;&#20102;&#65281;&#12301;&#10;&#12300;&#25105;&#20840;&#36996;&#21407;&#20102;&#65281;&#29694;&#22312;&#36305;&#30340;&#26159;&#25105;&#27794;&#25913;&#36942;&#21069;&#30340;&#29256;&#26412;&#65281;&#12301;&#10;&#12300;&#19981;&#31649;&#65281;&#21152;&#29677;&#20462;&#22909;&#23427;&#65281;&#12301;&#10;&#12300;&#25105;&#19981;&#23601;&#35498;&#27794;&#28204;&#35430;&#30340;&#35441;&#26371;&#26377;&#21839;&#38988;&#21966;&#65281;&#65311;&#28858;&#20160;&#40636;&#19981;&#32893;&#65281;&#65311;&#12301;&#10;&#12300;&#20320;&#32769;&#38342;&#25105;&#32769;&#38342;&#65311;&#20877;&#38752;&#26479;&#35430;&#35430;&#30475;&#65281;&#12301;&#10;&#19977;&#22825;&#24460;...&#10;&#12300;&#22816;&#20102;&#65281;&#25294;&#26479;&#36208;&#65281;&#25105;&#19981;&#24819;&#20877;&#25913;&#36889;&#20491;&#31995;&#32113;&#20102;&#65281; (&#25684;&#26479;&#23376;) &#12301;</span><br></pre></td></tr></table></figure>
<p>假設今天有時間讓你調整這個系統，在不知道線上系統整體如何運作的狀況下，你會怎麼讓它容易被維護與增添新功能呢？</p>
<p>上週所介紹的<a href="http://jaceju.net/2015/05/17/skilltree-tdd/">單元測試</a>其實還沒辦法可以讓我們立刻套用在這樣的系統上，所以本週課程的重點就在：如何為已經上線的 legacy code 加上測試。</p>
<p>為了達成這個目標，講師介紹了兩個招式： Web Testing 與 Refactoring 。以下我就以我的方式來介紹我所學到的心得。</p>]]>
    
    </summary>
    
      <category term="Selenium" scheme="http://jaceju.net/tags/Selenium/"/>
    
      <category term="TDD" scheme="http://jaceju.net/tags/TDD/"/>
    
      <category term="測試" scheme="http://jaceju.net/tags/%E6%B8%AC%E8%A9%A6/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[自動測試與 TDD 實務開發 - 上課心得 (上)]]></title>
    <link href="http://jaceju.net/2015/05/17/skilltree-tdd/"/>
    <id>http://jaceju.net/2015/05/17/skilltree-tdd/</id>
    <published>2015-05-17T02:07:11.000Z</published>
    <updated>2015-06-03T02:44:11.000Z</updated>
    <content type="html"><![CDATA[<p>測試一直以來是很多開發者心中的痛，當被老闆問到：「你的程式碼是否都測試過了？」你是否能摸著自己的良心，並且拿出證據來說自己真的做過測試讓所有功能都符合需求了？我相信絕大多數的開發者這時一定都是面有難色，因為其實你不但騙了老闆，還騙了你自己。</p>
<p>從我自己開始研究測試後，其實一直覺得自己還是沒有真正把測試當成是開發的一部份，更別說坊間很多程式書籍和教學課程能把這兩者真真實實地融合在一起，傳授給廣大的開發人員。</p>
<p>這次正好有機會去聽 Joey (91 哥) 在 <a href="http://skilltree.my/" target="_blank" rel="external">SkillTree</a> 開的 <a href="http://skilltree.my/events/ebg" target="_blank" rel="external">TDD 課程</a>，我心想這實在是太棒了！ Joey 是業界在 TDD 領域相當有研究，而且也已經在實務上經過千錘百鍊的高手，如果能親眼見到他是如何把測試融入開發中的話，一定能大大提升自己的經驗值！</p>
<p>不過課程中精采的部份太多了，恕我無法一一介紹；而且我也不覺得我能在這篇心得中，表達出講師在實務面的深厚功力。以下就讓我來為大家介紹這個課程為什麼值得你去聽的心得。</p>
<a id="more"></a>
<h2 id="絕對有料的課程內容">絕對有料的課程內容</h2><p>我個人很喜歡這個課程一開始就把整個學習地圖展開來，讓學員知道接下來幾週裡會由淺入深地學到哪些技巧，例如本週 (第一週) 是單元測試的基礎。雖然說是基礎，但卻是最關鍵的一環！因為講師會將所有至今你對測試的錯誤認知重新洗掉，注入最正確的觀念！</p>
<p>而接下來講師就舉出了幾個血淋淋的真實案例，告訴你為什麼測試是重要的。測試不是用來應付流程的工具，而是紥紥實實讓你對自己的程式碼能更有信心的魔法。你不見得能在一開始就體會測試的重要性，但你一定會在出大包時懊悔：如果當時有測試就好了 (曾經待過 EC 產業的我感同身受) 。</p>
<p>講師也三不五時會讓學員用回答問題的方式，讓學員重新審視對剛剛所教授的內容是否有真的瞭解，講師也會適時地再補充。講師也會針對同學的發問，來分享自己在實務上遇到的問題。</p>
<h2 id="一定能懂的教學方式">一定能懂的教學方式</h2><p>有趣的是，這次課程講師是用 C# 教學，但在 PHP 中打滾的我卻對範例沒有絲毫的疏離感。因為講師舉的例子相當生活化，而且寫出來的程式碼非常平易近人，只要有寫過程式的人一定都能看懂。</p>
<p>課程中包含了讓學員動手的實戰練習，每個範例都是從實務中淬鍊出來的，像是如何測試亂數？如何測試只會在特定日期啟用的功能等等；而且範例經過縝密的安排，能循序漸進地讓學員瞭解如何在測試中套用課程中講授的原則。你會很驚訝原來這些我們以為難測試的問題，其實在測試中解決的方式竟然是這麼簡單。</p>
<p>當然對不會寫 C# 的我來說，原本以為沒辦法用 IDE 實際演練會是件憾事；但這些實例設計之精巧，卻能夠讓我用 PHP 來重新代入；所謂一法通萬法通，測試的觀念本來就不限程式語言，這樣讓人興奮的實戰訓練，會讓人有手停不下來的感覺。</p>
<h2 id="教你怎麼快速建立測試又能寫出好測試">教你怎麼快速建立測試又能寫出好測試</h2><p>我們都知道開發應該針對需求，然而測試也是。但我們卻容易想得太多，導致我們做了太多無謂的工作。在課程中，講師會告訴你如何讓測試去配合需求，而不是為了測試而測試。搭配測試的原則，我們就不會再認為測試是多出來的工，而應該是開發的一部份。</p>
<p>例如關注點分離、 FIRST 、 3A 這類的測試原則，都是寫測試時所應該遵守的，但很少人知道要怎麼去應用。然而透過這個課程的範例教學，你會發現這麼多名詞和原則，竟能從中得到驗證！因為它們其實都是從實務所演化出來的，只是我們一直以教條式的方法來學習它們；當真正看到這些原則是這麼輕鬆地被講師透過實例展現出來時，那種震憾是文字難以描述的。</p>
<p>當然我們在實際撰寫測試碼時，還是很容易覺得它是件麻煩事，這時候測試工具就非常重要了。一個好的測試框架會影響你撰寫測試的意願，如果這工具沒辦法很容易又很簡潔地寫出符合意圖的測試，那麼也就不能怪其他同事質疑寫測試是不是有幫助了。</p>
<p>而號稱地表最強 IDE 之一的 Visual Studio 在這方面當然不會落於人後，簡單幾個指令和操作，測試骨架就立刻出現。再搭配第三方的測試工具，輕輕鬆鬆地就在這些骨架加血添肉，轉眼間測試就完成了。寫測試這件事從此融入整套開發流程裡，怎麼可能讓人不興奮呢？</p>
<h2 id="實務上面臨的問題">實務上面臨的問題</h2><p>Demo (SkillTree 主辦人) 在課餘時間聊到：「即便寫測試變得容易，而我們也開始把它變成自己的習慣，但你還是一定會碰到同事有這樣的質疑：『這程式碼就是正確的呀！為什麼還需要測試？』」我想一定有想在團隊裡導入測試的朋友常會遇到這種狀況，在這個課程中也可以讓你學到用什麼方法可以處理這樣的質疑。</p>
<p>在導入測試時時，最重要的就是讓同事對測試有信心；當寫測試變得很容易時，你所要做的，就是怎麼讓同事也能體會寫測試能為他帶來什麼效益。這方面講師也會提出他的實務經驗供我們參考，而且不論是技巧上和人性上，都是相當值得參考的建議。</p>
<p>最後還有一個可能會面臨到的，就是測試對程式碼的涵蓋率。講師的回答其實讓我非常驚訝 (容我賣個關子) ，這其實讓我對「測試對團隊的影響力」這件事大大地改觀。雖然我自己也在某個部份應用上了類似的觀念，但我以為這只是影響了我自己，沒想到這確實是一種對人性的實務作法，不禁讓我對它更有信心；我相信透過這些小小的改變，就有機會讓整個團隊變得更有勇氣去面對測試這件事情。</p>
<h2 id="小結">小結</h2><p>上完第一週的課程，我腦海裡一直迴響著：『你必須對你的測試有信心，測試才能讓你對你的程式有信心。』</p>
<p>測試應該要做什麼？怎麼寫有效的測試？怎麼快速地寫測試？怎麼讓同事一起來寫測試？原本以為自己已經可以回答這些問題，卻在這個課程中讓我的觀念整個大洗牌，也開始讓我心中已經消逝很久的熱血又漸漸沸騰起來。</p>
<p>再次感謝講師 Joey 和 SkillTree 的 Demo 舉辦這麼實用且精采的課程，我越來越期待接下來幾週的內容了。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>測試一直以來是很多開發者心中的痛，當被老闆問到：「你的程式碼是否都測試過了？」你是否能摸著自己的良心，並且拿出證據來說自己真的做過測試讓所有功能都符合需求了？我相信絕大多數的開發者這時一定都是面有難色，因為其實你不但騙了老闆，還騙了你自己。</p>
<p>從我自己開始研究測試後，其實一直覺得自己還是沒有真正把測試當成是開發的一部份，更別說坊間很多程式書籍和教學課程能把這兩者真真實實地融合在一起，傳授給廣大的開發人員。</p>
<p>這次正好有機會去聽 Joey (91 哥) 在 <a href="http://skilltree.my/">SkillTree</a> 開的 <a href="http://skilltree.my/events/ebg">TDD 課程</a>，我心想這實在是太棒了！ Joey 是業界在 TDD 領域相當有研究，而且也已經在實務上經過千錘百鍊的高手，如果能親眼見到他是如何把測試融入開發中的話，一定能大大提升自己的經驗值！</p>
<p>不過課程中精采的部份太多了，恕我無法一一介紹；而且我也不覺得我能在這篇心得中，表達出講師在實務面的深厚功力。以下就讓我來為大家介紹這個課程為什麼值得你去聽的心得。</p>]]>
    
    </summary>
    
      <category term="TDD" scheme="http://jaceju.net/tags/TDD/"/>
    
      <category term="測試" scheme="http://jaceju.net/tags/%E6%B8%AC%E8%A9%A6/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[部落格系統換成 Hexo]]></title>
    <link href="http://jaceju.net/2015/05/11/change-blog-system-to-hexo/"/>
    <id>http://jaceju.net/2015/05/11/change-blog-system-to-hexo/</id>
    <published>2015-05-11T10:51:13.000Z</published>
    <updated>2015-05-11T11:00:26.000Z</updated>
    <content type="html"><![CDATA[<p>部落格很久沒更新了，因為某些原因讓我不太再想留在社群網站上，所以就回來繼續寫寫文。</p>
<p>然後 Octopress 實在是很慢，所以就換到 Hexo 這個以 Node.js 開發的 Blog Framework 。</p>
<p>原來的文章網址也不打算留了，讓 Google 重新爬吧； GA 什麼的就找時間再調整。</p>
<p>還是想有個能專心寫文的地方，希望能有空把手邊的草稿整理完。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>部落格很久沒更新了，因為某些原因讓我不太再想留在社群網站上，所以就回來繼續寫寫文。</p>
<p>然後 Octopress 實在是很慢，所以就換到 Hexo 這個以 Node.js 開發的 Blog Framework 。</p>
<p>原來的文章網址也不打算留了，讓 Go]]>
    </summary>
    
      <category term="未分類" scheme="http://jaceju.net/tags/%E6%9C%AA%E5%88%86%E9%A1%9E/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[理解 Dependency Injection 實作原理]]></title>
    <link href="http://jaceju.net/2014/07/27/php-di-container/"/>
    <id>http://jaceju.net/2014/07/27/php-di-container/</id>
    <published>2014-07-27T09:45:50.000Z</published>
    <updated>2015-05-11T06:20:09.000Z</updated>
    <content type="html"><![CDATA[<p>現代較新的 Web Framework 都強調自己有 Dependency Injection (以下簡稱 DI ) 的特色，只是很多人對它的運作原理還是一知半解。</p>
<p>所以接下來我將用一個簡單的範例，來為各位介紹在 PHP 中如何實現簡易的 DI 。</p>
<a id="more"></a>
<h2 id="基本範例">基本範例</h2><p>這是一個應用程式的範例，它只包含了登入處理程序。在這個範例中， <code>App</code> 類別的建構式參考了新的 <code>Auth</code> 與 <code>Session</code> 的物件實體，並在 <code>App::login()</code> 中使用。</p>
<p>註：請特別注意，為了呈現重點，我忽略掉很多程式碼，同時也沒有進行良好的架構設計；所以請不要把這個範例用在你的程式中，或是對為什麼我沒有進行錯誤處理，以及為什麼要採用奇怪的設計提出質疑。</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">App</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">protected</span> <span class="variable">$auth</span> = <span class="keyword">null</span>;</span><br><span class="line">    <span class="keyword">protected</span> <span class="variable">$session</span> = <span class="keyword">null</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span><span class="params">(<span class="variable">$dsn</span>, <span class="variable">$username</span>, <span class="variable">$password</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="variable">$this</span>-&gt;auth = <span class="keyword">new</span> Auth(<span class="variable">$dsn</span>, <span class="variable">$username</span>, <span class="variable">$password</span>);</span><br><span class="line">        <span class="variable">$this</span>-&gt;session = <span class="keyword">new</span> Session();</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">login</span><span class="params">(<span class="variable">$username</span>, <span class="variable">$password</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (<span class="variable">$this</span>-&gt;auth-&gt;check(<span class="variable">$username</span>, <span class="variable">$password</span>)) &#123;</span><br><span class="line">            <span class="variable">$this</span>-&gt;session-&gt;set(<span class="string">'username'</span>, <span class="variable">$username</span>);</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">false</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>而 <code>Auth</code> 類別是從資料庫驗證使用者身份，這裡我僅用簡單的描述來呈現效果。</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Auth</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span><span class="params">(<span class="variable">$dsn</span>, <span class="variable">$user</span>, <span class="variable">$pass</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">echo</span> <span class="string">"Connecting to '$dsn' with '$user'/'$pass'...\n"</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">check</span><span class="params">(<span class="variable">$username</span>, <span class="variable">$password</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">echo</span> <span class="string">"Checking username, password from database...\n"</span>;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p><code>Session</code> 類別也是概念性的實作：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Session</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">set</span><span class="params">(<span class="variable">$name</span>, <span class="variable">$value</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">echo</span> <span class="string">"Set session variable '$name' to '$value'."</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>最後我們讓程式動起來， client 程式如下：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="variable">$app</span> = <span class="keyword">new</span> App(<span class="string">'mysql://localhost'</span>, <span class="string">'username'</span>, <span class="string">'password'</span>);</span><br><span class="line"><span class="variable">$username</span> = <span class="string">'jaceju'</span>;</span><br><span class="line"><span class="keyword">if</span> (<span class="variable">$app</span>-&gt;login(<span class="variable">$username</span>, <span class="string">'password'</span>)) &#123;</span><br><span class="line">    <span class="keyword">echo</span> <span class="string">"$username just signed in.\n"</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>註：這裡的 client 程式指的是實際操作這些物件實體的程式。</p>
<p>各位可以先試著想想這個程式在可擴充性上有什麼問題？例如我想把身份認證方式換成第三方服務的機制，或是改用其他媒介來存放 session 內容等。</p>
<p>還有如果想在沒有資料庫連線、或是沒有 HTTP session 的環境下對 <code>App::login()</code> 方法的邏輯進行隔離測試，各位會怎麼做呢？</p>
<h2 id="解除依賴關係">解除依賴關係</h2><p>上面的範例因為 <code>App</code> 類別已經依賴了 <code>Auth</code> 類別和 <code>Session</code> 類別，而這兩個類別都有實作跟系統環境有關的程式邏輯，這麼一來就會讓 <code>App</code> 類別難以進行底層機制的切換或是隔離測試。所以接下來我們要做的，就是把它們的依賴關係解除。</p>
<p>修改後的 <code>App</code> 類別如下：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">App</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">protected</span> <span class="variable">$auth</span> = <span class="keyword">null</span>;</span><br><span class="line">    <span class="keyword">protected</span> <span class="variable">$session</span> = <span class="keyword">null</span>;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span><span class="params">(Auth <span class="variable">$auth</span>, Session <span class="variable">$session</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="variable">$this</span>-&gt;auth = <span class="variable">$auth</span>;</span><br><span class="line">        <span class="variable">$this</span>-&gt;session = <span class="variable">$session</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="variable">$auth</span> = <span class="keyword">new</span> Auth(<span class="string">'mysql://localhost'</span>, <span class="string">'username'</span>, <span class="string">'password'</span>);</span><br><span class="line"><span class="variable">$session</span> = <span class="keyword">new</span> Session();</span><br><span class="line"><span class="variable">$app</span> = <span class="keyword">new</span> App(<span class="variable">$auth</span>, <span class="variable">$session</span>);</span><br></pre></td></tr></table></figure>
<p>首先我們在 <code>App</code> 類別的建構式 <code>__construct</code> 原本的資料庫設定參數移除，並將原來直接以 <code>new</code> 關鍵字所產生的物件實體，改用方法參數的方式來注入。而使用 <code>new</code> 關鍵字產生物件實體的程式碼，就移到 <code>App</code> 類別外。</p>
<p>這種「將依賴的類別改用方法參數來注入」的作法，就是我們說的「依賴注入 (Dependency Injection) 」。</p>
<p>常見依賴注入的方式有兩種： Constructor Injection 及 Setter Injection 。它們的實作形式並沒有什麼不同，差別只在於是不是類別建構式而已。</p>
<p>不過 Constructor Injection 必須在建立物件實體時就進行注入，而 Setter Injection 則是可以在物件實體建立後才透過 setter 函式來進行注入。而這裡為了方便解說，我採用的是 Constructor Injection 。</p>
<h2 id="依賴抽象介面">依賴抽象介面</h2><p>好了，現在的問題是 <code>Auth</code> 類別的實作還是依賴在資料庫上，所以我們也要讓 <code>Auth</code> 類別跟資料庫之間解除依賴關係，讓它成為一個抽象介面。</p>
<p>這裡的抽象介面是指觀念上的意義，而非語言層級上的抽象類別 (Abstract Class) 或介面 (Interface) 。至於在實作上該用抽象類別還是介面，在這個範例裡並沒有差別，大家可以自行判斷；這裡我用介面 (Interface) ，因為我僅需要 <code>Auth::check()</code> 這個介面方法的定義而已。</p>
<p>這一步首先我把原來的 <code>Auth</code> 類別重新命名為 <code>DbAuth</code> 類別：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">DbAuth</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span><span class="params">(<span class="variable">$dsn</span>, <span class="variable">$user</span>, <span class="variable">$pass</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">echo</span> <span class="string">"Connecting to '$dsn' with '$user'/'$pass'...\n"</span>;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">check</span><span class="params">(<span class="variable">$username</span>, <span class="variable">$password</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">echo</span> <span class="string">"Checking username, password from database...\n"</span>;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>接著建立一個 <code>Auth</code> 介面，它包含了 <code>Auth::check()</code> 方法的定義：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">interface</span> <span class="title">Auth</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">check</span><span class="params">(<span class="variable">$username</span>, <span class="variable">$password</span>)</span></span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>然後讓 <code>DbAuth</code> 類別實作 <code>Auth</code> 介面：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">DbAuth</span> <span class="keyword">implements</span> <span class="title">Auth</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>最後把原來初始化 <code>Auth</code> 類別的物件實體的程式碼，改為初始化 <code>DbAuth</code> 的物件實體。</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="variable">$auth</span> = <span class="keyword">new</span> DbAuth(<span class="string">'mysql://localhost'</span>, <span class="string">'username'</span>, <span class="string">'password'</span>);</span><br><span class="line"><span class="variable">$session</span> = <span class="keyword">new</span> Session();</span><br><span class="line"><span class="variable">$app</span> = <span class="keyword">new</span> App(<span class="variable">$auth</span>, <span class="variable">$session</span>);</span><br></pre></td></tr></table></figure>
<p>透過 <code>Auth</code> 介面的幫助，我們已經讓 <code>App</code> 類別與實際的資料庫操作類別分離開來了。現在只要是實作 <code>Auth</code> 介面的類別，都可以被 <code>App</code> 類別所接受，例如我們可能會改用 HTTP 認證來取代資料庫認證：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">HttpAuth</span> <span class="keyword">implements</span> <span class="title">Auth</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">check</span><span class="params">(<span class="variable">$username</span>, <span class="variable">$password</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">echo</span> <span class="string">"Checking username, password from HTTP Authentication...\n"</span>;</span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">true</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="variable">$auth</span> = <span class="keyword">new</span> HttpAuth();</span><br><span class="line"><span class="variable">$session</span> = <span class="keyword">new</span> Session();</span><br><span class="line"><span class="variable">$app</span> = <span class="keyword">new</span> App(<span class="variable">$auth</span>, <span class="variable">$session</span>);</span><br></pre></td></tr></table></figure>
<p>當然其他類型的認證方式也可以透過建立新的類別來使用，而不會影響到 <code>App</code> 類別的內部實作。</p>
<h2 id="DI_容器">DI 容器</h2><p>現在又有個問題， client 程式還是依賴於 <code>DbAuth</code> 類別或是 <code>HttpAuth</code> 類別；通常這種狀況在需要編譯型的語言 (例如 Java ) 中，程式一旦編譯完成佈署出去後，就很難再進行修改。</p>
<p>如果我們可以改用設定的方式來告訴程式，在不同的狀況下對應不同的類別，然後讓程式自行判斷環境來產生需要的物件實體，這樣就可以解開 client 程式對實作類別的依賴關係。</p>
<p>這裡要引入一個技術，稱為 DI 容器 (Dependency Injection Container) 。 DI 容器主要的作用在於幫我們解決產生物件實體時，應該參考哪一個類別。我們先來看看用法：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line">Container::register(<span class="string">'Auth'</span>, <span class="string">'DbAuth'</span>, [<span class="string">'mysql://localhost'</span>, <span class="string">'username'</span>, <span class="string">'password'</span>]);</span><br><span class="line"></span><br><span class="line"><span class="variable">$auth</span> = Container::get(<span class="string">'Auth'</span>);</span><br><span class="line"><span class="variable">$session</span> = <span class="keyword">new</span> Session();</span><br><span class="line"><span class="variable">$app</span> = <span class="keyword">new</span> App(<span class="variable">$auth</span>, <span class="variable">$session</span>);</span><br></pre></td></tr></table></figure>
<p>首先我們在 DI 容器中先以 <code>Container::register()</code> 方法來註冊 <code>Auth</code> 這個別名實際上要對應哪個類別，以及建立物件實體時會用到的初始化參數。要注意，這裡的別名並不是指真正的類別或介面，但我們可以用相同的名稱以避免認知上的問題。</p>
<p>然後我們用 <code>Container::get()</code> 方法取得別名所對應類別的物件實體，上面例子裡的 <code>$auth</code> 就是 <code>DbAuth</code> 類別的物件實體。</p>
<p>這麼一來，我們就可以把註冊的程式碼移出 client 程式之外，並將註冊參數改用設定檔引入，順利解開 client 程式對實作類別的依賴。</p>
<h2 id="DI_容器原理">DI 容器原理</h2><p>那麼 DI 容器的原理是怎麼運作的呢？首先在 <code>Container::register()</code> 方法註冊的部份，它其實只是把參數記到 <code>$map</code> 這個類別靜態屬性裡。</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Container</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">protected</span> <span class="keyword">static</span> <span class="variable">$map</span> = [];</span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="function"><span class="keyword">function</span> <span class="title">register</span><span class="params">(<span class="variable">$name</span>, <span class="variable">$class</span>, <span class="variable">$args</span> = null)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">static</span>::<span class="variable">$map</span>[<span class="variable">$name</span>] = [<span class="variable">$class</span>, <span class="variable">$args</span>];</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>重點在 <code>Container::get()</code> 方法，它透過 <code>$name</code> 別名，把 <code>$map</code> 屬性中對應的類別名稱和初始化參數取出；接著判斷類別是不是存在，如果存在的話就建立對應的物件實體。</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Container</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="function"><span class="keyword">function</span> <span class="title">get</span><span class="params">(<span class="variable">$name</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">list</span>(<span class="variable">$class</span>, <span class="variable">$args</span>) = <span class="keyword">isset</span>(<span class="keyword">static</span>::<span class="variable">$map</span>[<span class="variable">$name</span>]) ?</span><br><span class="line">                              <span class="keyword">static</span>::<span class="variable">$map</span>[<span class="variable">$name</span>] :</span><br><span class="line">                              [<span class="variable">$name</span>, <span class="keyword">null</span>];</span><br><span class="line"></span><br><span class="line">        <span class="keyword">if</span> (class_exists(<span class="variable">$class</span>, <span class="keyword">true</span>)) &#123;</span><br><span class="line">            <span class="variable">$reflectionClass</span> = <span class="keyword">new</span> ReflectionClass(<span class="variable">$class</span>);</span><br><span class="line">            <span class="keyword">return</span> !<span class="keyword">empty</span>(<span class="variable">$args</span>) ?</span><br><span class="line">                   <span class="variable">$reflectionClass</span>-&gt;newInstanceArgs(<span class="variable">$args</span>) :</span><br><span class="line">                   <span class="keyword">new</span> <span class="variable">$class</span>();</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>比較特別的是，如果初始化參數不是空值 (<code>null</code>) 時，則必須透過 <code>ReflectionClass::newInstanceArgs()</code> 方法來建立物件實體。 <code>ReflectionClass</code> 類別可以映射出指定類別的內部結構，並提供方法來操作這個結構； Reflection 是現代語言常見的機制， PHP 在這方面也提供了完整的 API 供開發者使用，請參考： <a href="http://php.net/manual/en/book.reflection.php" target="_blank" rel="external">PHP: Reflection</a> 。</p>
<p><code>Container::get()</code> 方法也可以在沒有註冊的狀況下，直接把別名當成類別名稱，然後協助我們初始化對應的物件實體；例如：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="variable">$session</span> = Container::get(<span class="string">'Session'</span>);</span><br></pre></td></tr></table></figure>
<h2 id="手動注入">手動注入</h2><p>現在我們的 client 程式已經修改成以下的樣子：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="variable">$auth</span> = Container::get(<span class="string">'Auth'</span>);</span><br><span class="line"><span class="variable">$session</span> = Container::get(<span class="string">'Session'</span>);</span><br><span class="line"><span class="variable">$app</span> = <span class="keyword">new</span> App(<span class="variable">$auth</span>, <span class="variable">$session</span>);</span><br></pre></td></tr></table></figure>
<p>不過當初始化參數較多的狀況下，重複寫好幾次 <code>Container::get()</code> 看起來也是挺囉嗦的。</p>
<p>接下來我們實作一個 <code>Container::inject()</code> 方法，提供開發者可以一次注入所有依賴物件實體：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="variable">$app</span> = Container::inject(<span class="string">'Auth'</span>, <span class="string">'Session'</span>, <span class="function"><span class="keyword">function</span> <span class="params">(<span class="variable">$auth</span>, <span class="variable">$session</span>)</span> </span>&#123;</span><br><span class="line">    <span class="keyword">return</span> <span class="keyword">new</span> App(<span class="variable">$auth</span>, <span class="variable">$session</span>);</span><br><span class="line">&#125;);</span><br></pre></td></tr></table></figure>
<p>這裡我們讓 <code>Container::inject()</code> 接受不定個數的參數，除了最後一個參數必須是 callback 型態外，其他都是要傳遞給 <code>Container::get()</code> 的參數。 <code>Container::inject()</code> 的實作方式如下：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Container</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="function"><span class="keyword">function</span> <span class="title">inject</span><span class="params">()</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="variable">$args</span> = func_get_args();</span><br><span class="line">        <span class="variable">$callback</span> = array_pop(<span class="variable">$args</span>);</span><br><span class="line">        <span class="variable">$injectArgs</span> = [];</span><br><span class="line"></span><br><span class="line">        <span class="keyword">foreach</span> (<span class="variable">$args</span> <span class="keyword">as</span> <span class="variable">$name</span>) &#123;</span><br><span class="line">            <span class="variable">$injectArgs</span>[] = Container::get(<span class="variable">$name</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> call_user_func_array(<span class="variable">$callback</span>, <span class="variable">$injectArgs</span>);</span><br><span class="line">    &#125;</span><br></pre></td></tr></table></figure>
<p>在參數個數不定的狀況下，可以用 <code>func_get_args()</code> 函式來取得所有參數；而 <code>array_pop()</code> 可以取出最後一個參數值做為 callback 。剩下的參數就透過 <code>Container::get()</code> 來取得物件實體，最後再透過 <code>call_user_func_array()</code> 函式將處理好的參數傳遞給 callback 執行。</p>
<h2 id="自動解決所有依賴注入">自動解決所有依賴注入</h2><p>在我們的範例裡， <code>Container</code> 類別如果可以提供一個方法，自動為我們解決所有 <code>App</code> 類別依賴問題，那麼程式就可以更乾淨些。</p>
<p>要做到這點，我們就必須知道要注入的方法所需要參數的類型；而在 PHP 中的 <a href="http://php.net/manual/en/language.oop5.typehinting.php" target="_blank" rel="external">Type Hinting</a> ，就可以告訴我們參數所對應的變數類型或類別。</p>
<p>回到 <code>App::__construct()</code> 建構子上，我們看到 <code>$auth</code> 與 <code>$session</code> 兩個參數的 type hint 分別對應到 <code>Auth</code> 與 <code>Session</code> 這兩個類別，剛好就可以用來當做我們做自動依賴注入的條件。</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">App</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="keyword">public</span> <span class="function"><span class="keyword">function</span> <span class="title">__construct</span><span class="params">(Auth <span class="variable">$auth</span>, Session <span class="variable">$session</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p>接著我們為 <code>Container</code> 類別提供一個 <code>resolve()</code> 方法，它可以接受一個類別名稱用來建立物件實體，而不需要再使用 <code>new</code> 關鍵字。</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="variable">$app</span> = Container::resolve(<span class="string">'App'</span>);</span><br></pre></td></tr></table></figure>
<p>我們希望 <code>Container::resolve()</code> 方法會自動產生參數所對應的物件，解決這個類別建構子所需要的依賴關係。它的實作如下：</p>
<figure class="highlight php"><table><tr><td class="code"><pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">Container</span></span><br><span class="line"></span>&#123;</span><br><span class="line">    <span class="comment">// ...</span></span><br><span class="line"></span><br><span class="line">    <span class="keyword">public</span> <span class="keyword">static</span> <span class="function"><span class="keyword">function</span> <span class="title">resolve</span><span class="params">(<span class="variable">$name</span>)</span></span><br><span class="line">    </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (!class_exists(<span class="variable">$name</span>, <span class="keyword">true</span>)) &#123;</span><br><span class="line">            <span class="keyword">return</span> <span class="keyword">null</span>;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="variable">$reflectionClass</span> = <span class="keyword">new</span> ReflectionClass(<span class="variable">$name</span>);</span><br><span class="line">        <span class="variable">$reflectionConstructor</span> = <span class="variable">$reflectionClass</span>-&gt;getConstructor();</span><br><span class="line">        <span class="variable">$reflectionParams</span> = <span class="variable">$reflectionConstructor</span>-&gt;getParameters();</span><br><span class="line"></span><br><span class="line">        <span class="variable">$args</span> = [];</span><br><span class="line">        <span class="keyword">foreach</span> (<span class="variable">$reflectionParams</span> <span class="keyword">as</span> <span class="variable">$param</span>) &#123;</span><br><span class="line">            <span class="variable">$class</span> = <span class="variable">$param</span>-&gt;getClass()-&gt;getName();</span><br><span class="line">            <span class="variable">$args</span>[] = <span class="keyword">static</span>::get(<span class="variable">$class</span>);</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> !<span class="keyword">empty</span>(<span class="variable">$args</span>) ?</span><br><span class="line">               <span class="variable">$reflectionClass</span>-&gt;newInstanceArgs(<span class="variable">$args</span>) :</span><br><span class="line">               <span class="keyword">new</span> <span class="variable">$class</span>();</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>
<p><code>Container::resolve()</code> 方法與 <code>Container::get()</code> 方法的原理類似，但較特別的是它使用了 <code>ReflectionClass::getConstructor()</code> 方法來取得類別建構子的 <code>ReflectionMethod</code> 實體；接著再用 <code>ReflectionMethod::getParameters()</code> 取出參數的 <code>ReflectionParameter</code> 物件集合 (陣列) 。</p>
<p>而後我們就可以在迴圈中一一透過 <code>ReflectionParameter::getClass()</code> 方法與 <code>ReflectionClass::getName()</code> 方法來取得 type hint 所指向的類別或介面名稱。當有了參數所對應的類別或介面名稱後，就可以用 <code>Container::get()</code> 方法來取得參數的物件實體。</p>
<p>最後把這些物件帶回建構子的參數裡，並初始化我們所需要的物件實體，就完成了 <code>App</code> 類別的自動依賴注入。</p>
<h2 id="深入思考">深入思考</h2><p>再強調一次，這裡的範例只是為了介紹 DI 容器的原理，並不能真正用在實務上。因為一個完整的 DI 容器還要考慮以下的問題：</p>
<ul>
<li>類別不存在時的處理。</li>
<li>與其他非類別的參數整合。</li>
<li>如何建立設定檔機制以便切換依賴關係。</li>
<li>遞迴地自動注入物件實體。</li>
<li>取得 Singleton 物件實體。</li>
<li>可以透過原始碼上的 DocBlock 註解來註明依賴關係。</li>
</ul>
<p>目前已經有很多 DI Framework 幫我們處理好這些事情了，建議大家如果真的需要在專案中使用 DI 時，應該採用這些 Framework 。</p>
<h2 id="總結">總結</h2><p>如果專案並不會有太多變化性，那麼依賴注入對我們來說就不是那麼重要。但是如果希望程式對特定類別的依賴性降低，只針對抽象介面實作，那麼依賴注入就有其必要性。</p>
<p>在 PHP 上的 DI 容器的基本實作原理也不複雜，透過 Reflection 機制就可以看到類別內部的結構，讓我們對它的建構子注入我們想要的參數值。</p>
<p>DI 容器要考量的部份也不少，但這些功能都已經有 Framework 實作，我們應該在專案中使用它們而儘可能不要自行開發。</p>
<p>希望透過以上的介紹，可以讓大家對 Framework 的依賴注入機制有基本的認知。</p>
<p>註：上述程式碼都可以在 <a href="https://github.com/jaceju/php-di-container-examples" target="_blank" rel="external">php-di-container-examples
</a> 找到。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>現代較新的 Web Framework 都強調自己有 Dependency Injection (以下簡稱 DI ) 的特色，只是很多人對它的運作原理還是一知半解。</p>
<p>所以接下來我將用一個簡單的範例，來為各位介紹在 PHP 中如何實現簡易的 DI 。</p>]]>
    
    </summary>
    
      <category term="PHP" scheme="http://jaceju.net/tags/PHP/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[整理一些常見的 PHP 錯誤]]></title>
    <link href="http://jaceju.net/2014/07/21/summary-of-common-php-mistakes/"/>
    <id>http://jaceju.net/2014/07/21/summary-of-common-php-mistakes/</id>
    <published>2014-07-21T02:51:03.000Z</published>
    <updated>2015-05-11T06:20:09.000Z</updated>
    <content type="html"><![CDATA[<p>最近有數篇文章介紹了 PHP 開發者常見的錯誤，我順手整理如下：</p>
<a id="more"></a>
<p><a href="http://www.toptal.com/php/10-most-common-mistakes-php-programmers-make" target="_blank" rel="external">10 Common PHP Coding Errors</a></p>
<ol>
<li>在 foreach 迴圈中使用了迭代項目的參考。</li>
<li>誤用了 isset 。</li>
<li>搞混了回傳值是傳參考還是傳值。</li>
<li>在迴圈中執行不必要的 SQL Query 。</li>
<li>一次取得太多結果，造成不必要的記憶體浪費。</li>
<li>忽略了 Unicode / UTF-8 問題。</li>
<li>總是假設 $_POST 會包含 POST 資料。</li>
<li>沒有注意 PHP 不支援 char 字元型態。</li>
<li>對編碼標準的忽略，尤其是 PSR 標準 (當然有些依據不見得好，但也是大家討論出來的標準) 。</li>
<li>誤用了 empty 函式。</li>
</ol>
<p><a href="http://afilina.com/common-php-mistakes/" target="_blank" rel="external">Common PHP Mistakes</a></p>
<ol>
<li>忘了使用快取機制來減少大量 requests 對系統的衝擊。</li>
<li>沒有避開 SQL Injection 。</li>
<li>開發時關掉了錯誤回報。</li>
<li>表單 POST 後還停留在同一頁。</li>
<li>沒有善用已經存在的工具，而重造輪子。</li>
<li>關掉了 timeout 機制，讓 script 沒有停止的依據。</li>
<li>忽略了網站可用性。</li>
</ol>
<p><a href="http://www.sitepoint.com/7-mistakes-commonly-made-php-developers/" target="_blank" rel="external">7 More Mistakes Commonly Made by PHP Developers</a></p>
<ol>
<li>使用過時的 mysql extention 。</li>
<li>沒有使用 PDO 的參數機制來避免 SQL Injection 。</li>
<li>沒有重寫網址，讓它符合現今的網址守則。</li>
<li>抑制錯誤訊息的發生。</li>
<li>在條件判斷式中賦值。</li>
<li>曝露太多有關系統所使用的 Framework 資訊。</li>
<li>沒有移除掉開發時的設定檔。</li>
</ol>
]]></content>
    <summary type="html">
    <![CDATA[<p>最近有數篇文章介紹了 PHP 開發者常見的錯誤，我順手整理如下：</p>]]>
    
    </summary>
    
      <category term="PHP" scheme="http://jaceju.net/tags/PHP/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[CSS3 動畫基礎]]></title>
    <link href="http://jaceju.net/2014/06/03/css3-animation-notes/"/>
    <id>http://jaceju.net/2014/06/03/css3-animation-notes/</id>
    <published>2014-06-03T02:28:36.000Z</published>
    <updated>2015-05-11T06:20:09.000Z</updated>
    <content type="html"><![CDATA[<p>註：本文為作者發表於 OpenFoundry 之 <a href="http://www.openfoundry.org/en/tech-column/9233-css3-animation" target="_blank" rel="external">CSS3 動畫基礎</a>一文的備份。</p>
<script async src="//codepen.io/assets/embed/ei.js"></script>

<p>在 JSConf.Asia 2013 ， Lea Verou 介紹了 <a href="http://lea.verou.me/css-4d/#intro" target="_blank" rel="external">CSS in the 4th dimension</a>  (<a href="https://www.youtube.com/watch?v=NTJUFQmHbvc" target="_blank" rel="external">影片</a>) ，引發了整個 Web 界對 CSS 動畫的期盼；在 <a href="http://www.ruanyifeng.com/blog/2014/02/css_transition_and_animation.html" target="_blank" rel="external">CSS動畫簡介</a>一文也已經把重點整理好了。</p>
<p>以下我們將會介紹主要兩個 CSS3 在動畫的屬性： Transition 與 Animation ，並配合實例來練習這些技術，後面我也會介紹一些不錯的相關開發工具。</p>
<a id="more"></a>
<h2 id="Transition">Transition</h2><p>在以往 HTML 元素在兩種外觀之間的變換，只能從一種外觀直接跳到另一種外觀，瀏覽者並沒有辦法感受到這兩種外觀中間平滑的轉換，造成了視覺上的不適。</p>
<p data-height="268" data-theme-id="0" data-slug-hash="gwyao" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/gwyao/" target="_blank" rel="external">例：在滑鼠移過時，方塊的高度和寬度變化。</a></p>

<h3 id="基本的_transition">基本的 transition</h3><p>而 CSS 為了補足這方面的視覺轉換特效，特別加入 <code>transition</code> 屬性。 一個簡易的動畫效果就是在想要變化的狀態上，加入一個 <code>transition</code> 屬性，而其值為變化需歷時的秒數。</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">div:hover &#123;&#10;    ...&#10;    transition: 1s;&#10;&#125;</span><br></pre></td></tr></table></figure>
<p>這麼一來， <code>transition</code> 就會自動幫我們補足中間的過場動畫。例如我們希望上面的例子能平順地轉換，歷時一秒：</p>
<p data-height="268" data-theme-id="0" data-slug-hash="AGiEa" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/AGiEa/" target="_blank" rel="external">例：加入 transition 後的效果</a></p>

<p>我們也可以讓高度以外的屬性有動畫效果，例如顏色：</p>
<p data-height="268" data-theme-id="0" data-slug-hash="pwgnc" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/pwgnc/" target="_blank" rel="external">例：用 transition 改變顏色</a></p>

<h3 id="transition_屬性詳解"><code>transition</code> 屬性詳解</h3><p><code>transition</code> 屬性其實跟 <code>font</code> 或 <code>background</code> 屬性一樣是簡寫屬性，它是以下四個屬性的總和：</p>
<ul>
<li><code>transition-property</code>: 要做變換的 CSS 屬性</li>
<li><code>transition-duration</code>: 變換需要的時間，單位為 <code>s</code> 或 <code>ms</code></li>
<li><code>transition-delay</code>: 延遲多久後開始變換，單位為 <code>s</code> 或 <code>ms</code></li>
<li><code>transition-timing-function</code>: 稱為 Timing Funciton ，用名稱來定義變換時的加速度。</li>
</ul>
<p>這些屬性可以分開寫，也可以將它們的值同時寫在 <code>transition</code> 屬性裡；唯一要注意的是 <code>transition-duration</code> 與 <code>transition-delay</code> 的值寫在一起時有前述的順序關係。前面例子中的 <code>transition: 1s</code> ，其 <code>1s</code> 即為 <code>transition-duration</code> 的值。</p>
<h3 id="transition-property_可使用_transition_的屬性"><code>transition-property</code> 可使用 transition 的屬性</h3><p>不是所有 CSS 屬性都可以使用 <code>transition</code> ，可以參考這篇 <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_animated_properties" target="_blank" rel="external">CSS animated properties</a> 得知有哪些屬性可以使用 <code>transition</code> 。</p>
<p><code>transition</code> 預設是對所有可套用的屬性做轉場效果，也就是關鍵字 <code>all</code> ；但其實也可以只針對某個屬性做 <code>transition</code> 變化，其他屬性則維持原來的直接變化。</p>
<p data-height="268" data-theme-id="0" data-slug-hash="zpcEG" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/zpcEG/" target="_blank" rel="external">例：只對單一屬性加入 transition</a></p>

<h3 id="針對不同屬性同時做_transition">針對不同屬性同時做 transition</h3><p>如果希望對兩個以上的屬性做 <code>transition</code> ，可是又不希望影響其他屬性時，可以用逗號 <code>,</code> 將要做 transition 的屬性分隔開來。</p>
<p data-height="268" data-theme-id="0" data-slug-hash="ifbIG" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/ifbIG/" target="_blank" rel="external">例：對不同屬性同時做 transition</a></p>

<h3 id="transition-delay_延遲變換"><code>transition-delay</code> 延遲變換</h3><p>有時候我們需要先變換一個屬性，再變換另一個屬性，這時候就需要對後者加入一個延遲時間；它需要加在原先我們定義好的歷時時間之後。</p>
<p data-height="268" data-theme-id="0" data-slug-hash="xneaw" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/xneaw/" target="_blank" rel="external">例：先變換一個屬性，再變換另一個屬性</a></p>

<h3 id="transition-timing-function_Timing_Funciton"><code>transition-timing-function</code> Timing Funciton</h3><p>Timing Funciton 包含數種模式，下圖可以看出它們的加速度曲線。</p>
<p><img src="http://letrainde13h37.fr/wp-content/uploads/2012/09/trTimingFn.png" alt=""></p>
<ul>
<li><code>linear</code>: 匀速</li>
<li><code>ease</code>: 急加速後減速 (預設值)</li>
<li><code>ease-in</code>: 加速</li>
<li><code>ease-out</code>: 减速</li>
<li><code>ease-in-out</code>: 較平緩的 <code>ease</code></li>
<li><code>cubic-bezier</code>: 自定義速度模式</li>
</ul>
<h3 id="cubic-bezier_函式">cubic-bezier 函式</h3><p>利用貝茲曲線函式來定義加速曲線，可以直接使用線上工具 <a href="http://cubic-bezier.com/" target="_blank">cubic-bezier()</a> 來找出需要的數值。</p>
<h3 id="雙向的_transition">雙向的 transition</h3><p>Transition 的效果只會作用在有加入 <code>transition</code> 屬性的那個狀態，一旦要回復至原來的狀態時，就會失去 Transition 的平順效果了。這時我們需要對原先的狀態，也加入 <code>transition</code> 。</p>
<p data-height="268" data-theme-id="0" data-slug-hash="JhIux" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/JhIux/" target="_blank" rel="external">例：雙向的 transition</a></p>

<h3 id="Transition_的限制">Transition 的限制</h3><p><code>transition</code> 的開始和結束都必須是具體數值；例如以下的 CSS 屬性值之間是無法被計算的，就無法使用 <code>transition</code> ：</p>
<ul>
<li><code>height: auto</code> (不確定的值) 至 <code>height: 100px</code> (具體數值)</li>
<li><code>display: none</code> 至 <code>display: block</code></li>
<li><code>background: url(foo.jpg)</code> 至 <code>background: url(bar.jpg)</code></li>
</ul>
<p data-height="268" data-theme-id="0" data-slug-hash="DgFKd" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/DgFKd/" target="_blank" rel="external">例： transition 無法作用的狀況</a></p>

<p>另外 <code>transition</code> 需要事件來觸發它的動作，所以沒辦法在一進頁面自動產生效果。所以如果不透過 JavaScript 事件處理的話，就只能配合與事件有關的 Pseudo Classes (偽類別，即 <code>:hover</code> 、 <code>:focus</code> 等) 來呈現效果了。</p>
<p data-height="268" data-theme-id="0" data-slug-hash="jAaxk" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/jAaxk/" target="_blank" rel="external">例：搭配 :focus 偽類別</a></p>

<h3 id="搭配_jQuery">搭配 jQuery</h3><p>如果搭配 jQuery 等可以操作 DOM 元素的 library ，我們就可以做更複雜的操作。</p>
<p data-height="300" data-theme-id="0" data-slug-hash="lcCHb" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/lcCHb/" target="_blank" rel="external">例：透過切換 class 來做 transition</a></p>

<h3 id="瀏覽器支援">瀏覽器支援</h3><p>目前包含 IE 10+ 的主流瀏覽器都已經支援 <code>transition</code> ，可參考 <a href="http://caniuse.com/#search=transition" target="_blank" rel="external">Can I use</a> 。</p>
<h2 id="Animation">Animation</h2><p>雖然 <code>transition</code> 屬性簡單易用，但也有上述的侷限。因此就有了 <code>animation</code> 這個屬性來彌補其不足。</p>
<h3 id="基本的_Animation">基本的 Animation</h3><p>最基本的 <code>animation</code> 要指定動畫持續的時間，還有動畫的名稱。</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">div:hover &#123;&#10;  animation: 1s fat;&#10;&#125;</span><br></pre></td></tr></table></figure>
<p>而動畫的定義則是用 <code>@keyframes</code> 這個屬性，例如：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">@keyframes fat &#123;&#10;  0% &#123; width: 100px; &#125;&#10;  50% &#123; width: 150px; &#125;&#10;  100% &#123; width: 200px; &#125;&#10;&#125;</span><br></pre></td></tr></table></figure>
<p>在 <code>@keyframes</code> 中可以定義多個狀態，範圍可從 <code>0%</code> 至 <code>100%</code> 。另外 <code>0%</code> 可寫成 <code>from</code> ， <code>100%</code> 可寫成 <code>to</code> ，其他狀態還是使用數字百分比。</p>
<p data-height="268" data-theme-id="0" data-slug-hash="pyngJ" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/pyngJ/" target="_blank" rel="external">例：基本的 animation</a></p>

<h3 id="animation_屬性詳解"><code>animation</code> 屬性詳解</h3><p><code>animation</code> 屬性和 <code>transition</code> 屬性一樣，都是簡寫屬性。它代表以下屬性的總和：</p>
<ul>
<li><code>animation-name</code>: 動畫名稱</li>
<li><code>animation-duration</code>: 播放一次動畫需要的時間，單位為 <code>s</code> 或 <code>ms</code></li>
<li><code>animation-timing-function</code>: 動畫的加速度曲線</li>
<li><code>animation-delay</code>: 延遲多久後啟始動畫</li>
<li><code>animation-iteration-count</code>: 動畫播放次數，可用 <code>infinite</code></li>
<li><code>animation-direction</code>: 動畫播放方向</li>
<li><code>animation-fill-mode</code>: 指定動畫播放前後的狀態</li>
<li><code>animation-play-state</code>: 指定動畫播放或暫停</li>
</ul>
<p>其中 <code>animation-duration</code> 、 <code>animation-timing-function</code> 、 <code>animation-delay</code> 可參考上面 <code>transition</code> 相似屬性的介紹。</p>
<h3 id="animation-iteration-count_播放次數"><code>animation-iteration-count</code> 播放次數</h3><p>預設 <code>animation</code> 和 <code>transition</code> 一樣只會動作一次，但我們可以加入數字來指定動畫效果播放的次數。</p>
<p data-height="268" data-theme-id="0" data-slug-hash="lqCjD" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/lqCjD/" target="_blank" rel="external">例：指定 animation 的播放次數</a></p>

<p>或是以 <code>infinite</code> 這個關鍵字來無限次播放。</p>
<p data-height="268" data-theme-id="0" data-slug-hash="fybqJ" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/fybqJ/" target="_blank" rel="external">例：無限次播放 animation</a></p>

<h3 id="animation-direction_播放方向"><code>animation-direction</code> 播放方向</h3><p>所謂的播放方向是指從動畫效果 0% 到 100% 的方向，同時也是預設的 <code>normal</code> 值。可供設定的值如下：</p>
<ul>
<li><code>normal</code> ：每次播放都是從 0% 至 100%</li>
<li><code>reverse</code> ：每次播放都是從 100% 至 0%</li>
<li><code>alternate</code> ：播放兩次以上的話，會從 0% 至 100% ，再從 100% 回到 0% ，以此類推</li>
<li><code>alternate-reverse</code> ：跟 <code>alternate</code> 相反，會先從 100% 開始播放</li>
</ul>
<p><code>animation-direction: reverse</code> ：</p>
<p data-height="268" data-theme-id="0" data-slug-hash="vBtAd" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/vBtAd/" target="_blank" rel="external">例：animation 播放方向 reverse</a></p>

<p><code>animation-direction: alternate</code> ：</p>
<p data-height="268" data-theme-id="0" data-slug-hash="bwkCf" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/bwkCf/" target="_blank" rel="external">例：animation 播放方向 alternate</a></p>

<p><code>animation-direction: alternate-reverse</code> ：</p>
<p data-height="268" data-theme-id="0" data-slug-hash="kCfAE" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/kCfAE/" target="_blank" rel="external">例：animation 播放方向 alternate-reverse</a></p>

<h3 id="animation-fill-mode_動畫播放前後的狀態"><code>animation-fill-mode</code> 動畫播放前後的狀態</h3><p>如果想要控制動畫播放完後的最終狀態，可以用 <code>animation-fill-mode</code> 屬性，它可設定的值如下：</p>
<ul>
<li><code>none</code> ：回到未播放動畫效果前的狀態</li>
<li><code>forwards</code> ：停在動畫的最後一個狀態上</li>
<li><code>backwards</code> ：停在動畫的第一個狀態上 (實測不出來)</li>
<li><code>both</code> ：視 <code>animation-direction</code> 來決定停在哪一個狀態上。</li>
</ul>
<p data-height="268" data-theme-id="0" data-slug-hash="fazAe" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/fazAe/" target="_blank" rel="external">例：指定 animation 播放後的狀態 forwards</a></p>

<p>註： <code>backwards</code> 這個值我在 Chrome 和 Firefox 都試不出來。</p>
<h3 id="animation-play-state_指定動畫播放或暫停"><code>animation-play-state</code> 指定動畫播放或暫停</h3><p><code>animation-play-state</code> 有兩個屬性值： <code>running</code> 及 <code>paused</code> ，其中 <code>running</code> 是預設值。</p>
<p>這個屬性必須獨立定義，無法被放在 <code>animation</code> 屬性裡。</p>
<p data-height="268" data-theme-id="0" data-slug-hash="HhGqj" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/HhGqj/" target="_blank" rel="external">例：決定 animation 執行或暫停</a></p>

<h3 id="瀏覽器支援-1">瀏覽器支援</h3><p><code>animation</code> 屬性目前在 IE 10+ 以上主流瀏覽器都可以執行，但採用 Webkit 引擎的瀏覽器必須加上 <code>-webkit-</code> 前綴字串。</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">div:hover &#123;&#10;  -webkit-animation: 1s name;&#10;  animation: 1s name;&#10;&#125;&#10;&#10;@-webkit-keyframes name &#123;&#10;    ...&#10;&#125;&#10;&#10;@keyframes name &#123;&#10;    ...&#10;&#125;</span><br></pre></td></tr></table></figure>
<h2 id="實例">實例</h2><p>接下來我們用 Animation 搭配 Transform 來做簡單的旋轉動畫。 Transform 是用來讓 HTML 元素變形的屬性，雖然跟動畫沒有直接的關係，但它是可以套用動畫效果的。這邊我不打算詳細介紹它，只會用到旋轉的效果。</p>
<p>它的語法如下：</p>
<figure class="highlight plain"><table><tr><td class="code"><pre><span class="line">div &#123;&#10;    transform: rotate(&#952;);&#10;    transform-origin: x y;&#10;&#125;</span><br></pre></td></tr></table></figure>
<p><code>rotate(θ)</code> 是指讓指定元素以參考點為中心軸 2D 旋轉 θ 度， <code>transform-origin</code> 會將 <code>(x, y)</code> 設為參考點。當我們把 <code>transform: rotate(θ)</code> 放到 <code>@keyframes</code> 中時， <code>animation</code> 就會改變 <code>θ</code> 值來做出動畫效果。</p>
<p>以下模擬簡單的太陽、地球、月亮的週期變化。</p>
<p data-height="600" data-theme-id="0" data-slug-hash="gkdBx" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/gkdBx/" target="_blank" rel="external">範例：模擬太陽、地球、月亮的週期</a></p>

<p>更酷的範例參考：</p>
<ul>
<li><a href="http://www.creativebloq.com/css3/animation-with-css3-712437" target="_blank" rel="external">20 stunning examples of CSS3 animation</a></li>
<li><a href="http://neography.com/experiment/circles/solarsystem/" target="_blank" rel="external">Our Solar System</a></li>
<li><a href="http://designscrazed.com/css3-animation-examples/" target="_blank" rel="external">30 Best Creative CSS3 Animation Examples</a></li>
<li><a href="http://goo.gl/SHWmEx" target="_blank" rel="external">Codepen.io CSS Animation</a></li>
</ul>
<h2 id="開發工具">開發工具</h2><h3 id="CSS_3-0_Maker">CSS 3.0 Maker</h3><p><a href="http://www.css3maker.com" target="_blank" rel="external">CSS 3.0 Marker</a> 可以讓我們調整 CSS3 相關屬性的參數，並預覽效果。確認後就可以產生對應的 CSS 碼，套用到專案上。</p>
<h3 id="Animate-css">Animate.css</h3><p><a href="http://daneden.github.io/animate.css/" target="_blank" rel="external">Animate.css</a> 這個 CSS framework 提供很多組已經定義好動畫效果的 CSS class ，讓我們可以直接套在 HTML 元素上，或是搭配 jQuery 來操作 class 來產生動畫效果。</p>
<p data-height="268" data-theme-id="0" data-slug-hash="pDblC" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/pDblC/" target="_blank" rel="external">範例： Animation.css</a></p>

<h3 id="Animate_Mixin_for_Compass/SASS">Animate Mixin for Compass/SASS</h3><p><a href="http://thecssguru.freeiz.com/animate/" target="_blank" rel="external">Animate Mixin for Compass/SASS</a> 提供了一組很棒的 CSS3 Animation mixins ，讓我們可以直接套用。它其實就是從 Animation.css 移植過來的。</p>
<h3 id="AniJS">AniJS</h3><p><a href="http://anijs.github.io/" target="_blank" rel="external">AniJS</a> 是一個宣告式的 CSS 動畫 library ，它讓我們可以在 HTML 元素中加入一個 <code>data-anijs</code> 屬性，並用敘述式來定義動作事件、動畫效果、以及要作用在哪個元素上。要特別注意的是，它也必須搭配 Animation.css 使用。</p>
 <p data-height="268" data-theme-id="0" data-slug-hash="cEqhf" data-default-tab="result" class="codepen"><a href="http://codepen.io/jaceju/pen/cEqhf/" target="_blank" rel="external">範例： AniJS</a></p>]]></content>
    <summary type="html">
    <![CDATA[<p>註：本文為作者發表於 OpenFoundry 之 <a href="http://www.openfoundry.org/en/tech-column/9233-css3-animation">CSS3 動畫基礎</a>一文的備份。</p>
<script async src="//codepen.io/assets/embed/ei.js"></script>

<p>在 JSConf.Asia 2013 ， Lea Verou 介紹了 <a href="http://lea.verou.me/css-4d/#intro">CSS in the 4th dimension</a>  (<a href="https://www.youtube.com/watch?v=NTJUFQmHbvc">影片</a>) ，引發了整個 Web 界對 CSS 動畫的期盼；在 <a href="http://www.ruanyifeng.com/blog/2014/02/css_transition_and_animation.html">CSS動畫簡介</a>一文也已經把重點整理好了。</p>
<p>以下我們將會介紹主要兩個 CSS3 在動畫的屬性： Transition 與 Animation ，並配合實例來練習這些技術，後面我也會介紹一些不錯的相關開發工具。</p>]]>
    
    </summary>
    
      <category term="CSS" scheme="http://jaceju.net/tags/CSS/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[利用 generator-angular 來建立一個 AngularJS 專案]]></title>
    <link href="http://jaceju.net/2014/05/16/create-angularjs-project-with-generator-angular/"/>
    <id>http://jaceju.net/2014/05/16/create-angularjs-project-with-generator-angular/</id>
    <published>2014-05-16T09:57:33.000Z</published>
    <updated>2015-05-11T06:20:09.000Z</updated>
    <content type="html"><![CDATA[<p>參考了 <a href="https://speakerdeck.com/pearlchen/angularjs-at-devweek-2014" target="_blank" rel="external">AngularJS @ DevWeek 2014</a> 這篇的介紹，認真地試玩了一下 Yomen AngularJS Generator ，以下是簡單的筆記。<br><a id="more"></a><br>建立一個 AngularJS 專案的方式如下 (<code>$</code> 為命令列提示符號，不需輸入) ：</p>
<pre><code>$ [sudo] npm install --<span class="keyword">g</span> yo generator-angular
$ <span class="keyword">mkdir</span> &lt;<span class="keyword">app</span>-name&gt;
$ <span class="keyword">cd</span> &lt;<span class="keyword">app</span>-name&gt;
$ yo angular [<span class="keyword">app</span>-name]
$ npm install karma-chrome-launcher karma-jasmine --<span class="keyword">save</span>-dev
$ bower install --<span class="keyword">save</span> angular-cookies
</code></pre><p>預覽專案：</p>
<pre><code><span class="variable">$ </span>grunt serve
</code></pre><p>建立對應的 JavaScript 檔案：</p>
<pre><code><span class="variable">$ </span>yo <span class="symbol">angular:</span>controller [controllerName]
<span class="variable">$ </span>yo <span class="symbol">angular:</span>view [viewName]
<span class="variable">$ </span>yo <span class="symbol">angular:</span>route [routeName]
<span class="variable">$ </span>yo <span class="symbol">angular:</span>directive [directiveName]
</code></pre><p>測試專案：</p>
<pre><code>$ grunt <span class="built_in">test</span>
</code></pre><p>建置專案：</p>
<pre><code><span class="variable">$ </span>grunt build
</code></pre><p>另外需要注意幾個重點：</p>
<ul>
<li>跟 bower 的結合非常緊密，如果沒有什麼客製化需求的話，直接用就 bower 來管理 assets 就好。</li>
<li>因為有用 bower 和 grunt 直接在 <code>index.html</code> 上管理相依性和最小化的問題，所以不需要使用 RequestJS 。</li>
<li><p>要跟後端 framework 結合的話，可以把 app 這個目錄改成 public 。但還需要對應改以下兩個部份：</p>
<ul>
<li><code>bower.json</code> 加入 <code>&quot;appPath&quot;: &quot;public&quot;</code> 。</li>
<li><code>.bowerrc</code> 及 <code>karma.conf.js</code> 中的 <code>app/</code> 改成 <code>public/</code> 。</li>
</ul>
</li>
<li><p>儘量不要改 index.html 裡的註解，因為 Grunt 的 task 會用到它們來做管理 assets 。</p>
</li>
<li><code>yo angular:route &lt;route-name&gt; --route=&lt;path&gt;</code> 會自動幫你新增對應的 Controller / Route / View 。</li>
<li><code>yo angular:directive &lt;directive-name&gt;</code> 的 <code>directive-name</code> 要用 <code>-</code> (dash) 。</li>
<li><p><code>grunt test</code> 預設是用 Chrome 和 Jasmine 來執行測試，所以要先執行以下指令：</p>
<pre><code>npm install karma-chrome-launcher karma-jasmine --<span class="built_in">save</span>-<span class="built_in">dev</span>
</code></pre></li>
<li><p><code>grunt build</code> 後的所有檔案會放在 <code>dist</code> 目錄下，所以線上 Web 環境要把網站根目錄指到 <code>dist</code> 目錄，而不是 <code>public</code> 目錄。</p>
</li>
</ul>
<p>接下來要想想看是不是能改用 Gulp 處理，我對 Grunt 已經不是那麼有愛了。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>參考了 <a href="https://speakerdeck.com/pearlchen/angularjs-at-devweek-2014">AngularJS @ DevWeek 2014</a> 這篇的介紹，認真地試玩了一下 Yomen AngularJS Generator ，以下是簡單的筆記。<br>]]>
    
    </summary>
    
      <category term="AngularJS" scheme="http://jaceju.net/tags/AngularJS/"/>
    
      <category term="Grunt" scheme="http://jaceju.net/tags/Grunt/"/>
    
      <category term="Yeoman" scheme="http://jaceju.net/tags/Yeoman/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[20 個實用的前端開發參考資訊整理]]></title>
    <link href="http://jaceju.net/2014/04/25/20-docs-guides-front-end-developers/"/>
    <id>http://jaceju.net/2014/04/25/20-docs-guides-front-end-developers/</id>
    <published>2014-04-25T06:55:29.000Z</published>
    <updated>2015-05-11T06:20:09.000Z</updated>
    <content type="html"><![CDATA[<p>原文連結： <a href="http://www.sitepoint.com/20-docs-guides-front-end-developers/" target="_blank" rel="external">20 Useful Docs and Guides for Front-End Developers</a> </p>
<p>看到上面的文章收集了許多前端開發參考資訊，覺得非常實用，故將重點整理如下：</p>
<a id="more"></a>
<ol>
<li><a href="http://pumpula.net/p/apps/css-vocabulary/" target="_blank" rel="external">CSS Vocabulary</a><br>可以瞭解 CSS 中的名詞實際對應的部份。</li>
<li><a href="http://liquidapsive.com/" target="_blank" rel="external">Liquidapsive</a><br>介紹 Responsive / Adaptive / Liquid / Static 四種排版方式及其差異，網站本身即為實例。</li>
<li><a href="http://superherojs.com/" target="_blank" rel="external">Superhero.js</a><br>這個網站收集非常多有關 JavaScript 的文件、簡報或影片！</li>
<li><a href="http://howtocoffeescript.com/" target="_blank" rel="external">HowToCoffeeScript.com</a><br>把許多常用的 CoffeeScript 技巧整理成速查表。</li>
<li><a href="http://www.w3.org/html/landscape/" target="_blank" rel="external">The HTML Landscape</a><br>介紹 WHATWG / W3C’s HTML5.0 / W3C’s HTML5.1 三種 HTML 規格的差異。</li>
<li><a href="http://rawgithub.com/w3c/elements-of-html/master/index.html" target="_blank" rel="external">The Elements of HTML</a><br>把 2.0 ~ 5.1 各個版本的 HTML 元素整理出來了，非常詳盡！</li>
<li><a href="http://dorey.github.io/JavaScript-Equality-Table/" target="_blank" rel="external">JavaScript Equality Table</a><br>用二維表的形式來呈現 JavaScript 的 == / === / if 是如何比對值。</li>
<li><a href="http://a11yproject.com/checklist.html" target="_blank" rel="external">Web Accessibility Checklist</a><br>列出專案如果要達成無障礙所需要注意的項目。</li>
<li><a href="http://www.staticapps.org/" target="_blank" rel="external">Static Web Apps — A Field Guide</a><br>列出了常見的 Web Apps 開發注意事項或解決方案。</li>
<li><a href="http://qntm.org/files/re/re.html" target="_blank" rel="external">Learn regular expressions in about 55 minutes</a><br>列出正規表達式學習的重點，並輔以範例供參考。</li>
<li><a href="http://ref.openweb.io/CSS/" target="_blank" rel="external">Open Web CSS Reference</a><br>這個網站整理了 CSS 屬性與其進階特色的 W3C 連結。</li>
<li><a href="http://cssvalues.com/" target="_blank" rel="external">CSS Values</a><br>輸入 CSS 屬性後，可以看到它的屬性值參考、瀏覽器相容性及相關連結。</li>
<li><a href="https://github.com/lukehoban/es6features" target="_blank" rel="external">ES6features</a><br>整理了 ECMAScript 6 的特色。</li>
<li><a href="https://github.com/mozilla/servo/wiki/Relevant-spec-links" target="_blank" rel="external">Relevant Spec Links</a><br>列出許多有關前端技術的規格連結。</li>
<li><a href="http://overapi.com/" target="_blank" rel="external">OverAPI.com</a><br>幾乎把所有有關網站開發的語言或工具所使用的 API 都整理成速查表了。</li>
<li><a href="http://jstherightway.org/" target="_blank" rel="external">JavaScript: The Right Way</a><br>整理了所有有關 JavaScript 的開發相關資訊。</li>
<li><a href="http://html5index.org/" target="_blank" rel="external">The HTML5 JavaScript API Index</a><br>整理了 HTML5 在 JavaScript 的所有 API 。</li>
<li><a href="http://zealdocs.org/" target="_blank" rel="external">Zeal</a><br>類似 Mac 上的 Dash 參考文件整合軟體，是給 Linux / Windows 使用者。</li>
<li><a href="http://www.sketchingwithcss.com/samplechapter/cheatsheet.html" target="_blank" rel="external">The Ultimate Flexbox Cheat Sheet</a><br>整理有關 CSS FlexBox 的教學。</li>
<li><a href="http://jscode.org/" target="_blank" rel="external">jsCode</a><br>可以自訂並產生 JavaScript Coding Guideline 的服務。</li>
</ol>
]]></content>
    <summary type="html">
    <![CDATA[<p>原文連結： <a href="http://www.sitepoint.com/20-docs-guides-front-end-developers/">20 Useful Docs and Guides for Front-End Developers</a> </p>
<p>看到上面的文章收集了許多前端開發參考資訊，覺得非常實用，故將重點整理如下：</p>]]>
    
    </summary>
    
      <category term="Web 開發" scheme="http://jaceju.net/tags/Web-%E9%96%8B%E7%99%BC/"/>
    
      <category term="連結分享" scheme="http://jaceju.net/tags/%E9%80%A3%E7%B5%90%E5%88%86%E4%BA%AB/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[在 Safari 中遇到文字粗細顯示不正確的問題]]></title>
    <link href="http://jaceju.net/2014/03/20/font-render-problem-in-safari/"/>
    <id>http://jaceju.net/2014/03/20/font-render-problem-in-safari/</id>
    <published>2014-03-20T04:04:47.000Z</published>
    <updated>2015-05-11T06:20:09.000Z</updated>
    <content type="html"><![CDATA[<p>在製作公司官網時，遇到類似以下這個影片的問題。</p>
<p>​<a href="http://www.screenr.com/gZN8" target="_blank" rel="external">http://www.screenr.com/gZN8</a></p>
<p>也就是在有動畫效果時，某些元素上的文字粗細會被改變；然而實際用 DevTools 去查看該元素 CSS 時，會發現文字粗細樣式並沒有任何變化。</p>
<a id="more"></a>
<p>後來找到這篇解法： ​<a href="http://stackoverflow.com/questions/9733011/safari-changing-font-weights-when-unrelated-animations-are-running" target="_blank" rel="external">Safari changing font weights when unrelated animations are running</a></p>
<p>依照這一篇的解釋，是因為新的 webkit 在將 HTML 交由 GPU 繪製時如果有動畫效果，就會造成 <code>top</code> 和 <code>left</code> 的偏移；而 webkit 預設使用 <code>-web-font-smoothing: subpixel-antialiased</code> 來繪製文字，卻不是每個元素都套用，因此就會造成這個問題。</p>
<p>連結提供的解法有兩種：</p>
<p>解法一：從該動畫元素往上找到最上面那個位置會動的元素，為它加上 <code>position: relatvie</code> 及 <code>z-index</code> ，如果不行就用解法二。</p>
<p>解法二：強制指定 html 的 <code>-web-font-smoothing</code> 為 <code>antialiased</code> 或 <code>subpixel-antialiased</code> ，讓整個頁面的元素都繼承同一種文字樣式設定。</p>
<p>後來我用解法二，在 CSS 開頭加了這段：</p>
<figure class="highlight css"><table><tr><td class="code"><pre><span class="line"><span class="tag">html</span> <span class="rules">&#123;</span><br><span class="line">  <span class="rule"><span class="attribute">-webkit-font-smoothing</span>:<span class="value"> subpixel-antialiased</span></span>;</span><br><span class="line">&#125;</span></span><br></pre></td></tr></table></figure>
<p>問題就解決了。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>在製作公司官網時，遇到類似以下這個影片的問題。</p>
<p>​<a href="http://www.screenr.com/gZN8">http://www.screenr.com/gZN8</a></p>
<p>也就是在有動畫效果時，某些元素上的文字粗細會被改變；然而實際用 DevTools 去查看該元素 CSS 時，會發現文字粗細樣式並沒有任何變化。</p>]]>
    
    </summary>
    
      <category term="CSS" scheme="http://jaceju.net/tags/CSS/"/>
    
      <category term="WebKit" scheme="http://jaceju.net/tags/WebKit/"/>
    
  </entry>
  
  <entry>
    <title><![CDATA[專注的承諾]]></title>
    <link href="http://jaceju.net/2014/03/10/pointing-and-calling-in-software-development/"/>
    <id>http://jaceju.net/2014/03/10/pointing-and-calling-in-software-development/</id>
    <published>2014-03-10T03:55:53.000Z</published>
    <updated>2015-05-11T06:20:09.000Z</updated>
    <content type="html"><![CDATA[<p>如果你常常趕不上火車，你或許有機會看到火車離開月台後，月台上的站務人員以食指比向左右兩邊，似乎在指著什麼東西。如果你更好奇一點，往那兩個方向看去，你將會見到在遠處的鐵軌旁有個號誌燈。</p>
<a id="more"></a>
<p>其實鐵路站務人員是在確認那個號誌是否正確顯示，雖然用看的就可以，但他們會輔以動作來加強自己看到它的印象，這個動作稱為：指差確認。 (確認號誌是其中一項，可以看 <a href="http://goo.gl/GNTjPU" target="_blank" rel="external">Wiki 有更詳盡的說明</a>。)</p>
<p>這個動作雖然較常見於危險性較高的行業，但它其實在軟體開發上，也以不同的形式存在著，那就是測試。</p>
<p>身為 Web 開發者，可能常會在修正一個自認只是影響不大的問題後，就直接讓程式上線；結果沒多久後就發現這個小修正影響到了其他地方，只好再趕緊針對這個修正再做修正。而良好且完整的測試可以減少出現這種問題的機會，但問題是我們常常會忘了要執行它。這件事要是發生在其他高危險性的職業 (我可沒說是台鐵) ，那麼後續的責任可就不是只有露出〇〇這麼簡單了。</p>
<p>軟體工程師的指差確認，就是養成隨手執行測試並確認結果的習慣；不論是什麼開發環境或編輯器，都應該試著把自動化測試放在開發流程裡，並且用最簡化的操作方式來讓自己能隨時執行它，以最清楚的呈現方式來讓自己能隨時看到它。雖然在軟體開發上還有其他方式可以來輔助我們做好這件事，像是 pre-commit hook 或是 Continuous Integration 等；但是讓自己也確實注意到測試的結果，將會加強我們自己對軟體承諾的力度，也有助於我們在開發上的專注力。</p>
<p>「渙散不論在什麼行業，都是效率的殺手。」</p>
<p>如果有「我好像有確認但我不確定」的狀況發生時，快讓自己養成用指差確認的習慣吧。</p>
]]></content>
    <summary type="html">
    <![CDATA[<p>如果你常常趕不上火車，你或許有機會看到火車離開月台後，月台上的站務人員以食指比向左右兩邊，似乎在指著什麼東西。如果你更好奇一點，往那兩個方向看去，你將會見到在遠處的鐵軌旁有個號誌燈。</p>]]>
    
    </summary>
    
      <category term="Software Development" scheme="http://jaceju.net/tags/Software-Development/"/>
    
  </entry>
  
</feed>
