分层集成测试是一种好的实践吗?

11

我有一个应用程序,使用spring-mvc框架,基本上我们有一个演示层(控制器),服务层(业务单元,助手),集成层和数据访问层(jdbc / jpa存储库)。我们想要确保在未来对代码的任何添加都不会破坏先前正常工作的内容,为此我们正在使用单元测试(mockito)和集成测试(spring-test,spring-test-mvc)。

单元测试是针对每个类/组件进行的,基本上我们尝试为传入的输入和这些组件内可能的流程提供良好的覆盖,并且这个动作效果很好,在这里没有疑问,因为单元测试是关于确保单元按预期工作的。

集成测试则是另一回事,也是非常有争议的,因为目前我们有时会使用与设计单元测试相同的场景,但是使用真实平台和整个系统,但是我对最佳实践存在疑虑。

  1. 由于我们有控制器、服务和数据层,可以采用每层一个IT的方法,例如我们有UserService类,我们将拥有UserServiceTest作为单元测试和UserServiceIT,但是可维护性并不理想,有时候觉得我们重复使用相同的测试场景,但是现在使用真实系统。这种做法真的有意义吗?在哪种情况下有意义?如果我们已经通过单元测试覆盖了类的100%,为什么还需要IT呢?似乎我们只需要确保真正的组件将启动,这样才需要IT?是否所有相同的场景都是必要的,或者有哪些好的标准可以用来决定?

  2. 另一种方法是仅从控制器层调用最重要的测试用例进行集成测试,这意味着调用REST服务并验证JSON输出。这就足够了吗?我们不需要在其他层中验证更多内容吗?我知道调用真实的REST API将在底层使用所有层(控制器、服务、DAO),但这足够了吗?你会有一些考虑吗?

  3. 如果我们有一个助手类,我认为没有必要进行单元和IT测试,因为大多数方法只有一个目的,我认为在这里进行单元测试就足够了,您是否也这样认为?

  • 数据层中的某些类可以使用Criteria API、QueryDSL等技术,在某些情况下进行单元测试非常困难,那么这是一个有效的理由吗?

  • 我试图找到确保系统完整性的最佳方法、技巧和实践,同时考虑到这些内容的可维护性。

    3个回答

    7
    你提到了应用所需的整个测试策略。测试不仅涉及覆盖率和层次结构。例如:
    我们希望通过测试确保对代码的未来添加不会破坏先前的工作,为此我们使用单元测试(mockito)和集成测试(spring-test,spring-test-mvc)。
    这就是你实际支持回归测试的方式,它是一种类型。如果我们看看(详细的)测试金字塔。

    Test pyramid

    很容易看出,集成测试占据了很大一部分(建议5-15%)。集成跨越层,但也跨越组件API。您的业务组件自然会存在于同一层中,但仍需要确保它们与彼此的正常工作。使用mSOA将推动您支持如此广泛的接口集成测试。

    我同意你的看法

    集成测试是不同的故事,非常有争议

    一些专家甚至建议您仅保留单元测试和GUI E2E测试。在我看来,没有严格的最佳实践 - 只有好的实践。如果您对权衡感到满意,请使用适合您情况的任何东西。

    我觉得有时候我们会重复相同的测试场景,但现在使用真实系统。这种做法真的有意义吗?在哪些情况下有意义?如果我们已经通过单元测试实现了100%的类测试覆盖率,为什么还需要进行集成测试?似乎我们只需要确保真实组件将启动?是否有必要拥有所有相同的场景,或者有哪些好的标准可以决定?
    看起来你需要在这些场景中划清界限。简而言之,单元测试和模拟对象自然而然地结合在一起。组件测试将需要一些真实系统行为,它可以用于检查在各个单元之间传递的数据处理,或者子系统组件 - 如您的组件/服务数据库或非单元级任务的消息传递。
    从控制器层开始,也就是调用REST服务并验证JSON输出。这就足够了吗?我们不需要在其他层面验证更多的东西吗?我知道调用真实的REST API将使用所有层(控制器、服务、DAO),但这就足够了吗?

    并不完全正确 - 测试表示层也会同时测试底层...那么为什么还要进行其余的测试呢?如果您对这种方法感到满意 - Selenium团队建议采用此数据库验证方法。

    如果您在这里谈论BeansViewHelpers

    我们有一个助手类,我认为没有必要进行单元测试和集成测试,因为大多数方法只有一个目的,我认为这里只需要进行单元测试,你认为呢?

    你需要同时进行单元测试和集成测试,因为所有其他组件都有有效的原因。拥有单一职责并不否认需要进行集成测试。

    在某些情况下,进行单元测试非常困难,这是一个合理的理由吗?

    同样适用于所有封装的私有(和静态)类、方法、属性等。但是也有一种测试它们的方法 - 就像reflection。当然,这仅适用于单元测试旧代码或无法更改的API的特殊情况。如果您需要对自己的代码进行测试,则可能缺乏可测试性指向设计问题。

    1
    测试表现层也会同时测试底层,那么为什么还要进行其他测试呢?测试表现层很少能够测试到所有的底层。因此,测试底层可以增加测试范围,并测试通过表现层无法访问的边缘情况。 - Dave
    1
    正是我的观点。同时也要考虑两者的速度、维护和成本。但是,如果在@Koitoer的情况下这种方法是可行的,为什么不呢?毕竟,基于风险的分析应该建议可以选择哪种权衡。 - ekostadinov

    3
    基于测试Java EE 7和基于Spring的代码库的最新经验,我建议采用以下方法:
    使用每个功能的集成测试,避免单元测试和mock。每个集成测试应涵盖所有层的代码,从表示层到基础架构层(其中包含不是应用程序或域特定的可重用组件,但适用于选择的架构)。
    大多数集成测试应基于实际业务需求和输入数据。其他测试可能会被创建以执行代码库的剩余部分,根据从每个执行集成测试套件生成的代码覆盖率报告。
    所以,假设使用集成测试实现了“完全”的代码覆盖,并且它们运行足够快,那么没有理由进行单元测试。我的经验是,在编写单元测试时,开发人员往往使用过多的mock,往往会创建验证不必要实现细节的脆弱测试。此外,单元测试通常无法提供与集成测试相同的信心水平,因为它们通常不涵盖诸如数据库查询、ORM映射等事项。

    有趣的回答,但是开发过程中如何检查他们生成的代码呢?我不是在谈论TDD,看起来你是按层编写IT代码吗?还是只为整个系统执行几个IT测试? - Koitoer
    1
    我认为在每个层面上都进行集成测试并没有太大的价值;严格来说,如果它不涉及多个层面,那么它甚至不能算是一个真正的集成测试。一个典型的业务应用程序将需要数百或数千个集成测试;对于一个最近编写的 Java EE 7 Web 应用程序,虽然规模较小但逻辑复杂,我编写了150个这样的测试,实现了99%的代码行覆盖率。 - Rogério

    2

    单元测试适用于类和组件。其目的是:

    • 编写代码(TDD)。
    • 说明代码的使用方法并使其能够持续存在于时间和更改中。
    • 尽可能涵盖边界情况。

    当您遇到特定使用或参数的问题时,首先使用新测试重现它,然后修复它。

    模拟应仅在需要测试类或组件独立行为且模拟功能来自应用程序外部(例如电子邮件服务器)时使用。当代码已经被覆盖,而模拟重叠其他类型的测试(如集成测试)的责任时,这是过度和无用的。

    现在您知道每个代码块都可以正常工作了,那么这些块如何一起工作呢?这就是集成测试的作用,即测试组件在各种条件和环境下的交互方式。有时UT和IT之间几乎没有区别:例如数据访问层的测试。集成测试与单元测试具有相同的目的,但在更高的级别上,不那么原子化,以演示API、服务的用例等。

    你如何称呼“集成层”?

    表示层测试更多地是功能测试的责任,而不是单元测试或集成测试。

    您还没有谈到性能测试。

    最终,目标是将所有代码与测试一起编写,通过新测试重现错误并修复错误,并在所有可能的条件(操作系统、数据库、浏览器等)中累积所有测试类型的最大覆盖率。因此,您可以使用以下工具验证整体测试质量:

    • 计算覆盖率的工具。您可能需要为了评估功能测试中的覆盖率而对代码进行插装,或者使用高级JDK工具。
    • 来自某些组件、服务等缺少测试的错误数量。

    通常情况下,当我阅读它们时,我认为一堆测试很好,因为它们立即让我对它们所覆盖的代码的使用方式没有任何疑问,并且完全信任其合同、能力、输入和输出、历史记录以及在用例、强度和稳定性方面的穷尽性和错误管理和报告。

    覆盖率不仅是一个重要因素,但如果您专注于它们的质量:线程安全、由无序的方法和类组成、测试实际条件(没有“if test condition” hack),则可以减少一些测试的数量。

    回答您的问题:考虑到上述因素,您不必为每个层编写集成测试,因为您将为每个层选择不同的测试策略(单元、集成、功能、性能、烟雾、模拟...)。


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