跳转至

08 代码现代化:你的代码可测吗?

你好,我是姚琪琳。

从今天开始,我将用三讲来介绍代码现代化的主要模式。它们大体的脉络是这样的:

1.先对代码做可测试化重构,并添加测试;
2.在测试的保护下,安全地重构;
3.在测试的保护下,将代码分层。

我们今天先来看看如何让代码变得可测,这是遗留系统现代化的基本功,希望你重视起来。

一个软件的自动化测试,可以从内部表达这个软件的质量,我们通常管它叫做内建质量(Build Quality In)

然而国内的开发人员普遍缺乏编写自动化测试的能力,一方面是认为没什么技术含量,另一方面是觉得质量是测试人员的工作,与自己无关。然而你有没有想过,正是因为这样的误区,才导致软件的质量越来越差,系统也一步步沦为了遗留系统。

我虽然在第六节课分享了可以不加测试就重构代码的方法,但添加测试再重构的方法更加扎实,一步一个脚印。

你的代码可测吗?

我们先来看看不可测的代码都长什么样,分析出它们不可测的原因,再“按方抓药”。

可测的代码很相似,而不可测的代码各有各的不可测之处。我在第二节课举过一个不可测代码的例子,现在我们来一起复习一下:

public class EmployeeService {
  public EmployeeDto getEmployeeDto(long employeeId) {
    EmployeeDao employeeDao = new EmployeeDao();
    // 访问数据库获取一个Employee
    Employee employee = employeeDao.getEmployeeById(employeeId);
    // 其他代码  
  }
}

这段代码之所以不可测,是因为在方法内部直接初始化了一个可以访问数据库的Dao类,要想测试这个方法,就必须访问数据库了。倒不是说所有测试都不能连接数据库,但大多数直连数据库的测试跑起来都太慢了,而且数据的准备也会相当麻烦。

这属于不可测代码的第一种类型:在方法中构造了不可测的对象

我们再来看一个例子,与上面的代码非常类似:

public class EmployeeService {
  public EmployeeDto getEmployeeDto(long employeeId) {
    // 访问数据库获取一个Employee
    Employee employee = EmploeeDBHelper.getEmployeeById(employeeId);
    // 其他代码
  }
}

这段代码同样是不可测的,它在方法中调用了不可测的静态方法,因为这个静态方法跟前面的实例方法一样,也访问了数据库。

除了不能在测试中访问真实数据库以外,也不要在测试中访问其他需要部署的中间件、服务等,它们也会给测试带来极大的不便。

在测试中,我们通常把被测的元素(可能是组件、类、方法等)叫做SUT(System Under Test),把SUT所依赖的组件叫做DOC(Depended-on Component)。导致SUT无法测试的原因,通常都是DOC在当前的测试上下文中不可用

DOC不可用的原因通常有三种:

1.不能访问。比如DOC访问了数据库或其他需要部署的中间件、服务等,而本地环境没有这些组件,也很难部署这些组件。

2.不是当前测试期望的返回值。即使本地能够访问这些组件,但它们却无法返回我们想要的值。比如我们想要获取ID为1的员工信息,但数据库中却并没有这条数据。

3.执行这些DOC的方法会引发不想要的副作用。比如更新数据库,会破坏已有数据,影响其他测试。另外连接数据库,还会导致测试执行时间变长,这也是一种副作用。

要让SUT可测,就得让DOC可用,有哪些办法呢?

如何让代码变得可测?

其实很简单,就是要让DOC的行为可变。这种变化可以让DOC在测试时不再直接访问数据库或其他组件,从而变得“可以访问”、“能返回期望的值”以及“不会产生副作用”。

如何才能让DOC的行为可变呢?如果DOC是静态的或是在SUT内构造的,那自然不可改变。所以,我们要让DOC的构造和SUT本身分离,SUT只需使用外部构造好的DOC的实例,而不用关心它的构造。

这种可以让DOC的行为发生改变的位置,叫做接缝(seam),这是Michael Feathers在《修改代码的艺术》这本书里提出来的。

接缝这个隐喻非常形象,如果是一整块没有接缝的布料,你就无法做任何改动,它始终就是一块平面的布料。有了接缝,你不但可以连接不同的布料,还可以改变一块布的方向,从平面变得立体。有了接缝,身为“裁缝”的我们才可以充分发挥想象力,制作出各种丰富多彩的成品。

我把这种在SUT中创建接缝从而使SUT变得可测的方式,叫做提取接缝模式

想要用好这个模式,我们需要了解何处下剪子,针法选什么合适。也就是接缝的位置和类型,下面我们就结合代码例子分别看看。

接缝的位置

我在第二节课介绍了一种提取接缝模式的应用,也就是把EmployeeDao提取成EmployeeService的字段,并通过EmployeeService的构造函数注入进来。

public class EmployeeService {
  private EmployeeDao employeeDao;
  public EmployeeService(EmployeeDao employeeDao) {
    this.employeeDao = employeeDao;
  }

  public EmployeeDto getEmployeeDto(long employeeId) {
    Employee employee = employeeDao.getEmployeeById(employeeId);
    // 其他代码
  }
}

除了构造函数,接缝也可以位于方法参数中,即:

public class EmployeeService {
  public EmployeeDto getEmployeeDto(long employeeId, EmployeeDao employeeDao) {
    Employee employee = employeeDao.getEmployeeById(employeeId);
    // 其他代码
  }
}

如果你使用了依赖注入工具(比如Spring),也可以给字段加@Autowired注解,这样接缝的位置就成了字段。对于这三种接缝位置,我更倾向于构造函数,因为它更方便,而且与具体的工具无关。

学习完接缝的位置,我们再来看看接缝的类型。

接缝的类型

接缝的类型是指,通过什么样的方式来改变DOC的行为。第二节课中,提取完接缝后,我创建了一个EmployeeDao的子类,这个子类重写了getEmployeeById的默认行为,从而让这个DOC返回了我们“期望的值”。

public class InMemoryEmployeeDao extends EmployeeDao {
  @Override
  public Employee getEmployeeById(long employeeId) {
    return null;
  }
}

我把这种通过继承DOC来改变默认行为的接缝类型叫做对象接缝

除此之外,还可以将原来的EmployeeDao类重命名为DatabaseEmployeeDao,并提取出一个EmployeeDao接口。然后再让InMemoryEmployeeDao类实现EmployeeDao接口。

public interface EmployeeDao {
    Employee getEmployeeById(long employeeId);
}

在EmployeeService中,我们仍然通过构造函数来提供这个接缝,代码基本上可以保持不变。这样,我们和对象接缝一样,只需要在构造EmployeeService的时候传入InMemoryEmployeeDao就可以改变默认的行为,之后的测试也更方便。

这种通过将DOC提取为接口,并用其他实现类来改变默认行为的接缝类型,就叫做接口接缝

如果你的代码依赖的是一个接口,那么这种依赖或者说耦合就是很松散的。在接口本身不发生改变的前提下,不管是修改实现了该接口的类,还是添加了新的实现类,使用接口的代码都不会受到影响。

新生和外覆

提取了接缝,你就可以对遗留代码添加测试了。但这时你可能会说,虽然接缝很好,但是很多复杂的代码依赖了太多东西,一个个都提取接缝的话,需要很长时间,但无奈工期太紧,不允许这么做啊。

不要紧,《修改代码的艺术》中还介绍了两种不用提取接缝就能编写可测代码的模式,也就是新生(sprout)和外覆(wrap)

假设我们有这样一段代码,根据传入的开始和结束时间,计算这段时间内所有员工的工作时间:

public class EmployeeService {
    public Map<Long, Integer> getWorkTime(LocalDate startDate, LocalDate endDate) {
        EmployeeDao employeeDao = new EmployeeDao();
        List<Employee> employees = employeeDao.getAllEmployees();
        Map<Long, Integer> workTimes = new HashMap<>();
        for(Employee employee : employees) {
            WorkTimeDao workTimeDao = new WorkTimeDao();
            int workTime = workTimeDao.getWorkTimeByEmployeeId(employee.getEmployeeId(), startDate, endDate);
            workTimes.put(employee.getEmployeeId(), workTime);
        }
        return workTimes;
    }
}

我知道这段代码有很多槽点,但更痛的现实状况是:你根本没有时间去优化,因为新的需求已经来了,并且明天就要提测。

需求是这样的,业务人员拿到工时的报表后发现,有很多员工的工时都是0,原来他们早就离职了。现在要求你修改一下代码,过滤掉那些离职的员工。

如果不需要写测试,这样的需求对你来说就是小事一桩,你一定轻车熟路。

你可以在EmployeeDao中添加一个新的查询数据库的方法getAllActiveEmployees,只返回在职的Employee。也可以仍然使用getAllEmployees,并在内存中进行过滤。

public class EmployeeService {
    public Map<Long, Integer> getWorkTime(LocalDate startDate, LocalDate endDate) {
        EmployeeDao employeeDao = new EmployeeDao();
        List<Employee> employees = employeeDao.getAllEmployees()
                .stream()
                .filter(e -> e.isActive())
                .collect(toList());
        // 其他代码
    }
}

这样的修改不仅在遗留系统中,即使在所谓的新系统中,也是十分常见的。需求要求加一个过滤条件,那我就加一个过滤条件就好了。

然而,这样的代码仍然是不可测的,你加了几行代码,但你加的代码也是不可测的,系统没有因你的代码而变得更好,反而更糟了。

更好的做法是添加一个新生方法,去执行过滤操作,而不是在原来的方法内去过滤。

public class EmployeeService {
    public Map<Long, Integer> getWorkTime(LocalDate startDate, LocalDate endDate) {
        EmployeeDao employeeDao = new EmployeeDao();
        List<Employee> employees = filterInactiveEmployees(employeeDao.getAllEmployees());
        // 其他代码
    }
    public List<Employee> filterInactiveEmployees(List<Employee> employees) {
        return employees.stream().filter(e -> e.isActive()).collect(toList());
    }
}

这样一来,新生方法是可测的,你可以对它添加测试,以验证过滤逻辑的正确性。原来的方法虽然仍然不可测,但我们也没有让它变得更糟。

除了新生,你还可以使用外覆的方式来让新增加的功能可测。比如下面这段计算员工薪水的代码。

public class EmployeeService {
    public BigDecimal calculateSalary(long employeeId) {
        EmployeeDao employeeDao = new EmployeeDao();
        Employee employee = employeeDao.getEmployeeById();
        return SalaryEngine.calculateSalaryForEmployee(employee);
    }
}

如果我们现在要添加一个新的功能,有些调用端在计算完薪水后,需要立即给员工发短信提醒,而且其他调用端则保持不变。你脑子里可能有无数种实现方式,但最简单的还是直接在这段代码里添加一个新生方法,用来通知员工。

public class EmployeeService {
    public BigDecimal calculateSalary(long employeeId, bool needToNotify) {
        EmployeeDao employeeDao = new EmployeeDao();
        Employee employee = employeeDao.getEmployeeById();
        BigDecimal salary = SalaryEngine.calculateSalaryForEmployee(employee);
        notifyEmployee(employee, salary, needToNotify);
        return salary;
    }
}

这的确非常方便,但将needToNotify这种标志位一层层地传递下去,是典型的代码坏味道FlagArgument。你也可以在调用端根据情况去通知员工,但那样对调用端的修改又太多太重,是典型的霰弹式修改。

最好的方式是在原有方法的基础上外覆一个新的方法calculateSalaryAndNotify,它会先调用原有方法,然后再调用通知方法。

public BigDecimal calculateSalary(long employeeId) {
    // ...
}
public BigDecimal calculateSalaryAndNotify(long employeeId) {
    BigDecimal salary = calculateSalary(employeeId);
    notifyEmployee(employeeId, salary);
    return salary;
}
public void notifyEmployee(long employeeId, BigDecimal salary) {
    // 通知员工
}

通过这样的修改,调用端只需要根据情况选择调用哪个方法即可,这样的改动量最少。同时你还可以单独测试notifyEmployee,以确保这部分逻辑是正确的。

通过新生和外覆两种模式,我们新编写的代码就是可测的了。而通过提取接缝,旧代码的可测试化重构也可以基本搞定。接下来,我将通过构造函数注入和接口接缝演示一下,如何为这个EmployeeService编写测试。

为代码添加测试

我们先来回顾一下现在EmployeeService的完整代码:

public class EmployeeService {
    private EmployeeDao employeeDao;
    public EmployeeService(EmployeeDao employeeDao) {
        this.employeeDao = employeeDao;
    }
    public EmployeeDto getEmployeeDto(long employeeId) {
        Employee employee = employeeDao.getEmployeeById(employeeId);
        if (employee == null) {
            throw new EmployeeNotFoundException(employeeId);
        }
        return convertToEmployeeDto(employee);
    }
}

我们要添加的测试是当EmployeeDao的getEmployeeById方法返回一个null的时候,EmployeeService的getEmployeeDto方法会抛出一个异常。

public class EmployeeServiceTest {
    @Test
    public void should_throw_employee_not_found_exception_when_employee_not_exists() {
        EmployeeService employeeService = new EmployeeService(new InMemoryEmployeeDao());
        EmployeeNotFoundException exception = assertThrows(EmployeeNotFoundException.class,
            () -> employeeService.getEmployeeDto(1L));
        assertEquals(exception.getEmployeeId(), 1L);
    }
}

我们在测试中使用的InMemoryEmployeeDao,实际上就是一种测试替身(Test Double)。但是它只返回了null,有点单一,想测试正常的情况就没法用了。如果想让这个方法返回不同的值,再添加一个EmployeeDao的实现着实有点麻烦。这时可以使用Mock框架,让它可以针对不同的测试场景返回不同的值。

@Test
public void should_return_correct_employee_when_employee_exists() {
    EmployeeDao mockEmployeeDao = Mockito.mock(EmployeeDao.class);
    when(mockEmployeeDao.getEmployeeById(1L)).thenReturn(givenEmployee("John Smith"));
    EmployeeService employeeService = new EmployeeService(mockEmployeeDao);
    EmployeeDto employeeDto = employeeService.getEmployeeDto(1L);

    assertEquals(1L, employeeDto.getEmployeeId());
    assertEquals("John Smith", employeeDto.getName());
}

这里我们使用了Mockito这个Java中最流行的Mock框架。想了解更多关于测试替身和Mock框架的知识,可以参考郑晔老师的专栏文章

好了,代码也可测了,我们也知道怎么写测试了,那么应该按什么样的思路去添加测试呢?上面这种简单的例子,我相信你肯定是知道要怎么加测试,但是遗留系统中的那些“祖传”代码真的是什么样的都有,对于这种复杂代码,应该怎么去添加测试呢?

决策表模式

我们以著名的镶金玫瑰重构道场的代码为例,来说明如何为复杂遗留代码添加测试。

public void updateQuality() {
   for (int i = 0; i < items.length; i++) {
       if (!items[i].name.equals("Aged Brie")
               && !items[i].name.equals("Backstage passes to a TAFKAL80ETC concert")) {
           if (items[i].quality > 0) {
               if (!items[i].name.equals("Sulfuras, Hand of Ragnaros")) {
                   items[i].quality = items[i].quality - 1;
               }
           }
       } else {
           if (items[i].quality < 50) {
               items[i].quality = items[i].quality + 1;

               if (items[i].name.equals("Backstage passes to a TAFKAL80ETC concert")) {
                   if (items[i].sellIn < 11) {
                       if (items[i].quality < 50) {
                           items[i].quality = items[i].quality + 1;
                       }
                   }

                   if (items[i].sellIn < 6) {
                       if (items[i].quality < 50) {
                           items[i].quality = items[i].quality + 1;
                       }
                   }
               }
           }
       }

       if (!items[i].name.equals("Sulfuras, Hand of Ragnaros")) {
           items[i].sellIn = items[i].sellIn - 1;
       }

       if (items[i].sellIn < 0) {
           if (!items[i].name.equals("Aged Brie")) {
               if (!items[i].name.equals("Backstage passes to a TAFKAL80ETC concert")) {
                   if (items[i].quality > 0) {
                       if (!items[i].name.equals("Sulfuras, Hand of Ragnaros")) {
                           items[i].quality = items[i].quality - 1;
                       }
                   }
               } else {
                   items[i].quality = items[i].quality - items[i].quality;
               }
           } else {
               if (items[i].quality < 50) {
                   items[i].quality = items[i].quality + 1;
               }
           }
       }
   }
}

这是非常典型的遗留代码,if/else满天飞,可谓眼花缭乱;而且分支的规则不统一,有的按名字去判断,有的按数量去判断。

对于这种分支条件较多的代码,我们可以梳理需求文档(如果有的话)和代码,找出所有的路径,根据每个路径下各个字段的数据和最终的值,制定一张决策表,如下图所示。

图片

比如第一行,我们要测的是,系统每天会自动给所有商品的保质期和品质都减1,那么给出的条件是商品类型为normal,保质期为4天,品质为1,所期望的行为是保质期和品质都减少1。而第二行则是测试,当保质期减为0之后,品质会双倍地减少。以此类推,我们一共梳理出18个测试场景。

你会看到,这种决策表不但清晰地提供了所有测试用例,而且给出了相应的数据,你可以很轻松地基于它来构建整个方法的测试。

测试的类型和组织

说完了如何添加测试,我们接下来看看可以添加哪些类型的测试。

Mike Cohn在十几年前曾经提出过著名的“测试金字塔”理论,将测试划分为三个层次。从上到下分别是:UI测试、服务测试和单元测试。它们累加在一起,就像一个金字塔一样。需要指出的是,遗留系统很难应用测试金字塔,但我们还是有必要先来看看这三层测试都包含哪些内容。

图片

其中,最顶层的UI测试是指从页面点击到数据库访问的端到端测试,用自动化的方式去模拟用户或测试人员的行为。

早年间的所谓自动化测试大多都属于这种。但这样的测试十分不稳定,一个简单的页面调整就可能导致测试失败。

尽管现在很多工具推出了headless的方式,可以绕开页面,但它们仍然运行得很慢。而且还需要与很多服务、工具集成起来,环境的搭建也是个问题。所以UI测试位于测试金字塔的顶端,即只需要少量的这种测试,来验证服务和中间件等相互之间的访问是正常的。

需要指出的是,UI测试并不是针对前端元素或组件的测试。后者其实是前端的单元测试。

中间这层的服务测试也是某种意义的端到端测试,但它避开了UI的复杂性,而是直接去测试UI会访问的API。也有人管这种测试叫集成测试。它可以直接访问数据库,也可以用H2或SQLite等文件或内存数据库代替;它也可以直接访问其他服务,也可以用Moco等工具来模拟服务的行为。

这种测试的好处是,可以测试API级别的端到端行为,不管内部的代码如何重构,只要API的契约保持不变,测试就不需要修改。

最底层的单元测试就是对某个方法的测试,平时开发同学写的大多是这方面的测试。测试金字塔的建议是,尽可能多地写单元测试,它编写成本低、运行速度快,是整个测试金字塔的基座。

对于方法内用到的DOC,你可以用测试替身来替换。对于在多大程度上使用测试替身,有两种不同的观点。

一种观点认为不管任何依赖都应该使用测试替身来代替;一种观点则认为只要DOC不访问数据库、不访问文件系统、不访问进程外的服务或中间件,就可以不用测试替身。前者的学名叫solitary unit test,我管它叫“社恐症单元测试”;后者学名叫做sociable unit test,我管它叫“交际花单元测试”。

到底应该使用哪种类型的单元测试呢?这一点并没有定论,支持每种类型的人都不少。我个人更倾向于交际花单测,因为这样写起来更容易,而且对重构很友好。

遗留系统中的测试策略

学完了测试金字塔,你是不是已经准备按照由少到多的关系,在遗留系统中补测试了呢?先别急,遗留代码有很多特点,导致它们并不适合完全应用测试金字塔来组织测试。

首先,遗留系统的很多业务逻辑位于数据库的存储过程或函数中,代码只是用来传递参数而已。这样一来单元测试根本测不到什么东西。你也不能在服务测试(或API测试)中使用内存数据库,因为要在这些数据库中复制原数据库中的存储过程或函数,可能会有很多语法不兼容。

其次,遗留系统的很多业务还位于前端页面中,位于JSP、PHP、ASP的标签之中。这部分逻辑也是没法用单元测试来覆盖的。而服务测试脱离了页面,显然也无法覆盖。

因此,如果你的遗留系统在前端和数据库中都有不少业务逻辑,就可以多写一些UI测试,它们可以端到端地覆盖这些业务逻辑。你可以从系统中最重要的业务开始编写UI测试。

然而,UI测试毕竟十分不稳定,运行起来也很慢,当数量上来后这些缺点就会被放大。这时候你可以多编写一些服务测试。

对于数据库中的业务逻辑,你可以搭建一些基础设施,让开发人员可以在测试中直连数据库,并方便地编写集成测试。这些基础设施包括:

  • 一个数据库镜像,可以快速在本地或远端做初始化;需要将数据库容器化,满足开发和测试人员使用个人数据库的需求。
  • 一个数据复制工具,可以方便地从其他测试环境拉取数据到这个镜像,以方便数据的准备;可以考虑通过CI/CD来实现数据的复制。

除此之外,你可能还需要一个数据对比工具,用来帮你在重构完代码后,比较数据库的状态。比如将一个存储过程或函数中的逻辑迁移到Java代码中的时候,要验证迁移的正确性,只跑通集成测试是远远不够的,还要全方位地比较数据库中相关表的数据,以防漏掉一些不起眼的地方。

对于前端中的业务逻辑,你可以先重构这些逻辑,将它们迁移到后端中(我将在第十三节课详细讲解如何迁移),然后再编写单元测试或服务测试。

这时的测试策略有点像一个钻石的形状。

图片

确定好了测试的类型,还有一些测试编写方面的小细节我想跟你分享。

第一个细节是测试的命名。关于测试命名,不同书籍中都有不同的推荐,但我更倾向于像第四节课中介绍的那样,用“实例化需求”的方式,从业务角度来命名测试,使得测试可以和代码一起演进,成为活文档。

第二个细节是测试的组织。当测试变多时,如果不好好对测试进行分组,很快就会变得杂乱无章。这样的测试即使是活文档,也会增加认知负载。

最好的方法是,将单个类的测试都放在同一个包中,将不同方法的测试放在单独的测试类里。而对于同一个方法,要先写它Happy path的测试,再写Sad path。记住一个口诀:先简单,再复杂;先正常,再异常。也就是测试的场景要先从简单的开始,逐步递进到复杂的情况;而测试的用例要先写正常的Case,再逐步递进到异常的Case。

小结

今天学习的知识比较密集,需要好好总结一下。

我们首先学习了接缝的位置接缝的类型。接缝的位置是指那些可以让DOC的行为发生改变的位置,有构造函数、方法参数、字段三种;而接缝的类型则是说改变DOC行为的方式,包括对象接缝和接口接缝。

遗留代码中有了接缝,就可以着手写测试了。然而复杂的遗留代码很难去梳理清楚头绪,我想你推荐用决策表的方式,将测试用例一一列出来。

在遗留系统中,如果存储过程中包含大量的业务逻辑,传统的金字塔型的测试策略可能并不适合,你可以多写一些端到端的UI测试,以及与数据库交互的集成测试(服务测试)。这时的测试策略呈现一个钻石的形状。

图片

最后我想说的是,自动化测试是代码不可或缺的一部分,忽视了测试,即使是新系统,也在向着遗留系统的不归路上冲刺。然而技术社区对于测试的态度总是十分漠视,多年来不曾改观。

有一次我的同事在某语言群里询问,是否有人愿意给一个开源框架添加测试,然而大家想的却是什么“技术进步性”。

图片

开发人员如果忽视编写自动化测试,就放弃了将质量内建到软件(也就是自己证明自己质量)的机会,把质量的控制完全托付给了测试人员。这种靠人力去保证质量的方式,永远也不可能代表“技术先进性”。

有的时候你可能觉得,我就是写了一行代码,加不加测试无所谓吧?反正原来也没有测试。但是,希望你不要这么想,更不要这么做。犯罪心理学中有一个“破窗效应”,意思是说如果一栋楼有几扇窗户是破的,用不了几天所有的窗户都会破掉。这是一个加速熵增的过程,没有测试的系统,就是那座破了窗户的大楼。

你要记住的是“童子军原则”,也就是当露营结束离开的时候,要打扫营地,让它比你来的时候更干净。你写了一行代码,并把这一行代码的测试加上,你就没有去打破一扇新的窗户,而是让系统比你写代码之前变得更好了。这便是引入了一个负熵,让你的系统从无序向着有序迈出了一步。

莫以恶小而为之,莫以善小而不为。

思考题

感谢你听完我的絮絮叨叨,希望今天的课程能唤醒你写测试的意愿。今天的思考题请你分享一段你项目中的代码,并聊一聊你准备如何给它添加测试,别忘了给代码脱敏。

如果你觉得今天的课程对你有帮助,请把它分享给你的同事和朋友,我们一起来写测试吧。

精选留言(15)
  • 黄叶 👍(9) 💬(1)

    老师的课很棒,这里可以的话可以参考一下下面资料: 1.《重构2》3.8,关于霰弹式修改的问题 2.关于祖传代码:可以去这个网站看看,这是一个针对祖传代码的练习,看他是如何对祖传代码做重构的:https://linesh.gitbook.io/refactoring/xiang-jin-mei-gui/2-project-analyze 3.感觉老师的课,和TDD项目实战70讲很搭,可以结合看看

    2022-04-28

  • aoe 👍(2) 💬(2)

    再分享一个测试 public class SplitUtil { public static final List<Long> toLongList(String str) { return Arrays.stream(StringUtils.split(str, ",")) .map(Long::parseLong) .collect(Collectors.toList()); } } class SplitUtilTest { @Test void to_long_list() { List<Long> list = SplitUtil.toLongList(""); assertThat(list).asList(); list = SplitUtil.toLongList("1"); assertThat(list.size() == 1).isTrue(); assertThat(list.contains(1L)).isTrue(); list = SplitUtil.toLongList("1,2"); assertThat(list.size() == 2).isTrue(); assertThat(list.contains(1L)).isTrue(); assertThat(list.contains(2L)).isTrue(); } }

    2022-05-05

  • 飞翔 👍(1) 💬(1)

    老师 决策表里边throw error的测试 在原code中没有throw error的逻辑呀

    2022-07-08

  • 2022 👍(1) 💬(1)

    老师,请教一下,如果是端到端的自动化测试,怎么保证结果的正确性呢? 比如,从UI修改配置后,下发到另外一个设备,有什么好的办法得到期望的结果?

    2022-04-28

  • peter 👍(1) 💬(1)

    请教老师两个问题: Q1:数据比较,有开源工具吗? Q2:网站测试,有比较通用的自动化测试工具吗?

    2022-04-27

  • Geek_682837 👍(0) 💬(1)

    如果遇到需要mock的数据比较多,怎么比较方便构建mock数据,比如我需要mock一个List,可能有几十条数据的大小,自己mock的话就比较麻烦,如果直接访问数据库就可以比较方便拿到,但此时就依赖数据库了

    2023-06-14

  • 跫音 👍(0) 💬(1)

    老师,请教一个问题,将单个类的测试都放在同一个包中,将不同方法的测试放在单独的测试类里。这样会不会导致测试类以及包很多?目前我们都是一个类对应一个测试类

    2023-04-12

  • Geek_70dc13 👍(0) 💬(1)

    既然访问 db 的操作都是 mock ,那我如何保障 sql 那部分的正确性呢?看老师给的方案是集成测试。然而我一般使用的策略就是起 inmemory db,通过塞测试数据的方式,也会测 sql 那部分,缺点就是造数据和运行稍慢。 另外单元测试和集成测试的界限我可能并没有分的很清,单元测试是指每一层只测自己层的功能吗?比如:service 只测 service 的逻辑,mock掉 dao就是单元测试,不mock 掉dao也算是集成测试,还是说集成测试一定要构造 request 的方式

    2022-07-23

  • 飞翔 👍(0) 💬(1)

    老师 有没有手把手教怎么创建决策表 还是不是很理解怎么创造出来的

    2022-07-08

  • IanFaye 👍(0) 💬(2)

    老师,请问浏览器兼容性问题如何测试定位问题呢?

    2022-06-07

  • Michael 👍(0) 💬(2)

    请教老师一个问题,如果说我有一个方法它的逻辑很复杂,但是我这个方法没有返回值,这种情况应该怎么去重构呢?

    2022-05-29

  • aoe 👍(0) 💬(2)

    分享一个简单但却非常重要的测试,因为如果数据重复了,将导致支付功能异常 @Getter public enum PayWayEnum { APPLE(1, "Apple支付"), HUAWEI(2, "华为支付"), ALI_APP(3, "支付宝App支付"), WX_APP(4, "微信App支付"), ; private int value; private String name; PayWayEnum(int value, String name) { this.value = value; this.name = name; } } class PayWayEnumTest { @Test void no_duplicate_value(){ PayWayEnum[] values = PayWayEnum.values(); long size = Arrays.stream(values) .map(PayWayEnum::getValue) .distinct().count(); assertEquals(values.length, size); } }

    2022-05-04

  • fliyu 👍(1) 💬(0)

    mock框架整起来,GoConvey、GoStub、GoMock、GoMonkey和Sqlmock

    2023-02-06

  • Stay_Gold 👍(0) 💬(0)

    看了老师的课程真的惊为天人,从各种细节都可以看出老师是踩过重构和测试的各种坑,然后总结的一套适用于实际工作的修改遗留系统方法。 之前也都给看过《重构》,《修改代码的艺术》这些大名鼎鼎的书籍,但是看完后虽然也感觉很有收获,但是总是再结合实际项目操作的时候达不到想要的感觉。 看到老师的很多描述和实际代码的例子,让自己有了更加深刻的理解。 当然要达到灵活运用还是需要更多的练习,这是逃避不了的。 但是老师的课程真的给我一种豁然开朗的感觉,这也许就是真实项目和真实经验心心相通的原因。

    2024-10-23

  • pcz 👍(0) 💬(0)

    不能同意更多,越是高手,越注重单元测试

    2022-11-23