单元测试应该放在哪里?

21

我对单元测试还比较陌生,我发现虽然很多资料都说你应该写单元测试,但很少有指示它们应该放在项目的哪个位置。

唯一看到的建议是将它们与生产代码放在一起,或者构建一个与生产代码相似的目录结构。第一种方法的问题在于,当你对系统进行完整构建时,提取这些测试将变得困难;对于第二种方法,你如何测试仅在包级别可见的功能呢?

是否有一种合理的方式来组织单元测试,以便你可以轻松地自动化构建过程,并且仍然能够访问需要测试的所有内容?

编辑:对于那些关心特定语言的人,我主要使用Java。然而,我想找到一种相对语言无关的解决方案,这样我就可以在任何平台上应用它。


我将测试存储在它们自己的目录中,并在我的 Makefile 中拥有一个“make test”目标。但是从你的问题中,我推断出你的项目管理实践与我的不同。 - jmz
6个回答

6
您没有说明您使用的平台和工具,这会影响选项。我从.NET和Visual Studio的角度回答:
我的团队(正在开发一个相当大的项目)已经成功地使用了将代码和测试放在一起的方法。对于解决方案中的给定(子)命名空间,在其中有一个“_Test”文件夹(“_”是一个技巧,在解决方案浏览器中使它显示在该命名空间中的所有其他内容之上)。
我正在向团队介绍TDD,并且感到在测试和代码之间进行最小摩擦对于采用这种实践的成功至关重要。因此,测试始终与其测试的代码“物理上靠近”;您不希望在巨大的解决方案中来回查找以跳转到代码和测试。
当您考虑它时,这是有意义的。代码和测试就像阴阳一样彼此相辅相成。这也意味着只需重新构建1个程序集即可在1个程序集中修改类型(我们的解决方案中有超过30个项目,因此当通过TDD演化类时,可以安全地仅构建当前项目,比完整的解决方案构建快得多)。
将代码和测试放在一起还解决了测试内部类的问题。但是,在.NET平台上,如果您想保持分离,还可以使用InternalsVisibleTo程序集级别属性授予对单独测试程序集的访问权。
我能看到将测试放在单独的程序集中的唯一原因是为了避免发布该代码。但是,如果您正在使用构建自动化,则可以以另一种方式轻松解决此问题。由于Visual Studio的项目文件是XML,因此我们只需使用小型XSLT预处理项目文件即可在发布版本中剥离*/_Test/*文件夹中的所有内容。

另一种从发布版本中删除测试代码的方法是为测试方法添加一个条件属性。[Conditional("TEST")] - Jeffrey L Whitledge
1
可以使用True,或者在整个测试夹具周围使用丑陋的#if预处理器结构,但每个测试都必须手动完成。我们大约有4000个这样的测试;如果必须手动完成,某人肯定会在某个地方忘记它。通过遵循一个约定(在我们的情况下是Test子文件夹),轻松消除人为因素并实现自动化。自动化是好的。 :) - Cumbayah

5
第二种方法最好。为您的测试创建一个并行源代码树。设置相同的包结构。您没有说明使用的语言,但如果是Java,则测试源代码树中的代码将能够很好地访问主源代码树中的代码。事实上,包中有一些来自一个源代码树和另一个源代码树的代码是无关紧要的——生产代码和测试代码仍在同一个包中。

4
我认为第一种方法存在的问题是当你对系统进行完整构建时,提取这些测试用例会变得困难。你可以根据某些命名约定(例如在Java中不包括**/*Test.class)来过滤类进行打包。然而,我并不喜欢这种方法,我认为它很混乱而且易碎。
对于第二个问题,如果我没有理解错的话,我并不认为存在问题。你可以将类放置在相同的包中,但位于不同的目录中,例如:
  • src/main/java 用于应用程序源代码
    • src/main/java/foo/Bar.java
  • src/test/java 用于测试源代码
    • src/test/java/foo/BarTest.java
Bar.javaBarTest.java都在同一个foo包中,但位于不同的目录中。这是我采用的方法。
关于如何组织单元测试以便轻松自动化构建过程并仍然访问需要测试的所有内容,我的测试源代码和应用程序源代码实际上存储在同一个项目中但位于不同的目录树中,这对我非常有效。这实际上是Maven建议的默认布局(请参阅标准目录布局介绍)。无论你使用什么语言,这都可能会给你一些想法。

1

什么是测试,以及什么是应用程序?

让我们思考一下测试在这个过程中的作用...

  • 测试与公众关心的任何事情都没有直接的关系--他们只想让它工作
  • 测试是为开发人员和通常是企业服务的
  • 测试可以与模块在同一规模上,但处于二分法的位置
  • 如果测试文件可以与其模块在给定的封装结构中并排存在,则从逻辑上讲,在给定模块内部也必须在对象级别上分配测试的一部分。 (考虑,目录|_文件|_模块|_对象--其中system == component && component == system && (system == system || system == subsystem ))

最后一个是为了表达测试在本质上与模块是离散的,并且在每个规模上逻辑上不互补--要真正理解这一点,我建议您学习分形&自相似性类型论范畴论

然而,特定于应用程序的情况可以大大改变您拥有的选项...从JavaScript的角度来看,一个不可知论者,即使使用先进的架构方案,似乎将您的测试包含在/src内也可能会使您的构建过程复杂化。此外,当您的src目录中有多个甚至很少的测试时,您会为其他开发人员创建一个较不易读的逻辑绑定。确实希望保护和封装您的测试,使它们成为团队成员私有的,而不是任何与您的应用程序公开交互的人。
话虽如此,如果可以确保其安全且不会创建不必要的复杂性或扼杀自动化,则仍有可能走这条路。一些先进的架构利用这种方法来实现离散尺度上的全面封装,但这可能会以安全、自动化和生产率(因此影响业务)的代价为代价。
作为一个经验法则,请使用行业标准的脚手架:
/app
    |-lib
    |-src
    |-test

..你很可能做不错。


1

我建议您为单元测试单独创建一个项目(或多个项目)。

另外,最好在项目树上有一个文件夹专门用于测试项目。这样,如果您需要在不构建测试的情况下构建系统,则应该有一个构建配置,而不构建测试,尽管构建测试并将其作为CI(持续集成)的一部分运行,然后仅将生产代码打包到安装程序中或将其复制到特定文件夹中是个好主意。

测试仅在包级别可见的功能取决于所使用的技术-例如,.NET具有使用InternalVisibleTo属性使内部方法和类可见的能力。您应该问自己是否需要测试该功能,不建议测试私有方法-因为它们往往会发生变化。


在同一项目中使用单独的目录树与使用单独的项目相比,有什么优势? - derekerdmann
好处是您的生产代码中没有单元测试。 - Dror Helper
一个合理的担忧,然而可以通过其他方式轻松解决,详见我的回答。根据我的经验,在TDD周期中使用单独的项目会导致摩擦,需要不断在测试和代码之间切换。 - Cumbayah

1

这可能取决于您现在使用的是什么。对于Visual Studio / .NET,常见做法似乎是为每个生产DLL创建一个测试DLL,并在DLL中模糊地镜像类和测试类。对于Ruby / Python等语言,则需要一个单独的测试目录,并在其中建立与正在测试的内容相对应的层次结构。

您所说的“提取”这些测试是什么意思?对于您来说,完整的构建是否涉及编译和运行测试?

包级别的可见性可能是一个问题。我想,如果您的测试位于单独的包中,那么您只需要针对直接可见的内容进行测试,但这将间接地测试内部内容。


通过“提取”,我的意思是在构建最终项目时不需要包含单元测试的代码。当然,在进行构建时会运行它们; 我只是不希望它们出现在构建本身中。 - derekerdmann

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接