网站首页 > java教程 正文
为什么你的测试应该只验证可观察的行为,而不是实现细节
在本文中,我们将考虑我们的测试到底应该(不)验证什么以防止误报,以及为什么有时越少越好。为了更好地理解这个主题,我们将仔细研究脆性测试和可观察行为的定义,以便我们能够检测设计不良的测试并使其抵抗重构。
让我们开始吧!
当您的测试想知道太多时
回到过去,在我深入研究自动化测试这个主题之前,它已经发生在我身上很多次了。究竟是什么?好吧,以防万一,我想确保我的测试验证了比必要的更多的东西。我曾经相信我的测试包含的断言和类似的陈述越多,它们带来的价值就越大。
虽然上述方法看起来很合理,但从长远来看,选择它会让开发人员的生活变得困难。当我自己的测试让我不得不比我预期的更频繁地回到他们身边时,我很难发现这一点。一个理由?事实证明,这些测试与实现细节有关,而不是可观察到的行为,因此,在重构时,即使功能仍然可以正常工作,它们也会失败。
脆弱的测试? 可观察的行为? 实施细则?
在我们进一步讨论之前,让我们先定义一下这些神秘短语背后的含义,因为它们对于理解如何编写为我们的项目增加真正价值而不是不必要的包袱的良好测试至关重要。
- 它们无法承受重构,无论底层功能是否损坏,它们都会变红
换句话说,重构后的功能仍然可以产生正确的结果,但与此同时,如果他们检查某些东西是如何工作的,而不是检查可观察到的行为是什么,你的测试可能会失败。
- 要使一段代码成为系统可观察行为的一部分,它必须执行以下操作之一:
- 公开帮助客户实现其目标之一的操作。 操作是一种执行计算或产生副作用或两者兼而有之的方法。
- 暴露一种可以帮助客户实现其目标之一的状态。 状态是系统的当前状态。
- 任何不做这两件事的代码都是实现细节。
因此,当您在开发新功能时,请考虑调用您的代码的客户端的真正目标是什么(客户端代码期望从我们的解决方案中获得什么行为,或者您的功能应该涵盖哪些业务案例),然后忘记 暂时您想如何开发该功能(实现细节)。
这种方法应该让您在可观察的行为和实现细节之间有一个更清晰的区别。
案例研究:排行榜
让我们仔细看看下面用 Java 编写的示例:
- 我们正在为一款游戏开发排行榜,我们称这款游戏为“Chase and Race”
- 我们希望我们的排行榜根据得分返回最佳玩家
Player 类负责保存玩家的姓名和分数。 分数通过 Player#updateScore 函数更新。
Leaderboard 类允许我们通过 Leaderboard#addPlayer 函数将玩家添加到排行榜的列表中,并通过 Leaderboard#getBestPlayer 检索游戏中最好的玩家。
在 LeaderboardTesttest 类中,我们正在检查 Leaderboard#getBestPlayer 方法是否能够返回得分最高的玩家:
package chaseandrace.player;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class Leaderboard {
List<Player> players;
public Leaderboard() {
players = new ArrayList<>();
}
public void addPlayer(Player player) {
this.players.add(player);
}
public Player getBestPlayer() {
return players.stream()
.max(Comparator.comparing(Player::getScore))
.orElse(null);
}
}
package chaseandrace.player;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
public class LeaderboardTest {
@Test
void getPlayerWithHighestScore() {
var playerOne = new Player("I don't know what I'm doing here");
var playerTwo = new Player("Chase me");
var playerThree = new Player("Okie Dokie");
playerOne.updateScore(50);
playerTwo.updateScore(90);
playerThree.updateScore(85);
var board = new Leaderboard();
board.addPlayer(playerOne);
board.addPlayer(playerTwo);
board.addPlayer(playerThree);
var bestPlayer = board.getBestPlayer();
assertEquals(playerTwo, bestPlayer);
assertEquals(bestPlayer, board.players.get(1));
}
}
package chaseandrace.player;
public class Player {
private String name;
private int score;
public Player(String name) {
this.name = name;
}
public int getScore() {
return score;
}
public void updateScore(int points) {
score += points;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
到目前为止,一切都很好——如您所见,测试报告是绿色的。
后来,我们决定重构 Leaderboard 类的内部结构,因此每当我们向其中添加新玩家时,它都会按降序对玩家列表进行排序:
package chaseandrace.player;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class Leaderboard {
List<Player> players;
public Leaderboard() {
players = new ArrayList<>();
}
public void addPlayer(Player player) {
this.players.add(player);
this.players.sort(Comparator.comparing(Player::getScore, Comparator.reverseOrder()));
}
public Player getBestPlayer() {
if (players.isEmpty()) {
return null;
}
return players.get(0);
}
}
package chaseandrace.player;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.jupiter.api.Test;
public class LeaderboardTest {
@Test
void getPlayerWithHighestScore() {
var playerOne = new Player("I don't know what I'm doing here");
var playerTwo = new Player("Chase me");
var playerThree = new Player("Okie Dokie");
playerOne.updateScore(50);
playerTwo.updateScore(90);
playerThree.updateScore(85);
var board = new Leaderboard();
board.addPlayer(playerOne);
board.addPlayer(playerTwo);
board.addPlayer(playerThree);
var bestPlayer = board.getBestPlayer();
assertEquals(playerTwo, bestPlayer);
assertEquals(bestPlayer, board.players.get(1));
}
}
package chaseandrace.player;
public class Player {
private String name;
private int score;
public Player(String name) {
this.name = name;
}
public int getScore() {
return score;
}
public void updateScore(int points) {
score += points;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return name;
}
}
让我们省略关于是否需要此更改的讨论——我想向您展示的是更改实现细节如何影响现有测试。
如您所见,测试报告变为红色,但可观察到的行为保持不变——Leaderboard#getBestPlayer 函数仍然正常工作,它返回得分最高的玩家。
如何解决这个问题? 如果在重构代码库的情况下,一次编写的测试不需要我们额外关注,那将是最好的。 为此,Leaderboard#players 列表应该无法从外部访问,因此使用 private 修饰符标记这个集合就足够了:
package chaseandrace.player;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public class Leaderboard {
private List<Player> players;
public Leaderboard() {
players = new ArrayList<>();
}
public void addPlayer(Player player) {
this.players.add(player);
this.players.sort(Comparator.comparing(Player::getScore, Comparator.reverseOrder()));
}
public Player getBestPlayer() {
if (players.isEmpty()) {
return null;
}
return players.get(0);
}
}
但是测试呢? 它现在有一个编译错误:
由于您的测试应该只验证可观察的行为,我们可以安全地从第 25 行删除断言,因为它检查实现细节,这使得该测试变得脆弱。
结论
在项目中进行脆弱测试的后果可能非常严重。 例如,这样的测试可能会阻止开发人员重构代码,因为老实说——当你完成重构时,这导致了一堆失败的测试,这并不一定会让你心情愉快。 另一个可能的后果是,开发人员可以习惯于发出错误警报的测试,从而降低他们的整体警觉性,从而使错误潜入生产环境的机会增加。
猜你喜欢
- 2024-11-05 Java 进阶之异常处理(java中异常处理)
- 2024-11-05 为什么Java中的const关键字没有实现?
- 2024-11-05 Spring Boot 内置工具类(springboot 工具类调用service)
- 2024-11-05 深入理解Java:注解(Annotation)基本概念(3-1)
- 2024-11-05 Spring Boot集成validation用于优雅的校验API参数的合法性
- 2024-11-05 java初学者,如何学习java?(java该怎么学)
- 2024-11-05 Nacos网关gateway的断言,自定义断言,过滤器使用
- 2024-11-05 恕我直言,在座的各位根本不会写 Java!
- 2024-11-05 JAVA 命令之标准选项(java怎么选择)
- 2024-11-05 Java锁与线程的那些“不可描述”的事儿
你 发表评论:
欢迎- 最近发表
-
- 五,网络安全IDA Pro反汇编工具初识及逆向工程解密实战
- 「JAVA8」- Lambda 表达式(java lambda表达式原理)
- 深入探讨Java代码保护:虚拟机保护技术的新时代
- Nginx反向代理原理详解(图文全面总结)
- 逆向拆解日本IT,哪些Java技术栈薪资溢价高
- mybatis 逆向工程使用姿势不对,把表清空了,心里慌的一比
- Spring Boot集成ProGuard轻松实现Java 代码混淆, Java 应用固若金汤
- 从 Java 代码逆向工程生成 UML 类图和序列图
- 人与人相处:尊重是标配,靠谱是高配,厚道是顶配
- Windows系统安装日期如何修改(windows10怎么修改安装日期)
- 标签列表
-
- java反编译工具 (77)
- java反射 (57)
- java接口 (61)
- java随机数 (63)
- java7下载 (59)
- java数据结构 (61)
- java 三目运算符 (65)
- java对象转map (63)
- Java继承 (69)
- java字符串替换 (60)
- 快速排序java (59)
- java并发编程 (58)
- java api文档 (60)
- centos安装java (57)
- java调用webservice接口 (61)
- java深拷贝 (61)
- 工厂模式java (59)
- java代理模式 (59)
- java.lang (57)
- java连接mysql数据库 (67)
- java重载 (68)
- java 循环语句 (66)
- java反序列化 (58)
- java时间函数 (60)
- java是值传递还是引用传递 (62)
本文暂时没有评论,来添加一个吧(●'◡'●)