Barrett Cook2015-03-16T22:46:46+00:00http://barrettcook.comBarrett Cookbarrett.cook@gmail.comUnit Testing Controllers2013-08-13T00:00:00+00:00http://barrettcook.com/blog/2013/08/13/unit-testing-controllers
<p>It all started with a one line bug fix. This line, to be precise:</p>
<div class="highlight"><pre><code class="language-diff" data-lang="diff"><span class="gd">- if($basicInfo['enable_school_info']){</span>
<span class="gi">+ if($basicInfo['enable_school_info'] && $pageView !== self::PRIVATE_VIEW){</span></code></pre></div>
<p>This change lived in <code>/cool/www/mobile/profile.html</code>, where school information was showing up on mobile private profile pages, and it was virtually untestable.</p>
<h3 id="background">Background</h3>
<p>Our controller code in <code>/cool/www/</code> consists of <code>*.html</code> files. We leverage Apache as a router to look up the right file and execute it. Secretly living inside each of these <code>*.html</code> files is PHP code which executes procedurally.</p>
<h3 id="the-problem">The Problem</h3>
<p>Procedural PHP is incredibly difficult to unit test. It is possible, but requires some gnarly code like this:</p>
<div class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp"><?php</span>
<span class="k">public</span> <span class="k">function</span> <span class="nf">test_mobile_profile_controller</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">// Mock out dependencies</span>
<span class="nv">$can_view_profile</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">StaticMock</span><span class="p">(</span><span class="s1">'tag_privacy'</span><span class="p">,</span> <span class="s1">'can_view_profile'</span><span class="p">,</span> <span class="k">false</span><span class="p">);</span> <span class="c1">// Declare a private profile</span>
<span class="c1">// Execute the procedural controller code and trap the output</span>
<span class="nb">ob_start</span><span class="p">();</span>
<span class="k">require</span><span class="p">(</span><span class="nx">BASE_DIR</span><span class="o">.</span><span class="s1">'/cool/www/mobile/profile.html'</span><span class="p">);</span>
<span class="nv">$result</span> <span class="o">=</span> <span class="nb">ob_get_clean</span><span class="p">();</span>
<span class="c1">// Assert dependencies were called</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">assertNotContains</span><span class="p">(</span><span class="s1">'<span class="school_info">'</span><span class="p">,</span> <span class="nv">$result</span><span class="p">);</span> <span class="c1">// What happens if the template changes?</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">assertTrue</span><span class="p">(</span><span class="nv">$can_view_profile</span><span class="o">-></span><span class="na">calledOnce</span><span class="p">());</span>
<span class="p">}</span></code></pre></div>
<p>Generally, each controller instantiates a new <code>tag_page</code> object which <code>render()</code>’s and outputs a template to stdout. This means we are not able to verify that the controller gathered the correct data. Instead, we are verifying that the template was rendered correctly. Imagine trying to <code>$this->assertEquals()</code> against the entire <code><html></code> content of a page. If the template changes, this unit test could start to fail. But all we care about is that we never send school info for a private profile.</p>
<h3 id="the-breakthrough">The Breakthrough</h3>
<p>By moving all of the <code>mobile/profile.html</code> code into a new PHP <code>class</code>, I was able to create a class method that could be called over and over again instead of having to <code>require()</code> the file. The breakthrough here is that I could use dependency injection to pass in a fake <code>tag_page</code> object and assert that certain attributes were set on it. Now if the template changes it will not break this test.</p>
<div class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp"><?php</span>
<span class="k">public</span> <span class="k">function</span> <span class="nf">test_mobile_profile_controller</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">// Mock out dependencies</span>
<span class="nv">$can_view_profile</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">StaticMock</span><span class="p">(</span><span class="s1">'tag_privacy'</span><span class="p">,</span> <span class="s1">'can_view_profile'</span><span class="p">,</span> <span class="k">false</span><span class="p">);</span> <span class="c1">// Declare a private profile</span>
<span class="nv">$page</span> <span class="o">=</span> <span class="nx">Phockito</span><span class="o">::</span><span class="na">mock</span><span class="p">(</span><span class="s1">'tag_page'</span><span class="p">);</span>
<span class="nv">$controller</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">tag_controller_mobile</span><span class="p">(</span><span class="nv">$page</span><span class="p">);</span>
<span class="c1">// Execute the controller code</span>
<span class="nv">$result</span> <span class="o">=</span> <span class="nv">$controller</span><span class="o">-></span><span class="na">profile</span><span class="p">();</span>
<span class="c1">// Assert dependencies were called</span>
<span class="nx">Phockito</span><span class="o">::</span><span class="na">verify</span><span class="p">(</span><span class="nv">$page</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span><span class="o">-></span><span class="na">assign</span><span class="p">(</span><span class="s1">'schoolInfo'</span><span class="p">,</span> <span class="nx">anything</span><span class="p">());</span> <span class="c1">// Make sure assign was called zero (0) times with key 'schoolInfo'</span>
<span class="o">...</span>
<span class="nx">Phockito</span><span class="o">::</span><span class="na">verify</span><span class="p">(</span><span class="nv">$page</span><span class="p">)</span><span class="o">-></span><span class="na">render</span><span class="p">(</span><span class="s1">'/mobile/profile/profile.php'</span><span class="p">);</span> <span class="c1">// Make sure render() was called with the correct template</span>
<span class="nv">$this</span><span class="o">-></span><span class="na">assertTrue</span><span class="p">(</span><span class="nv">$can_view_profile</span><span class="o">-></span><span class="na">calledOnce</span><span class="p">());</span>
<span class="p">}</span></code></pre></div>
<p>Suddenly our controller (<code>mobile/profile.html</code>) looks very small.</p>
<div class="highlight"><pre><code class="language-php" data-lang="php"><span class="cp"><?php</span>
<span class="k">require_once</span> <span class="s1">'../include/config.php'</span><span class="p">;</span>
<span class="nv">$page</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">tag_page</span><span class="p">();</span>
<span class="nv">$controller</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">tag_controller_mobile</span><span class="p">(</span><span class="nv">$page</span><span class="p">);</span>
<span class="nv">$controller</span><span class="o">-></span><span class="na">profile</span><span class="p">();</span></code></pre></div>
<p>You can see the full code change in <a href="https://github.tagged.com/tagged/web/pull/5191/files">this pull request</a>.</p>