代码编织梦想

一套可以运行的业务代码往往只是一个软件产品中的一部分,一个完备的软件产品应该包含至少以下元素:

  • 业务代码
  • 自动化测试
  • 文档
  • 持续集成/持续交付(CI/CD)
  • 产品协议

单纯的代码是不够的,只有以上内容被集成到一个坚实的系统中,才能被称为一个软件产品。而其中测试在软件开发中是至关重要的,在一些架构设计方法论中,测试甚至可以主导系统的架构设计(如TDD),从而进一步的保证了代码的可测试性。

可维护和可读的测试代码对于提升单元测试覆盖率至关重要,当我们修改系统中的一些功能或进行重构时,这些单元测试又可以检测我们是否对功能进行了破坏。

同时单元测试可以是优秀的代码文档,一个对系统的行为不了解的人可以通过单元测试快速了解各个类的目的和API使用方法。

1. 单元测试与集成测试的区别

每个开发人员都有编写单元测试的经验,我们都知道它们的目的和编写方式,但是要给单元测试下一个严格的定义是比较困难的,最核心的在于如何理解“单元”。

Wiki百科对单元测试和集成测试分别进行了以下解释:

单元测试:又称为模块测试,是针[程序模块(软件设计的最小单位)来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类(超类)、抽象类、或者派生类(子类)中的方法

集成测试:即对程序模块采用一次性或增值方式组装起来,对系统的接口进行正确性检验的测试工作,一般在单元测试之后、系统测试之前进行

简单地说,做单元测试时只测试一个单元的代码,一次一个方法,不包括所有与被测试的组件交互的其他方法

而集成测试是测试方法之间的集成。由于单元测试中所有方法的行为是独立,我们不知道它们之间如何协同工作,此时就需要集成测试来进行验证。

2. 单元测试的要求

单元测试并不是看起来那么简单的,他们要能够实际验证代码的行为,而不仅仅是使用一些断言来查看错误信息,也不仅仅是用一些特定参数来进行mock。为了能够保证单元测试的质量,需要保证单测代码符合以下原则

2.1 测试四要素

一个单元测试理论上应该包含四个部分:

  • mock:被测方法执行过程中,调用的被测类以外的方法都应当被mock处理。如果被测方法没有这种调用,则可以忽略这个部分
  • 输入:测试准备,例如入参数据或需要使用到的配置
  • 执行:调用想要测试的方法或动作
  • 输出:并非简单打印执行结果,而是执行断言,验证输出或动作的正确性
@Test
public void findProduct() {
    insertIntoDatabase(new Product(100, "IPad"));

    Product product = dao.findProduct(100);

    assertThat(product.getName()).isEqualTo("IPad");
}

2.2 单元测试的FIRST原则

  • F:Fast,快速
  • I:Independent,独立
  • R:Repeatable,可复验
  • S:Self-Validating,自足验证
  • T:Thorough,彻底/及时

Fast,快速

单元测试是执行一个特定任务的一小段代码。与集成测试不同的是,单元测试很小,没有网络通信,不执行数据库操作,所以它们应当运行得非常快。这样的特性也可以保证开发者在实现应用程序功能时,可以经常运行单元测试。

Independent,独立

单元测试必须是相互独立的。一个单元测试不应该依赖于另一个单元测试所产生的结果,因为在大多数情况下,单元测试是以随机的顺序运行的。

被测试的代码或系统也应该与它的依赖隔离开。为了确保这些依赖中的错误不影响单元测试,确保结果的准确性,通常会使用mock或stub的方式进行数据处理。

Repeatable,可复验

一个单元测试在不同的计算机、不同的时间点多次运行,都应该产生相同的结果。这就是为什么单元测试是独立于环境和其他单元测试的。

Self-Validating,自足验证

自足验证的含义是,开发人员如果要了解一个单元测试是否通过,不应该在测试完成后做任何额外的检查。

单元测试需要自动验证被测方法所产生的结果,并由它自己决定是通过还是失败。

因此,不需要在单元测试中添加任何打印日志的语句。如果只有打印出日志才能判断单元测试是否通过,就需要重新审视你的单元测试看看哪里出了问题。

Thorough,彻底/及时

在测试一个功能时,我们除了考虑主要逻辑路径以外,还要关注边界或负面的场景。因此在多数时候,我们除了要创建一个具有有效入参的单元测试,还需要准备其他使用了无效入参的单元测试。例如被测方法入参有一个范围,从MIN到MAX,那么应该创建额外的单元测试来测试输入为MIN和MAX时是否能正确处理。

也有的文章将Thorough理解为及时的。这种理解的含义是:最好在编写新功能的时候就创建单元测试。这样就能够尽快验证该功能确实能按预期的效果进行工作,也就有更少的可能去引入一个错误。因此,在将代码push到生产分支之前,应该用单元测试覆盖你所修改的代码。

2.3 被测类不应打破依赖反转原则

打破这一规则将使单元测试难以编写和执行。看一下下面这段代码

public class OrderService {
  private final OrderRepository orderRepository = new OrderRepositoryImpl();
  private final CommodityRepository commodityRepository = new CommodityRepositoryImpl();
  ...
}

CommentService声明了两个外部依赖(本文中的“外部”均指当前类的外部,而非系统外部),但这两个依赖都绑定在了CommentService的内部,使得我们很难通过stub、mock的方式来隔离验证当前类的行为(虽然可以通过反射的方式曲线救国)

2.4 单元测试应保证确定性

一个单元测试应该只依赖于输入参数,而不依赖于外部状态(系统时间、CPU数量、默认编码等),因为我们不能保证团队中的每个开发人员都有相同的硬件设置。假设你的电脑有8个CPU,一段单元测试对此做了一个假设和断言,那么另一个拥有16个CPU的同事可能会因为每次该测试都运行失败而感到恼火。

举一个栗子。想象一下,我们想测试一个工具方法,它可以告诉我们所提供的日期时间是否是早晨,如果你的单元测试是这样的:

@Test
public void shouldBeMorning() {
    OffsetDateTime now = OffsetDateTime.now();
    assertTrue(DateUtil.isMorning(now));
  }

这个测试在结果上是不确定的,只有当你在早晨运行这段代码时,它才会成功。

这里的最佳做法是避免通过调用非确定性函数来声明测试数据,这些数据包括

  • 当前日期时间
  • 系统时区
  • 硬件参数
  • 随机数
  • 文件绝对地址

2.5 单元测试应避免任何外部依赖

只有抛弃所有的外部依赖,才能保证在任何环境下每次运行测试都有相同的结果,否则难以使用断言对执行结果的正确性进行推断。

假设我们正在创建一个提供需求状态的服务,该服务的入参是一段Open Api的URL。下面的代码片段是该服务的单元测试,用于检测处理需求信息的逻辑是否正确:

	String apiUrl = "http://api.jagile.jd.com/demand?id=123";
    DemandService demandService = new DemandService();

    DemandInfo demandInfo = demandService.getDemandDetail(apiUrl);

    assertEquals("Good Test",demandInfo.getName());

问题是外部服务或依赖可能是不稳定的,不能保证外部服务会一直保持正常且一致的响应。即使外部服务做到了,运行构建的CI服务器仍然有可能存在操作限制,例如可能存在防火墙的限制。

单元测试应当是是一段坚实的代码,不需要依赖任何外部服务就能成功运行。

3. 最佳实践

3.1 使用断言

为了满足“自足验证”的要求,需要使用断言来验证预期与实际的结果。可以使用JUnit的Assert类中的各种方法。

例如,可以使用Assert.assertEquals方法或assertNotEquals来进行值断言。其他方法如assertNotNull、assertTrue和assertNotSame可以在不同的场景中选用。

3.2 使用前缀标明预期值与实际值

如果要在断言中使用变量,尽量在变量前加上“actual*” 和“expected*”,分别表示实际执行的结果和预期的结果。这样可以增加可读性,并且明确了变量的意图,避免在断言中出现混淆

不要这么做👎

ProductDTO product1 = requestProduct(1);
ProductDTO product2 = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(product1).isEqualTo(product2);

这样更好👍

ProductDTO actualProduct = requestProduct(1);
ProductDTO expectedProduct = new ProductDTO("1", List.of(State.ACTIVE, State.REJECTED))
assertThat(actualProduct).isEqualTo(expectedProduct); // 👍🏻

3.3 避免使用随机数据

使用随机数据会导致每次测试得到的结果都不一致,这样会很难调试。如果为了让测试通过而不使用断言,又难以发现问题并追溯错误代码。

相反,如果对所有数据都使用固定值,则可以更容易地创建可复用的测试。或者将产生随机变量的功能进行封装,可以让单元测试更方便的mock

不要这么做👎

Instant ts1 = Instant.now(); // 1557582788
Instant ts2 = ts1.plusSeconds(1); // 1557582789
int randomAmount = new Random().nextInt(500); // 232
UUID uuid = UUID.randomUUID(); // d5d1f61b-0a8b-42be-b05a-bd458bb563ad

同样的,单元测试代码中也应尽量避免使用如Instant.now()new Date()等会产生随机数据的方法,因为每次执行单元测试得到的结果都是不同的,所以难以使用断言进行结果校验

不要这么做👎

public class ProductDAO {
    public void updateDateModified(String productId) {
        Instant now = Instant.now(); // !
        Update update = Update().set("dateModified", now);
        Query query = Query().addCriteria(where("_id").eq(productId));
        return mongoTemplate.updateOne(query, update, ProductEntity.class);
    }
}

3.4 避免使用静态成员

每个测试用例应该相互独立,因此永远不要在测试代码中使用静态数据成员,因为它们的值很有可能被其他单元测试修改。但是如果一定要这样使用,请记住在执行每个测试用例之前对其重新初始化。

3.5 隔离外部依赖

如果要测试的代码与外部资源有交互,应当考虑使用Mock框架隔离这些外部依赖,避免访问真实的外部资源,从而保证单元测试的稳定性。Mockito框架就是一个很好的选择。

@RunWith(MockitoJUnitRunner.class)
public class CarServiceTest {

	private CarService carService;

	@Mock
	private RateFinder rateFinder;

	@Before
	public void init() {
    	carService = new CarService(rateFinder);
	}

	@Test
	public void shouldInteractWithRateFinderToFindBestRate() {
    	carService.schedulePickup(new Date(), new Route());
    	verify(rateFinder, times(1)).findBestRate(any(Route.class));
	}
}

3.6 避免void返回值

具有返回值的函数更容易测试,因为可以很方便地对方法返回值进行断言。

如果方法是void返回值,那么单元测试就需要对方法入参对象内部的某些值进行断言,单元测试就与方法的内部实现出现了紧耦合,必须对被测方法的内部逻辑有了解才能实现断言。

如果一个方法的返回值只能是void,那么一般有以下3中方法进行测试

  • 使用mock来验证该方法内部所依赖的其他方法是否被调用
  • 验证该方法的入参是否发生了预期改变
  • 验证是否抛出了一个已知的异常

3.7 不要吞掉异常

单元测试中不要catch住异常后不进行任何后续处理,否则会隐藏掉被测方法中的异常。如以下展示的两个单元测试,第一个无论是不是符合预期永远会通过,第二个更加明确,如果没有抛出预期的异常,则会失败

不要这样做👎

@Test
public void myTest(){
		try{
      	……
    } catch(Exception e){
      	assertTrue(true);
    }
}

这样更好👍

@Test(expected=ExceptionClass.class)
public void myTest() throws ExceptionClass{
		……
}

3.8 减少对Spring容器的依赖

此处的容器依赖是指完全靠Spring的依赖反转来对bean的方法进行测试,因为启动Spring框架往往需要较长的时间,且越复杂、外部依赖越多的应用启动时间越长,这会拉长测试执行时间,减慢反馈的周期。

手动创建所需要的对象并使用Mock的方式进行注入可以解决依赖Spring容器的问题。但是如果想要测试的是配置读取、容器启动时自动运行的方法等,那么对容器的依赖是必不可少的。

可以尝试使用@Before和@After方法来设置所有测试用例的前提条件。如果需要在@Before或@After中支持多个不同的测试用例,那么就要考虑创建新的测试类了。

3.9 多使用辅助函数

将细节或重复代码提取到子函数中,并且取一个描述性比较强的名字,可以使测试代码更简短,同时可以让开发人员更快速的了解当前测试的核心内容是什么。

使用辅助函数创建复杂对象或断言时,只传递和当前测试有关的参数给辅助函数,对其他数据可以合理使用默认值

不要这么做👎

@Test
public void categoryQueryParameter() throws Exception {
    List<ProductEntity> products = List.of(
            new ProductEntity().setId("1").setName("Envelope").setCategory("Office").setDescription("An Envelope").setStockAmount(1),
            new ProductEntity().setId("2").setName("Pen").setCategory("Office").setDescription("A Pen").setStockAmount(1),
            new ProductEntity().setId("3").setName("Notebook").setCategory("Hardware").setDescription("A Notebook").setStockAmount(2)
    );
    for (ProductEntity product : products) {
        template.execute(createSqlInsertStatement(product));
    }
    String responseJson = client.perform(get("/products?category=Office"))
            .andExpect(status().is(200))
            .andReturn().getResponse().getContentAsString();

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("1", "2");
}

这样更好👍

@Test
public void categoryQueryParameter2() throws Exception {
    insertIntoDatabase(
            createProductWithCategory("1", "Office"),
            createProductWithCategory("2", "Office"),
            createProductWithCategory("3", "Hardware")
    );
    String responseJson = requestProductsByCategory("Office");

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("1", "2");
}

3.10 避免过度使用变量

开发人员通常喜欢将多次使用的值提取到变量中,但在一些情况下,这会大大增加开发测试代码的工作量,同时也增加了追溯相关失败代码行的复杂度。

测试代码应尽可能的保持简短,如果多处地方使用了相同的值,那么使用变量是没有问题的。

不要这么做👎

@Test
public void variables() throws Exception {
    String relevantCategory = "Office";
    String id1 = "4243";
    String id2 = "1123";
    String id3 = "9213";
    String irrelevantCategory = "Hardware";
    insertIntoDatabase(
            createProductWithCategory(id1, relevantCategory),
            createProductWithCategory(id2, relevantCategory),
            createProductWithCategory(id3, irrelevantCategory)
    );
    String responseJson = requestProductsByCategory(relevantCategory);

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly(id1, id2);
}

这样更好👍

@Test
public void variables() throws Exception {
    insertIntoDatabase(
            createProductWithCategory("4243", "Office"),
            createProductWithCategory("1123", "Office"),
            createProductWithCategory("9213", "Hardware")
    );

    String responseJson = requestProductsByCategory("Office");

    assertThat(toDTOs(responseJson))
            .extracting(ProductDTO::getId)
            .containsOnly("4243", "1123");
}

3.11 不要在现有的测试上做新功能的扩展

在已有的测试代码中增加一个case实现起来非常简单,但这会逐渐使这个测试变得更大,更难理解,你很难去掌握这个大测试涵盖了哪些测试案例。而且如果这个测试失败了,很难看出到底是什么地方出了问题。

相反,最好为新的逻辑或执行路径创建一个新的测试方法,并且使用一个描述性的名字表达出来这个方法的预期行为。这肯定会需要更多的编码工作,但创建出的测试是更加清晰和明确的。

简而言之,如果想要验证多个场景,需要创建多个单独的测试用例,而不是向同一个单元测试添加多个断言。

不要这么做👎

public class ProductControllerTest {
    @Test
    public void happyPath() {
        // a lot of code comes here...
    }
}

这样更好👍

public class ProductControllerTest {
    @Test
    public void multipleProductsAreReturned() {}
    @Test
    public void allProductValuesAreReturned() {}
    @Test
    public void filterByCategory() {}
    @Test
    public void filterByDateCreated() {}
}
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u013620635/article/details/127210172

java开发阶段总结(下)——方法的设计、单元测试、代码review_x362829417的博客-爱代码爱编程

7、工程规划中的编程风格与抽象层级与方法设计的看法         很多人认为在做工程设计的时候就要有接口、抽象类,要弄一些实现类、子类,信奉这样一句话就是面向接口编程会给你带来无穷的好处,但我认为这东西以及这些东西带来的好处还是分阶段分层次的。如果说你当前这个阶段的业务逻辑就是非常简单且基本上不怎么扩展的话,要这些抽象、接口就没什么用,因为这个阶段你没有

spring-boot写入、解析csv,支持上传、下载_regulus_li的博客-爱代码爱编程_springboot 解析csv

工作中遇到了将所有数据整合成CSV文件并下载、上传CSV文件并解析,这两个需求。现在把处理方法记录下来,作为总结。 项目构建 jar包引入 CSV的解析和写入使用到的是commons-csv的包,pom中的定义如下

java代码生成器——基于模板快速生成web项目结构_regulus_li的博客-爱代码爱编程

功能介绍 根据数据库表的元数据生成支持Rest、RPC协议的工程服务(标准化的代码分层结构工程)。 加速新工程的建设。 代码结构: 生成代码的结构依赖于模板的定义。本工程中定义分为三个工程 ${projectName

软测学习笔记——软件测试基础理论(记忆篇)_闲小憨的博客-爱代码爱编程

一、测试基础概念 1、软件测试的目标 预防错误和发现错误 2、软件测试对象有哪些?如何测试这些对象? 测试对象:程序+数据+文档 程序:单元测试、集成测试、系统测试、验收测试等 数据:输入与输出,数据库等 文档:需求

利用注解指定Spring启动时加载的bean-爱代码爱编程

在开发的过程中,一个接口往往有多个实现类。但根据需求,不一定会使用到所有的实现类。 以本人当前遇到的需求为例,一个系统不同的国家部署时,需要使用不同的实现类。在此给出基于注解的实现方法。 1.注解定义 @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) public @inter

敏捷中的测试价值观及实施方法(理论篇)-爱代码爱编程

一、敏捷测试价值观 价值驱动测试:不为测试测试而测试,也不为出统计数据而做测试目标赋能:以共同业务为目标,业务驱动测试社区文化:鼓励学习型文化,共同学习、共同进步全员测试:整个团队要对交付的质量负责,而不仅仅是测试二、敏捷团队中测试/测试leader的具体实施 促进测试文化提升/用户需求、反馈/指导团队质量工作 获取和明确用户的质量期望建立合适的系统

软件测试理论基础-爱代码爱编程

什么是软件测试软件测试是为了发现错误而执行程序的过程。或者说,软件测试是根据软件开发各阶段的规格说明和程序的内部结构而精心设计一批测试用例(即输入数据及其预期的输出结果),并利用这些测试用例去运行程序,以发现程序错误的过程。 软件测试在软件生存期中横跨两个阶段:通常在编写出每一个模块之后就对它做必要的测试(称为单元测试)。模块的编写者与测试者是同一个人。

软件测试-常见理论概念整理(一)-爱代码爱编程

软件测试-理论概念整理(一) 文章目录 软件测试-理论概念整理(一)1.软件产品质量模型1.1功能性1.2可靠性1.3易用性1.4效率1.5可维护性1.6可移植性2.软件测试3.软件测试分类-测试方法3.1 是否考虑覆盖源代码(内部结构)【重要!】3.1.1 黑盒测试(在另一篇有专门详细的说明)3.1.2 白盒测试3.1.3 灰盒测试3.2

关于测试基础理论知识的整理(其一)-爱代码爱编程

前言 现在基于目前我们国内大部分人员对于测试工作的认知不够,总有人说测试是点点点的工作没有技术含量。这种错误认知导致使得一部分人听信了这种不正确的言论,通过各种渠道马马虎虎的了解了一些理论知识之后就匆匆忙忙的踏入的测试行业。但是当你进入了这个行业之后就会发现你所了解的那些知识是远远不够的。互联网行业技术更新迭代快这是所有人都有的普遍认知。但是这个技术的更

【面试宝典】软件测试工程师2021烫手精华版(第一章测试理论篇)-爱代码爱编程

前言: 翻了很多论坛博客关于面试的文章,很多都是不完整的,还都是比较常见规规矩矩的,那大家刷过的基本都不拿出来了,都是一些大家平时见得不多,但是面试官很看中的一些题。 第一章 测试理论 一、 软件工程 阐述软件生命周期都有哪些阶段?常见的软件生命周期模型有哪些? 软件生命周期是指一个计算机软件从功能确定、设计,到开发成功投入使用,并

【ZStack】自动化测试系统1——集成测试-爱代码爱编程

测试,对于一个IaaS软件的可靠性、成熟度和可维护性而言,是一个重要的因素.测试在ZStack中是全自动的。这个自动化测试系统包括了三个部分:集成测试,系统测试,基于模块的测试。其中集成测试构建于Junit之上,使用了模拟器。通过这个集成测试系统提供的各种各样的功能,开发人员可以快速的写出测试用例,用于验证一个新特性或者一个缺陷修复。 概述 这个关键因

如何写软件测试的归档报告?-爱代码爱编程

软件测试是产品研发的重要环节,虽然不似编程与设计那样复杂,但是软件测试非常注重工作流程以及归档总结。 一般情况下软件测试之前需要根据软件的特性制定整体的测试计划,包括业务处理的过程以及整个软件测试的重点在哪里。然后还需要设计测试用例、软件测试执行,最终需要根据测试结果以及修改情况进行归档报告。那如何写软件测试的归档报告呢?下面小编就和大家分享一下。 介

软件测试开发需要具备哪些测试能力?-爱代码爱编程

软件测试开发需要具备哪些测试能力? 测试工作在项目中起到了承上启下的作用,会熟练使用测试工具,做工具开发需要具备一定的代码能力。做个测试要求比较高不仅要懂测试还要回开发敲代码,除此之外你需要用户基础测试能力、环境治理能力、专项测试能力、工具开发能力等,接下来我们就来具体看看。 软件测试开发必备能力一、基础测试能力 测试基础是指测试的基本功,首先要理解