KINTO Tech Blog
iOS

Snapshot Testing in the my route App

Cover Image for Snapshot Testing in the my route App

<p data-line="1" class="code-line">Hi! I’m Ryomm, developing the iOS app <em>my route</em> at KINTO Technologies. My fellow developers, Hosaka-san and Chang-san, along with another business partner and I, successfully implemented and integrated our Snapshot Testing.</p>
<h1 id="introduction" data-line="4" class="code-line"><a class="header-anchor-link" href="#introduction" aria-hidden="true"></a> Introduction</h1>
<p data-line="6" class="code-line">Currently, the <em>my route</em> app team is moving towards transitioning to SwiftUI, so we have decided to implement Snapshot Testing as a foundational step. We began this transition by initially replacing only the content, while keeping UIViewController as the base. This approach ensures that the implemented Snapshot Testing will be directly applicable.</p>
<p data-line="8" class="code-line">Let me introduce the techniques and trial-and-error methods we used to apply Snapshot Testing to an app built with UIKit.</p>
<h1 id="what-is-snapshot-testing%3F" data-line="10" class="code-line"><a class="header-anchor-link" href="#what-is-snapshot-testing%3F" aria-hidden="true"></a> What is Snapshot Testing?</h1>
<p data-line="12" class="code-line">It is a type of testing that verifies whether there are any differences between screenshots taken before and after code modifications. We use the Point-Free library for modifications <a href="https://github.com/pointfreeco/swift-snapshot-testing" target="_blank" rel="nofollow noopener noreferrer">https://github.com/pointfreeco/swift-snapshot-testing</a>.</p>
<p data-line="14" class="code-line">While developing <em>my route</em>, we extend XCTestCase to create a method that wraps assertSnapshots as follows: We determined the threshold to be at 98.5% after various trials to ensure that very fine tolerance variances were accommodated successfully.</p>
<div class="code-block-container"><pre class="language-swift"><code class="language-swift code-line" data-line="16"><span class="token keyword">extension</span> <span class="token class-name">XCTestCase</span> <span class="token punctuation">{</span>
<span class="token keyword">var</span> precision<span class="token punctuation">:</span> <span class="token class-name">Float</span> <span class="token punctuation">{</span> <span class="token number">0.985</span> <span class="token punctuation">}</span>
<span class="token keyword">func</span> <span class="token function-definition function">testSnapshot</span><span class="token punctuation">(</span>vc<span class="token punctuation">:</span> <span class="token class-name">UIViewController</span><span class="token punctuation">,</span> record<span class="token punctuation">:</span> <span class="token class-name">Bool</span> <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
file<span class="token punctuation">:</span> <span class="token class-name">StaticString</span><span class="token punctuation">,</span> function<span class="token punctuation">:</span> <span class="token class-name">String</span><span class="token punctuation">,</span> line<span class="token punctuation">:</span> <span class="token class-name">UInt</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token function">assert</span><span class="token punctuation">(</span><span class="token class-name">UIDevice</span><span class="token punctuation">.</span>current<span class="token punctuation">.</span>name <span class="token operator">==</span> <span class="token string-literal"><span class="token string">"iPhone 15"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"Please run the test by iPhone 15"</span></span><span class="token punctuation">)</span>
<span class="token comment">// SnapshotConfig is an enum that specifies the list of devices to be tested</span>
<span class="token class-name">SnapshotConfig</span><span class="token punctuation">.</span>allCases<span class="token punctuation">.</span>forEach <span class="token punctuation">{</span>
<span class="token function">assertSnapshots</span><span class="token punctuation">(</span>matching<span class="token punctuation">:</span> vc<span class="token punctuation">,</span> <span class="token keyword">as</span><span class="token punctuation">:</span> <span class="token punctuation">[</span><span class="token punctuation">.</span><span class="token function">image</span><span class="token punctuation">(</span>on<span class="token punctuation">:</span> <span class="token short-argument">$0</span><span class="token punctuation">.</span>viewImageConfig<span class="token punctuation">,</span> precision<span class="token punctuation">:</span> precision<span class="token punctuation">)</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
record<span class="token punctuation">:</span> record<span class="token punctuation">,</span> file<span class="token punctuation">:</span> file<span class="token punctuation">,</span> testName<span class="token punctuation">:</span> function <span class="token operator">+</span> <span class="token short-argument">$0</span><span class="token punctuation">.</span>rawValue<span class="token punctuation">,</span> line<span class="token punctuation">:</span> line<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div><p data-line="31" class="code-line">The Snapshot Testing for each screen is written as follows.</p>
<div class="code-block-container"><pre class="language-swift"><code class="language-swift code-line" data-line="33"><span class="token keyword">final</span> <span class="token keyword">class</span> <span class="token class-name">SampleVCTests</span><span class="token punctuation">:</span> <span class="token class-name">XCTestCase</span> <span class="token punctuation">{</span>
<span class="token comment">// snapshot test whether it is in recording mode or not</span>
<span class="token keyword">var</span> record <span class="token operator">=</span> <span class="token boolean">false</span>
<span class="token keyword">func</span> <span class="token function-definition function">testViewController</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token punctuation">{</span>
<span class="token keyword">let</span> <span class="token class-name">SampleVC</span> <span class="token operator">=</span> <span class="token class-name">SampleVC</span><span class="token punctuation">(</span>coder<span class="token punctuation">:</span> coder<span class="token punctuation">)</span>
<span class="token keyword">let</span> navi <span class="token operator">=</span> <span class="token class-name">UINavigationController</span><span class="token punctuation">(</span>rootViewController<span class="token punctuation">:</span> <span class="token class-name">SampleVC</span><span class="token punctuation">)</span>
navi<span class="token punctuation">.</span>modalPresentationStyle <span class="token operator">=</span> <span class="token punctuation">.</span>fullScreen
<span class="token comment">// This is where the lifecycle methods are called</span>
<span class="token class-name">UIApplication</span><span class="token punctuation">.</span>shared<span class="token punctuation">.</span>rootViewController <span class="token operator">=</span> navi
<span class="token comment">// The lifecycle methods starting from viewDidLoad are invoked for each test device</span>
<span class="token function">testSnapshot</span><span class="token punctuation">(</span>vc<span class="token punctuation">:</span> navi<span class="token punctuation">,</span> record<span class="token punctuation">:</span> record<span class="token punctuation">,</span> file<span class="token punctuation">:</span> <span class="token literal constant">#file</span><span class="token punctuation">,</span> function<span class="token punctuation">:</span> <span class="token literal constant">#function</span><span class="token punctuation">,</span> line<span class="token punctuation">:</span> <span class="token literal constant">#line</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div><h1 id="tips" data-line="49" class="code-line"><a class="header-anchor-link" href="#tips" aria-hidden="true"></a> Tips</h1>
<h2 id="we-need-to-wait-for-the-data-fetched-by-the-api-to-be-reflected-in-the-view-after-the-viewwillappear-method-and-subsequent-methods." data-line="51" class="code-line"><a class="header-anchor-link" href="#we-need-to-wait-for-the-data-fetched-by-the-api-to-be-reflected-in-the-view-after-the-viewwillappear-method-and-subsequent-methods." aria-hidden="true"></a> We need to wait for the data fetched by the API to be reflected in the View after the viewWillAppear method and subsequent methods.</h2>
<p data-line="53" class="code-line">To ensure the Snapshot Testing run after the API data is reflected in View, we have encountered issues where the tests execute too early, causing problems like the indicator still being visible.</p>
<p data-line="55" class="code-line">Since it is difficult to determine if the data from the API call has been reflected in the view, we will implement a delegate to handle this verification.</p>
<div class="code-block-container"><pre class="language-swift"><code class="language-swift code-line" data-line="57"><span class="token keyword">protocol</span> <span class="token class-name">BaseViewControllerDelegate</span><span class="token punctuation">:</span> <span class="token class-name">AnyObject</span> <span class="token punctuation">{</span>
<span class="token keyword">func</span> <span class="token function-definition function">viewDidDraw</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
</code></pre></div><p data-line="63" class="code-line">In the ViewController class, create a delegate property that conforms to the previously prepared delegate. If no delegate is specified during initialization, this property defaults to nil.</p>
<div class="code-block-container"><pre class="language-swift"><code class="language-swift code-line" data-line="65"><span class="token keyword">class</span> <span class="token class-name">SampleVC</span><span class="token punctuation">:</span> <span class="token class-name">BaseViewController</span> <span class="token punctuation">{</span>
<span class="token comment">// ...</span>
<span class="token keyword">weak</span> <span class="token keyword">var</span> baseDelegate<span class="token punctuation">:</span> <span class="token class-name">BaseViewControllerDelegate</span><span class="token operator">?</span>
<span class="token comment">// ....</span>
<span class="token keyword">init</span><span class="token punctuation">(</span>baseDelegate<span class="token punctuation">:</span> <span class="token class-name">BaseViewControllerDelegate</span><span class="token operator">?</span> <span class="token operator">=</span> <span class="token nil constant">nil</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">self</span><span class="token punctuation">.</span>baseDelegate <span class="token operator">=</span> baseDelegate
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token keyword">init</span><span class="token punctuation">(</span>nibName<span class="token punctuation">:</span> <span class="token nil constant">nil</span><span class="token punctuation">,</span> bundle<span class="token punctuation">:</span> <span class="token nil constant">nil</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token comment">// ...</span>
<span class="token punctuation">}</span>
</code></pre></div><p data-line="78" class="code-line">When calling the API and updating the view, for example, after receiving the results with Combine and reflecting them on the screen, call <code>baseDelegate.viewDidDraw()</code>. This notifies the Snapshot Testing that the view has been successfully updated with the data.</p>
<div class="code-block-container"><pre class="language-swift"><code class="language-swift code-line" data-line="80">someAPIResult<span class="token punctuation">.</span><span class="token function">receive</span><span class="token punctuation">(</span>on<span class="token punctuation">:</span> <span class="token class-name">DispatchQueue</span><span class="token punctuation">.</span>main<span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">sink</span><span class="token punctuation">(</span>receiveValue<span class="token punctuation">:</span> <span class="token punctuation">{</span> <span class="token punctuation">[</span><span class="token keyword">weak</span> <span class="token keyword">self</span><span class="token punctuation">]</span> result <span class="token keyword">in</span>
<span class="token keyword">guard</span> <span class="token keyword">let</span> <span class="token keyword">self</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token punctuation">}</span>
<span class="token keyword">switch</span> result <span class="token punctuation">{</span>
<span class="token keyword">case</span> <span class="token punctuation">.</span><span class="token function">success</span><span class="token punctuation">(</span><span class="token keyword">let</span> item<span class="token punctuation">)</span><span class="token punctuation">:</span>
<span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">hideIndicator</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">updateView</span><span class="token punctuation">(</span>with<span class="token punctuation">:</span> item<span class="token punctuation">)</span>
<span class="token comment">// Timing of data reflection completion</span>
<span class="token keyword">self</span><span class="token punctuation">.</span>baseDelegate<span class="token operator">?</span><span class="token punctuation">.</span><span class="token function">viewDidDraw</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">case</span> <span class="token punctuation">.</span><span class="token function">failure</span><span class="token punctuation">(</span><span class="token keyword">let</span> error<span class="token punctuation">)</span><span class="token punctuation">:</span>
<span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">hideIndicator</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">showError</span><span class="token punctuation">(</span>error<span class="token punctuation">:</span> error<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token punctuation">.</span><span class="token function">store</span><span class="token punctuation">(</span><span class="token keyword">in</span><span class="token punctuation">:</span> <span class="token operator">&</span>cancellables<span class="token punctuation">)</span>
</code></pre></div><p data-line="98" class="code-line">As we want to wait for <code>baseDelegate.viewDidDraw()</code> to be executed, we add XCTestExpectation to the Snapshot Testing.</p>
<div class="code-block-container"><pre class="language-swift"><code class="language-swift code-line" data-line="100"><span class="token keyword">final</span> <span class="token keyword">class</span> <span class="token class-name">SampleVCTests</span><span class="token punctuation">:</span> <span class="token class-name">XCTestCase</span> <span class="token punctuation">{</span>
<span class="token keyword">var</span> record <span class="token operator">=</span> <span class="token boolean">false</span>
<span class="token keyword">var</span> expectation<span class="token punctuation">:</span> <span class="token class-name">XCTestExpectation</span><span class="token operator">!</span>
<span class="token keyword">func</span> <span class="token function-definition function">testViewController</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token punctuation">{</span>
<span class="token keyword">let</span> <span class="token class-name">SampleVC</span> <span class="token operator">=</span> <span class="token class-name">SampleVC</span><span class="token punctuation">(</span>coder<span class="token punctuation">:</span> coder<span class="token punctuation">,</span> baseDelegate<span class="token punctuation">:</span> <span class="token keyword">self</span><span class="token punctuation">)</span>
<span class="token keyword">let</span> navi <span class="token operator">=</span> <span class="token class-name">UINavigationController</span><span class="token punctuation">(</span>rootViewController<span class="token punctuation">:</span> <span class="token class-name">SampleVC</span><span class="token punctuation">)</span>
navi<span class="token punctuation">.</span>modalPresentationStyle <span class="token operator">=</span> <span class="token punctuation">.</span>fullScreen
<span class="token class-name">UIApplication</span><span class="token punctuation">.</span>shared<span class="token punctuation">.</span>rootViewController <span class="token operator">=</span> navi
expectation <span class="token operator">=</span> <span class="token function">expectation</span><span class="token punctuation">(</span>description<span class="token punctuation">:</span> <span class="token string-literal"><span class="token string">"callSomeAPI finished"</span></span><span class="token punctuation">)</span>
<span class="token function">wait</span><span class="token punctuation">(</span><span class="token keyword">for</span><span class="token punctuation">:</span> <span class="token punctuation">[</span>expectation<span class="token punctuation">]</span><span class="token punctuation">,</span> timeout<span class="token punctuation">:</span> <span class="token number">5.0</span><span class="token punctuation">)</span>
viewController<span class="token punctuation">.</span>baseViewControllerDelegate <span class="token operator">=</span> <span class="token nil constant">nil</span>
<span class="token function">testSnapshot</span><span class="token punctuation">(</span>vc<span class="token punctuation">:</span> navi<span class="token punctuation">,</span> record<span class="token punctuation">:</span> record<span class="token punctuation">,</span> file<span class="token punctuation">:</span> <span class="token literal constant">#file</span><span class="token punctuation">,</span> function<span class="token punctuation">:</span> <span class="token literal constant">#function</span><span class="token punctuation">,</span> line<span class="token punctuation">:</span> <span class="token literal constant">#line</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">func</span> <span class="token function-definition function">viewDidDraw</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
expectation<span class="token punctuation">.</span><span class="token function">fulfill</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div><p data-line="120" class="code-line">When there are multiple sets of data to be retrieved from the API that need to be reflected (when calling <code>baseDelegate.viewDidDraw()</code> in multiple places), you can specify <code>expectedFulfillmentCount</code> or <code>assertForOverFulfill</code>.</p>
<div class="code-block-container"><pre class="language-swift"><code class="language-swift code-line" data-line="122"><span class="token keyword">final</span> <span class="token keyword">class</span> <span class="token class-name">SampleVCTests</span><span class="token punctuation">:</span> <span class="token class-name">XCTestCase</span> <span class="token punctuation">{</span>
<span class="token keyword">var</span> record <span class="token operator">=</span> <span class="token boolean">false</span>
<span class="token keyword">var</span> expectation<span class="token punctuation">:</span> <span class="token class-name">XCTestExpectation</span><span class="token operator">!</span>
<span class="token keyword">func</span> <span class="token function-definition function">testViewController</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token punctuation">{</span>
<span class="token keyword">let</span> <span class="token class-name">SampleVC</span> <span class="token operator">=</span> <span class="token class-name">SampleVC</span><span class="token punctuation">(</span>coder<span class="token punctuation">:</span> coder<span class="token punctuation">,</span> baseDelegate<span class="token punctuation">:</span> <span class="token keyword">self</span><span class="token punctuation">)</span>
<span class="token keyword">let</span> navi <span class="token operator">=</span> <span class="token class-name">UINavigationController</span><span class="token punctuation">(</span>rootViewController<span class="token punctuation">:</span> <span class="token class-name">SampleVC</span><span class="token punctuation">)</span>
navi<span class="token punctuation">.</span>modalPresentationStyle <span class="token operator">=</span> <span class="token punctuation">.</span>fullScreen
<span class="token class-name">UIApplication</span><span class="token punctuation">.</span>shared<span class="token punctuation">.</span>rootViewController <span class="token operator">=</span> navi
expectation <span class="token operator">=</span> <span class="token function">expectation</span><span class="token punctuation">(</span>description<span class="token punctuation">:</span> <span class="token string-literal"><span class="token string">"callSomeAPI finished"</span></span><span class="token punctuation">)</span>
<span class="token comment">// When viewDidDraw() is called twice</span>
expectation<span class="token punctuation">.</span>expectedFulfillmentCount <span class="token operator">=</span> <span class="token number">2</span>
<span class="token comment">// When viewDidDraw() is called more times than specified, any additional calls should be ignored</span>
expectation<span class="token punctuation">.</span>assertForOverFulfill <span class="token operator">=</span> <span class="token boolean">false</span>
<span class="token function">wait</span><span class="token punctuation">(</span><span class="token keyword">for</span><span class="token punctuation">:</span> <span class="token punctuation">[</span>expectation<span class="token punctuation">]</span><span class="token punctuation">,</span> timeout<span class="token punctuation">:</span> <span class="token number">5.0</span><span class="token punctuation">)</span>
viewController<span class="token punctuation">.</span>baseViewControllerDelegate <span class="token operator">=</span> <span class="token nil constant">nil</span>
<span class="token function">testSnapshot</span><span class="token punctuation">(</span>vc<span class="token punctuation">:</span> navi<span class="token punctuation">,</span> record<span class="token punctuation">:</span> record<span class="token punctuation">,</span> file<span class="token punctuation">:</span> <span class="token literal constant">#file</span><span class="token punctuation">,</span> function<span class="token punctuation">:</span> <span class="token literal constant">#function</span><span class="token punctuation">,</span> line<span class="token punctuation">:</span> <span class="token literal constant">#line</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">func</span> <span class="token function-definition function">viewDidDraw</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
expectation<span class="token punctuation">.</span><span class="token function">fulfill</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div><p data-line="146" class="code-line">If the baseViewControllerDelegate from the previous screen remains active, running the Snapshot Testing across all screens will call viewDidLoad and subsequent lifecycle methods for each test device every time <code>testSnapshot()</code> is invoked. This causes the API to be called multiple times and <code>viewDidDraw()</code> to be executed repeatedly, resulting in multiple calls error.</p>
<p data-line="148" class="code-line">Therefore, we clear the baseViewControllerDelegate after calling <code>wait()</code>.</p>
<h2 id="frame-misalignment-on-devices" data-line="150" class="code-line"><a class="header-anchor-link" href="#frame-misalignment-on-devices" aria-hidden="true"></a> Frame misalignment on devices</h2>
<p data-line="151" class="code-line">While Snapshot Testing can generate snapshots for multiple devices, we encountered issues where the layout and size of elements were misaligned on some devices.</p>
<p data-line="153" class="code-line"><img src="/assets/blog/authors/ryomm/2024-04-17-SnapshotTest/01_snapshot_failured_frame_1.png" alt="Misaligned" /> <em>Misaligned</em></p>
<p data-line="155" class="code-line">This issue is caused by the lifecycle of the Snapshot Testing execution. In a Snapshot Testing, it starts loading on one device, and then other devices are rendered by changing the size without reloading. This means that <code>viewDidLoad()</code> is executed only once at the beginning, and for the other devices, it starts from <code>viewWillAppear()</code>.</p>
<p data-line="157" class="code-line">As a solution, create a MockViewController that wraps the viewcontroller you want to test. Override <code>viewWillAppear()</code> to call the methods that are originally called in <code>viewDidLoad()</code>.</p>
<div class="code-block-container"><pre class="language-swift"><code class="language-swift code-line" data-line="159"><span class="token keyword">import</span> <span class="token class-name">XCTest</span>
<span class="token attribute atrule">@testable</span> <span class="token keyword">import</span> <span class="token class-name">App</span>

<span class="token keyword">final</span> <span class="token keyword">class</span> <span class="token class-name">SampleVCTests</span><span class="token punctuation">:</span> <span class="token class-name">XCTestCase</span> <span class="token punctuation">{</span>
<span class="token comment">// snapshot test whether it is in recording mode or not</span>
<span class="token keyword">var</span> record <span class="token operator">=</span> <span class="token boolean">false</span>
<span class="token keyword">func</span> <span class="token function-definition function">testViewController</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token punctuation">{</span>
<span class="token comment">// Write it the same way as when calling the screen</span>
<span class="token keyword">let</span> storyboard <span class="token operator">=</span> <span class="token class-name">UIStoryboard</span><span class="token punctuation">(</span>name<span class="token punctuation">:</span> <span class="token string-literal"><span class="token string">"Sample"</span></span><span class="token punctuation">,</span> bundle<span class="token punctuation">:</span> <span class="token nil constant">nil</span><span class="token punctuation">)</span>
<span class="token keyword">let</span> <span class="token class-name">SampleVC</span> <span class="token operator">=</span> storyboard<span class="token punctuation">.</span><span class="token function">instantiateViewController</span><span class="token punctuation">(</span>identifier<span class="token punctuation">:</span> <span class="token string-literal"><span class="token string">"Sample"</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> coder <span class="token keyword">in</span>
<span class="token comment">// VC wrapped for Snapshot Test</span>
<span class="token class-name">MockSampleVC</span><span class="token punctuation">(</span>coder<span class="token punctuation">:</span> coder<span class="token punctuation">,</span> completeHander<span class="token punctuation">:</span> <span class="token nil constant">nil</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">let</span> navi <span class="token operator">=</span> <span class="token class-name">UINavigationController</span><span class="token punctuation">(</span>rootViewController<span class="token punctuation">:</span> <span class="token class-name">SampleVC</span><span class="token punctuation">)</span>
navi<span class="token punctuation">.</span>modalPresentationStyle <span class="token operator">=</span> <span class="token punctuation">.</span>fullScreen
<span class="token class-name">UIApplication</span><span class="token punctuation">.</span>shared<span class="token punctuation">.</span>rootViewController <span class="token operator">=</span> navi
<span class="token function">testSnapshot</span><span class="token punctuation">(</span>vc<span class="token punctuation">:</span> navi<span class="token punctuation">,</span> record<span class="token punctuation">:</span> record<span class="token punctuation">,</span> file<span class="token punctuation">:</span> <span class="token literal constant">#file</span><span class="token punctuation">,</span> function<span class="token punctuation">:</span> <span class="token literal constant">#function</span><span class="token punctuation">,</span> line<span class="token punctuation">:</span> <span class="token literal constant">#line</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">class</span> <span class="token class-name">MockSampleVC</span><span class="token punctuation">:</span> <span class="token class-name">SampleVC</span> <span class="token punctuation">{</span>
<span class="token keyword">required</span> <span class="token keyword">init</span><span class="token operator">?</span><span class="token punctuation">(</span>coder<span class="token punctuation">:</span> <span class="token class-name">NSCoder</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token function">fatalError</span><span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"init(coder: </span><span class="token interpolation-punctuation punctuation">(</span><span class="token interpolation">coder</span><span class="token interpolation-punctuation punctuation">)</span><span class="token string"> has not been implemented"</span></span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">override</span> <span class="token keyword">init</span><span class="token operator">?</span><span class="token punctuation">(</span>coder<span class="token punctuation">:</span> <span class="token class-name">NSCoder</span><span class="token punctuation">,</span>
completeHander<span class="token punctuation">:</span> <span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token omit keyword"></span> readString<span class="token punctuation">:</span> <span class="token class-name">String</span><span class="token operator">?</span><span class="token punctuation">)</span> <span class="token operator">-></span> <span class="token class-name">Void</span><span class="token punctuation">)</span><span class="token operator">?</span> <span class="token operator">=</span> <span class="token nil constant">nil</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token keyword">init</span><span class="token punctuation">(</span>coder<span class="token punctuation">:</span> coder<span class="token punctuation">,</span> completeHander<span class="token punctuation">:</span> completeHander<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">override</span> <span class="token keyword">func</span> <span class="token function-definition function">viewWillAppear</span><span class="token punctuation">(</span><span class="token omit keyword">
</span> animated<span class="token punctuation">:</span> <span class="token class-name">Bool</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">viewWillAppear</span><span class="token punctuation">(</span>animated<span class="token punctuation">)</span>
<span class="token comment">// The following methods are originally called in viewDidLoad()</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">setNavigationBar</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">setCameraPreviewMask</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">cameraPreview</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">stopCamera</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div><p data-line="198" class="code-line"><img src="/assets/blog/authors/ryomm/2024-04-17-SnapshotTest/02_snapshot_fix_viewWillAppear.png" alt="Still not fixed" /> <em>Still not fixed・・・</em></p>
<p data-line="200" class="code-line">If the rendering is still misaligned, calling the <code>layoutIfNeeded()</code> method to update the frames often resolves the issue.</p>
<div class="code-block-container"><pre class="language-swift"><code class="language-swift code-line" data-line="202"><span class="token keyword">import</span> <span class="token class-name">XCTest</span>
<span class="token attribute atrule">@testable</span> <span class="token keyword">import</span> <span class="token class-name">App</span>
<span class="token keyword">final</span> <span class="token keyword">class</span> <span class="token class-name">SampleVCTests</span><span class="token punctuation">:</span> <span class="token class-name">XCTestCase</span> <span class="token punctuation">{</span>
<span class="token keyword">var</span> record <span class="token operator">=</span> <span class="token boolean">false</span>
<span class="token keyword">func</span> <span class="token function-definition function">testViewController</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token punctuation">{</span>
<span class="token keyword">let</span> storyboard <span class="token operator">=</span> <span class="token class-name">UIStoryboard</span><span class="token punctuation">(</span>name<span class="token punctuation">:</span> <span class="token string-literal"><span class="token string">"Sample"</span></span><span class="token punctuation">,</span> bundle<span class="token punctuation">:</span> <span class="token nil constant">nil</span><span class="token punctuation">)</span>
<span class="token keyword">let</span> <span class="token class-name">SampleVC</span> <span class="token operator">=</span> storyboard<span class="token punctuation">.</span><span class="token function">instantiateViewController</span><span class="token punctuation">(</span>identifier<span class="token punctuation">:</span> <span class="token string-literal"><span class="token string">"Sample"</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> coder <span class="token keyword">in</span>
<span class="token class-name">MockSampleVC</span><span class="token punctuation">(</span>coder<span class="token punctuation">:</span> coder<span class="token punctuation">,</span> completeHander<span class="token punctuation">:</span> <span class="token nil constant">nil</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">let</span> navi <span class="token operator">=</span> <span class="token class-name">UINavigationController</span><span class="token punctuation">(</span>rootViewController<span class="token punctuation">:</span> <span class="token class-name">SampleVC</span><span class="token punctuation">)</span>
navi<span class="token punctuation">.</span>modalPresentationStyle <span class="token operator">=</span> <span class="token punctuation">.</span>fullScreen
<span class="token class-name">UIApplication</span><span class="token punctuation">.</span>shared<span class="token punctuation">.</span>rootViewController <span class="token operator">=</span> navi
<span class="token function">testSnapshot</span><span class="token punctuation">(</span>vc<span class="token punctuation">:</span> navi<span class="token punctuation">,</span> record<span class="token punctuation">:</span> record<span class="token punctuation">,</span> file<span class="token punctuation">:</span> <span class="token literal constant">#file</span><span class="token punctuation">,</span> function<span class="token punctuation">:</span> <span class="token literal constant">#function</span><span class="token punctuation">,</span> line<span class="token punctuation">:</span> <span class="token literal constant">#line</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">fileprivate</span> <span class="token keyword">class</span> <span class="token class-name">MockSampleVC</span><span class="token punctuation">:</span> <span class="token class-name">SampleVC</span> <span class="token punctuation">{</span>
<span class="token keyword">required</span> <span class="token keyword">init</span><span class="token operator">?</span><span class="token punctuation">(</span>coder<span class="token punctuation">:</span> <span class="token class-name">NSCoder</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token function">fatalError</span><span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"init(coder: </span><span class="token interpolation-punctuation punctuation">(</span><span class="token interpolation">coder</span><span class="token interpolation-punctuation punctuation">)</span><span class="token string"> has not been implemented"</span></span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">override</span> <span class="token keyword">init</span><span class="token operator">?</span><span class="token punctuation">(</span>coder<span class="token punctuation">:</span> <span class="token class-name">NSCoder</span><span class="token punctuation">,</span> completeHander<span class="token punctuation">:</span> <span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token omit keyword"></span> readString<span class="token punctuation">:</span> <span class="token class-name">String</span><span class="token operator">?</span><span class="token punctuation">)</span> <span class="token operator">-></span> <span class="token class-name">Void</span><span class="token punctuation">)</span><span class="token operator">?</span> <span class="token operator">=</span> <span class="token nil constant">nil</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token keyword">init</span><span class="token punctuation">(</span>coder<span class="token punctuation">:</span> coder<span class="token punctuation">,</span> completeHander<span class="token punctuation">:</span> completeHander<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">override</span> <span class="token keyword">func</span> <span class="token function-definition function">viewWillAppear</span><span class="token punctuation">(</span><span class="token omit keyword">
</span> animated<span class="token punctuation">:</span> <span class="token class-name">Bool</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">viewWillAppear</span><span class="token punctuation">(</span>animated<span class="token punctuation">)</span>
<span class="token comment">// Update the frame before calling rendering methods</span>
<span class="token keyword">self</span><span class="token punctuation">.</span>videoView<span class="token punctuation">.</span><span class="token function">layoutIfNeeded</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">self</span><span class="token punctuation">.</span>targetView<span class="token punctuation">.</span><span class="token function">layoutIfNeeded</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">setNavigationBar</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">setCameraPreviewMask</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">cameraPreview</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">stopCamera</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div><p data-line="238" class="code-line"><img src="/assets/blog/authors/ryomm/2024-04-17-SnapshotTest/03_snapthot_success.png" alt="Looks good" /> <em>Looks good</em></p>
<h2 id="snapshot-for-webview-screens" data-line="240" class="code-line"><a class="header-anchor-link" href="#snapshot-for-webview-screens" aria-hidden="true"></a> Snapshot for WebView screens</h2>
<p data-line="242" class="code-line">There are situations where you may want to apply Snapshot Testing to toolbars to other elements, but not the content displayed in a Webview. In such cases, it is good to separate the part that loads the WebView content from the WebView’s configuration and mock the loading part during tests.</p>
<p data-line="244" class="code-line">For the implementation, we separate the method that calls <code>self.WebView.load(urlRequest)</code> etc. to display the Webview content from the method that configures the WebView itself.</p>
<div class="code-block-container"><pre class="language-swift"><code class="language-swift code-line" data-line="246"><span class="token comment">// Implementation in the VC</span>
<span class="token keyword">class</span> <span class="token class-name">SampleWebviewVC</span><span class="token punctuation">:</span> <span class="token class-name">BaseViewController</span> <span class="token punctuation">{</span>
<span class="token comment">// ...</span>
<span class="token keyword">override</span> <span class="token keyword">func</span> <span class="token function-definition function">viewDidLoad</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">viewDidLoad</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">setNavigationBar</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token operator"></span><span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">setWebView</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator"></span>
<span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">setToolBar</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">override</span> <span class="token keyword">func</span> <span class="token function-definition function">viewDidAppear</span><span class="token punctuation">(</span><span class="token omit keyword">_</span> animated<span class="token punctuation">:</span> <span class="token class-name">Bool</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">viewDidAppear</span><span class="token punctuation">(</span>animated<span class="token punctuation">)</span>
<span class="token operator"></span><span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">setWebViewContent</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator"></span>
<span class="token punctuation">}</span>
<span class="token comment">// ...</span>

<span class="token comment">/**
* Separate the method for configuring the WebView from the method for setting its content
*/</span>
<span class="token comment">/// Configure the WebView</span>
<span class="token keyword">func</span> <span class="token function-definition function">setWebView</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">self</span><span class="token punctuation">.</span>webView<span class="token punctuation">.</span>uiDelegate <span class="token operator">=</span> <span class="token keyword">self</span>
<span class="token keyword">self</span><span class="token punctuation">.</span>webView<span class="token punctuation">.</span>navigationDelegate <span class="token operator">=</span> <span class="token keyword">self</span>
<span class="token comment">// Monitor the loading state of the web page</span>
webViewObservers<span class="token punctuation">.</span><span class="token function">append</span><span class="token punctuation">(</span><span class="token keyword">self</span><span class="token punctuation">.</span>webView<span class="token punctuation">.</span><span class="token function">observe</span><span class="token punctuation">(</span><span class="token punctuation"></span><span class="token punctuation"></span><span class="token punctuation">.</span>estimatedProgress<span class="token punctuation">,</span> options<span class="token punctuation">:</span> <span class="token punctuation">.</span>new<span class="token punctuation">)</span> <span class="token punctuation">{</span> <span class="token punctuation">[</span><span class="token keyword">weak</span> <span class="token keyword">self</span><span class="token punctuation">]</span> <span class="token omit keyword">_</span><span class="token punctuation">,</span> change <span class="token keyword">in</span>
<span class="token keyword">guard</span> <span class="token keyword">let</span> <span class="token keyword">self</span> <span class="token operator">=</span> <span class="token keyword">self</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token punctuation">}</span>
<span class="token keyword">if</span> <span class="token keyword">let</span> newValue <span class="token operator">=</span> change<span class="token punctuation">.</span>newValue <span class="token punctuation">{</span>
<span class="token keyword">self</span><span class="token punctuation">.</span>loadingProgress<span class="token punctuation">.</span><span class="token function">setProgress</span><span class="token punctuation">(</span><span class="token class-name">Float</span><span class="token punctuation">(</span>newValue<span class="token punctuation">)</span><span class="token punctuation">,</span> animated<span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token comment">/// Set content for the WebView</span>
<span class="token keyword">private</span> <span class="token keyword">func</span> <span class="token function-definition function">setWebViewContent</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">let</span> request <span class="token operator">=</span> <span class="token class-name">URLRequest</span><span class="token punctuation">(</span>url<span class="token punctuation">:</span> <span class="token keyword">self</span><span class="token punctuation">.</span>url<span class="token punctuation">,</span>
cachePolicy<span class="token punctuation">:</span> <span class="token punctuation">.</span>reloadIgnoringLocalCacheData<span class="token punctuation">,</span>
timeoutInterval<span class="token punctuation">:</span> <span class="token number">60</span><span class="token punctuation">)</span>
<span class="token keyword">self</span><span class="token punctuation">.</span>webView<span class="token punctuation">.</span><span class="token function">load</span><span class="token punctuation">(</span>request<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token comment">// ...</span>
<span class="token punctuation">}</span>
</code></pre></div><p data-line="288" class="code-line">Then, in the mock that wraps the VC under test, we make it so that the method that loads the WebView content is not called.</p>
<div class="code-block-container"><pre class="language-swift"><code class="language-swift code-line" data-line="290"><span class="token keyword">import</span> <span class="token class-name">XCTest</span>
<span class="token attribute atrule">@testable</span> <span class="token keyword">import</span> <span class="token class-name">App</span>

<span class="token keyword">final</span> <span class="token keyword">class</span> <span class="token class-name">SampleWebviewVCTests</span><span class="token punctuation">:</span> <span class="token class-name">XCTestCase</span> <span class="token punctuation">{</span>
<span class="token keyword">private</span> <span class="token keyword">let</span> record <span class="token operator">=</span> <span class="token boolean">false</span>
<span class="token keyword">func</span> <span class="token function-definition function">testViewController</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">throws</span> <span class="token punctuation">{</span>
<span class="token keyword">let</span> storyboard <span class="token operator">=</span> <span class="token class-name">UIStoryboard</span><span class="token punctuation">(</span>name<span class="token punctuation">:</span> <span class="token string-literal"><span class="token string">"SampleWebview"</span></span><span class="token punctuation">,</span> bundle<span class="token punctuation">:</span> <span class="token punctuation">.</span>main<span class="token punctuation">)</span>
<span class="token keyword">let</span> <span class="token class-name">SampleWebviewVC</span> <span class="token operator">=</span> storyboard<span class="token punctuation">.</span><span class="token function">instantiateViewController</span><span class="token punctuation">(</span>identifier<span class="token punctuation">:</span> <span class="token string-literal"><span class="token string">"SampleWebview"</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span> coder <span class="token keyword">in</span>
<span class="token class-name">MockSampleWebviewVC</span><span class="token punctuation">(</span>coder<span class="token punctuation">:</span> coder<span class="token punctuation">,</span> url<span class="token punctuation">:</span> <span class="token function">URL</span><span class="token punctuation">(</span>string<span class="token punctuation">:</span> <span class="token string-literal"><span class="token string">"<https://top.myroute.fun/>"</span></span><span class="token punctuation">)</span><span class="token operator">!</span><span class="token punctuation">,</span> linkType<span class="token punctuation">:</span> <span class="token punctuation">.</span><span class="token class-name">Foobar</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">let</span> navi <span class="token operator">=</span> <span class="token class-name">UINavigationController</span><span class="token punctuation">(</span>rootViewController<span class="token punctuation">:</span> <span class="token class-name">SampleWebviewVC</span><span class="token punctuation">)</span>
navi<span class="token punctuation">.</span>modalPresentationStyle <span class="token operator">=</span> <span class="token punctuation">.</span>fullScreen
<span class="token class-name">UIApplication</span><span class="token punctuation">.</span>shared<span class="token punctuation">.</span>rootViewController <span class="token operator">=</span> navi
<span class="token function">testSnapshot</span><span class="token punctuation">(</span>vc<span class="token punctuation">:</span> navi<span class="token punctuation">,</span> record<span class="token punctuation">:</span> record<span class="token punctuation">,</span> file<span class="token punctuation">:</span> <span class="token literal constant">#file</span><span class="token punctuation">,</span> function<span class="token punctuation">:</span> <span class="token literal constant">#function</span><span class="token punctuation">,</span> line<span class="token punctuation">:</span> <span class="token literal constant">#line</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token keyword">fileprivate</span> <span class="token keyword">class</span> <span class="token class-name">MockSampleWebviewVC</span><span class="token punctuation">:</span> <span class="token class-name">SampleWebviewVC</span> <span class="token punctuation">{</span>
<span class="token keyword">override</span> <span class="token keyword">init</span><span class="token operator">?</span><span class="token punctuation">(</span>coder<span class="token punctuation">:</span> <span class="token class-name">NSCoder</span><span class="token punctuation">,</span> url<span class="token punctuation">:</span> <span class="token constant">URL</span><span class="token punctuation">,</span> linkType<span class="token punctuation">:</span> <span class="token class-name">LinkNamesItem</span><span class="token operator">?</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token keyword">init</span><span class="token punctuation">(</span>coder<span class="token punctuation">:</span> coder<span class="token punctuation">,</span> url<span class="token punctuation">:</span> url<span class="token punctuation">,</span> linkType<span class="token punctuation">:</span> linkType<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">required</span> <span class="token keyword">init</span><span class="token operator">?</span><span class="token punctuation">(</span>coder<span class="token punctuation">:</span> <span class="token class-name">NSCoder</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token function">fatalError</span><span class="token punctuation">(</span><span class="token string-literal"><span class="token string">"init(coder:) has not been implemented"</span></span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">override</span> <span class="token keyword">func</span> <span class="token function-definition function">viewWillAppear</span><span class="token punctuation">(</span><span class="token omit keyword"></span> animated<span class="token punctuation">:</span> <span class="token class-name">Bool</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// Change the method that was called in viewDidLoad to be called in viewWillAppear</span>
<span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">setNavigationBar</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">setWebView</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">self</span><span class="token punctuation">.</span><span class="token function">setToolBar</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">viewWillAppear</span><span class="token punctuation">(</span>animated<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token keyword">override</span> <span class="token keyword">func</span> <span class="token function-definition function">viewDidAppear</span><span class="token punctuation">(</span><span class="token omit keyword">
</span> animated<span class="token punctuation">:</span> <span class="token class-name">Bool</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token comment">// Do nothing</span>
<span class="token comment">// Override to avoid calling the method that sets the WebView content</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div><h2 id="snapshot-of-the-screen-that-is-calling-the-camera" data-line="328" class="code-line"><a class="header-anchor-link" href="#snapshot-of-the-screen-that-is-calling-the-camera" aria-hidden="true"></a> Snapshot of the screen that is calling the camera</h2>
<p data-line="330" class="code-line">Call the camera and also take the snapshot of the screen which displays a customized view. However, since the camera does not work on the simulator, it is necessary to find a way to disable the camera part while still being able to test the overlay.</p>
<p data-line="332" class="code-line">There was also a suggestion to insert a dummy image to make the camera work on the simulator, but it seems too costly to implement this just for the Snapshot Testing of a non-primary screen.</p>
<p data-line="334" class="code-line">In myroute’s Snapshot Testing, we used mocks to override the parts that handle the camera input and the parts that set up the capture to be displayed in AVCaptureVideoPreviewLayer, so they are not called. This way, the AVCaptureVideoPreviewLayer displays as a blank screen without any input, allowing the customized View to be shown on top.</p>
<p data-line="336" class="code-line">In the actual implementation, it is written as follows:</p>
<div class="code-block-container"><pre class="language-swift"><code class="language-swift code-line" data-line="337"><span class="token keyword">class</span> <span class="token class-name">UseCameraVC</span><span class="token punctuation">:</span> <span class="token class-name">BaseViewController</span> <span class="token punctuation">{</span>
<span class="token comment">// ...</span>
<span class="token keyword">override</span> <span class="token keyword">func</span> <span class="token function-definition function">viewDidLoad</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">viewDidLoad</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">self</span><span class="token punctuation">.</span>videoView<span class="token punctuation">.</span><span class="token function">layoutIfNeeded</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token function">setNavigationBar</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token function">setCameraPreviewMask</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">do</span> <span class="token punctuation">{</span>
<span class="token keyword">guard</span> <span class="token keyword">let</span> videoDevice <span class="token operator">=</span> <span class="token class-name">AVCaptureDevice</span><span class="token punctuation">.</span><span class="token keyword">default</span><span class="token punctuation">(</span><span class="token keyword">for</span><span class="token punctuation">:</span> <span class="token class-name">AVMediaType</span><span class="token punctuation">.</span>video<span class="token punctuation">)</span> <span class="token keyword">else</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token punctuation">}</span>
<span class="token keyword">let</span> videoInput <span class="token operator">=</span> <span class="token keyword">try</span> <span class="token class-name">AVCaptureDeviceInput</span><span class="token punctuation">(</span>device<span class="token punctuation">:</span> videoDevice<span class="token punctuation">)</span> <span class="token keyword">as</span> <span class="token class-name">AVCaptureDeviceInput</span>
<span class="token keyword">if</span> captureSession<span class="token punctuation">.</span><span class="token function">canAddInput</span><span class="token punctuation">(</span>videoInput<span class="token punctuation">)</span> <span class="token punctuation">{</span>
captureSession<span class="token punctuation">.</span><span class="token function">addInput</span><span class="token punctuation">(</span>videoInput<span class="token punctuation">)</span>
<span class="token keyword">let</span> videoOutput <span class="token operator">=</span> <span class="token class-name">AVCaptureVideoDataOutput</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">if</span> captureSession<span class="token punctuation">.</span><span class="token function">canAddOutput</span><span class="token punctuation">(</span>videoOutput<span class="token punctuation">)</span> <span class="token punctuation">{</span>
captureSession<span class="token punctuation">.</span><span class="token function">addOutput</span><span class="token punctuation">(</span>videoOutput<span class="token punctuation">)</span>
videoOutput<span class="token punctuation">.</span><span class="token function">setSampleBufferDelegate</span><span class="token punctuation">(</span><span class="token keyword">self</span><span class="token punctuation">,</span> queue<span class="token punctuation">:</span> <span class="token class-name">DispatchQueue</span><span class="token punctuation">.</span>main<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span> <span class="token keyword">catch</span> <span class="token punctuation">{</span> <span class="token keyword">return</span> <span class="token punctuation">}</span>
<span class="token function">cameraPreview</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>

<span class="token keyword">override</span> <span class="token keyword">func</span> <span class="token function-definition function">viewWillAppear</span><span class="token punctuation">(</span><span class="token omit keyword"></span> animated<span class="token punctuation">:</span> <span class="token class-name">Bool</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">viewWillAppear</span><span class="token punctuation">(</span>animated<span class="token punctuation">)</span>
<span class="token comment">// Since the camera cannot be used in the simulator, disable it</span>
<span class="token directive property"><span class="token directive-name">#if</span> targetEnvironment<span class="token punctuation">(</span>simulator<span class="token punctuation">)</span></span>
<span class="token function">stopCamera</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token function">dismiss</span><span class="token punctuation">(</span>animated<span class="token punctuation">:</span> <span class="token boolean">true</span><span class="token punctuation">)</span>
<span class="token directive property"><span class="token directive-name">#else</span></span>
captureSession<span class="token punctuation">.</span><span class="token function">startRunning</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token directive property"><span class="token directive-name">#endif</span></span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div><p data-line="373" class="code-line">Override them with mocks as follows: Due to the reasons described regarding the frame misalignment issue, we call the methods from <code>viewWillAppear()</code> that were originally called in <code>viewDidLoad()</code>.</p>
<div class="code-block-container"><pre class="language-swift"><code class="language-swift code-line" data-line="374"><span class="token keyword">class</span> <span class="token class-name">MockUseCameraVC</span><span class="token punctuation">:</span> <span class="token class-name">UseCameraVC</span> <span class="token punctuation">{</span>
<span class="token comment">// ...</span>
<span class="token keyword">override</span> <span class="token keyword">func</span> <span class="token function-definition function">viewWillAppear</span><span class="token punctuation">(</span><span class="token omit keyword">
</span> animated<span class="token punctuation">:</span> <span class="token class-name">Bool</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">viewWillAppear</span><span class="token punctuation">(</span>animated<span class="token punctuation">)</span>
<span class="token keyword">self</span><span class="token punctuation">.</span>videoView<span class="token punctuation">.</span><span class="token function">layoutIfNeeded</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">setNavigationBar</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">setCameraPreviewMask</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">cameraPreview</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token keyword">super</span><span class="token punctuation">.</span><span class="token function">stopCamera</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div><p data-line="388" class="code-line">The <code>cameraPreview()</code> method uses AVCaptureVideoPreviewLayer to display the camera image from the <code>captureSession</code>, but since we override it to have no input, it renders as a white view.</p>
<h2 id="ci-strategy" data-line="390" class="code-line"><a class="header-anchor-link" href="#ci-strategy" aria-hidden="true"></a> CI Strategy</h2>
<p data-line="392" class="code-line">At the initial stage of introducing Snapshot Testing, we uploaded reference images to a single S3 bucket. During reviews, we downloaded the reference images each time and ran the tests. However, when a view was modified and the reference images were updated simultaneously, there was an issue where tests for other PRs would fail until the PR with the updated reference images was merged.</p>
<p data-line="394" class="code-line"><img src="/assets/blog/authors/ryomm/2024-04-17-SnapshotTest/04_SnapshotTest1.PNG" alt="Existing issues" /></p>
<p data-line="396" class="code-line">To address the issue, we created two directories within the bucket hosting the reference images. One directory hosts the images during PR reviews, and once a PR is merged, the images are copied to the other directory. By doing so, we ensure that updates to the reference images do not interfere with the tests of other PRs.</p>
<p data-line="398" class="code-line"><img src="/assets/blog/authors/ryomm/2024-04-17-SnapshotTest/05_SnapshotTest2.PNG" alt="New testing strategy" /></p>
<h3 id="useful-shells" data-line="400" class="code-line"><a class="header-anchor-link" href="#useful-shells" aria-hidden="true"></a> Useful shells</h3>
<p data-line="402" class="code-line"><em>my route</em> provides four shells for snapshots.</p>
<p data-line="405" class="code-line">The first one downloads all the reference images for the current screen. This allows the tests to pass locally.</p>
<p data-line="407" class="code-line"><img src="/assets/blog/authors/ryomm/2024-04-17-SnapshotTest/06_SnapshotTest_setup.PNG" alt="setup_snapshot.sh" /></p>
<div class="code-block-container"><div class="code-block-filename-container"><span class="code-block-filename">setup_snapshot.sh</span></div><pre class="language-bash"><code class="language-bash code-line" data-line="409">Used when switching from the <span class="token comment"># develop branch</span>
<span class="token comment"># Example: Sh setup_snapshot.sh</span>
<span class="token comment"># Clean up the old files from the reference images directory</span>
<span class="token function">rm</span> <span class="token parameter variable">-r</span> AppTests/Snapshot/Snapshots/
<span class="token comment"># Download reference images from S3</span>
aws s3 <span class="token function">cp</span> <span class="token variable">$awspath</span>/AppTests/Snapshot/Snapshots <span class="token parameter variable">--recursive</span> <span class="token parameter variable">--profile</span> user
</code></pre></div><p data-line="419" class="code-line">The second shell uploads modified reference images to the PR review S3 bucket when creating a Pull Request.</p>
<p data-line="421" class="code-line"><img src="/assets/blog/authors/ryomm/2024-04-17-SnapshotTest/07_SnapshotTest_upload.PNG" alt="upload_snapshot.sh" /></p>
<div class="code-block-container"><div class="code-block-filename-container"><span class="code-block-filename">upload_snapshot.sh</span></div><pre class="language-bash"><code class="language-bash code-line" data-line="423"><span class="token comment"># When creating a PR, upload the modified tests as arguments.</span>
<span class="token comment"># Example: Sh upload_snapshot.sh ×××Tests</span>
<span class="token assign-left variable">path</span><span class="token operator">=</span><span class="token string">"./SpotTests/Snapshot/Snapshots"</span>
<span class="token assign-left variable">awspath</span><span class="token operator">=</span><span class="token string">"s3://strl-mrt-web-s3b-mat-001-jjkn32-e/mobile-app-test/ios/feature/Snapshots"</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token variable">$#</span> <span class="token operator">=</span> <span class="token number">0</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
<span class="token builtin class-name">echo</span> <span class="token string">"No arguments provided"</span>
<span class="token keyword">else</span>
<span class="token keyword">for</span> <span class="token for-or-select variable">testName</span> <span class="token keyword">in</span> <span class="token string">"<span class="token variable">${@}</span>"</span><span class="token punctuation">;</span>
<span class="token keyword">do</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable">$testName</span> <span class="token operator">==</span> <span class="token string">"Tests"</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
<span class="token builtin class-name">echo</span> <span class="token string">"<span class="token variable">path</span>/<span class="token variable">testName</span>"</span>
aws s3 <span class="token function">cp</span> <span class="token string">"<span class="token variable">path</span>/<span class="token variable">testName</span>"</span> <span class="token string">"<span class="token variable">awspath</span>/<span class="token variable">testName</span>"</span> <span class="token parameter variable">--exclude</span> <span class="token string">".DS_Store"</span> <span class="token parameter variable">--recursive</span> <span class="token parameter variable">--profile</span> user
<span class="token keyword">else</span>
<span class="token builtin class-name">echo</span> <span class="token string">"(<span class="token variable">$0testName</span>) No tests found"</span>
<span class="token keyword">fi</span>
<span class="token keyword">done</span>
<span class="token keyword">fi</span>
</code></pre></div><p data-line="444" class="code-line">The third shell individually downloads the reference images for the modified screens. It is used when reviewing a Pull Requests that includes screen changes.</p>
<p data-line="446" class="code-line"><img src="/assets/blog/authors/ryomm/2024-04-17-SnapshotTest/08_SnapshotTest_download.PNG" alt="download_snapshot.sh" /></p>
<div class="code-block-container"><div class="code-block-filename-container"><span class="code-block-filename">download_snapshot.sh</span></div><pre class="language-bash"><code class="language-bash code-line" data-line="448"><span class="token comment"># When reviewing tests, download the reference images for the specific tests</span>
<span class="token comment"># Example: Sh download_snapshot.sh ×××Tests</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token variable">$#</span> <span class="token operator">=</span> <span class="token number">0</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
<span class="token builtin class-name">echo</span> <span class="token string">"No arguments provided"</span>
<span class="token keyword">else</span>
<span class="token function">rm</span> <span class="token parameter variable">-r</span> AppTests/Snapshot/Snapshots/
<span class="token keyword">for</span> <span class="token for-or-select variable">testName</span> <span class="token keyword">in</span> <span class="token string">"<span class="token variable">${@}</span>"</span><span class="token punctuation">;</span>
<span class="token keyword">do</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable">$testName</span> <span class="token operator">==</span> <span class="token string">"Tests"</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
<span class="token builtin class-name">echo</span> <span class="token string">"<span class="token variable">localpath</span>/<span class="token variable">testName</span>"</span>
aws s3 <span class="token function">cp</span> <span class="token string">"<span class="token variable">awspath</span>/<span class="token variable">testName</span>"</span> <span class="token string">"<span class="token variable">localpath</span>/<span class="token variable">testName</span>"</span> <span class="token parameter variable">--recursive</span> <span class="token parameter variable">--profile</span> user
<span class="token keyword">else</span>
<span class="token builtin class-name">echo</span> <span class="token string">"(<span class="token variable">$0testName</span>) No tests found"</span>
<span class="token keyword">fi</span>
<span class="token keyword">done</span>
<span class="token keyword">fi</span>
</code></pre></div><p data-line="468" class="code-line">The fourth shell forcibly updates the reference images. Although it is basically unnecessary because the reference images for screens with modified test files are automatically copied, it is useful when changes to reference images occur without modifying the test files, such as when common components are updated.</p>
<p data-line="470" class="code-line"><img src="/assets/blog/authors/ryomm/2024-04-17-SnapshotTest/09_SnapshotTest_force.png" alt="force_upload_snapshot.sh" /></p>
<div class="code-block-container"><div class="code-block-filename-container"><span class="code-block-filename">force_upload_snapshot.sh</span></div><pre class="language-bash"><code class="language-bash code-line" data-line="472"><span class="token comment"># If changes affect reference images other than the modified test files, (for example, when common components are updated),</span>
<span class="token comment"># Please upload manually</span>
<span class="token comment"># Please use it after merging</span>
<span class="token comment"># Example: Sh force_upload_snapshot.sh × × × Tests</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token variable">$#</span> <span class="token operator">=</span> <span class="token number">0</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
<span class="token builtin class-name">echo</span> <span class="token string">"No arguments provided"</span>
<span class="token keyword">else</span>
<span class="token builtin class-name">echo</span> <span class="token string">"Do you want to forcibly upload to the AWS S3 develop folder? 【yes/no】"</span>
<span class="token builtin class-name">read</span> question
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token variable">$question</span> <span class="token operator">=</span> <span class="token string">"yes"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
<span class="token keyword">for</span> <span class="token for-or-select variable">testName</span> <span class="token keyword">in</span> <span class="token string">"<span class="token variable">${@}</span>"</span><span class="token punctuation">;</span>
<span class="token keyword">do</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable">$testName</span> <span class="token operator">==</span> <span class="token string">"Tests"</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
<span class="token builtin class-name">echo</span> <span class="token string">"<span class="token variable">localpath</span>/<span class="token variable">testName</span>"</span>
aws s3 <span class="token function">cp</span> <span class="token string">"<span class="token variable">localpath</span>/<span class="token variable">testName</span>"</span> <span class="token string">"<span class="token variable">awsFeaturePath</span>/<span class="token variable">testName</span>"</span> <span class="token parameter variable">--exclude</span> <span class="token string">".DS_Store"</span> <span class="token parameter variable">--recursive</span> <span class="token parameter variable">--profile</span> user
aws s3 <span class="token function">cp</span> <span class="token string">"<span class="token variable">localpath</span>/<span class="token variable">testName</span>"</span> <span class="token string">"<span class="token variable">awsDevelopPath</span>/<span class="token variable">testName</span>"</span> <span class="token parameter variable">--exclude</span> <span class="token string">".DS_Store"</span> <span class="token parameter variable">--recursive</span> <span class="token parameter variable">--profile</span> user
<span class="token keyword">else</span>
<span class="token builtin class-name">echo</span> <span class="token string">"(<span class="token variable">$testName</span>) No tests found"</span>
<span class="token keyword">fi</span>
<span class="token keyword">done</span>
<span class="token keyword">else</span>
<span class="token builtin class-name">echo</span> <span class="token string">"Termination"</span>
<span class="token keyword">fi</span>
<span class="token keyword">fi</span>
</code></pre></div><p data-line="500" class="code-line">Since having four shells can be confusing regarding when and who should use them, we have defined them in the Taskfile and made the explanations easily accessible. When executing, we have to use <code>--</code> when passing arguments such as specifying file names, making the command bit longer. As a result, we often call the shells directly. However, having this setup is valuable just for the sake of clear explanations.</p>
<div class="code-block-container"><div class="code-block-filename-container"><span class="code-block-filename">Taskfile.yml</span></div><pre class="language-yml"><code class="language-yml code-line" data-line="502"><span class="token directive important">% task</span>
<span class="token key atrule">task</span><span class="token punctuation">:</span> <span class="token punctuation">[</span>default<span class="token punctuation">]</span> task <span class="token punctuation">-</span>l <span class="token punctuation">-</span><span class="token punctuation">-</span>sort none
<span class="token key atrule">task</span><span class="token punctuation">:</span> <span class="token key atrule">Available tasks for this project</span><span class="token punctuation">:</span>

  • default<span class="token punctuation">:</span> show commands
  • setup_snapshot<span class="token punctuation">:</span> <span class="token punctuation">[</span>For Assignee<span class="token punctuation">]</span> <span class="token punctuation">[</span>After branch switch<span class="token punctuation">]</span> Used when making changes to Snapshot Testing after switching from the develop branch.
    (Example) task setup_snapshot or sh setup_snapshot.sh
  • upload_snapshot<span class="token punctuation">:</span> <span class="token punctuation">[</span>For Assignee<span class="token punctuation">]</span> <span class="token punctuation">[</span>During PR creation<span class="token punctuation">]</span> Upload the snapshot images to the S3 bucket for PR review by passing the modified tests as arguments
    (Example) task upload_snapshot <span class="token punctuation">-</span><span class="token punctuation">-</span> ×××Tests or sh upload_snapshot.sh ×××Tests
  • Download_snapshot<span class="token punctuation">:</span> <span class="token punctuation">[</span>For Reviewer<span class="token punctuation">]</span> <span class="token punctuation">[</span>During review<span class="token punctuation">]</span> Download the reference images by passing the relevant tests as arguments
    (Example) task download_snapshot <span class="token punctuation">-</span><span class="token punctuation">-</span> ×××Tests or sh download_snapshot.sh ×××Tests
  • force_upload_snapshot<span class="token punctuation">:</span> <span class="token punctuation">[</span>For Assignee<span class="token punctuation">]</span> <span class="token punctuation">[</span>After merging<span class="token punctuation">]</span> If changes affect reference images other than the modified test files<span class="token punctuation">,</span> (for example<span class="token punctuation">,</span> when common components are updated)<span class="token punctuation">,</span> manually upload the changes by passing the modified tests as arguments.
    (Example) task force_upload_snapshot <span class="token punctuation">-</span><span class="token punctuation">-</span> ×××Tests or sh force_upload_snapshot.sh ×××Tests
    </code></pre></div><p data-line="517" class="code-line">Additionally, this is something I have set up personally, but I find it convenient to have an alias that changes the hardcoded profile name in the shell to the profile configured in your environment. (For those who prefer their own profile names) In this case, the profile hardcoded as <code>user</code> is changed to <code>myroute-user</code>.</p>
    <div class="code-block-container"><pre class="language-bash"><code class="language-bash code-line" data-line="519"><span class="token builtin class-name">alias</span> <span class="token assign-left variable">sett</span><span class="token operator">=</span><span class="token string">"gsed -i 's/user/myroute-user/' setup_snapshot.sh && gsed -i 's/user/myroute-user/' upload_snapshot.sh && gsed -i 's/user/myroute-user/' download_snapshot.sh && gsed -i 's/user/myroute-user/' force_upload_snapshot.sh"</span>
    </code></pre></div><h3 id="bitrise" data-line="523" class="code-line"><a class="header-anchor-link" href="#bitrise" aria-hidden="true"></a> Bitrise</h3>
    <p data-line="524" class="code-line">In <em>my route</em>, we use Bitrise for CI.</p>
    <p data-line="526" class="code-line">When a PR that includes changes to Snapshot Testing is merged, Bitrise automatically detects these changes and copies the reference images from the feature folder to the develop folder.</p>
    <p data-line="528" class="code-line">This ensures that the snapshot tests always run correctly in all situations.</p>
    <h2 id="detecting-subtle-differences-in-reference-images" data-line="530" class="code-line"><a class="header-anchor-link" href="#detecting-subtle-differences-in-reference-images" aria-hidden="true"></a> Detecting subtle differences in reference images</h2>
    <p data-line="531" class="code-line">Sometimes, differences are too subtle to see with the naked eye, but snapshot tests will still detect them and report errors.</p>
    <p data-line="533" class="code-line">Can’t see anything (3_3)?</p>
    <p data-line="535" class="code-line">In such cases, using ImageMagick to overlay the images can help you spot the differences more easily. By running the following command:</p>
    <div class="code-block-container"><pre class="language-bash"><code class="language-bash code-line" data-line="537">convert Snapshot/refarence.png -color-matrix <span class="token string">"6x3: 1 0 0 0 0 0.4 0 1 0 0 0 0 0 0 1 0 0 0"</span> ~/changeColor.png <span class="token punctuation"></span>
    <span class="token operator">&&</span> magick Snapshot/failure.png ~/changeColor.png <span class="token parameter variable">-compose</span> dissolve <span class="token parameter variable">-define</span> compose:args<span class="token operator">=</span><span class="token string">'60,100'</span> <span class="token parameter variable">-composite</span> ~/Desktop/blend.png <span class="token punctuation"></span>
    <span class="token operator">&&</span> <span class="token function">rm</span> ~/changeColor.png
    </code></pre></div><p data-line="543" class="code-line"><img src="/assets/blog/authors/ryomm/2024-04-17-SnapshotTest/10_Compare.png" alt="Blended image" /></p>
    <p data-line="545" class="code-line">You can see the overlaid images. Changing the hue of the reference image to a reddish tint before overlaying can make it easier to spot differences.</p>
    <p data-line="547" class="code-line">For added convenience, I recommend adding this command to your bashrc.</p>
    <div class="code-block-container"><div class="code-block-filename-container"><span class="code-block-filename">~/.bashrc</span></div><pre class="language-bash"><code class="language-bash code-line" data-line="549"><span class="token function-name function">compare</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    convert <span class="token variable">$1</span> -color-matrix <span class="token string">"6x3: 1 0 0 0 0 0.4 0 1 0 0 0 0 0 0 1 0 0 0"</span> ~/Desktop/changeColor.png<span class="token punctuation">;</span>
    magick <span class="token variable">$1</span> ~/Desktop/changeColor.png <span class="token parameter variable">-compose</span> dissolve <span class="token parameter variable">-define</span> compose:args<span class="token operator">=</span><span class="token string">'60,100'</span> <span class="token parameter variable">-composite</span> ~/Desktop/blend.png<span class="token punctuation">;</span>
    <span class="token function">rm</span> ~/Desktop/changeColor.png
    <span class="token punctuation">}</span>
    </code></pre></div><p data-line="557" class="code-line">If the files are generally placed in the same location, you may only need to pass the test name as an argument instead of the entire path. Additionally, since images hosted online can also be processed, this method can be useful during reviews.</p>
    <h1 id="to-wrap-things-up%2C-i-bring-surprise-interviews!" data-line="559" class="code-line"><a class="header-anchor-link" href="#to-wrap-things-up%2C-i-bring-surprise-interviews!" aria-hidden="true"></a> To wrap things up, I bring Surprise Interviews!</h1>
    <p data-line="560" class="code-line">I interviewed my colleagues to get feedback on the implementation of Snapshot Test!</p>
    <p data-line="562" class="code-line">Chang-san said: "Thanks to Hosaka-san’s initial research, we are now able to handle snapshots in a more convenient way. With the help of Ryomm-san, various implementation methods were organized into documents to ensure we didn’t forget anything. It has been really great, and I am very greatful🙇‍♂️. Hosaka-san said: “The biggest bottleneck is the time it takes to run full tests, so I would like to work on reducing that in the future."</p>
    <p data-line="564" class="code-line">As for myself, I’ve noticed the frustration of having to fix Snapshot Tests when the logic changes but the screen remains unaffected. However, it’s been helpful to confirm that there were no differences when transitioning to SwiftUI, which I think was good!</p>
Facebook

関連記事 | Related Posts

We are hiring!

【バックエンドエンジニア】my route開発G/東京

my routeについてmy routeの概要 my routeは、移動需要を創出するために「魅力ある地域情報の発信」、「最適な移動手段の提案」、「交通機関や施設利用のスムーズな予約・決済」をワンストップで提供する、スマートフォン向けマルチモーダルモビリティサービスです。

【オープンポジション】「気になる!」方はまずはこちらからご応募ください。/東京・名古屋・大阪・福岡

業務内容国内外のKINTOサービスや、トヨタグループの金融、モビリティサービスの内製開発組織である同社にて、ご経験・ご志向性に応じて配属を決定し、ご活躍いただきます。