為什么你的測(cè)試應(yīng)該只驗(yàn)證可觀察的行為,而不是實(shí)現(xiàn)細(xì)節(jié)
在本文中,我們將考慮我們的測(cè)試到底應(yīng)該(不)驗(yàn)證什么以防止誤報(bào),以及為什么有時(shí)越少越好。為了更好地理解這個(gè)主題,我們將仔細(xì)研究脆性測(cè)試和可觀察行為的定義,以便我們能夠檢測(cè)設(shè)計(jì)不良的測(cè)試并使其抵抗重構(gòu)。
讓我們開(kāi)始吧!
當(dāng)您的測(cè)試想知道太多時(shí)
回到過(guò)去,在我深入研究自動(dòng)化測(cè)試這個(gè)主題之前,它已經(jīng)發(fā)生在我身上很多次了。究竟是什么?好吧,以防萬(wàn)一,我想確保我的測(cè)試驗(yàn)證了比必要的更多的東西。我曾經(jīng)相信我的測(cè)試包含的斷言和類(lèi)似的陳述越多,它們帶來(lái)的價(jià)值就越大。
雖然上述方法看起來(lái)很合理,但從長(zhǎng)遠(yuǎn)來(lái)看,選擇它會(huì)讓開(kāi)發(fā)人員的生活變得困難。當(dāng)我自己的測(cè)試讓我不得不比我預(yù)期的更頻繁地回到他們身邊時(shí),我很難發(fā)現(xiàn)這一點(diǎn)。一個(gè)理由?事實(shí)證明,這些測(cè)試與實(shí)現(xiàn)細(xì)節(jié)有關(guān),而不是可觀察到的行為,因此,在重構(gòu)時(shí),即使功能仍然可以正常工作,它們也會(huì)失敗。
脆弱的測(cè)試? 可觀察的行為? 實(shí)施細(xì)則?
在我們進(jìn)一步討論之前,讓我們先定義一下這些神秘短語(yǔ)背后的含義,因?yàn)樗鼈儗?duì)于理解如何編寫(xiě)為我們的項(xiàng)目增加真正價(jià)值而不是不必要的包袱的良好測(cè)試至關(guān)重要。
- 它們無(wú)法承受重構(gòu),無(wú)論底層功能是否損壞,它們都會(huì)變紅
換句話說(shuō),重構(gòu)后的功能仍然可以產(chǎn)生正確的結(jié)果,但與此同時(shí),如果他們檢查某些東西是如何工作的,而不是檢查可觀察到的行為是什么,你的測(cè)試可能會(huì)失敗。
- 要使一段代碼成為系統(tǒng)可觀察行為的一部分,它必須執(zhí)行以下操作之一:
- 公開(kāi)幫助客戶(hù)實(shí)現(xiàn)其目標(biāo)之一的操作。 操作是一種執(zhí)行計(jì)算或產(chǎn)生副作用或兩者兼而有之的方法。
- 暴露一種可以幫助客戶(hù)實(shí)現(xiàn)其目標(biāo)之一的狀態(tài)。 狀態(tài)是系統(tǒng)的當(dāng)前狀態(tài)。
- 任何不做這兩件事的代碼都是實(shí)現(xiàn)細(xì)節(jié)。
因此,當(dāng)您在開(kāi)發(fā)新功能時(shí),請(qǐng)考慮調(diào)用您的代碼的客戶(hù)端的真正目標(biāo)是什么(客戶(hù)端代碼期望從我們的解決方案中獲得什么行為,或者您的功能應(yīng)該涵蓋哪些業(yè)務(wù)案例),然后忘記 暫時(shí)您想如何開(kāi)發(fā)該功能(實(shí)現(xiàn)細(xì)節(jié))。
這種方法應(yīng)該讓您在可觀察的行為和實(shí)現(xiàn)細(xì)節(jié)之間有一個(gè)更清晰的區(qū)別。
案例研究:排行榜
讓我們仔細(xì)看看下面用 JAVA 編寫(xiě)的示例:
- 我們正在為一款游戲開(kāi)發(fā)排行榜,我們稱(chēng)這款游戲?yàn)?ldquo;Chase and Race”
- 我們希望我們的排行榜根據(jù)得分返回最佳玩家
Player 類(lèi)負(fù)責(zé)保存玩家的姓名和分?jǐn)?shù)。 分?jǐn)?shù)通過(guò) Player#updateScore 函數(shù)更新。
Leaderboard 類(lèi)允許我們通過(guò) Leaderboard#addPlayer 函數(shù)將玩家添加到排行榜的列表中,并通過(guò) Leaderboard#getBestPlayer 檢索游戲中最好的玩家。
在 LeaderboardTesttest 類(lèi)中,我們正在檢查 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;
}
}
到目前為止,一切都很好——如您所見(jiàn),測(cè)試報(bào)告是綠色的。
后來(lái),我們決定重構(gòu) Leaderboard 類(lèi)的內(nèi)部結(jié)構(gòu),因此每當(dāng)我們向其中添加新玩家時(shí),它都會(huì)按降序?qū)ν婕伊斜磉M(jìn)行排序:
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;
}
}
讓我們省略關(guān)于是否需要此更改的討論——我想向您展示的是更改實(shí)現(xiàn)細(xì)節(jié)如何影響現(xiàn)有測(cè)試。
如您所見(jiàn),測(cè)試報(bào)告變?yōu)榧t色,但可觀察到的行為保持不變——Leaderboard#getBestPlayer 函數(shù)仍然正常工作,它返回得分最高的玩家。
如何解決這個(gè)問(wèn)題? 如果在重構(gòu)代碼庫(kù)的情況下,一次編寫(xiě)的測(cè)試不需要我們額外關(guān)注,那將是最好的。 為此,Leaderboard#players 列表應(yīng)該無(wú)法從外部訪問(wèn),因此使用 private 修飾符標(biāo)記這個(gè)集合就足夠了:
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);
}
}
但是測(cè)試呢? 它現(xiàn)在有一個(gè)編譯錯(cuò)誤:
由于您的測(cè)試應(yīng)該只驗(yàn)證可觀察的行為,我們可以安全地從第 25 行刪除斷言,因?yàn)樗鼨z查實(shí)現(xiàn)細(xì)節(jié),這使得該測(cè)試變得脆弱。
結(jié)論
在項(xiàng)目中進(jìn)行脆弱測(cè)試的后果可能非常嚴(yán)重。 例如,這樣的測(cè)試可能會(huì)阻止開(kāi)發(fā)人員重構(gòu)代碼,因?yàn)槔蠈?shí)說(shuō)——當(dāng)你完成重構(gòu)時(shí),這導(dǎo)致了一堆失敗的測(cè)試,這并不一定會(huì)讓你心情愉快。 另一個(gè)可能的后果是,開(kāi)發(fā)人員可以習(xí)慣于發(fā)出錯(cuò)誤警報(bào)的測(cè)試,從而降低他們的整體警覺(jué)性,從而使錯(cuò)誤潛入生產(chǎn)環(huán)境的機(jī)會(huì)增加。