45

I have a lot of functions that generate plots, typically with ggplot2. Right now, I'm generating the plot and testing the underlying data. But I'd like to know if there's a reasonable way to test that the plot contains the layers/options I expect it to or that graphical elements match expectations.

For example:

library(ggplot2)
library(scales) # for percent()
library(testthat)

df <- data.frame(
  Response = LETTERS[1:5],
  Proportion = c(0.1,0.2,0.1,0.2,0.4)
)

#' @export plot_fun
plot_fun <- function(df) {
  p1 <- ggplot(df, aes(Response, Proportion)) +
    geom_bar(stat='identity') + 
    scale_y_continuous(labels = percent)
return(p1)
}

test_that("Plot returns ggplot object",{
  p <- plot_fun(df)
  expect_is(p,"ggplot")
})

test_that("Plot uses correct data", {
  p <- plot_fun(df)
  expect_that(df, equals(p$data))

})

This is where I'm stuck

test_that("Plot layers match expectations",{
  p <- plot_fun(df)
  expect_that(...,...)
})

test_that("Scale is labelled percent",{
  p <- plot_fun(df)
  expect_that(...,...)
})

Perhaps there's a more direct approach?

Viliam Simko
  • 1,711
  • 17
  • 31
Brandon Bertelsen
  • 43,807
  • 34
  • 160
  • 255
  • The ggplot2 [repo](https://github.com/hadley/ggplot2/blob/master/tests/test-all.R) has no tests, so maybe it's not implemented? Would be a nice addition. – jeremycg Jun 24 '15 at 23:47
  • 1
    I'm aware, hence the question - and incoming bounty. – Brandon Bertelsen Jun 25 '15 at 00:10
  • 3
    [This](https://github.com/wch/ggplot2/wiki) might be of use, though I'm not sure how much development the visual test suite has gotten since it was implemented. – joran Jun 25 '15 at 01:24
  • Why couldn't you look at the structure of the plot object itself? It's extremely straightforward to look at the list elements representing the layers (`p$layers`) as are the scales and axis labels (`p$labels`). It seems to me you could generate a test function operating on the object. – Forrest R. Stevens Jun 25 '15 at 01:44
  • @ForrestR.Stevens well formed examples (an answer) formed on your comment would be sure to garner upvotes. – Brandon Bertelsen Jun 25 '15 at 03:24
  • @BrandonBertelsen I hope my answer might be of use to you or someone else. Just to note, you are missing a parentheses after the `aes()` call in the `plot_fun()` declaration. – Forrest R. Stevens Jun 25 '15 at 04:56
  • Thanks Forrest, updated the question accordingly. – Brandon Bertelsen Jun 26 '15 at 06:01
  • @jeremycg Actually, there are a lot of tests [here](https://github.com/hadley/ggplot2/tree/master/inst/tests), and there are some of them that may be helpful. Though most of them are quite trivial, frankly. – tonytonov Jun 26 '15 at 12:52
  • Actually, I've got a weird idea: you can write a test that would `ggsave` the plot and compare it to the "benchmark" plot (by size, by some hash or pixel by pixel maybe?). You'll have to prepare a full suite of benchmarks by hand, obviously, but that shouldn't be that bad. – tonytonov Jun 26 '15 at 12:59
  • I was thinking of something like that tonytonov... using a hash, or perhaps a datauri. But I have no experience nor knowhow on that front. I don't if a hash would work, but perhaps a datauri might. – Brandon Bertelsen Jun 26 '15 at 21:49

3 Answers3

28

This seems to be what you're aiming at, though specific requirements for plotting parameters and contents will vary of course. But for the example you nicely crafted above these tests should all pass:

##  Load the proto library for accessing sub-components of the ggplot2
##    plot objects:
library(proto)

test_that("Plot layers match expectations",{
  p <- plot_fun(df)
  expect_is(p$layers[[1]], "proto")
  expect_identical(p$layers[[1]]$geom$objname, "bar")
  expect_identical(p$layers[[1]]$stat$objname, "identity")
})

test_that("Scale is labelled 'Proportion'",{
  p <- plot_fun(df)
  expect_identical(p$labels$y, "Proportion")
})

test_that("Scale range is NULL",{
  p <- plot_fun(df)
  expect_null(p$scales$scales[[1]]$range$range)
})

This question and its answers offer a good starting point on other ways to characterize ggplot objects in case you have other things you'd like to test.

Community
  • 1
  • 1
Forrest R. Stevens
  • 3,435
  • 13
  • 21
  • 2
    Just an update to this great answer: in the current ggplot2 version, `$objname` no longer exists. Instead, use `class(p$layers[[1]]$stat)` etc. – Lukas Wallrich Aug 24 '21 at 11:29
11

It's worth noting that the vdiffr package is designed for comparing plots. A nice feature is that it integrates with the testthat package -- it's actually used for testing in ggplot2 -- and it has an add-in for RStudio to help manage your testsuite.

Dylan
  • 745
  • 8
  • 10
8

What I also find useful in addition to the existing answers, is to test if a plot can actually be printed.

library(ggplot2)
library(scales) # for percent()
library(testthat)

# First, 'correct' data frame
df <- data.frame(
    Response   = LETTERS[1:5],
    Proportion = c(0.1,0.2,0.1,0.2,0.4)
)

# Second data frame where column has 'wrong' name that does not match aes()
df2 <- data.frame(
    x          = LETTERS[1:5],
    Proportion = c(0.1,0.2,0.1,0.2,0.4)
)

plot_fun <- function(df) {
    p1 <- ggplot(df, aes(Response, Proportion)) +
        geom_bar(stat='identity') + 
        scale_y_continuous(labels = percent)
    return(p1)
}

# All tests succeed
test_that("Scale is labelled 'Proportion'",{
    p <- plot_fun(df)
    expect_true(is.ggplot(p))
    expect_identical(p$labels$y, "Proportion")

    p <- plot_fun(df2)
    expect_true(is.ggplot(p))
    expect_identical(p$labels$y, "Proportion")
})

# Second test with data frame df2 fails
test_that("Printing ggplot object actually works",{
    p <- plot_fun(df)
    expect_error(print(p), NA)

    p <- plot_fun(df2)
    expect_error(print(p), NA)
})
#> Error: Test failed: 'Printing ggplot object actually works'
#> * `print(p)` threw an error.
#> Message: object 'Response' not found
#> Class:   simpleError/error/condition
hplieninger
  • 3,214
  • 27
  • 32