测试气味Test Smells-整洁单元测试

简介: 摘要:本文讨论了代码中的“Code Smell”现象,即可能表明代码质量问题的模式。这些包括重复代码、过长函数、过大类、过长参数列表等。识别并重构Code Smell有助于提升代码质量和可维护性。在单元测试中,也有类似的“测试味道”问题,如无信息的测试名称、缺少arrange-act-assert结构、不恰当的变量名和重复使用以及杀虫剂效应。好的单元测试应有明确的命名、遵循arrange-act-assert模式、使用有意义的变量名,并避免重复测试同一情况,以提供有价值的错误信息。

背景

"Code smell" 是软件开发中的一个术语,指的是代码中可能表明存在问题的某些迹象或模式。这些迹象本身并不表示代码一定有错误,但它们通常表明代码可能难以理解、维护或扩展。Code smells 可以视为一种警告,提示开发者需要进一步检查代码以确定是否存在更深层次的问题。

常见的 code smells 包括:

  1. 重复代码(Duplication):代码中有重复的逻辑或结构。
  2. 过长函数(Long Method):一个函数执行太多任务,难以理解和维护。
  3. 过大类(Large Class):类包含太多的属性和方法,违反了单一职责原则。
  4. 过长参数列表(Long Parameter List):函数或方法有过多的参数,难以使用。
  5. 数据泥团(Data Clumps):多个类或方法中出现相同的数据结构。
  6. 基本类型偏执(Primitive Obsession):过度使用基本数据类型,而不是创建更有意义的类。
  7. 切换/状态/类型(Switch/State/Type):使用大量的条件语句来处理不同的状态或类型。
  8. 霰弹式修改(Shotgun Surgery):修改代码时需要在多个地方进行更改。
  9. 特征嫉妒(Feature Envy):函数或方法似乎更关心另一个类的数据而不是自己的。
  10. 数据类(Data Class):类只包含数据和访问器,没有行为。

识别和解决 code smells 是重构过程的一部分,可以帮助提高代码质量和可维护性。

单元测试中代码味道

    在实际工作中,知道如何不编写代码可能与知道如何编写代码同样重要。测试代码也是如此;今天,我们将探讨编写单元测试时常见的错误。虽然编写单元测试是程序员的常见做法,但测试仍常常被视为二等代码。编写好的测试并不容易--就像在任何编程领域一样,有模式也有反模式。在 Gerard Meszaros 关于 xUnit 模式的书中,有一些关于测试气味的章节很有帮助,互联网上也有更多好东西。

一次测试的演变。 首先,我们要测试什么?一个原始函数:

public String hello(String name) {

    return "Hello " + name + "!";

}

我们开始为它编写单元测试:

@Test

void test() {

}

就这样,我们的代码已经有了味道。

1. 无信息的名称

当然,只写 test、test1、test2 要比写一个翔实的名称简单得多。而且,这样也更简短!

但是,写起来容易的代码远不如读起来容易的代码重要--我们花在阅读代码上的时间更多,而可读性差会浪费大量时间。名称应该传达意图;应该告诉我们正在测试什么。

也许我们可以把测试命名为 testHello,因为它测试的是 hello 函数?不行,因为我们不是在测试方法,而是在测试行为。所以好的名字应该是 shouldReturnHelloPhrase:

@Test

void shouldReturnHelloPhrase() {

    assert(hello("John")).matches("Hello John!");

}

除了框架之外,没有人会直接调用测试方法,因此名称太长也没有关系。它应该是一个描述性的、有意义的短语(DAMP)。


2. 没有 arrange-act-assert

名字还可以,但现在一行中塞进了太多的代码。最好把准备工作、我们要测试的行为和关于该行为的断言(rangement-act-assert)分开。

@Test

void shouldReturnHelloPhrase() {

    String a = "John";

   String b = hello("John");

   assert(b).matches("Hello John!");

}

在 BDD 中,习惯使用 Given-When-Then 模式,在本例中也是如此。


3. 变量名不正确,没有变量重复使用

但看起来还是写得很匆忙。a "是什么?b "是什么?你可以推断出一些,但想象一下,这只是测试运行中失败的几十个测试中的一个(在几千个测试的测试套件中完全有可能)。在对测试结果进行排序时,你需要做大量的推断工作!

因此,我们需要正确的变量名。

我们在匆忙中还做了一件事--我们所有的字符串都是硬编码的。硬编码某些内容是可以的,但必须与其他硬编码内容无关!

也就是说,当您阅读测试时,数据之间的关系应该是显而易见的。a' 中的 "John "与断言中的 "John "是否相同?在阅读或修复测试时,我们不应该在这个问题上浪费时间。

因此,我们可以这样重写测试:

@Test

void shouldReturnHelloPhrase() {

    String name = "John";

   String result = hello(name);

    String expectedResult = "Hello " + name + "!";

   assert(result).contains(expectedResult);

}


4. 杀虫剂效应

   这里还有一件事值得思考:自动化测试很好,因为你可以用很少的成本重复测试,但这也意味着随着时间的推移,它们的有效性会下降,因为你只是在重复测试完全相同的东西。这就是所谓的 "杀虫剂悖论"(Boris Beizer 在 20 世纪 80 年代创造的术语):虫子会对你用来杀死它们的东西产生抗药性。

要完全克服杀虫剂悖论可能是不可能的,但有一些工具可以通过在测试中引入更多的可变性来减少其影响,例如 Java Faker。让我们用它来创建一个随机名称:

@Test

void shouldReturnHelloPhrase() {

    Faker faker = new Faker();

    String name = faker.name().firstName();

   String result = hello(name);

    String expectedResult = "Hello " + name + "!";

   assert(result).contains(expectedResult);

}

好在我们在上一步中将名称改为了变量--现在我们不必再查看测试,找出所有的 "约翰 "了。


5. 信息不全的错误信息

     如果我们在匆忙中编写了测试,那么另一件我们可能没有考虑到的事情就是错误信息。在对测试结果进行分类时,您需要尽可能多的数据,而错误信息是最重要的信息来源。然而,默认的错误信息非常缺乏信息量:

java.lang.AssertionError at org.example.UnitTests.shouldReturnHelloPhrase(UnitTests.java:58)

太好了。我们唯一知道的就是断言没有通过。幸好,我们可以使用 JUnit`Assertions` 类中的断言。具体方法如下

@Test

void shouldReturnHelloPhrase4() {

    Faker faker = new Faker();

    String name = faker.name().firstName();

   String result = hello(name);

    String expectedResult = "Hello " + name + "";

   Assertions.assertEquals(

        result,

        expectedResult

    );

}

这是新的错误信息:

Expected :Hello Tanja! Actual :Hello Tanja

......这立刻告诉了我们出错的原因:我们忘记了感叹号!

相关文章
|
12天前
|
测试技术 开发者 UED
探索软件测试的深度:从单元测试到自动化测试
【10月更文挑战第30天】在软件开发的世界中,测试是确保产品质量和用户满意度的关键步骤。本文将深入探讨软件测试的不同层次,从基本的单元测试到复杂的自动化测试,揭示它们如何共同构建一个坚实的质量保证体系。我们将通过实际代码示例,展示如何在开发过程中实施有效的测试策略,以确保软件的稳定性和可靠性。无论你是新手还是经验丰富的开发者,这篇文章都将为你提供宝贵的见解和实用技巧。
|
2月前
|
IDE 测试技术 持续交付
Python自动化测试与单元测试框架:提升代码质量与效率
【9月更文挑战第3天】随着软件行业的迅速发展,代码质量和开发效率变得至关重要。本文探讨了Python在自动化及单元测试中的应用,介绍了Selenium、Appium、pytest等自动化测试框架,以及Python标准库中的unittest单元测试框架。通过详细阐述各框架的特点与使用方法,本文旨在帮助开发者掌握编写高效测试用例的技巧,提升代码质量与开发效率。同时,文章还提出了制定测试计划、持续集成与测试等实践建议,助力项目成功。
85 5
|
3月前
|
测试技术 C# 开发者
“代码守护者:详解WPF开发中的单元测试策略与实践——从选择测试框架到编写模拟对象,全方位保障你的应用程序质量”
【8月更文挑战第31天】单元测试是确保软件质量的关键实践,尤其在复杂的WPF应用中更为重要。通过为每个小模块编写独立测试用例,可以验证代码的功能正确性并在早期发现错误。本文将介绍如何在WPF项目中引入单元测试,并通过具体示例演示其实施过程。首先选择合适的测试框架如NUnit或xUnit.net,并利用Moq模拟框架隔离外部依赖。接着,通过一个简单的WPF应用程序示例,展示如何模拟`IUserRepository`接口并验证`MainViewModel`加载用户数据的正确性。这有助于确保代码质量和未来的重构与扩展。
76 0
|
3月前
|
测试技术 Java Spring
Spring 框架中的测试之道:揭秘单元测试与集成测试的双重保障,你的应用真的安全了吗?
【8月更文挑战第31天】本文以问答形式深入探讨了Spring框架中的测试策略,包括单元测试与集成测试的有效编写方法,及其对提升代码质量和可靠性的重要性。通过具体示例,展示了如何使用`@MockBean`、`@SpringBootTest`等注解来进行服务和控制器的测试,同时介绍了Spring Boot提供的测试工具,如`@DataJpaTest`,以简化数据库测试流程。合理运用这些测试策略和工具,将助力开发者构建更为稳健的软件系统。
57 0
|
3月前
|
测试技术 Java
全面保障Struts 2应用质量:掌握单元测试与集成测试的关键策略
【8月更文挑战第31天】Struts 2 的测试策略结合了单元测试与集成测试。单元测试聚焦于单个组件(如 Action 类)的功能验证,常用 Mockito 模拟依赖项;集成测试则关注组件间的交互,利用 Cactus 等框架确保框架拦截器和 Action 映射等按预期工作。通过确保高测试覆盖率并定期更新测试用例,可以提升应用的整体稳定性和质量。
74 0
|
3月前
|
测试技术 数据库
探索JSF单元测试秘籍!如何让您的应用更稳固、更高效?揭秘成功背后的测试之道!
【8月更文挑战第31天】在 JavaServer Faces(JSF)应用开发中,确保代码质量和可维护性至关重要。本文详细介绍了如何通过单元测试实现这一目标。首先,阐述了单元测试的重要性及其对应用稳定性的影响;其次,提出了提高 JSF 应用可测试性的设计建议,如避免直接访问外部资源和使用依赖注入;最后,通过一个具体的 `UserBean` 示例,展示了如何利用 JUnit 和 Mockito 框架编写有效的单元测试。通过这些方法,不仅能够确保代码质量,还能提高开发效率和降低维护成本。
52 0
|
3月前
|
JavaScript 前端开发 测试技术
Vue.js开发者必看!Vue Test Utils携手端到端测试,打造无懈可击的应用体验,引领前端测试新风尚!
【8月更文挑战第30天】随着Vue.js的普及,构建可靠的Vue应用至关重要。测试不仅能确保应用质量,还能提升开发效率。Vue Test Utils作为官方测试库,方便进行单元测试,而结合端到端(E2E)测试,则能构建全面的测试体系,保障应用稳定性。本文将带你深入了解如何使用Vue Test Utils进行单元测试,通过具体示例展示如何测试组件行为;并通过Cypress进行E2E测试,确保整个应用流程的正确性。无论是单元测试还是E2E测试,都能显著提高Vue应用的质量,让你更加自信地交付高质量的应用。
66 0
|
3月前
|
Java 测试技术 API
SpringBoot单元测试快速写法问题之复杂的业务逻辑设计有效的单元测试如何解决
SpringBoot单元测试快速写法问题之复杂的业务逻辑设计有效的单元测试如何解决
|
3月前
|
Java 测试技术 API
SpringBoot单元测试快速写法问题之计算测试用例的分支覆盖率如何解决
SpringBoot单元测试快速写法问题之计算测试用例的分支覆盖率如何解决
|
3月前
|
Java 测试技术 开发者
在软件开发中,测试至关重要,尤以单元测试和集成测试为然
在软件开发中,测试至关重要,尤以单元测试和集成测试为然。单元测试聚焦于Java中的类或方法等最小单元,确保其独立功能正确无误,及早发现问题。集成测试则着眼于模块间的交互,验证整体协作效能。为实现高效测试,需编写可测性强的代码,并选用JUnit等合适框架。同时,合理规划测试场景与利用Spring等工具也必不可少。遵循最佳实践,可提升测试质量,保障Java应用稳健前行。
49 1