【问题标题】:How to write unit tests for a sequence of data transformations?如何为一系列数据转换编写单元测试?
【发布时间】:2017-12-31 17:04:29
【问题描述】:

我正在尝试学习 TDD,同时编写一个将输入数据转换为一系列函数的脚本。不管我是用python还是R写的,问题都是类似的。我猜它与TDD理解更相关。

# Look of main in python
def main():
    data = get_data()
    data_a = transform_fun1(data)
    data_b = transform_fun2(data_a)
    data_c = transform_fun3(data_b)
    ....
    return data_x

# Look of main in R
main <- function() {
    data <- get_data() %>%
      transform_fun1() %>%
      transform_fun2() %>%
      transform_fun3() %>%
      ...
    data_x
}

为每个transform_fun 编写单元测试的最佳过程是什么,知道它们需要前一个transform_fun 的结果作为输入?

一开始它看起来很干净,但随着我越来越远,我开始在每次测试中复制越来越多的main,这闻起来不太好。重现main 过程的整个部分看起来与单元测试的想法有悖常理。

# in python (pytest)
def test_transform_fun_n(data):
    data_a = transform_fun1(data)
    data_b = transform_fun2(data_a)
    ...
    data_n = transform_fun_n(data_n-1)
    assert data_n == blabla

# in R (testthat)
test_that("transform_fun_n do what I expect", {
    data_a <- transform_fun1(data)
    data_b <- transform_fun2(data_a)
    ...
    data_n <- transform_fun_n(data_n-1)
    expect_that(data_n, equals(blabla))
})

我也尝试在每个步骤之间添加夹具(至少在 python 中),但它看起来也不理想。

-- 编辑-- 试图勾勒出 VoiceOfUnreason 的答案会是什么样子。

def transformV1(data):
     return data + x

def transformV2(data):
     return transformV1(data) + y

def transformV3(data):
     return transformV2(data) + z

def main():
     data = get_data()
     return transformV3(data)

【问题讨论】:

  • 我开始在每个测试中重现越来越多的 main - main() 中的逻辑是否比代码示例中显示的调用序列更复杂?
  • 目前我设置了一些值,但没有任何内容不能放入其中一个 transformVx(data) 函数中。

标签: python r unit-testing tdd testthat


【解决方案1】:

一开始它看起来很干净,但是随着我越来越远,我开始在每次测试中重现越来越多的 main,这闻起来并不好闻。复制主流程的整个部分看起来与单元测试的想法有悖常理。

是的,你是对的。该代码试图告诉您您的规范(和您的生产代码)是在错误的抽象级别编写的。

def test_transformV1(data, expected):
    actual = transformV1(data)
    assert actual == expected

def main():
    data = getData()
    return transformV1(data)

当需求改变时,你编写一个新的测试,使用新的规范

def test_transformV2(data, expected):
    actual = transformV2(data)
    assert actual == expected

def test_transformV1(data, expected):
    actual = transformV1(data)
    assert actual == expected

def main():
    data = getData()
    return transformV2(data)

这里的关键思想是 (a) 您的单元测试执行生产代码提供的功能 (b) 新需求意味着新功能——新功能可以根据其他功能来实现,但测试只是检查新函数返回正确的结果。

如果 main 很难测试(an imperative shell 的一个常见问题),那么您希望尽可能地使其

简单到没有明显的缺陷

需要从外壳到核心重构长链转换;给定一个名字,等等。

你的意思是代码应该写得更像我在问题末尾添加的那样

是的,就是这样:命令式 shell 使用与测试之一相同的入口点访问功能核心。

【讨论】:

  • 非常感谢您的回答。我还是有点困惑。你的意思是代码应该写得更像我在问题末尾添加的内容(代码在 cmets 中看起来没有格式化)。
【解决方案2】:

由于您已经确定了一系列转换函数,因此合乎逻辑的做法是单独测试它们。另一方面,测试 main() 的价值值得怀疑,只要它仍然是一个简单的调用序列。

因为每个函数都将前一个函数的结果作为输入,所以您可能很想在测试执行中将它们链接起来,就像它们在主程序中链接一样。但是,这种方法会破坏 unit 测试方法,并且可能会让您专注于通往最终结果的快乐路径,从而阻止您在流程的每个步骤中探索可能存在问题的输入值边缘情况。

相反,请尝试依靠您的函数输入类型来提出一堆具有不同输入值的测试,这些测试反映了每个函数的不同场景。您甚至可以使用类似 QuickCheck 的基于属性的工具为您生成随机值。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-11-16
    • 1970-01-01
    • 1970-01-01
    • 2018-01-17
    • 2012-01-06
    • 2019-03-17
    相关资源
    最近更新 更多