Git Product home page Git Product logo

unittest's Introduction

CRAN version badge CRAN Checks CRAN RStudio mirror total downloads badge CRAN RStudio mirror monthly downloads badge R-CMD-check DOI

unittest: Concise, TAP-compliant, R package for writing unit tests

Given a simple function you'd like to test in the file myfunction.R:

biggest <- function(x,y) { max(c(x,y)) }

A test script for this function test_myfunction.R would be:

library(unittest)

source('myfunction.R')  # Or library(mypackage) if part of a package

ok(biggest(3,4) == 4, "two numbers")
ok(biggest(c(5,3),c(3,4)) == 5, "two vectors")

You can then run this test in several ways:

  • source('test_myfunction.R') from R
  • Rscript --vanilla test_myfunction.R from the command prompt
  • R CMD check, if test_myfunction.R is inside the tests directory of mypackage being tested. unittest doesn't require any further setup in your package.

If writing tests as part of a package, see the "Adding Tests to Packages" vignette for more information.

The workhorse of the unittest package is the ok function which prints "ok" when the expression provided evaluates to TRUE and "not ok" if the expression evaluates to anything else or results in an error. There are several ut_cmp_* helpers designed to work with ok:

  • ok(ut_cmp_equal( biggest(1/3, 2/6), 2/6), "two floating point numbers"): Uses all.equal to compare within a tolerance.
  • ok(ut_cmp_identical( biggest("c", "d") ), "two strings"): Uses identical to make sure outputs are identical.
  • ok(ut_cmp_error(biggest(3), '"y".*missing'), "single argument is an error"): Make sure the code produces an error matching the regular expression.

In all cases you get detailed, colourised output on what the difference is, for example:

Output of ok(ut_cmp_identical(list(1,3,3,4), list(1,2,3,4)))

The package was inspired by Perl's Test::Simple.

If you want more features there are other unit testing packages out there; see testthat, RUnit, svUnit.

Installing from CRAN

In an R session type

install.packages('unittest')

Or add Suggests: unittest to your package's DESCRIPTION file.

Installing the latest development version directly from GitHub

To install the latest development version, use remotes:

# install.packages("remotes")
remotes::install_github("ravingmantis/unittest")

unittest's People

Contributors

lentinj avatar hennesseya avatar

Stargazers

Andrew Allen Bruce avatar  avatar Thomas J. Leeper avatar  avatar

Watchers

 avatar James Cloos avatar  avatar

Forkers

johndelara1

unittest's Issues

Automagic diffs for ``==``, etc.

When the unevaluated call to ok is to == (or another known function), redirect it to a ut_cmp_eq() that can:

  • Implicitly do all(x == y) (possibly this is something ok() should be doing anyway, and accepting all(isTRUE(result)) as success)
  • Generate useful failure output by evaluating each half of the == on it's own, in the appropriate environment

Include line numbers of failing tests

See 8953e16 for an implementation.

However, this doesn't work with Rscript / CMD check. option(keep.source) gives you a number within the first code-block (i.e. ok_group()).

#10 would make this a bit less of a problem.

Remove \dontrun{} uses

\dontrun should only be used as last-resort. I don't think that's the case here, we're just attempting to format examples in a non-standard fashion.

Document better ways to fail

Put the following somewhere in the suggested boilerplate:

if (!interactive()) options(warn=2, error = function() { sink(stderr()) ; traceback(3) ; q(status = 1) })

...we could have a helper to do this, but I'm guessing there's too much that will anger CRAN checks to make it worthwhile.

Test failure on r-devel-windows-x86_64-new-UL

There's currently a test failure: https://cran.r-project.org/web/checks/check_results_unittest.html

 not ok - Environments get converted to lists
 # Test returned non-TRUE value:
 # Length mismatch: comparison on first 2 components
 # Component "a": Mean relative difference: 0.6666667
 # <1b>[1m--- as.environment(list(a = 3, b = 4))<1b>[m
 # <1b>[1m+++ as.environment(list(a = 5, b = 4, c = 9))<1b>[m
 # <1b>[32m{+$c
 +}<1b>[m
 # <1b>[32m{+[1] 9+}<1b>[m
 #
 # $b<1b>[m
 # [1] 4<1b>[m
 #
 # $a<1b>[m
 # [1] <1b>[31m[-3-]<1b>[m<1b>[32m{+5+}<1b>[m

...without escapes (which should be filtered before comparsion) this looks like:

# Test returned non-TRUE value:
# Length mismatch: comparison on first 2 components
# Component "a": Mean relative difference: 0.6666667
# --- as.environment(list(a = 3, b = 4))
# +++ as.environment(list(a = 5, b = 4, c = 9))
# {+$c+}
# {+[1] 9+}
#
# $b
# [1] 4
#
# $a
# [1] [-3-]{+5+}

The current version on CRAN will show the raw output from the test that failed. The only difference to the expected output is the carriage return after $c, but I have no idea where it comes from.

No way of replicating this, other than re-releasing and seeing if it fails again.

Discourage ok_group(), or at least nesting

ok_group("moo", { ok(..) }) will result in all tests clumped together in CRAN output, instead of per-line output.

As a result it might be better to write ok_group("moo") ; ok(...).

Add ut_cmp_warnings

I've just put this together:

ut_cmp_warnings <- function(code, expected_regexes, ignore.case = FALSE, perl = FALSE, fixed = FALSE) {
    stopifnot(is.character(expected_regexes) || length(expected_regexes) == 0)
    all_warnings <- list()
    rv <- withCallingHandlers(code, warning = function (w) {
        all_warnings <<- c(all_warnings, list(w))
        invokeRestart("muffleWarning")
    })

    if (length(expected_regexes) != length(all_warnings)) {
        return(c(
            paste0("Expected ", length(expected_regexes), " warnings, ", "not ", length(all_warnings), ":"),
            vapply(all_warnings, function (w) w$message, character(1)) ))
    }
    out <- c()
    for (i in seq_along(expected_regexes)) {
        if (!grepl(expected_regexes[[i]], all_warnings[[i]]$message,
                ignore.case = ignore.case, perl = perl, fixed = fixed)) {
            out <- c(all_warnings[[i]]$message, "Did not match:-", expected_regexes[[i]])
        }
    }
    return(if (length(out) == 0) TRUE else out)
}

It expects a character vector of regexes, one per warning, and they should be in order.

Insisting that we provide one regex per-warning is probably reasonable, if they all match you ought still care about the number of warnings. You could do something like rep(regex, count) to say you're expecting count warnings matching regex.

What's more awkward is if you want to check for your warnings, but ignore third-party warnings out of your control. We could instead make sure there is at least one warning matching each regex, but I'm more tempted to say "if you really want to ignore extra warnings, then you're on your own, this isn't something we encourage".

EDIT: Insisting they're in order is also debatable, but it's pretty unlikely you're testing multi-threaded code.

More default options for filter

The filter options flip through a string representation of an object until it finds one that doesn't match:

unittest/R/ut_cmp.R

Lines 20 to 31 in cf19036

for (f in list(
# Add any custom filtering function
filter,
# Strings can be compared directly using writeLines
ifelse(is.character(a) && is.character(b), writeLines, 'ignore'),
# Convert environments to list, print that
ifelse(is.environment(a) && is.environment(b), function (x) print(as.list(x)), 'ignore'),
# print will pick up any generics defined for custom types
print,
# Fall back to parsing with str
function (x) utils::str(x, vec.len = 1000, digits.d = 5, nchar.max = 1000),
NULL)) {

The theory was it should end with multiple str calls, increasing length / accuracy until it generates output that is different. Something like:-

    function (x) utils::str(x, vec.len = 1000, digits.d = 5, nchar.max =
1000),
   function (x) utils::str(x, vec.len = 1e10, digits.d = 10, nchar.max =
1e10),
   function (x) utils::str(x, vec.len = 1e20, digits.d =
.Machine$double.digits, nchar.max = 1e20),

Document how to test vignettes

Move package documentation out of faq into it's own vignette, and include stuff on testing vignettes, basically:

{r, message=FALSE, echo=FALSE}
logging::setLevel('WARN')
library(unittest)
# Redirect ok() output to stderr
ok <- function(...) capture.output(unittest::ok(...), file = stderr())

(parallel) test runner

Currently we tell people to run tests with CMD check or Rscript & a for loop, either is a bit ropey.

Include a test runner, borrowing ideas from tinytest that:

  • Allows for parallel tests without Makefile magic
  • Is compatible with the wip-line-numbers branch

Better comparison helper support

I can make a helper to compare environments, e.g.:

cmp_environment <- function (a, b) {
    a <- as.list(a)
    b <- as.list(b)
    if (length(a) == 0) {
        # Can't order an empty list
        ut_cmp_identical(a, b)
    } else {
        ut_cmp_identical(a[order(names(a))], b[order(names(b))])
    }
}
ok(cmp_environment( as.environment(list(a=1, b=2)), as.environment(list(a=1, b=2))   )  )

...but then the diff output doesn't include the previous expressions as.environment(list(a=1, b=2)), just +++ a[order(names(a))], which doesn't help. Need to either:-

  • Tell cmp_inner that we're a helper function, and the -1 should be upped to -2
  • Supply a "comparison filter" to convert to (ordered) list (which makes the helper shorter, but doesn't really solve the problem)
  • Expose cmp_inner, allow us to write our own comparison function.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.