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接口。
在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测试,以及与数据库交互的集成测试(服务测试)。这时的测试策略呈现一个钻石的形状。
最后我想说的是,自动化测试是代码不可或缺的一部分,忽视了测试,即使是新系统,也在向着遗留系统的不归路上冲刺。然而技术社区对于测试的态度总是十分漠视,多年来不曾改观。
有一次我的同事在某语言群里询问,是否有人愿意给一个开源框架添加测试,然而大家想的却是什么“技术进步性”。
开发人员如果忽视编写自动化测试,就放弃了将质量内建到软件(也就是自己证明自己质量)的机会,把质量的控制完全托付给了测试人员。这种靠人力去保证质量的方式,永远也不可能代表“技术先进性”。
有的时候你可能觉得,我就是写了一行代码,加不加测试无所谓吧?反正原来也没有测试。但是,希望你不要这么想,更不要这么做。犯罪心理学中有一个“破窗效应”,意思是说如果一栋楼有几扇窗户是破的,用不了几天所有的窗户都会破掉。这是一个加速熵增的过程,没有测试的系统,就是那座破了窗户的大楼。
你要记住的是“童子军原则”,也就是当露营结束离开的时候,要打扫营地,让它比你来的时候更干净。你写了一行代码,并把这一行代码的测试加上,你就没有去打破一扇新的窗户,而是让系统比你写代码之前变得更好了。这便是引入了一个负熵,让你的系统从无序向着有序迈出了一步。
莫以恶小而为之,莫以善小而不为。
思考题
感谢你听完我的絮絮叨叨,希望今天的课程能唤醒你写测试的意愿。今天的思考题请你分享一段你项目中的代码,并聊一聊你准备如何给它添加测试,别忘了给代码脱敏。
如果你觉得今天的课程对你有帮助,请把它分享给你的同事和朋友,我们一起来写测试吧。
- 黄叶 👍(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