<p><div><div><div><div data-show-faces="false" data-height="21" data-width="100" data-layout="button_count" data-send="true" data-href="https://www.r-bloggers.com/rstudioaddins-part-4-unit-testing-coverage-investigation-and-improvement-made-easy/"></div></div><div></div></div><div></div></div>
<div>
(This article was first published on <strong><a href="https://jozefhajnala.gitlab.io/r/r104-unit-testing-coverage/"> Jozef's Rblog</a></strong>, and kindly contributed to <a rel="nofollow" href="https://www.r-bloggers.com/">R-bloggers)</a> </div>
<div>
<h1>Introduction</h1>
<p>A developer always pays his technical debts! And we have a debt to pay to the gods of coding best practices, as we did not present many unit tests for our functions yet. Today we will show how to efficiently investigate and improve unit test coverage for our R code, with focus on functions governing our RStudio addins, which have their own specifics.</p>
<blockquote>
<p>As a practical example, we will do a simple resctructuring of one of our functions to increase its test coverage from a mere 34% to over 90%.</p>
</blockquote>
<div>
<img alt="The pretty rewards for your tests" src="https://i2.wp.com/jozefhajnala.gitlab.io/r/img/r104-01-testthat-so-pretty.gif?w=456&ssl=1" data-recalc-dims="1">
<p>The pretty rewards for your tests</p>
</div>
</div>
<div>
<h1>Contents</h1>
<ol>
<li><a rel="nofollow" href="https://jozefhajnala.gitlab.io/r/r104-unit-testing-coverage/#fly-through-of-unit-testing-in-r" target="_blank">Fly-through of unit testing in R</a></li>
<li><a rel="nofollow" href="https://jozefhajnala.gitlab.io/r/r104-unit-testing-coverage/#investigating-test-coverage-within-a-package" target="_blank">Investigating test coverage within a package</a></li>
<li><a rel="nofollow" href="https://jozefhajnala.gitlab.io/r/r104-unit-testing-coverage/#test-coverage-for-rstudio-addin-functions" target="_blank">Test coverage for RStudio addin functions</a></li>
<li><a rel="nofollow" href="https://jozefhajnala.gitlab.io/r/r104-unit-testing-coverage/#rewriting-an-addin-function-for-better-coverage" target="_blank">Rewriting an addin function for better coverage</a></li>
<li><a rel="nofollow" href="https://jozefhajnala.gitlab.io/r/r104-unit-testing-coverage/#testing-the-rewritten-function-and-gained-coverage" target="_blank">Testing the rewritten function and gained coverage</a></li>
<li><a rel="nofollow" href="https://jozefhajnala.gitlab.io/r/r104-unit-testing-coverage/#tldr---just-give-me-the-package" target="_blank">TL;DR – Just give me the package</a></li>
<li><a rel="nofollow" href="https://jozefhajnala.gitlab.io/r/r104-unit-testing-coverage/#references" target="_blank">References</a></li>
</ol>
</div>
<div>
<h1>Fly-through of unit testing in R</h1>
<p>Much has been written on the importance of unit testing, so we will not spend more time on convincing the readers, but rather very quickly provide a few references in case the reader is new to unit testing with R. In the later parts of the article we assume that these basics are known.</p>
<p>In a few words</p>
<ul>
<li><a rel="nofollow" href="https://github.com/r-lib/devtools" target="_blank"><code>devtools</code></a> – Makes package development easier by providing R functions that simplify common tasks</li>
<li><a rel="nofollow" href="https://github.com/r-lib/testthat" target="_blank"><code>testthat</code></a>– Is the most popular unit testing package for R</li>
<li><a rel="nofollow" href="https://github.com/r-lib/covr" target="_blank"><code>covr</code></a>– Helps track test coverage for R packages and view reports locally or (optionally) upload the results</li>
</ul>
<p>For a start guide to use <code>testthat</code> within a package, visit the <a rel="nofollow" href="http://r-pkgs.had.co.nz/tests.html" target="_blank">Testing section</a> of R packages by Hadley Wickham. I would also recommend checking out the <a rel="nofollow" href="https://www.tidyverse.org/articles/2017/12/testthat-2-0-0/" target="_blank">showcase on the 2.0.0</a> release of the testthat itself.</p>
</div>
<div>
<h1>Investigating test coverage within a package</h1>
<p>For the purpose of investigating the test coverage of a package we can use the <code>covr</code> package. Within an R project, we can call the <code>package_coverage()</code> function to get a nicely printed high-level overview, or we can provide a specific path to a package root directory and call it as follows:</p>
<pre><code># This looks much prettier in the R console ;)
covr::package_coverage(pkgPath)</code></pre>
<pre><code>## jhaddins Coverage: 59.05%</code></pre>
<pre><code>## R/viewSelection.R: 34.15%</code></pre>
<pre><code>## R/addRoxytag.R: 40.91%</code></pre>
<pre><code>## R/makeCmd.R: 92.86%</code></pre>
<p>For a deeper investigation, converting the results to a <code>data.frame</code> might be very useful. The below shows the count of number of times that given expression was called during the running of our tests for each group of code lines:</p>
<pre><code>covResults &lt;- covr::package_coverage(pkgPath)
as.data.frame(covResults)[, c(1:3, 5, 11)]</code></pre>
<pre><code>##             filename         functions first_line last_line value
## 1     R/addRoxytag.R            roxyfy         10        12     6
## 2     R/addRoxytag.R            roxyfy         11        11     2
## 3     R/addRoxytag.R            roxyfy         13        15     4
## 4     R/addRoxytag.R            roxyfy         14        14     2
## 5     R/addRoxytag.R            roxyfy         16        16     2
## 6     R/addRoxytag.R            roxyfy         17        17     2
## 7     R/addRoxytag.R            roxyfy         18        18     2
## 8     R/addRoxytag.R        addRoxytag         29        29     0
## 9     R/addRoxytag.R        addRoxytag         30        37     0
## 10    R/addRoxytag.R        addRoxytag         32        34     0
## 11    R/addRoxytag.R        addRoxytag         38        38     0
## 12    R/addRoxytag.R    addRoxytagCode         44        44     0
## 13    R/addRoxytag.R    addRoxytagLink         50        50     0
## 14    R/addRoxytag.R     addRoxytagEqn         56        56     0
## 15       R/makeCmd.R           makeCmd         20        24     5
## 16       R/makeCmd.R           makeCmd         21        21     0
## 17       R/makeCmd.R           makeCmd         23        23     5
## 18       R/makeCmd.R           makeCmd         25        27     5
## 19       R/makeCmd.R           makeCmd         26        26     4
## 20       R/makeCmd.R           makeCmd         28        32     5
## 21       R/makeCmd.R           makeCmd         33        35     5
## 22       R/makeCmd.R           makeCmd         34        34     2
## 23       R/makeCmd.R           makeCmd         36        38     5
## 24       R/makeCmd.R           makeCmd         37        37     1
## 25       R/makeCmd.R           makeCmd         39        39     5
## 26       R/makeCmd.R      replaceTilde         48        50     1
## 27       R/makeCmd.R      replaceTilde         49        49     1
## 28       R/makeCmd.R      replaceTilde         51        51     1
## 29       R/makeCmd.R        executeCmd         61        61     5
## 30       R/makeCmd.R        executeCmd         62        66     5
## 31       R/makeCmd.R        executeCmd         68        72     3
## 32       R/makeCmd.R        executeCmd         69        69     0
## 33       R/makeCmd.R        executeCmd         71        71     3
## 34       R/makeCmd.R runCurrentRscript         90        90     1
## 35       R/makeCmd.R runCurrentRscript         91        91     1
## 36       R/makeCmd.R runCurrentRscript         92        96     1
## 37       R/makeCmd.R runCurrentRscript         93        95     1
## 38       R/makeCmd.R runCurrentRscript         94        94     0
## 39 R/viewSelection.R     viewSelection          7         7     0
## 40 R/viewSelection.R     viewSelection          8        12     0
## 41 R/viewSelection.R     viewSelection         10        10     0
## 42 R/viewSelection.R     viewSelection         13        13     0
## 43 R/viewSelection.R  getFromSysframes         24        24     6
## 44 R/viewSelection.R  getFromSysframes         25        25     3
## 45 R/viewSelection.R  getFromSysframes         26        26     3
## 46 R/viewSelection.R  getFromSysframes         28        28     3
## 47 R/viewSelection.R  getFromSysframes         29        29     3
## 48 R/viewSelection.R  getFromSysframes         30        30     3
## 49 R/viewSelection.R  getFromSysframes         31        31    92
## 50 R/viewSelection.R  getFromSysframes         32        32    92
## 51 R/viewSelection.R  getFromSysframes         33        33    92
## 52 R/viewSelection.R  getFromSysframes         34        34     2
## 53 R/viewSelection.R  getFromSysframes         37        37     1
## 54 R/viewSelection.R        viewObject         56        56     3
## 55 R/viewSelection.R        viewObject         57        57     3
## 56 R/viewSelection.R        viewObject         58        58     3
## 57 R/viewSelection.R        viewObject         61        61     0
## 58 R/viewSelection.R        viewObject         64        64     0
## 59 R/viewSelection.R        viewObject         65        65     0
## 60 R/viewSelection.R        viewObject         66        66     0
## 61 R/viewSelection.R        viewObject         69        69     0
## 62 R/viewSelection.R        viewObject         70        70     0
## 63 R/viewSelection.R        viewObject         71        71     0
## 64 R/viewSelection.R        viewObject         74        74     0
## 65 R/viewSelection.R        viewObject         76        76     0
## 66 R/viewSelection.R        viewObject         77        77     0
## 67 R/viewSelection.R        viewObject         79        79     0
## 68 R/viewSelection.R        viewObject         81        81     0
## 69 R/viewSelection.R        viewObject         82        82     0
## 70 R/viewSelection.R        viewObject         83        83     0
## 71 R/viewSelection.R        viewObject         88        88     0
## 72 R/viewSelection.R        viewObject         89        89     0
## 73 R/viewSelection.R        viewObject         91        91     0
## 74 R/viewSelection.R        viewObject         92        92     0
## 75 R/viewSelection.R        viewObject         93        93     0
## 76 R/viewSelection.R        viewObject         96        96     0</code></pre>
<p>Calling <code>covr::zero_coverage</code> with a overage object returned by <code>package_coverage</code> will provide a data.frame with locations that have 0 test coverage. The nice thing about running it within RStudio is that it outputs the results on the Markers tab in RStudio, where we can easily investigate:</p>
<pre><code>zeroCov &lt;- covr::zero_coverage(covResults)</code></pre>
<div>
<img alt="zero_coverage markers" src="https://i2.wp.com/jozefhajnala.gitlab.io/r/img/r104-02-zero-coverage.gif?w=456&ssl=1" data-recalc-dims="1">
<p>zero_coverage markers</p>
</div>
</div>
<div>
<h1>Test coverage for RStudio addin functions</h1>
<p>Investigating our code, let us focus on the results for the <code>viewSelection.R</code>, which has a very weak 34% test coverage. We can analyze exactly which lines have no test coverage in a specific file:</p>
<pre><code>zeroCov[zeroCov$filename == &quot;R/viewSelection.R&quot;, &quot;line&quot;]</code></pre>
<pre><code>##  [1]  7  8  9 10 11 12 13 61 64 65 66 69 70 71 74 76 77 79 81 82 83 88 89
## [24] 91 92 93 96</code></pre>
<p><a rel="nofollow" href="https://gitlab.com/jozefhajnala/jhaddins/blob/bd5979cecfbf4b81a6f8c02e5593fe109baed172/R/viewSelection.R" target="_blank">Looking at the code</a>, we can see that the first chuck of lines – 7:13 represent the <code>viewSelection</code> function, which just calls <code>lapply</code> and invisibly returns <code>NULL</code>.<br>
The main weak spot however is the function <code>viewObject</code>, out of which we only test the early return in case of invalid <code>chr</code> argument provided. None of the other functionality is tested.</p>
<p>The reason behind this is that when running the tests, RStudio functionality is not available and therefore we would not be able to test even the not-so-well designed return values, as they are almost always preceded by a call to <code>rstudioapi</code> or other RStudio-related functionality such as the object viewer, because that is what they are designed to do. This means we must restructure the code in such a way that we contain the RStudio-dependent functionality to a necessary minimum, keeping a big majority of the code testable – only calling the side-effecting <code>rstudioapi</code> when actually executing the addin functionality itself.</p>
</div>
<div>
<h1>Rewriting an addin function for better coverage</h1>
<p>We will now show one potential way to solve this issue for the particular case of our <code>viewObject</code> function.</p>
<blockquote>
<p>The idea behind the solution is to only return the arguments for the call to the RStudio API related functionality, instead of executing them in the function itself – hence the rename to <code>getViewArgs</code>.</p>
</blockquote>
<p>This way we can test the function’s return value against the expected arguments and only execute them with <code>do.call</code> in the addin execution wrapper itself. A picture may be worth a thousand words, so here is the diff with relevant changes:</p>
<div>
<img alt="Refactoring for testability" src="https://i2.wp.com/jozefhajnala.gitlab.io/r/img/r104-03-testability-refactor.png?w=456&ssl=1" data-recalc-dims="1">
<p>Refactoring for testability</p>
</div>
</div>
<div>
<h1>Testing the rewritten function and gained coverage</h1>
<p>Now that our return values are testable across the entire <code>getViewArgs</code> function, we can easily write tests to cover the entire function, a couple examples:</p>
<pre><code>test_that(&quot;getViewArgs for function&quot;
        , expect_equal(
            getViewArgs(&quot;reshape&quot;)
          , list(what = &quot;View&quot;, args = list(x = reshape, title = &quot;reshape&quot;))
          )
        )</code></pre>
<pre><code>test_that(&quot;getViewArgs for data.frame&quot;
        , expect_equal(
            getViewArgs(&quot;datasets::women&quot;)
          , list(what = &quot;View&quot;,
                 args = list(x = data.frame(
                     height = c(58, 59, 60, 61, 62, 63, 64, 65,
                                66, 67, 68, 69, 70, 71, 72),
                     weight = c(115, 117, 120, 123, 126, 129, 132, 135,
                                139, 142, 146, 150, 154, 159, 164)
                     ),
                   title = &quot;datasets::women&quot;
                   )
            )
          )
        )</code></pre>
<p>Looking at the test coverage provided after our changes, we can see that we are at more than 90% percent coverage for <code>viewSelection.R</code>:</p>
<pre><code># This looks much prettier in the R console ;)
covResults &lt;- covr::package_coverage(pkgPath)
covResults</code></pre>
<pre><code>## jhaddins Coverage: 82.05%</code></pre>
<pre><code>## R/addRoxytag.R: 40.91%</code></pre>
<pre><code>## R/viewSelection.R: 90.57%</code></pre>
<pre><code>## R/makeCmd.R: 92.86%</code></pre>
<p>And looking at the lines that not covered for <code>viewSelection.R</code>, we can indeed see that the only uncovered lines left are in fact those with the <code>viewSelection</code> function, which is responsible only for executing the addin itself:</p>
<pre><code>covResults &lt;- as.data.frame(covResults)
covResults[covResults$filename == &quot;R/viewSelection.R&quot; &amp;
             covResults$value == 0, c(1:3, 5, 11)]</code></pre>
<pre><code>##             filename     functions first_line last_line value
## 39 R/viewSelection.R viewSelection         13        17     0
## 40 R/viewSelection.R viewSelection         15        15     0
## 41 R/viewSelection.R viewSelection         16        16     0</code></pre>
<p>In the ideal world we would of course want to also automate the testing of our addin execution itself by examining if their effects in the RStudio IDE are as expected, however this is far beyond the scope of this post. For some of our addin functionality we can however even directly test the side-effects, such as when the addin should produce a file with certain content.</p>
</div>
<div>
<h1>TL;DR – Just give me the package</h1>
<ul>
<li>get the <a rel="nofollow" href="https://gitlab.com/jozefhajnala/jhaddins/tags/addins3-viewer" target="_blank">status of the package after this article</a></li>
<li>or use <code>git clone</code> from <a rel="nofollow" href="https://gitlab.com/jozefhajnala/jhaddins.git" target="_blank"><code>https://gitlab.com/jozefhajnala/jhaddins.git</code></a></li>
</ul>
</div>
<div>
<h1>References</h1>
<ul>
<li><a rel="nofollow" href="http://testthat.r-lib.org/index.html" target="_blank">Testthat – unit testing for R</a></li>
<li><a rel="nofollow" href="http://r-pkgs.had.co.nz/tests.html" target="_blank">Testing chapter</a> of R packages by Hadley Wickham</li>
<li><a rel="nofollow" href="https://covr.r-lib.org/" target="_blank">covr – Track test coverage for your R package</a></li>
</ul>
<h2>Did you find the article helpful or interesting? Help others find it by sharing</h2>
<div></div>
</div>
	<p>
<div>
<div>To <strong>leave a comment</strong> for the author, please follow the link and comment on their blog: <strong><a href="https://jozefhajnala.gitlab.io/r/r104-unit-testing-coverage/"> Jozef's Rblog</a></strong>.</div>
<hr>
<a rel="nofollow" href="https://www.r-bloggers.com/">R-bloggers.com</a> offers <strong><a rel="nofollow" href="https://feedburner.google.com/fb/a/mailverify?uri=RBloggers">daily e-mail updates</a></strong> about <a rel="nofollow" href="https://www.r-project.org/" title="The R Project for Statistical Computing">R</a> news and <a rel="nofollow" href="https://www.r-bloggers.com/search/tutorial" title="R tutorials">tutorials</a> on topics such as: <a rel="nofollow" href="https://www.r-bloggers.com/search/data%20science" title="Data science">Data science</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/Big%20Data" title="Big Data">Big Data, <a rel="nofollow" href="https://www.r-users.com/" title="R jobs">R jobs</a>, visualization (<a rel="nofollow" href="https://www.r-bloggers.com/search/ggplot2" title="ggplot and ggplot2 tutorials">ggplot2</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/boxplot" title="Boxplots using lattice and ggplot2 tutorials">Boxplots</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/map" title="Maps and gis">maps</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/animation" title="Animation in R">animation</a>), programming (<a rel="nofollow" href="https://www.r-bloggers.com/search/RStudio" title="RStudio IDE for R">RStudio</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/sweave" title="Sweave and literate programming">Sweave</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/LaTeX" title="LaTeX in R">LaTeX</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/SQL" title="SQL and databases">SQL</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/eclipse" title="Eclipse IDE for R">Eclipse</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/git" title="git and github, Version Control System">git</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/hadoop" title="Large data in R using Hadoop">hadoop</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/Web+Scraping" title="Web Scraping of google, facebook, yahoo, twitter and more using R">Web Scraping</a>) statistics (<a rel="nofollow" href="https://www.r-bloggers.com/search/regression" title="Regressions and ANOVA analysis tutorials">regression</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/PCA" title="principal component analysis tutorial">PCA</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/time+series" title="Time series">time series</a>, <a rel="nofollow" href="https://www.r-bloggers.com/search/trading" title="finance trading">trading</a>) and more...
<img width="1" alt="" src="http://feeds.feedburner.com/~r/RBloggers/~4/rGlm5Z3exmQ" height="1"></a></div>