如何测试函数的图形输出?

How to test graphical output of functions?

我想知道如何测试生成图形的函数。我有一个简单的 绘图函数 img:

img <- function() {
  plot(1:10)
}

在我的包中,我喜欢使用 testthat 为此功能创建一个单元测试。 因为 plot 和它在 base graphics 中的朋友只是 return NULL 一个简单的 expect_identical 不工作:

library("testthat")

## example for a successful test
expect_identical(plot(1:10), img()) ## equal (as expected)

## example for a test failure
expect_identical(plot(1:10, col="red"), img()) ## DOES NOT FAIL!
# (because both return NULL)

首先我考虑绘制到一个文件中并将 md5 校验和与 确保函数的输出相等:

md5plot <- function(expr) {
  file <- tempfile(fileext=".pdf")
  on.exit(unlink(file))
  pdf(file)
  expr
  dev.off()
  unname(tools::md5sum(file))
}

## example for a successful test
expect_identical(md5plot(img()),
                 md5plot(plot(1:10))) ## equal (as expected)

## example for a test failure
expect_identical(md5plot(img()),
                 md5plot(plot(1:10, col="red"))) ## not equal (as expected)

这在 Linux 上效果很好,但在 Windows 上效果不佳。出奇 md5plot(plot(1:10)) 在每次调用时产生一个新的 md5sum。 除了这个问题,我还需要创建很多临时文件。

接下来我使用了recordPlot(首先创建一个空设备,调用绘图 函数并记录其输出)。这按预期工作:

recPlot <- function(expr) {
  pdf(NULL)
  on.exit(dev.off())
  dev.control(displaylist="enable")
  expr
  recordPlot()
}

## example for a successful test
expect_identical(recPlot(plot(1:10)),
                 recPlot(img())) ## equal (as expected)

## example for a test failure
expect_identical(recPlot(plot(1:10, col="red")),
                 recPlot(img())) ## not equal (as expected)

有人知道测试函数图形输出的更好方法吗?

编辑:关于@josilber 在他的评论中提出的要点。

虽然 recordPlot 方法效果很好,但您必须在单元测试中重写整个绘图函数。对于复杂的绘图函数,这变得很复杂。最好有一种方法可以存储一个文件(*.RData*.pdf,...),其中包含您可以在未来测试中比较的图像。 md5sum 方法不起作用,因为 md5sums 在不同平台上不同。通过 recordPlot 你可以创建一个 *.RData 文件但是你不能依赖它的格式(来自 recordPlot 手册页):

The format of recorded plots may change between R versions. Recorded plots can not be used as a permanent storage format for R plots.

也许可以存储图像文件(*.png*.bmp 等),导入它并逐像素比较...

EDIT2:以下代码说明了使用 svg 作为输出的所需参考文件方法。首先需要的辅助函数:

## plot to svg and return file contant as character
plot_image <- function(expr) {
  file <- tempfile(fileext=".svg")
  on.exit(unlink(file))
  svg(file)
  expr
  dev.off()
  readLines(file)
}

## the IDs differ at each `svg` call, that's why we simple remove them
ignore_svg_id <- function(lines) {
  gsub(pattern = "(xlink:href|id)=\"#?([a-z0-9]+)-?(?<![0-9])[0-9]+\"",
       replacement = "\1=\"\2\"", x = lines, perl = TRUE)
}

## compare svg character vs reference
expect_image_equal <- function(object, expected, ...) {
  stopifnot(is.character(expected) && file.exists(expected))
  expect_equal(ignore_svg_id(plot_image(object)),
               ignore_svg_id(readLines(expected)), ...)
}

## create reference image
create_reference_image <- function(expr, file) {
  svg(file)
  expr
  dev.off()
}

测试将是:

create_reference_image(img(), "reference.svg")

## create tests
library("testthat")

expect_image_equal(img(), "reference.svg") ## equal (as expected)
expect_image_equal(plot(1:10, col="red"), "reference.svg") ## not equal (as expected)

遗憾的是,这不适用于不同的平台。顺序(和名称) Linux 和 Windows.

上的 svg 元素完全不同

pngjpegrecordPlot 也存在类似问题。结果文件 在所有平台上都不同。

目前唯一可行的解​​决方案是上面的 recPlot 方法。但因此 我需要在我的单元测试中重写整个绘图函数。


P.S.: 我对 Windows 上的不同 md5sums 感到很困惑。似乎它们取决于临时文件的创建时间:

# on Windows
table(sapply(1:100, function(x)md5plot(plot(1:10))))
#4693c8bcf6b6cb78ce1fc7ca41831353 51e8845fead596c86a3f0ca36495eacb
#                              40                               60

Mango Solutions 发布了一个开源包 visualTest,它可以对图进行模糊匹配,以解决此用例。

包在 github,所以安装使用:

devtools::install_github("MangoTheCat/visualTest")
library(visualTest)

然后使用函数getFingerprint()为每个图提取指纹,并使用函数isSimilar()进行比较,指定一个合适的阈值。

首先,在文件上创建一些图:

png(filename = "test1.png")
img()
dev.off()

png(filename = "test2.png")
plot(1:11, col="red")
dev.off()

指纹是一个数值向量:

> getFingerprint(file = "test1.png")
 [1]  4  7  4  4 10  4  7  7  4  7  7  4  7  4  5  9  4  7  7  5  6  7  4  7  4  4 10
[28]  4  7  7  4  7  7  4  7  4  3  7  4  4  3  4  4  5  5  4  7  4  7  4  7  7  7  4
[55]  7  7  4  7  4  7  5  6  7  7  4  8  6  4  7  4  7  4  7  7  7  4  4 10  4  7  4

> getFingerprint(file = "test2.png")
 [1]  7  7  4  4 17  4  7  4  7  4  7  7  4  5  9  4  7  7  5  6  7  4  7  7 11  4  7
[28]  7  5  6  7  4  7  4 14  4  3  4  7 11  7  4  7  5  6  7  7  4  7 11  7  4  7  5
[55]  6  7  7  4  8  6  4  7  7  4  4  7  7  4 10 11  4  7  7

使用isSimilar()比较:

> isSimilar(file = "test2.png",
+           fingerprint = getFingerprint(file = "test1.png"),
+           threshold = 0.1
+ )
[1] FALSE

您可以在 http://www.mango-solutions.com/wp/products-services/r-services/r-packages/visualtest/

阅读有关该软件包的更多信息

值得注意的是,vdiffr 包也支持比较绘图。一个不错的功能是它与 testthat 包集成——它实际上用于在 ggplot2 中进行测试——并且它有一个用于 RStudio 的插件来帮助管理你的测试套件。