如何为一系列数据转换编写单元测试?

时间:2017-07-25 16:29:14

标签: python r unit-testing tdd testthat

我正在尝试学习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)

2 个答案:

答案 0 :(得分:0)

  

一开始它看起来相当干净,但随着我越走越远,我开始在每次测试中重现越来越多的主要内容,这种感觉并不好。重现主要过程的整个部分看起来与单元测试的想法相反。

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

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

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

当需求发生变化时,您可以使用新规范

编写 new 测试
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的常见问题),那么您希望尽可能将其设为 thin

  

使它变得如此简单以至于显然没有缺陷

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

  

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

是的,这就是主意:命令式shell使用与其中一个测试相同的入口点来访问功能核心。

答案 1 :(得分:0)

由于您已经确定了一系列转换函数,因此逻辑行动方针是单独测试它们。另一方面,测试main()只要它仍然是一个简单的调用序列就具有可疑价值。

因为每个函数都将前一个函数的结果作为输入,所以您可能会试图将它们链接到测试执行中,就像它们在主程序中链接一样。但是,这种方法会破坏单元测试方法,并且可能会让您注意到最终结果的快乐路径,从而使您无法在流程的每个步骤中探索可能存在问题的边缘情况。

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