How can we test functions that aren't exposed when building R packages?
I'm currently developing a graphical analysis package for R. We're trying to use principles from both Clean Code and Test-Driven Development (TDD). But, we've run into a conceptual problem.
How can you test unexposed functions?
Consider the following simplified example. Outer()
is a function we're building in our package, and we expose it to users by listing it in the NAMESPACE
file. Inner()
is a short (~5 line) utility function:
Outer <- function(...) {
Inner <- function(...) {
return(x)
}
return( Inner() )
}
Most of the user-exposed functions in our package are collections of short, single-behavior Inner()
style functions that lie under the umbrella of a single Outer()
function the user can call. Granova.ds.ggplot()
is one example. Such a modular design is strongly advocated by Clean Code, and we're quite happy with the results. But we don't know how to build unit tests for it, since the functions we want to test aren't accessible outside the scope of the Granova.ds.ggplot()
function given how we designed it.
Several ideas/solutions oc开发者_JAVA技巧curred to us:
- Tests should only have access to public APIs. Since functions like
Inner()
are by design not public (they're not exported inNAMESPACE
) we shouldn't even be trying to test them. - Hadley Wickham's
testthat
package wiki says it supports testing of "non-exported functions" using anR CMD check
workflow. - If all else fails, we could somehow manually break our
Outer()
functions for testing purposes
None of these solutions has worked so far
Solution 1 seems like a cop-out. We believe that especially in R, it's sensible to have a top-level function that calls various short, single-responsibility utility methods. Not being able to test such methods seems silly, and like the type of thing there's a solution for but we just haven't found yet.
Solution 2 might work, but we haven't gotten it to work so far. If you try cloning our repository, then sourcing inst/dev.R
I believe you'll find in the test output that it cannot find the function InitializeGgplot()
, even though said function is defined in lines 613-615 of granova.1w.ggplot()
. Similarly, running R CMD check
at the terminal doesn't seem to execute any of our tests at all. It does take a great deal of time and throw insulting errors at us, though :-(
Solution 3 is in a sense pragmatic, but counter to the aim of always moving toward a goal state of the project. It doesn't make sense to us.
What would be the ideal solution
Ideally, we're looking for a way to leverage a package like testthat
to rapidly provide feedback as we code, and to allow us to test functions like Inner()
that exist within functions like Outer()
. Even better would be a way to do it without resorting to R CMD check
, which due to the 3-d complexity of some of our functions takes almost a full minute to run each time.
So, what are we missing? Should TDD practices allow for testing Outer/Inner style setups in R? If they do, how can we do so in developing our package? Any feedback is welcome, and I'll try to respond to anything that's unclear.
Thanks!
If Inner
implements non-trivial functionality that you want to test, I'd suggest moving Inner
to the top-level, but not exporting it. Generally, I avoid nesting functions within other functions for exactly this reason - they're hard to test.
You can test during development with the usual testthat functions because you're probably just sourcing in all your R code and not worrying about namespaces (at least that's how I develop). You then use R CMD check
in conjunction with test_package
to ensure the tests still work at build time - test_packages
runs the tests in the package namespace so they can test non-exported functions.
IMO there is no problem here -- Inner is just a nondetachable part of Outer, so testing Outer tests Inner. Would you be willing to test anonymous functions? Same here.
精彩评论