--- tags: Coding --- # 軟體測試 所有作業與考試都放在 https://github.com/Bruce762/Software-Testing ## 作業 ### HW1 MLB 用來製作季後賽晉級表 ![image](https://hackmd.io/_uploads/rk7WmtOCA.png) ![image](https://hackmd.io/_uploads/HJlGXtuAA.png) 晉級表觀念圖 ![IMG_1193](https://hackmd.io/_uploads/r1mVRFuCR.jpg) ### pom.xml :::spoiler ```xml= <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>untitled</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>21</maven.compiler.source> <maven.compiler.target>21</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.15.0</version> </dependency> <!-- SLF4J API --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>2.0.7</version> </dependency> <!-- Logback Classic (實現 SLF4J API) --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.4.11</version> </dependency> </dependencies> </project> ``` ::: ### team2023battle.json 沒放完整 大概像這樣 ::: spoiler ```json= { "season": "2023", "teamRecord": [ { "team": "BAL", "wins": 101, "losses": 61, "league": "AL", "division": "East" }, { "team": "TB", "wins": 99, "losses": 63, "league": "AL", "division": "East" }, { "team": "TOR", "wins": 89, "losses": 73, "league": "AL", "division": "East" }, { "team": "NYY", "wins": 82, "losses": 80, "league": "AL", "division": "East" } ], "playoffTeams": [ { "team": "TEX", "series": [ { "opponent": "TB", "advanced": true }, { "opponent": "BAL", "advanced": true }, { "opponent": "HOU", "advanced": true }, { "opponent": "ARI", "advanced": true } ] }, { "team": "TB", "series": [ { "opponent": "TEX", "advanced": false } ] }, { "team": "MIN", "series": [ { "opponent": "TOR", "advanced": true }, { "opponent": "HOU", "advanced": false } ] }, { "team": "TOR", "series": [ { "opponent": "MIN", "advanced": false } ] }, { "team": "ARI", "series": [ { "opponent": "MIL", "advanced": true }, { "opponent": "LAD", "advanced": true }, { "opponent": "PHI", "advanced": true }, { "opponent": "TEX", "advanced": false } ] }, { "team": "MIL", "series": [ { "opponent": "ARI", "advanced": false } ] }, { "team": "PHI", "series": [ { "opponent": "MIA", "advanced": true }, { "opponent": "ATL", "advanced": true }, { "opponent": "ARI", "advanced": false } ] }, { "team": "MIA", "series": [ { "opponent": "PHI", "advanced": false } ] }, { "team": "BAL", "series": [ { "opponent": "TEX", "advanced": false } ] }, { "team": "HOU", "series": [ { "opponent": "MIN", "advanced": true }, { "opponent": "TEX", "advanced": false } ] }, { "team": "ATL", "series": [ { "opponent": "PHI", "advanced": false } ] }, { "team": "LAD", "series": [ { "opponent": "ARI", "advanced": false } ] } ] } ``` ::: ### SeasonData.java :::spoiler ```javascript= package org.example; import java.util.List; public class SeasonData { private String season; private List<TeamRecord> teamRecord; private List<PlayoffTeam> playoffTeams; // Getters and Setters public String getSeason() { return season; } public void setSeason(String season) { this.season = season; } public List<TeamRecord> getTeamRecord() { return teamRecord; } public void setTeamRecord(List<TeamRecord> teamRecord) { this.teamRecord = teamRecord; } public List<PlayoffTeam> getPlayoffTeams() { return playoffTeams; } public void setPlayoffTeams(List<PlayoffTeam> playoffTeams) { this.playoffTeams = playoffTeams; } } class TeamRecord { private String team; private int wins; private int losses; private String league; private String division; // Getters and Setters public String getTeam() { return team; } public void setTeam(String team) { this.team = team; } public int getWins() { return wins; } public void setWins(int wins) { this.wins = wins; } public int getLosses() { return losses; } public void setLosses(int losses) { this.losses = losses; } public String getLeague() { return league; } public void setLeague(String league) { this.league = league; } public String getDivision() { return division; } public void setDivision(String division) { this.division = division; } } class PlayoffTeam { private String team; private List<Series> series; // Getters and Setters public String getTeam() { return team; } public void setTeam(String team) { this.team = team; } public List<Series> getSeries() { return series; } public void setSeries(List<Series> series) { this.series = series; } } class Series { private String opponent; private boolean advanced; // Getters and Setters public String getOpponent() { return opponent; } public void setOpponent(String opponent) { this.opponent = opponent; } public boolean getAdvanced() { return advanced; } public void setAdvanced(boolean advanced) { this.advanced = advanced; } } ``` ::: ### teamPromotion :::spoiler ```java= package org.example; public class teamPromotion { private String name; private int win; public teamPromotion(String name, int win) { this.name = name; this.win = win; } public String getName() { return name; } public int getWin() { return win; } public void setName(String name) { this.name = name; } public void setWin(int win) { this.win = win; } } ``` ::: ### Main :::spoiler ```java= package org.example; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.File; import java.io.IOException; import java.util.*; import java.util.logging.FileHandler; import java.util.logging.Level; import java.util.logging.Logger; import java.util.logging.SimpleFormatter; public class Main { private static final Logger logger = Logger.getLogger(Main.class.getName()); public static void teamsort(List<teamPromotion> obj){ obj.sort(new Comparator<teamPromotion>() { @Override public int compare(teamPromotion o1, teamPromotion o2) { if(o1.getWin()<o2.getWin()){ return 1; }else if(o1.getWin()==o2.getWin() && o1.getName().compareTo(o2.getName())>0){ return 1; }else return -1; } });//這裡排序以防得到的數據是亂的 } public static void allTeamsort(List<TeamRecord> obj){ obj.sort(new Comparator<TeamRecord>() { @Override public int compare(TeamRecord o1, TeamRecord o2) { if(o1.getWins()==o2.getWins() && o1.getLeague().equals(o2.getLeague()) && o1.getDivision().equals(o2.getDivision())){ return o1.getTeam().compareTo(o2.getTeam()); } else if(o1.getLeague().equals(o2.getLeague()) && o1.getDivision().equals(o2.getDivision()) ){ return o2.getWins()-o1.getWins();//當大於0時交換 大的會被換到前面 }else if(o1.getLeague().equals(o2.getLeague())) { return divisnioCmp(o1.getDivision())-divisnioCmp(o2.getDivision()); }else{ return leagueCmp(o1.getLeague())-leagueCmp(o2.getLeague()); } } public int divisnioCmp(String a){ if(a.equals("East"))return 1; else if(a.equals("Central"))return 2; else if(a.equals("West"))return 3; return 0; } public int leagueCmp(String a){ if(a.equals("AL"))return 1; else if(a.equals("NL"))return 2; else return 0; } });//這裡排序以防得到的數據是亂的 } public static Optional<PlayoffTeam> findTeamByName(List<PlayoffTeam> playoffTeams, String teamName) { return playoffTeams.stream() .filter(team -> team.getTeam().equals(teamName)) .findFirst(); } public static String whoWin(String left,String right,List<PlayoffTeam> data)throws Exception{ Optional<PlayoffTeam> optionalTeam = findTeamByName(data, left); if (optionalTeam.isPresent()) { PlayoffTeam targetTeam = optionalTeam.get(); for (int i=0;i<targetTeam.getSeries().size();i++){ if(targetTeam.getSeries().get(i).getOpponent().equals(right)){ if(targetTeam.getSeries().get(i).getAdvanced()){ return left; } else return right; } } return null; } return null; } public static int checkReapeat(String[] obj){ List<String> list = Arrays.asList(obj); for(int i=0;i<obj.length;i++){ if(obj[i]!=null && Collections.frequency(list, obj[i]) > 1){ return i; } } return -1; } public static void main(String[] args) { ObjectMapper objectMapper =new ObjectMapper(); SeasonData Team; List<teamPromotion> AlSeed = new ArrayList<>(); List<teamPromotion> NlSeed = new ArrayList<>(); List<teamPromotion> AlWildCard = new ArrayList<>(); List<teamPromotion> NlWildCard = new ArrayList<>(); String[] Post = new String[32]; String divisionCmp=" "; try{ File jsonFile=new File("src/main/java/org/example/team2023battle.json"); if(!jsonFile.exists()){ throw new IOException("File not found: "+jsonFile.getAbsolutePath()); }//因為new File不會偵測錯誤 所以要在這邊先檢查檔案存不存在 並且列出絕對路徑方便除錯 FileHandler fileHandler = new FileHandler("app.log", true); // true 表示追加到文件中 fileHandler.setFormatter(new SimpleFormatter()); // 設定格式為簡單格式 logger.addHandler(fileHandler); Team=objectMapper.readValue(jsonFile,SeasonData.class);//讀取檔案 for(int i=0;i<Team.getTeamRecord().size();i++){ TeamRecord obj =Team.getTeamRecord().get(i); if((obj.getWins()+obj.getLosses())!=162){ logger.warning(obj.getTeam()+"沒有打滿或超過162場比賽"); }//沒打滿代表常規賽可能還沒打完 易出錯 if(obj.getLeague().equals("America League"))obj.setLeague("AL"); if(obj.getLeague().equals("Natinonal League"))obj.setLeague("NL"); //轉換America League 成AL if(!(obj.getLeague().equals("AL") || obj.getLeague().equals("NL"))){ throw new Exception(obj.getLeague()+" is not AL or NL"); } } allTeamsort(Team.getTeamRecord()); for(int i=0;i<Team.getTeamRecord().size()-3;i++){ if(!Team.getTeamRecord().get(i).getDivision().equals(divisionCmp)){ for (int j=0;j<3;j++){ if(!Team.getTeamRecord().get(j).getDivision().equals(Team.getTeamRecord().get(j+1).getDivision())) throw new Exception("資料給不足或是排序錯誤"); if(!Team.getTeamRecord().get(j).getLeague().equals(Team.getTeamRecord().get(j+1).getLeague())) throw new Exception("資料給不足或是排序錯誤"); }//確保給的資料是完整的沒有缺東缺西 if(Team.getTeamRecord().get(i).getLeague().equals("AL")){ AlSeed.add(new teamPromotion(Team.getTeamRecord().get(i).getTeam(),Team.getTeamRecord().get(i).getWins())); AlWildCard.add(new teamPromotion(Team.getTeamRecord().get(i+1).getTeam(),Team.getTeamRecord().get(i+1).getWins())); AlWildCard.add(new teamPromotion(Team.getTeamRecord().get(i+2).getTeam(),Team.getTeamRecord().get(i+2).getWins())); AlWildCard.add(new teamPromotion(Team.getTeamRecord().get(i+3).getTeam(),Team.getTeamRecord().get(i+3).getWins())); }else if(Team.getTeamRecord().get(i).getLeague().equals("NL")){ NlSeed.add(new teamPromotion(Team.getTeamRecord().get(i).getTeam(),Team.getTeamRecord().get(i).getWins())); NlWildCard.add(new teamPromotion(Team.getTeamRecord().get(i+1).getTeam(),Team.getTeamRecord().get(i+1).getWins())); NlWildCard.add(new teamPromotion(Team.getTeamRecord().get(i+2).getTeam(),Team.getTeamRecord().get(i+2).getWins())); NlWildCard.add(new teamPromotion(Team.getTeamRecord().get(i+3).getTeam(),Team.getTeamRecord().get(i+3).getWins())); } } divisionCmp=Team.getTeamRecord().get(i).getDivision(); } teamsort(AlSeed); teamsort(AlWildCard); teamsort(NlSeed); teamsort(NlWildCard); Post[16]=AlWildCard.get(2).getName(); Post[17]=AlSeed.get(2).getName(); Post[9]=AlSeed.get(1).getName(); Post[20]=AlWildCard.get(1).getName(); Post[21]=AlWildCard.get(0).getName();// Post[11]=AlSeed.get(0).getName(); Post[24]=NlWildCard.get(2).getName(); Post[25]=NlSeed.get(2).getName(); Post[13]=NlSeed.get(1).getName();// Post[28]=NlWildCard.get(1).getName(); Post[29]=NlWildCard.get(0).getName(); Post[15]=NlSeed.get(0).getName(); int tmpcheck=checkReapeat(Post); if(tmpcheck!=-1){ throw new Exception(Post[tmpcheck]+" is repeated team data"); }//檢查如果外卡跟季後賽有任何人重複的話就報錯 for(int i=30;i>0;i-=2){ if(Post[i]!=null && Post[i+1]!=null){ Post[i/2]=whoWin(Post[i],Post[i+1],Team.getPlayoffTeams()); if(Post[i/2]==null)Post[i/2]="?"; } } System.out.printf("%-3s ────┐\n ├───%-3s──┐\n%-3s ────┘ │\n %-3s──┐\n %-3s────────┘ │\n%-3s ────┐ %-3s──┐\n ├───%-3s──┐ │ │\n%-3s ────┘ │ │ │\n %-3s──┘ │\n %-3s────────┘ │\n%-3s ────┐ %-3s\n ├───%-3s──┐ │\n%-3s ────┘ │ │\n %-3s──┐ │\n %-3s────────┘ │ │\n%-3s ────┐ %-3s──┘\n ├───%-3s──┐ │\n%-3s ────┘ │ │\n %-3s──┘\n %-3s────────┘\n",Post[16],Post[8],Post[17],Post[4],Post[9],Post[20],Post[2],Post[10],Post[21],Post[5],Post[11],Post[24],Post[1],Post[12],Post[25],Post[6],Post[13],Post[28],Post[3],Post[14],Post[29],Post[7],Post[15]); }catch(Exception e){ e.printStackTrace(); }; } } ``` ::: ## 期中考考古題與解答 ::: spoiler Q1 MLB 世界大賽票價 (50%) ### 題目: MLB 世界大賽的票價嚇人,起碼 2 萬元起跳。假設規則如下:(1) 一般票價 20,000 (2) 比賽當天若為六日,則價格為 25,000 (3) 如果是內野票價,比上述票價再高 5,000 元; 貴賓席則貴 15,000 元。(4) 如果透過 Ticketmaster 購買可以打九折。 請以等價分割的「強涵蓋」設計測試案例,以表格的方式描述測試案例。 撰寫程式碼並用JUnit 進行完整測試,並說明測試結果與你的完成度。 ### 答案: 首先,测试用例数据保存为 CSV 文件,例如 ticket_price_test_data.csv: ```csv= dayType,seatType,isTicketmaster,expectedPrice 平日,一般,true,18000.0 平日,一般,false,20000.0 平日,内野,true,22500.0 平日,内野,false,25000.0 平日,贵宾席,true,31500.0 平日,贵宾席,false,35000.0 周末,一般,true,22500.0 周末,一般,false,25000.0 周末,内野,true,27000.0 周末,内野,false,30000.0 周末,贵宾席,true,36000.0 周末,贵宾席,false,40000.0 ``` **主程式** ```java= public class TicketPriceCalculator { public static double calculatePrice(String dayType, String seatType, boolean isTicketmaster) { double basePrice; // 基础票价根据比赛日类型 if (dayType.equalsIgnoreCase("平日")) { basePrice = 20000; } else if (dayType.equalsIgnoreCase("周末")) { basePrice = 25000; } else { throw new IllegalArgumentException("无效的比赛日类型"); } // 根据座位类型调整价格 if (seatType.equalsIgnoreCase("一般")) { // 无需调整 } else if (seatType.equalsIgnoreCase("内野")) { basePrice += 5000; } else if (seatType.equalsIgnoreCase("贵宾席")) { basePrice += 15000; } else { throw new IllegalArgumentException("无效的座位类型"); } // 如果通过 Ticketmaster 购买,打九折 if (isTicketmaster) { basePrice *= 0.9; } return basePrice; } } ``` **test程式** ```java= import static org.junit.jupiter.api.Assertions.assertEquals; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvFileSource; public class TicketPriceCalculatorTest { @ParameterizedTest @CsvFileSource(resources = "/ticket_price_test_data.csv", numLinesToSkip = 1) public void testCalculatePrice(String dayType, String seatType, boolean isTicketmaster, double expectedPrice) { double actualPrice = TicketPriceCalculator.calculatePrice(dayType, seatType, isTicketmaster); assertEquals(expectedPrice, actualPrice, 0.001); } } ``` ::: :::spoiler Q2 德州遊騎兵 (50%) ### 題目 2023 美國棒球大聯盟 MLB 落幕,恭喜德州遊騎兵打敗亞利桑那響尾蛇,拿到隊史成軍 63 年以來第一次的世界大賽冠軍。 int score(inningA[], inningB[], playerA[], playerB[]) 會回傳 A 隊勝 B 隊的分數,其中: inningA[], inningB[] 分別紀錄各局的得分。原則上是打滿九局,但如果九上結束後攻者已經領先前攻者,則不需進行九下,分數以 -1 或 X 標記(不可標記為 0)。若九局結束仍然平分,則繼續進行第十局直到勝負。請檢查這兩個資料是否符合常規,若否則拋出例外。 playerA[], playerB[] 分別紀錄兩隊隊員的得分,A 隊隊員得分之總和應與 inningA[] 之個局之總和相同,依此類推。若不符合常規則拋出例外。 若資料檢查無誤,則回傳 A 隊勝 B 隊的分數。若為負數表示 A 隊輸,反之則 A 隊贏。不可能為零。 撰寫程式碼並用 JUnit 進行完整測試,並說明測試結果與你的完成度。 Hint 九局正常結束(沒有提前) inningA, inningB, playerA, playerB [1,1,1,1,1,1,1,1][1,1,1,1,1,1,1,2] [2,0,1,1,1,1,0,2] [1,1,3,0,0,1,1,2] => -1 B win [1,1,1,1,1,1,1,2][1,1,1,1,1,1,1,1] [1,1,3,0,0,1,1,2] [2,0,1,1,1,1,0,2] => 1 A win 至少九局,小於九局就拋出例外 inningA[] = [1,2,3]; inningB[] = [3,4,5] ⇒ Exception(“局數小於九局”) 九局有提前結束 (分數為 X) 如果第 9 局有 X 分數者,則該隊前 8 局的分數和大於另一對的 9 局分數和; 且 X 分者,必定為後攻球隊 [1,1,1,1,1,1,1,1,1] [1,1,1,1,1,1,1,3,X]⇒ -1 (差分 1; B win) [1,1,1,1,1,1,1,1,1] [1,1,1,1,1,1,1,1,X]⇒ Exception(“不合理的提前結束”) 延長 合理正常的延長 [1,1,1,1,1,1,1,1,2][1,1,1,1,1,1,1,1,3] ⇒ -1 B win, 延長到 10局 [1,1,1,1,1,1,1,1,3][1,1,1,1,1,1,1,1,2] ⇒ 1 A win, 延長到 10局 無必要的延長 [1,1,1,1,1,1,1,2,2][1,1,1,1,1,1,1,1,3] => Exception(“沒有必要的延長局”); 九局時 A win 延長局必定上下局都會打 [1,1,1,1,1,1,1,1,1,2] [1,1,1,1,1,1,1,1,1,X]⇒ Exception(“提前結束只可能出現在九下”) 所有全員的總得分 = 該隊為各局的總得分 [1,1,1,1,1,1,1,2][1,1,1,1,1,1,1,1] => [1,1,3,0,0,1,1,2] [2,0,1,1,1,1,0,2] ⇒ A隊的總分為 10, 但A隊全員的總得分只有 9 => Exception(“總分不一致”) 必須分出勝負 (差分 !=0) [1,1,1,1,1,1,1,1][1,1,1,1,1,1,1,1] => Exception(不可以和局) ### 答案 這題如果用csv檔好像會變的很複雜 所以直接把測試資料寫在test裡面 **主程式** ```java= public class BaseballScoreCalculator { public static int score(int[] inningA, int[] inningB, int[] playerA, int[] playerB) throws Exception { // 局数检查 if (inningA.length < 9 || inningB.length < 9) { throw new Exception("局數小於九局"); } // 检查局数是否相等 if (inningA.length != inningB.length) { throw new Exception("兩隊局數不一致"); } int totalInningA = 0; int totalInningB = 0; int lastInningIndex = inningA.length - 1; // 检查提前结束和延长赛 boolean isEarlyTermination = false; boolean isExtraInning = inningA.length > 9; for (int i = 0; i < inningA.length; i++) { int scoreA = inningA[i]; int scoreB = inningB[i]; // 检查是否有提前结束的情况 if (i == 8 && (scoreA == -1 || scoreB == -1)) { isEarlyTermination = true; // 提前结束只能发生在后攻球队,即 B 队(假设 B 队为后攻) if (scoreB != -1) { throw new Exception("不合理的提前結束"); } // 检查前 8 局 B 队的总分是否领先 A 队的 9 局总分 int sumA = 0; int sumB = 0; for (int j = 0; j < 8; j++) { sumA += inningA[j]; sumB += inningB[j]; } sumA += inningA[8]; // A 队第 9 局上半局得分 if (sumB <= sumA) { throw new Exception("不合理的提前結束"); } // 将第 9 局下半局的得分设为 0 以便于计算总分 inningB[8] = 0; break; } // 检查提前结束只可能发生在第 9 局下半局 if (i > 8 && (scoreA == -1 || scoreB == -1)) { throw new Exception("提前結束只可能出現在九下"); } // 累加总分 totalInningA += (scoreA == -1) ? 0 : scoreA; totalInningB += (scoreB == -1) ? 0 : scoreB; } // 检查是否有不必要的延长赛 if (isExtraInning) { int sumA9 = 0; int sumB9 = 0; for (int i = 0; i < 9; i++) { sumA9 += (inningA[i] == -1) ? 0 : inningA[i]; sumB9 += (inningB[i] == -1) ? 0 : inningB[i]; } if (sumA9 != sumB9) { throw new Exception("沒有必要的延長局"); } } // 检查总得分是否一致 int totalPlayerA = 0; for (int score : playerA) { totalPlayerA += score; } if (totalPlayerA != totalInningA) { throw new Exception("總分不一致"); } int totalPlayerB = 0; for (int score : playerB) { totalPlayerB += score; } if (totalPlayerB != totalInningB) { throw new Exception("總分不一致"); } // 计算比分差 int scoreDifference = totalInningA - totalInningB; // 检查比赛是否平局 if (scoreDifference == 0) { throw new Exception("不可以和局"); } return scoreDifference; } } ``` junit test ```java= import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.api.Test; public class BaseballScoreCalculatorTest { @Test public void testNormalGame_AWins() throws Exception { int[] inningA = {1,1,1,1,1,1,1,2}; int[] inningB = {1,1,1,1,1,1,1,1}; int[] playerA = {1,1,3,0,0,1,1,2}; int[] playerB = {2,0,1,1,1,1,0,2}; int result = BaseballScoreCalculator.score(inningA, inningB, playerA, playerB); assertEquals(1, result); } @Test public void testNormalGame_BWins() throws Exception { int[] inningA = {1,1,1,1,1,1,1,1}; int[] inningB = {1,1,1,1,1,1,1,2}; int[] playerA = {2,0,1,1,1,1,0,2}; int[] playerB = {1,1,3,0,0,1,1,2}; int result = BaseballScoreCalculator.score(inningA, inningB, playerA, playerB); assertEquals(-1, result); } @Test public void testLessThanNineInnings() { int[] inningA = {1,2,3}; int[] inningB = {3,4,5}; int[] playerA = {6}; int[] playerB = {12}; Exception exception = assertThrows(Exception.class, () -> { BaseballScoreCalculator.score(inningA, inningB, playerA, playerB); }); assertEquals("局數小於九局", exception.getMessage()); } @Test public void testEarlyTermination_BWins() throws Exception { int[] inningA = {1,1,1,1,1,1,1,1,1}; int[] inningB = {1,1,1,1,1,1,1,3,-1}; int[] playerA = {1,1,1,1,1,1,1,1,1}; int[] playerB = {1,1,1,1,1,1,1,3}; int result = BaseballScoreCalculator.score(inningA, inningB, playerA, playerB); assertEquals(-1, result); } @Test public void testInvalidEarlyTermination() { int[] inningA = {1,1,1,1,1,1,1,1,1}; int[] inningB = {1,1,1,1,1,1,1,1,-1}; int[] playerA = {1,1,1,1,1,1,1,1,1}; int[] playerB = {1,1,1,1,1,1,1,1}; Exception exception = assertThrows(Exception.class, () -> { BaseballScoreCalculator.score(inningA, inningB, playerA, playerB); }); assertEquals("不合理的提前結束", exception.getMessage()); } @Test public void testValidExtraInnings_BWins() throws Exception { int[] inningA = {1,1,1,1,1,1,1,1,2}; int[] inningB = {1,1,1,1,1,1,1,1,3}; int[] playerA = {1,1,1,1,1,1,1,1,2}; int[] playerB = {1,1,1,1,1,1,1,1,3}; int result = BaseballScoreCalculator.score(inningA, inningB, playerA, playerB); assertEquals(-1, result); } @Test public void testValidExtraInnings_AWins() throws Exception { int[] inningA = {1,1,1,1,1,1,1,1,3}; int[] inningB = {1,1,1,1,1,1,1,1,2}; int[] playerA = {1,1,1,1,1,1,1,1,3}; int[] playerB = {1,1,1,1,1,1,1,1,2}; int result = BaseballScoreCalculator.score(inningA, inningB, playerA, playerB); assertEquals(1, result); } @Test public void testUnnecessaryExtraInnings() { int[] inningA = {1,1,1,1,1,1,1,2,2}; int[] inningB = {1,1,1,1,1,1,1,1,3}; int[] playerA = {1,1,1,1,1,1,1,2,2}; int[] playerB = {1,1,1,1,1,1,1,1,3}; Exception exception = assertThrows(Exception.class, () -> { BaseballScoreCalculator.score(inningA, inningB, playerA, playerB); }); assertEquals("沒有必要的延長局", exception.getMessage()); } @Test public void testInvalidEarlyTerminationInExtraInnings() { int[] inningA = {1,1,1,1,1,1,1,1,1,2}; int[] inningB = {1,1,1,1,1,1,1,1,1,-1}; int[] playerA = {1,1,1,1,1,1,1,1,1,2}; int[] playerB = {1,1,1,1,1,1,1,1,1}; Exception exception = assertThrows(Exception.class, () -> { BaseballScoreCalculator.score(inningA, inningB, playerA, playerB); }); assertEquals("提前結束只可能出現在九下", exception.getMessage()); } @Test public void testTotalScoreMismatch() { int[] inningA = {1,1,1,1,1,1,1,2}; int[] inningB = {1,1,1,1,1,1,1,1}; int[] playerA = {1,1,3,0,0,1,1,2}; // Total 9 int[] playerB = {2,0,1,1,1,1,0,2}; // Total 8 Exception exception = assertThrows(Exception.class, () -> { BaseballScoreCalculator.score(inningA, inningB, playerA, playerB); }); assertEquals("總分不一致", exception.getMessage()); } @Test public void testTieGameException() { int[] inningA = {1,1,1,1,1,1,1,1,1}; int[] inningB = {1,1,1,1,1,1,1,1,1}; int[] playerA = {1,1,1,1,1,1,1,1,1}; int[] playerB = {1,1,1,1,1,1,1,1,1}; Exception exception = assertThrows(Exception.class, () -> { BaseballScoreCalculator.score(inningA, inningB, playerA, playerB); }); assertEquals("不可以和局", exception.getMessage()); } } ``` ::: ## 期中考 ::: spoiler 第一題 測試資料 ![截圖 2024-11-06 下午3.01.51](https://hackmd.io/_uploads/rkEhEqOZ1x.png) ```csv= isHoliday,isMember,isPlayoff,isPackageTicket,expect, 0,0,0,0,500, 0,0,0,1,250, 0,0,1,0,1000, 0,0,1,1,1000, 0,1,0,0,400, 0,1,0,1,250, 0,1,1,0,800, 0,1,1,1,800, 1,0,0,0,600, 1,0,0,1,250, 1,0,1,0,1000, 1,0,1,1,1000, 1,1,0,0,480, 1,1,0,1,250, 1,1,1,0,800, 1,1,1,1,800, ``` 主程式 ```java= package org.example; /* 例行賽的時候非假日票價 500 元,假日 600 元,如果是會員則打八折,但如果是買套票則一律為 250 元。 季後賽票價 1000 元,會員打八折但沒有套票,也不分是否為例假日。 */ public class TicketCalculator {//由code review改進程式名稱規範 public static int calPrice(int isHoliday, int isMember, int isPlayoff, int isPackageTicket) throws Exception{ int price; if(isPlayoff==1){ if(isMember==1){ return 800; }else if(isMember==0){ return 1000; } throw new Exception("格式錯誤"); }else if(isPlayoff==0){ if(isPackageTicket==0) { if (isHoliday == 1) {//600 if(isMember==1){ return 480; }else if(isMember==0){ return 600; } throw new Exception("格式錯誤"); } else if (isHoliday == 0) {//500 if(isMember==1){ return 400; }else if(isMember==0){ return 500; } throw new Exception("格式錯誤"); } throw new Exception("格式錯誤"); }else if(isPackageTicket==1){ return 250; } throw new Exception("格式錯誤"); } throw new Exception("格式錯誤"); } } ``` test檔 ```java= package org.example; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvFileSource; import static org.junit.jupiter.api.Assertions.*; class calTest { @ParameterizedTest @CsvFileSource(resources = "/test.csv", numLinesToSkip = 1) void calpriceal(int isHoliday, int isMember, int isPlayoff, int isPackageTicket,int expect)throws Exception { int p= TicketCalculator.calPrice(isHoliday,isMember,isPlayoff,isPackageTicket); assertEquals(p, expect); } } ``` 測試成果 ![截圖 2024-11-06 下午3.43.32](https://hackmd.io/_uploads/HyIO0cOWkl.png) code review報告 ![image](https://hackmd.io/_uploads/H175A9u-ye.png) ::: ## code review ### 法一 maven設定中 要加上build那一大包 注意ruleset的位置要加./ ```xml= <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>untitled1</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>19</maven.compiler.source> <maven.compiler.target>19</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <!--這一大包--> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-pmd-plugin</artifactId> <version>3.25.0</version> <configuration> <rulesets> <!-- 指定使用的 PMD 規則集 --> <ruleset>./src/main/resources/pmd/ruleset.xml</ruleset> </rulesets> </configuration> <executions> <execution> <goals> <!-- 執行 PMD 的 check 工作 --> <goal>check</goal> </goals> </execution> </executions> </plugin> </plugins> </build> <!--到這裡--> </project> ``` ![截圖 2024-10-21 晚上7.25.28](https://hackmd.io/_uploads/HJtA5nQgke.png) 接著到resources創建pmd資料夾 裡面再創兩個檔案 **ruleset.xml 自訂的規則集** ```xml= <?xml version="1.0" encoding="UTF-8"?> <ruleset name="Custom Rules" xmlns="http://pmd.sourceforge.net/ruleset/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 ./ruleset_2_0_0.xsd"> <description>Every Java Rule in PMD</description> <!-- the rules to be checked --> <rule ref="category/java/bestpractices.xml"> <exclude name="SystemPrintln"/> <exclude name="AvoidReassigningParameters"/> </rule> <rule ref="category/java/codestyle.xml" /> <rule ref="category/java/design.xml" /> <rule ref="category/java/documentation.xml" /> <rule ref="category/java/errorprone.xml" /> <rule ref="category/java/multithreading.xml" /> <rule ref="category/java/performance.xml" /> <rule ref="category/java/security.xml" /> <!-- override the rules --> <rule ref="category/java/documentation.xml/CommentSize"> <properties> <property name="maxLines" value="6" /> <property name="maxLineLength" value="80" /> </properties> </rule> </ruleset> ``` **ruleset_2_0_0.xsd 自己子載入xml** ```xml= <?xml version="1.0"?> <xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns="http://pmd.sourceforge.net/ruleset/2.0.0" targetNamespace="http://pmd.sourceforge.net/ruleset/2.0.0" elementFormDefault="qualified"> <xs:element name="ruleset"> <xs:complexType> <xs:sequence> <xs:element name="description" type="xs:string" minOccurs="1" maxOccurs="1" /> <xs:element name="exclude-pattern" type="xs:string" minOccurs="0" maxOccurs="unbounded" /> <xs:element name="include-pattern" type="xs:string" minOccurs="0" maxOccurs="unbounded" /> <xs:element name="rule" type="rule" minOccurs="1" maxOccurs="unbounded" /> </xs:sequence> <xs:attribute name="name" type="xs:string" use="required" /> </xs:complexType> </xs:element> <xs:complexType name="rule"> <xs:sequence> <xs:element name="description" type="xs:string" minOccurs="0" maxOccurs="1" /> <xs:element name="priority" type="xs:int" default="5" minOccurs="0" maxOccurs="1"/> <xs:element name="properties" type="properties" minOccurs="0" maxOccurs="1" /> <xs:element name="exclude" type="exclude" minOccurs="0" maxOccurs="unbounded" /> <xs:element name="example" type="xs:string" minOccurs="0" maxOccurs="unbounded" /> </xs:sequence> <xs:attribute name="language" type="xs:string" use="optional" /> <xs:attribute name="minimumLanguageVersion" type="xs:string" use="optional" /> <xs:attribute name="maximumLanguageVersion" type="xs:string" use="optional" /> <xs:attribute name="name" type="xs:ID" use="optional" /> <xs:attribute name="since" type="xs:string" use="optional" /> <xs:attribute name="ref" type="xs:string" use="optional" /> <xs:attribute name="message" type="xs:string" use="optional" /> <xs:attribute name="externalInfoUrl" type="xs:string" use="optional" /> <xs:attribute name="class" type="xs:NMTOKEN" use="optional" /> <xs:attribute name="dfa" type="xs:boolean" use="optional" /> <!-- rule uses dataflow analysis --> <xs:attribute name="typeResolution" type="xs:boolean" default="false" use="optional" /> <xs:attribute name="deprecated" type="xs:boolean" default="false" use="optional" /> </xs:complexType> <xs:complexType name="properties"> <xs:sequence> <xs:element name="property" type="property" minOccurs="1" maxOccurs="unbounded" /> </xs:sequence> </xs:complexType> <xs:complexType name="property"> <xs:sequence> <xs:element name="value" type="xs:string" minOccurs="0" maxOccurs="1" /> </xs:sequence> <xs:attribute name="name" type="xs:NMTOKEN" use="required" /> <xs:attribute name="value" type="xs:string" use="optional" /> <xs:attribute name="description" type="xs:string" use="optional" /> <xs:attribute name="type" type="xs:string" use="optional" /> <xs:attribute name="delimiter" type="xs:string" use="optional" /> <xs:attribute name="min" type="xs:string" use="optional" /> <xs:attribute name="max" type="xs:string" use="optional" /> </xs:complexType> <xs:complexType name="exclude"> <xs:attribute name="name" type="xs:NMTOKEN" use="required" /> </xs:complexType> </xs:schema> ``` 之後按右邊verify之後 ![image](https://hackmd.io/_uploads/HJyJp2Xg1l.png) 會在target的report出現pmd.html ![截圖 2024-10-21 晚上7.37.28](https://hackmd.io/_uploads/H1MQThmeJe.png) 會報錯也不要關他(應該啦) ![image](https://hackmd.io/_uploads/r1RahnQeye.png) ### 法二 ![image](https://hackmd.io/_uploads/SJZyCh7xye.png) 去下載pmd插件 ![image](https://hackmd.io/_uploads/SyHg03mlkl.png) 可以在這邊加ruleset自訂規則 ![截圖 2024-10-21 晚上7.41.53](https://hackmd.io/_uploads/H1I7A37xJx.png) 選擇檔案位置 ![image](https://hackmd.io/_uploads/B1-p02Qg1e.png) ruleset.xml ```xml= <?xml version="1.0" encoding="UTF-8"?> <ruleset name="Custom Rules" xmlns="http://pmd.sourceforge.net/ruleset/2.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://pmd.sourceforge.net/ruleset/2.0.0 ./ruleset_2_0_0.xsd"> <description>Every Java Rule in PMD</description> <!-- the rules to be checked --> <rule ref="category/java/bestpractices.xml"> <exclude name="SystemPrintln"/> <exclude name="AvoidReassigningParameters"/> </rule> <rule ref="category/java/codestyle.xml" /> <rule ref="category/java/design.xml" /> <rule ref="category/java/documentation.xml" /> <rule ref="category/java/errorprone.xml" /> <rule ref="category/java/multithreading.xml" /> <rule ref="category/java/performance.xml" /> <rule ref="category/java/security.xml" /> <!-- override the rules --> <rule ref="category/java/documentation.xml/CommentSize"> <properties> <property name="maxLines" value="6" /> <property name="maxLineLength" value="80" /> </properties> </rule> </ruleset> ``` 選這個就可以執行 ![image](https://hackmd.io/_uploads/rylK1TXgye.png) ## unit test 用junit5 在要測試的程式中按右鍵 ![image](https://hackmd.io/_uploads/ry3IRs8xJx.png) ![截圖 2024-10-24 凌晨1.12.07](https://hackmd.io/_uploads/Hk5K0iLekg.png) 選擇要測試的函式 之後就會多一個test的檔案 ![截圖 2024-10-24 凌晨1.12.16](https://hackmd.io/_uploads/Sy9KRi8xyl.png) ### 用法 測試上方的註解 ![image](https://hackmd.io/_uploads/BJfpZ6g-ke.png) ```java= import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assumptions.*; import org.junit.jupiter.api.*; public class ExampleTest { // @BeforeAll: 在所有測試開始之前執行一次。通常用於初始化一次性的資源。 @BeforeAll static void initAll() { // 例如,初始化資料庫連線。 System.out.println("Initializing resources before all tests."); } // @BeforeEach: 每次測試方法執行之前執行,用於每個測試之前進行初始化工作。 @BeforeEach void init() { // 例如,在每個測試之前重置變數或準備測試資料。 System.out.println("Initializing before each test."); } // @Test: 標註這是一個測試方法,會進行測試的執行。 @Test void succeedingTest() { // 這是一個空的測試方法,沒有任何錯誤,會被標記為通過。 System.out.println("This test is succeeding."); } // @Test: 這是一個會失敗的測試方法,因為直接使用了 fail("a failing test")。 @Test void failingTest() { // 例如,測試某個方法的返回值不符合預期。 System.out.println("This test is expected to fail."); fail("a failing test"); } // @Test + @Disabled: 標註這個測試被禁用,不會執行。通常用於跳過某些不需要運行的測試。 @Test @Disabled("for demonstration purposes") void skippedTest() { // 此方法不會被執行。 System.out.println("This test is skipped."); } // @Test: 這是一個演示測試中斷的例子。使用 assumeTrue() 來檢查條件,如果條件不滿足,測試會被中斷。 @Test void abortedTest() { // assumeTrue(): 檢查條件是否為 true,如果不為 true,則中止測試。 System.out.println("This test may be aborted if the assumption fails."); assumeTrue("abc".contains("Z")); // 因為 "abc" 不包含 "Z",所以測試會被中斷,而不會執行 fail()。 fail("test should have been aborted"); } // @AfterEach: 每次測試方法執行完後執行,用於清理每次測試後的狀態。 @AfterEach void tearDown() { // 例如,關閉開啟的檔案或清除暫存資料。 System.out.println("Cleaning up after each test."); } // @AfterAll: 在所有測試執行完成後執行一次,通常用於清理一次性的資源。 @AfterAll static void tearDownAll() { // 例如,關閉資料庫連線。 System.out.println("Cleaning up resources after all tests."); } } ``` ### JUnit 5 中常用的斷言方法及其簡單說明: 除了以下方法,也可Google `junit api` 來找出更多 Assertion 的方法。 1. assertEquals(expected, actual) 用途:檢查預期值和實際值是否相等。如果兩者不相等,測試失敗。 範例:assertEquals(10, calculator.add(7, 3)); 2. assertNotEquals(unexpected, actual) 用途:檢查不期望值與實際值是否不同。如果相等,測試失敗。 範例:assertNotEquals(5, calculator.add(7, 3)); 3. assertTrue(condition) 用途:檢查條件是否為 true。如果條件為 false,測試失敗。 範例:assertTrue(user.isActive()); 4. assertFalse(condition) 用途:檢查條件是否為 false。如果條件為 true,測試失敗。 範例:assertFalse(user.isBlocked()); 5. assertNull(object) 用途:檢查對象是否為 null。如果對象不為 null,測試失敗。 範例:assertNull(user.getMiddleName()); 6. assertNotNull(object) 用途:檢查對象是否不為 null。如果對象為 null,測試失敗。 範例:assertNotNull(user.getFirstName()); 7. assertArrayEquals(expectedArray, actualArray) 用途:檢查兩個陣列的內容是否相等。如果不相等,測試失敗。 範例:assertArrayEquals(new int[]{1, 2}, actualArray); 8. assertThrows(exceptionClass, executable) 用途:檢查是否拋出指定類型的例外。如果沒有拋出或拋出不同類型的例外,測試失敗。 範例:assertThrows(IllegalArgumentException.class, () -> someMethod()); 9. assertDoesNotThrow(executable) 用途:檢查執行某段程式碼時不會拋出任何例外。如果拋出例外,測試失敗。 範例:assertDoesNotThrow(() -> someMethod()); 10. assertTimeout(duration, executable) 用途:檢查某段程式碼是否在給定的時間內執行完成。如果超過時間,測試失敗。 範例:assertTimeout(Duration.ofMillis(100), () -> slowMethod()); 11. assertAll(executables...) 用途:允許同時驗證多個斷言並收集所有失敗訊息。這讓測試更加健全,不會因為第一個失敗而中斷。 範例:assertAll(() -> assertTrue(x), () -> assertEquals(y, z)); ### 實際使用 Calculator.java ```java= package demo; public class Calculator { public int add(int a, int b) { return a+b; } public int multiply(int a, int b) { return a*b; } public double divide(int a, int b) throws Exception { if(b==0)throw new Exception("divided by zero"); return (double) a/b; } } ``` CalculatorTest.java ```java= record CalculatorTest() { //test可以有無限多個 其實就是分成很多小區塊去測試函式功能 //這些區塊都可以隨意增減 想加就加想刪就刪 @Test @DisplayName("Calculator ok 的啦")//可以把名子test_plus改成Calculator ok 的啦 void test_plus() { Calculator cal = new Calculator(); int expected = 2; int real = cal.add(1,1); assertEquals(expected, real); } @Test void test_assertAll() { Calculator cal = new Calculator(); //assertEquals(3, cal.add(1,1));直接執行的話會卡在這邊 //assertEquals(0, cal.add(1, -1));這個就不會執行到 //要兩個都可以執行就要用assertAll assertAll("positive + positive", ()-> assertEquals(2, cal.add(1,1)), ()-> assertEquals(0, cal.add(1, -1)) ); assertAll("positive + negative", ()-> assertEquals(3, cal.add(1,1)), ()-> assertEquals(0, cal.add(1, -1)) ); } @org.junit.jupiter.api.Test //這東西與@test一樣 @DisplayName("Calculator div OK 的啦") void test_div() throws Exception { Calculator cal = new Calculator(); int expected = 7; double real = cal.divide(4,2); assertEquals(expected, real); } @Test public void testAddition() { Calculator calculator = new Calculator(); int result = calculator.add(2, 2); assertEquals(5, result, () -> "計算結果錯誤: ");//可以自己加字。Lazily evaluating message 是一種技巧,用於在斷言(assertions)中延遲評估訊息(lazy evaluating message)的建立,僅在斷言失敗時才進行評估。這種技巧可以提高執行效能。 } @Test void test_div2(){ Calculator cal =new Calculator(); Exception exception= assertThrows(Exception.class ,()->cal.divide(2,0)); //預期他會拋出錯誤 所以沒拋出錯誤反而會出錯 } } ``` 其他例子 TriangleTest.java ```java= class TriangleTest { @Test void cal_triangle() { Triangle t = new Triangle(); String expect="這是正三角形。"; assertEquals(expect,t.cal_triangle(1,1,1));//可以比較物件 字串 整數 浮點數等等等 //t.cal_triangle(1,1,1)會回傳字串 } } ``` PeopleTest.java ```java= @Test void test_bmi_height_change() { People jack = new People("Jack", 1.7, 80, 2000); //jack.setHeight(1.6); assertEquals(27.6, jack.bmi(),0.1);//0.1是誤差 可以有誤差的方法 } ``` ### 多參數 Parameterized Test #### 直接指定值 ```java= import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; public class MathTest { @ParameterizedTest @ValueSource(ints = {2, 3, 5, 11})//會依序把這四個數字帶入number public void testPrime(int number) { Math math = new Math(); assertTrue(math.isPrime(number)); } @ParameterizedTest @ValueSource(ints = {1, 100, 50, 99, 51}) public void testNotPrime(int number) { Math math = new Math(); assertFalse(math.isPrime(number)); } } ``` #### 用csv格式 對 `tomorrow()` 進行測試, csv 內部前三個參數是輸入的日期,後三個數字是預期的輸出。輸出的結果我們都轉為 String 一次比較年月日是否相同 ```java= @ParameterizedTest @CsvSource({ "1901, 1, 1, 1901, 1, 2", "1901, 2, 28, 1901, 3, 1", "2000, 2, 28, 2000, 2, 29", "2000, 12, 31, 2001, 1, 1", "2000, 6, 30, 2000, 7, 1", "2000, 7, 30, 2000, 7, 31", }) void testTomorrow(int y1, int m1, int d1, int y2, int m2, int d2) { MyDate today = new MyDate(y1, m1, d1); MyDate expected_tomorrow = new MyDate(y2, m2, d2); assertEquals(expected_tomorrow.toString(), today.tomorrow().toString()); } ``` **csv用讀取的** - .csv 檔案放在 /resource 下 - 跳過第一行: numLinesToSkip = 1 ![image](https://hackmd.io/_uploads/BkbXvpeW1g.png) 上面圖只是resource示意圖 與下面程式不一樣 ```java= @ParameterizedTest @CsvFileSource(resources = "/two-columns.csv", numLinesToSkip = 1) void testWithCsvFileSourceFromClasspath(String country, int reference) { assertNotNull(country); assertNotEquals(0, reference); } ``` ### 改名子與多參數 ```java= class YearUtils { static boolean isLeapYear(int year) { // 判斷閏年邏輯:能被 4 整除且不能被 100 整除,或者能被 400 整除 return (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0); } } ``` ```java= import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayNameGeneration; import org.junit.jupiter.api.DisplayNameGenerator; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInstance; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; class DisplayNameGeneratorDemo { @Nested @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class A_year_is_not_supported { @Test void if_it_is_zero() { assertFalse(YearUtils.isLeapYear(0)); // 測試 0 年是否為閏年,應該是 false } @DisplayName("A negative value for year is not supported by the leap year computation.") @ParameterizedTest(name = "For example, year {0} is not supported.") @ValueSource(ints = { -1, -4 }) // 傳入負數年份 void if_it_is_negative(int year) { assertFalse(YearUtils.isLeapYear(year)); // 負數年份應該不是閏年 } } @Nested @DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class) class A_year_is_a_leap_year { @Test void if_it_is_divisible_by_4_but_not_by_100() { assertTrue(YearUtils.isLeapYear(2024)); // 測試 2024 年是否為閏年,應該是 true } @ParameterizedTest(name = "Year {0} is a leap year.") @ValueSource(ints = { 2016, 2020, 2048 }) // 傳入閏年年份 void if_it_is_one_of_the_following_years(int year) { assertTrue(YearUtils.isLeapYear(year)); // 驗證這些年份是否為閏年,應該是 true } } } ``` ```csharp= DisplayNameGeneratorDemo A year is not supported if it is zero ✔ A negative value for year is not supported by the leap year computation. For example, year -1 is not supported. ✔ For example, year -4 is not supported. ✔ A year is a leap year if it is divisible by 4 but not by 100 ✔ Year 2016 is a leap year. ✔ Year 2020 is a leap year. ✔ Year 2048 is a leap year. ✔ ``` ## 黑箱(邊界測試) 有點像是盲測 只要知道輸入資料的可能性就可以規劃測試資料 ### 獨立型邊界測試 min, min+, max, max-, norm。其中 norm 表示 normal 就是在 min 與 max 之間取一個一般的代表值。假設有 n 個變數,每個變數有 min, min+, max, max- 等四個值,再加上一個非邊界的代表值,共是 $4n+1$ 個測試資料。 | case\# | a | b | c | 輸出 | |--------|-----|-----------|---------|----------| | 1 | 100 | 100(norm) | 1(min) | 等腰 | | 2 | 100 | 100(norm) | 2(min+) | 等腰 | | 3 | 100 | 100(norm) | 199(max-)| 等腰 | | 4 | 100 | 100(norm) | 200(max)| ! | | 5 | 100 | 1 | 100 | 等腰 | | 6 | 100 | 2 | 100 | 等腰 | | 7 | 100 | 199 | 100 | 等腰 | | 8 | 100 | 200 | 100 | ! | | 9 | 1 | 100 | 100 | 等腰 | | 10 | 2 | 100 | 100 | 等腰 | | 11 | 199 | 100 | 100 | 等腰 | | 12 | 200 | 100 | 100 | ! | | 13 | 100 | 100 | 100 | 正三角形 | ### 獨立強固邊界測試 :::success :basketball: 以下是一個游泳池收費系統的規則: * (1) 一般票價 200 * (2) 星期六日250元,除會員以外不打折 * (3) 12歲以下、60歲(含)以上打八折,限定 3-75 歲可入內游泳 * (4) 七點以前八折 * (5) 團體打七折 * (6) 會員打五折 * (7) 各打折不得重疊使用,以顧客最有利方案定價 採用「獨立強固邊界測試」設計測試案例 * 有哪些變數? * 哪些變數非列舉,有明顯的邊界?其 min, min-, max, max+ 為何? * 請透過 excel,採用強固邊界測試進行測試案例規劃 * 請透過 JUnit 進行程式碼測試 ::: ![截圖 2024-10-30 下午2.32.58](https://hackmd.io/_uploads/HJdivPkW1g.png) ### 非獨立型邊界測試 每一個變數有 min, min+, norm, max-, max 等五種狀況的排列組合,所以共需要 $5^n$ 個測試案例。這是一個不小的數目,如果有五個變數,則需要 $5^5=3,125$ 個測試案例。 | case\# | a | b | c | 輸出 | |--------|-----|-----------|------------|----------| | 1 | 100 | 100(norm) | 1(min) | 等腰 | | 2 | 100 | 100(norm) | 2(min+) | 等腰 | | 3 | 100 | 100(norm) | 199(max-) | 等腰 | | 4 | 100 | 100(norm) | 200(max) | 等腰 | | 5 | 100 | 100(norm) | 100(norm) | 正三角 | | ... | ... | ... | ... | ... | | 125 | ... | ... | ... | ... | ### 非獨立型強固邊界測試 同上,這次我們考慮強固的方案,亦即例外的狀況 max+, min- 考慮進去,所以共需要 $7^n$ 個測試案例,因為每個變數會有以下 7 個資料: - max+ - max - max- - normal - min+ - min - min- 當然,這樣的情況測試案例就會更多,以五個變數而言,供需要 $7^5=16,807$ 個測試案例。通常例外的狀況是相當獨立的,不需要與每一種組合做乘積。 如果有n個變數,我們僅對元素的五個狀態進行乘積(每個元素的五個狀況排列組合)max max- normal min+ min,$5^n$。再加上每個變數的max+ min- 這兩個例外,$2n$。則共需要 $5^n + 2n$ 個測試案例。$5^n$是max max- normal min+ min這五個排列組合, ### 分割條件 分割條件有點像離散化,把條件用成一區一區的。用狀況做分割。 上面的圖做分割後 ![截圖 2024-10-30 下午2.50.07](https://hackmd.io/_uploads/rywjPwJZJg.png) ### 弱涵蓋測試 有點像是挑重點側,不然用排列組合的方式去看測資課能是m個分割n個參數共$m^n$個資料要去測。 就是每個條件混合出現過就好(挑重點 去測比較危險的邊界測資) 像是y有 y1 y2這兩種狀況 像是x有 x1 x2 x3這三種狀況 (x1,y2) (x2,y1) (x3,y2) x與y所有條件都有出現過只是順序是混著的 ![image](https://hackmd.io/_uploads/SyrFpflZ1l.png) 等價分割測試(圓形表示弱涵蓋測試、三角形表示強涵蓋測試) ### 強涵蓋測試 就是每個條件的排列組合 像是y有 y1 y2這兩種狀況 像是x有 x1 x2 x3這三種狀況 所有的可能是 (x1,y1) (x1,y2) (x2,y1) (x2,y2) (x3,y1) (x3,y2) #### 範例題目 ::: success Lab: Swimming pool 以下是一個游泳池收費系統的規則: 一般票價 200 星期六日250元,除會員以外不打折 12歲以下、60歲(含)以上打八折,限定 3-75 歲可入內游泳 七點以前八折 團體打七折 會員打五折 各打折不得重疊使用,以顧客最有利方案定價 請用強涵蓋測試以及 CsvFileSource 來進行測試 ::: **判斷程式** ```java= public class SwimmingPoolPricing { /** * 根據顧客條件計算最優惠的入場費用 * @param age 顧客年齡 * @param isMember 是否為會員 * @param isWeekend 是否為週末 * @param isGroup 是否為團體 * @param hour 進場時間(小時) * @return 最優惠價格 */ public double calculatePrice(int age, boolean isMember, boolean isWeekend, boolean isGroup, int hour) { // 基本票價:平日200元,週末250元 double price = isWeekend ? 250 : 200; // 年齡限制:3-75歲才可以進入游泳 if (age < 3 || age > 75) { throw new IllegalArgumentException("年齡不符合入場條件"); } // 計算折扣,初始無折扣 double discount = 1.0; // 判斷不同優惠條件,優先取顧客最有利的方案 if (isMember) { discount = 0.5; // 會員五折 } else if (isGroup) { discount = 0.7; // 團體七折 } else if (age <= 12 || age >= 60) { discount = 0.8; // 年齡符合條件,享八折 } else if (hour < 7) { discount = 0.8; // 早上七點前進場,享八折 } // 最後價格 = 基本票價 * 折扣 return price * discount; } } ``` **test程式** ```java= import static org.junit.jupiter.api.Assertions.*; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvFileSource; public class SwimmingPoolPricingTest { private final SwimmingPoolPricing pricing = new SwimmingPoolPricing(); /** * 測試calculatePrice方法的各種情況,驗證是否符合需求 * 使用 @CsvFileSource 從 CSV 檔案中讀取參數 * @param age 顧客年齡 * @param isMember 是否為會員 * @param isWeekend 是否為週末 * @param isGroup 是否為團體 * @param hour 進場時間(小時) * @param expectedPrice 預期票價 */ @ParameterizedTest @CsvFileSource(resources = "/pricing_test_cases.csv", numLinesToSkip = 1) void testCalculatePrice(int age, boolean isMember, boolean isWeekend, boolean isGroup, int hour, double expectedPrice) { // 當期望價格為 -1 時,檢查是否拋出 IllegalArgumentException 異常 if (expectedPrice == -1) { assertThrows(IllegalArgumentException.class, () -> { pricing.calculatePrice(age, isMember, isWeekend, isGroup, hour); }); } else { // 當期望價格不為 -1 時,檢查計算價格是否與期望價格相等 assertEquals(expectedPrice, pricing.calculatePrice(age, isMember, isWeekend, isGroup, hour), 0.01); } } } ``` **強涵蓋測試資料** 條件說明 依照排列組合 總共$7*2*2*2*2=112$個值 1. 年齡(age):7 個測試值 遠低於下限:age = -1 略低於下限:age = 2 等於下限:age = 3 正常值:age = 30 等於上限:age = 75 略高於上限:age = 76 遠高於上限:age = 100 2. 進場時間(hour):2 個測試值 七點前:hour = 6 七點後(含七點):hour = 7 3. 是否為會員(isMember):true、false 4. 是否為週末(isWeekend):true、false 5. 是否為團體(isGroup):true、false 可以用程式或excel去生這些值 程式方法(但有點本末倒置) ```java= import java.io.FileWriter; import java.io.IOException; public class TestCaseGenerator { public static void main(String[] args) throws IOException { int[] ages = {-1, 2, 3, 30, 75, 76, 100}; int[] hours = {6, 7}; boolean[] booleans = {false, true}; FileWriter writer = new FileWriter("pricing_test_cases.csv"); writer.write("age,hour,isMember,isWeekend,isGroup,expectedPrice\n"); for (int age : ages) { for (int hour : hours) { for (boolean isMember : booleans) { for (boolean isWeekend : booleans) { for (boolean isGroup : booleans) { double expectedPrice; try { expectedPrice = calculateExpectedPrice(age, isMember, isWeekend, isGroup, hour); } catch (IllegalArgumentException e) { expectedPrice = -1; } writer.write(String.format("%d,%d,%b,%b,%b,%.1f\n", age, hour, isMember, isWeekend, isGroup, expectedPrice)); } } } } } writer.close(); } private static double calculateExpectedPrice(int age, boolean isMember, boolean isWeekend, boolean isGroup, int hour) { // 年齡限制 if (age < 3 || age > 75) { throw new IllegalArgumentException("年齡不符合入場條件"); } double price = isWeekend ? 250 : 200; double discount = 1.0; if (isMember) { discount = 0.5; // 會員五折 } else if (isGroup) { discount = 0.7; // 團體七折 } else if (age <= 12 || age >= 60) { discount = 0.8; // 年齡折扣八折 } else if (hour < 7) { discount = 0.8; // 七點前八折 } return price * discount; } } ``` ### 全成對組合測試 如有 $n$ 個變數,每一個可能有 $m$ 個分割,如果我們每個分割取一個值來做非獨立測試,則有 $m^n$ 個測試案例需要測試。這樣近乎窮盡式的測試方法,在成本可能太高。 所以全成對就是任意取兩個參數,其參數是每種組合都有試過就好,而不用全部參數的排列組合都有試過。 #### 交錯法 最多可能的變數放左邊(W),依序把所有變數往右排放。接著放置W 的可能值,w1, w2, w3, 因為他的下一個 X 的可能值是 2 個,所以我們重複的寫上 2個 w1 (行 1, 2)。中間的空行是作為預留之用,往後才會用到。X 的可能放上去,這時候交錯的放 x1, x2, x1, x2 ... 使得 W 與 X 可以交錯的對應到。 接著放 Y。這時候要注意如果 Y 的還是 y1, y2 依序放的話,x1 永遠都只會對到 y1, x2 只會對到 y2,所以我們在 行4 時做一個對調,這樣 W, X, Y 兩兩成對。接著放 Z。和上面的情況類似,這時候我們 行7,行8 做對調,這樣才會全成對。 總結方法: 變數由左到右,多到少。然後如果跟前面有重複的話就依序第一列組交換,如果前面換過第一列組那就換第二列組,依此類推。(因為只要任意取兩個參數,其參數是每種組合都有試過就好,而不用全部參數的排列組合都有試過) | | W | X | Y | Z | P | Q | |-------|-----|------|------|------|------|------| | 1 | w1 | x1 | y1 | z1 | *p2* | | | 2 | w1 | x2 | y2 | z2 | *p1* | | | 3 | | | | | | | | 4 | w2 | x1 | *y2* | z1 | p1 | | | 5 | w2 | x2 | *y1* | z2 | p2 | | | 6 | | | | | | | | 7 | w3 | x1 | y1 | *z2* | p1 | | | 8 | w3 | x2 | y2 | *z1* | p2 | | 像是q前面的y,z,p都已經用掉三組可以交換的列了,就可以新增額外的組合到原本預設的空白區。 \`\`代表同上 | | W | X | Y | Z | P | Q | |-------|-----|------|------|------|------|------| | 1 | w1 | x1 | y1 | z1 | p2 | q1 | | 2 | w1 | x2 | y2 | z2 | p1 | q2 | | 3 | \`\` | *x2* | \`\` | \`\` | \`\` | q1 | | 4 | w2 | x1 | *y2* | z1 | p1 | q1 | | 5 | w2 | x2 | *y1* | z2 | p2 | q2 | | 6 | \`\` | *x1* | \`\` | \`\` | \`\` | q2 | | 7 | w3 | x1 | y1 | *z2* | p1 | q1 | | 8 | w3 | x2 | y2 | *z1* | p2 | q2 | ![截圖 2024-11-13 下午3.06.09](https://hackmd.io/_uploads/SJtiBRWG1l.png) ## 白箱(各種涵蓋度與變異體) 雖然可以依照程式內條件去測試 但條件判斷可能多到很誇張所以比較適合小型的單元測試 ### 測試涵蓋度 #### 法一 intellij內建 看有沒有每一個if條件判斷都有執行過 看執行到全部程式的幾\% 但不一定每個程式都有測試涵蓋度100%的測資,有時候為了表達清楚而會有邏輯錯誤,但這個邏輯錯誤不會影響到程式的輸出運作。 ![image](https://hackmd.io/_uploads/r1DrMR-Mke.png =80%x) ![image](https://hackmd.io/_uploads/rJwgVAZMJe.png =80%x) #### line 就是敘述涵蓋度 只要程式曾經有走過不管true或false都可以 #### branch branch總數是每個if中true與false的hits的總數相加的/% 且每一個if中的條件會由左到右檢查。 ##### or中 假如有if(a\=\=b \|\| b\=\=c)的話,如果a等於b成立時,就不會檢查b有沒有等於c。 此時 a\=\=b 的 true hits 加一,但 b\=\=c 的 hits 不會變 ##### and中 假如有if(a\=\=b \&\& b\=\=c)的話,如果a等於b不成立時,就不會檢查b有沒有等於c。 此時 a\=\=b 的 false hits 加一,但b \=\=c 的 hits 不會變 稱之為「快捷求值」(short-circuit evaluation) 來進行邏輯運算,此運算下,條件涵蓋度百分百,也會滿足分支涵蓋度百分百。 當 p 為 False, q 並不會進行求值,稱之為「快捷求值」。 | | p | q | p & q | |-----|------|------|--------| | t1 | True | False| False | | t2 | False| **x** | False | 因此,為了滿足 q 為 True,必須增加一個測試案例: | | p | q | p & q | |-----|------|------|--------| | t1 | True | False| False | | t2 | False| x | False | | t3 | True | **True** | **True** | 因此,(t1, t2, t3) 可以滿足條件涵蓋度與分支涵蓋度百分百。 ##### 總結 如果branch要100%的話,就要每個if中的每個條件的true hits與false hits都要至少大於0(有走過)。 ![image](https://hackmd.io/_uploads/ByyGJOifke.png =80%x) example 修改前主程式 ```java= package demo; public class Triangle { public static String getTriangleType(double a, double b, double c) { if (a <= 0 || b <= 0 || c <= 0 || a + b <= c || a + c <= b || b + c <= a) { return "Not a valid triangle"; } // Check for equilateral triangle if (a == b && b == c && a == c) { //問題出在這 永遠不可能出現a==b b==c a!=b //所以a==b的false hits永遠不可能實現 return "Equilateral"; } // Check for isosceles triangle if (a == b || b == c || a == c) { return "Isosceles"; } // Otherwise, it's a scalene triangle return "Scalene"; // 不等邊三角形 } } ``` ```java= package demo; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class TriangleTest { @Test void not_valid_triangle() { assertAll("not validate", ()-> assertEquals("Not a valid triangle", Triangle.getTriangleType(-1, 2, 3)), ()-> assertEquals("Not a valid triangle", Triangle.getTriangleType(2, -2, 3)), ()-> assertEquals("Not a valid triangle", Triangle.getTriangleType(2, 2, -3)) ); } } ``` 測試結果 ![image](https://hackmd.io/_uploads/S1jXqPifke.png) 修改後 ```java= package demo; public class Triangle { public static String getTriangleType(double a, double b, double c) { if (a <= 0 || b <= 0 || c <= 0 || a + b <= c || a + c <= b || b + c <= a) { return "Not a valid triangle"; } if (a == b && b == c) { return "Equilateral"; } // Check for isosceles triangle if (a == b || b == c || a == c) { return "Isosceles";// Check for equilateral triangle } // Otherwise, it's a scalene triangle return "Scalene"; // 不等邊三角形 } } ``` ```java= package demo; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.*; class TriangleTest { @Test void not_valid_triangle() { assertAll("not validate", ()-> assertEquals("Not a valid triangle", Triangle.getTriangleType(-1, 2, 3)), ()-> assertEquals("Not a valid triangle", Triangle.getTriangleType(2, -2, 3)), ()-> assertEquals("Not a valid triangle", Triangle.getTriangleType(2, 2, -3)), ()-> assertEquals("Not a valid triangle", Triangle.getTriangleType(2, 2, -3)), ()-> assertEquals("Not a valid triangle", Triangle.getTriangleType(2, 2, 4)), ()-> assertEquals("Not a valid triangle", Triangle.getTriangleType(4, 2, 2)), ()-> assertEquals("Not a valid triangle", Triangle.getTriangleType(2, 4, 2)) ); } @Test void valid_triangle() { assertAll("not validate", ()-> assertEquals("Equilateral", Triangle.getTriangleType(2, 2, 2)), ()-> assertEquals("Isosceles", Triangle.getTriangleType(3, 2, 2)), ()-> assertEquals("Isosceles", Triangle.getTriangleType(2, 3, 2)), ()-> assertEquals("Isosceles", Triangle.getTriangleType(2, 2, 3)), ()-> assertEquals("Scalene", Triangle.getTriangleType(3, 4, 5)) ); } } ``` 測試結果 ![image](https://hackmd.io/_uploads/Hk8yV_ozJx.png) #### 法二 jacoco pom檔裡面要先設定好 裡面還有加上之前code review pmd、junit等其他設定(與jacoco無關) ```pom= <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>hw03</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>20</maven.compiler.source> <maven.compiler.target>20</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <junit.version>5.11.0</junit.version> </properties> <dependencies> <!-- JUnit Jupiter API --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <!-- JUnit Jupiter Engine --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <!-- Maven Compiler Plugin --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>${maven.compiler.source}</source> <target>${maven.compiler.target}</target> </configuration> </plugin> <!-- Maven Surefire Plugin --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>3.1.2</version> </plugin> <!-- jacoco 插件的地方 --> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.11</version> <executions> <execution> <id>prepare-agent</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>test</phase> <goals> <goal>report</goal> </goals> </execution> <execution> <id>check</id> <goals> <goal>check</goal> </goals> <configuration> <rules> <rule> <element>PACKAGE</element> <limits> <limit> <counter>LINE</counter> <value>COVEREDRATIO</value> <minimum>0.80</minimum> </limit> </limits> </rule> </rules> </configuration> </execution> </executions> </plugin> </plugins> </build> </project> ``` 按verify ![image](https://hackmd.io/_uploads/rk6kOlmXyx.png =80%x) 之後開啟target中jacoco下的index.html ![image](https://hackmd.io/_uploads/rJ5Lugmmke.png =80%x) 點開網頁就成功了 ![截圖 2024-11-26 下午3.27.34](https://hackmd.io/_uploads/S1Bh_e7mJe.png) ### 敘述涵蓋度 只要兩個if有走過就好,非常簡略 輸入為 (A,B,X) = (2,0,3) 就可覆蓋所有可執行指令,結果會得到 3。不是很嚴謹:如果我們犯了以下的錯誤: - 行號 2 的 AND 改成 OR,或是 - 行號 3 Y=A 改成其他的敘述 - 行號 4 的 X>1 改成 X>0,或是 測試案例 (2,0,3) 並不能找出你犯的錯誤,這表示這組測試案例太弱了。 ```java= input A,B,X if (A>1) and (B=0) then Y=A if (A=2) or (X>1) then Y=X print Y ``` ![IMG_1567](https://hackmd.io/_uploads/SJonhS2Mke.jpg =30%x) ### 分支涵蓋度 要讓每一個if的true與false有走過就好 A,B,X若為(3,0,3)和(3,1,1)能使得 (A>1) AND (B=0)和(A=2) OR (X>1)這兩個布林運算式均能產生真和假的值。 <img src=https://hackmd.io/_uploads/rkX6xoZNT.png width=400> ![image](https://hackmd.io/_uploads/r1w9zL3fye.png =30%x) ### 條件涵蓋度 針對if內的每一項條件的true與false,只要都曾經達成過就好了。 例如 (A>1) AND (B=0) 運算式包含 A>1 和 B=0 兩條件。前述程式具有下列四條件 A>1、B=0、A=2、X>1。 使這四個條件都能產生真與假的值,測試案例須包含以下八種案例: ``` (1) A>1, (2) A<=1 (3) B=0, (4) B<>0 (5) A=2, (6) A<>2 (7) X>1, (8) X<=1 ``` (2,0,3) 滿足 (1)、(3)、(5)、(7)。(1,1,1) 滿足(2)、(4)、(6)、(8),在下面左圖表示。 但條件涵蓋度不一定滿足分之涵蓋度,例如下面右邊的圖,每個if中每個條件都有達成true與false過,但if的布林值卻都一樣。 ![image](https://hackmd.io/_uploads/S1GCD8nMJl.png) ### 多重條件涵蓋 ```java= input A,B,X if (A>1) and (B=0) then Y=A if (A=2) or (X>1) then Y=X print Y ``` 滿足前面條件、敘述、分支含共涵蓋度,全部混合在一起。 ![image](https://hackmd.io/_uploads/Bkh75U2GJe.png =50%x) 每個if中的每個條件都有滿足過。例如前述程式,第一個布林運算式有下列 4 種條件組合: ```java (1) A>1,B=0 (2) A>1,B<>0 (3) A<=1,B=0 (4) A<=1,B<>0 ``` 第二個布林運算式也有下列4種條件組合: ```java (5) A=2,X>1 (6) A=2,X<=1 (7) A<>2,X>1 (8) A<>2,X<=1 ``` 測試資料 (2,0,4) 滿足1、5。(2,1,1) 滿足 2、6。(1,0,2) 滿足3、7。(1,1,1) 滿足4、8。因此這四個測試資料所組合成的測試資料群便可涵蓋上述 8 種條件組合。 ### 路徑涵蓋度 用路徑去考慮,每一條路的走法都要有走過 例如上述的多重條件分之中 (2,0,4), (2,1,1), (1,0,2), (1,1,1) 測試資料,雖然可以把所有的 敘述、條件、分支都涵蓋,但以路徑的角度來看,他只涵蓋了 a-c-e, a-b-e, a-b-d 等三個路徑。 ![path coverage](https://hackmd.io/_uploads/ryRlBc-4T.png ) ![image](https://hackmd.io/_uploads/H16W9Inzyl.png =50%x) 而路徑涵蓋度要走過所有路徑。 ``` (2,0,4): 走 a-c-e (2,1,1): 走 a-b-e (1,0,2): 走 a-b-e (1,1,1): 走 a-b-d ``` 如果我們要走過所有路徑的話,我們需要 $2^2=4$ 個路徑,這看起來不難,但如果有迴圈的話,假設走 20 遍,我們就需要 $4^{20} = 2^{40}=1.0995116e+12$,這已經失去測試的意義了。即便 a-c-d 這個路徑有其意義(或許 c 的動作可能會影響 d 的行為),但 all-path 的測試成本實在太高了。 ### 各種涵蓋度的關係 最上方(路徑涵蓋度)是最嚴謹的,依序往下。 分支涵蓋度與條件涵蓋度沒有絕對的關係,因此平行。分支涵蓋度百分百時,敘述涵蓋度也為百分百。值得一提的是,條件涵蓋度百分百時並不保證敘述涵蓋度為百分百。 ![各種涵蓋度的關係](https://hackmd.io/_uploads/BJgLrcZVT.png) ### 變異體 一組測資可以刪除的越多越好,代表說可以辨識的變異體越多。變異指的是受測程式的微幅修改。例如把部分的 = 改成 != 或是 > 改成 <,如果測資可以辨識的出來原程式不等於變異程式的話,代表測資有效果 修改程式的運算元(operator)我們稱為 變異運算元(mutation operator),例如: - 把某些判斷句換成 true, false, 或加上 not - 把 + 換成 *, - 換成 / - 把 > 換成 >=, == 或是 != - 把某些運算作為幅的修改,例如 x 變成 x+1, y 換成 y-1 原程式為: ```java int getMax(int x, int y) { int max; if (x>y) max =x; else max = y; return max; } ``` 變異後的為: ```java int getMax2(int x, int y) { int max; if (x<y) max =x; else max = y; return max; } ``` 如果只有一組測試資料 (1,1), 則在 getMax() 與 getMax2() 執行出來的結果都是一樣的,代表測試資料不足。 #### 作法 - 產生程式 (P) 的變異群,$P^m =\{P_1, P_2, P_i, ... P_n\}$; - T 為測試資料集合,$T = \{t_1, t_2, ... t_m\}$; - 用 T 來測試 P 及 $P^m$ 內的每個程式,當 $\exists t_i \in T, P(t_i) \neq P_j(t_i)$ 時(原程式與變異程式輸出不一樣時),表示 $t_i$ 可以識別出兩者的差異,此時 $P_j$ 被移除,稱之為被刪除變異體(killed mutant),沒有被移除的稱之為存活變異體(alive mutant)。 - 如果剩下的 存活變異體 不是等價變異,則增加測試案例。 - 回到步驟 3,直到 存活變異體 為空。 :::spoiler example :football: 考慮以下的程式 *P*。若我們有兩個變異 $P_1$ 是把 && 改成 ||, $P_2$ 是把 == 改成 !=,請問 test case (x=10, y=10) 能夠 kill 哪些變異?其所代表的涵意為何?} ```java read(X, Y) if ((X>=10) && (Y==1)) X=1; else if ((X-Y) < 2) X=2; if (Y >10) X=3; print X ``` 測試案例 (10, 10) 對原來 $P$ 的測試 $P(10,10)$ 產出的結果為 $X=2$,$P_1(10,10)$ 為 $X=1$,輸出的結果不同了,表示此測試案例能夠偵測出不同,故刪除(kill)$P_1$。$P_2(10,10)$ 結果仍然為 $X=2$ 保持不變,無法刪除 $P_2$。因此我們必須再加入測試案例。 例如我們加入 (10,1) 的測試案例,$P(10, 1)$ 為 1, $P_2(10, 1)$ 為10, 兩者不同,這時候我們可以刪除 $P_2$, 因為所有的變異都被移除,所以測試案例 OK。 ::: #### 變異刪除率 變異刪除率 (mutation score) 用來檢視測試資料的完整度。 $ms=K/(M–E)$ 其中 K 是被刪除變異體的個數,E 是等價變異體的個數,M 是變異的個數。 變異刪除率越高,表示測試案例越有效。 > 在變異測試這個遊戲中,我們的目的就是- 盡所能的(設計測試資料以)刪除變異。 #### 等價變異 有些變異後程式碼的行為是一樣的,所以不論測試資料多麼好,也無法觀察出差異進而刪除,例如: ``` var a, b, c: integer; begin if a < b then c:= a; end 以下為三個變異: p1: if a <= b-1 then c := a; p2: if a +1 <= b then c:= a; p3: if a < b then c:= a + 0; ``` $p_1$, $p_2$, $p_3$ 這三個變異都是等價變異,任何資料都無法將之刪除。 #### 範例 原程式 ```java= public class Triangle { public static String getTriangleType(double a, double b, double c) { if (a <= 0 || b <= 0 || c <= 0 || a + b <= c || a + c <= b || b + c <= a) { return "Not a valid triangle"; } // Check for equilateral triangle if (a == b && b == c && a == c) { return "Equilateral"; } // Check for isosceles triangle if (a == b || b == c || a == c) { return "Isosceles"; } // Otherwise, it's a scalene triangle return "Scalene"; // 不等邊三角形 } } ``` 變異體r2 ```java= public class Triangle02 { public static String getTriangleType(double a, double b, double c) { if (a < 0 || b <= 0 || c <= 0 || a + b <= c || a + c <= b || b + c <= a) { return "Not a valid triangle"; } // Check for equilateral triangle if (a == b && b == c && a == c) { return "Equilateral"; } // Check for isosceles triangle if (a == b || b == c || a == c) { return "Isosceles"; } // Otherwise, it's a scalene triangle return "Scalene"; // 不等邊三角形 } } ``` 變異體r3 ```java= public class Triangle03 { public static String getTriangleType(double a, double b, double c) { if (a <= 0 || b <= 0 || c <= 0 || a + b <= c || a + c <= b || b + c <= a) { return "Not a valid triangle"; } // Check for equilateral triangle if (a != b && b == c && a == c) { return "Equilateral"; } // Check for isosceles triangle if (a == b || b == c || a == c) { return "Isosceles"; } // Otherwise, it's a scalene triangle return "Scalene"; // 不等邊三角形 } } ``` 變異體r4 ```java= public class Triangle04 { public static String getTriangleType(double a, double b, double c) { if (a <= 0 || b <= 0 || c <= 0 || a + b <= c || a + c <= b || b + c <= a) { return "Not a valid triangle"; } // Check for equilateral triangle if (a == b && b == c && a == c) { return "Equilateral"; } // Check for isosceles triangle if (a != b || b == c || a == c) { return "Isosceles"; } // Otherwise, it's a scalene triangle return "Scalene"; // 不等邊三角形 } } ``` 測試變異體的程式(不是用junit) ```java= public class TriangleMuTest { ArrayList<String> mutations = new ArrayList<>(); public static void main(String[] args) { TriangleMuTest main = new TriangleMuTest(); main.mutations.add("r2"); main.mutations.add("r3"); main.mutations.add("r4"); main.muTest(0, 2, 3); main.muTest(2, 0, 1); main.muTest(1, 2, 3); main.muTest(1, 1, 1); main.muTest(2, 2, 1); main.muTest(2, 2, 1); } void muTest(int a, int b, int c) { String r1 = Triangle.getTriangleType(a, b, c); String r2 = Triangle02.getTriangleType(a, b, c); String r3 = Triangle03.getTriangleType(a, b, c); String r4 = Triangle04.getTriangleType(a, b, c); if (! r1.equals(r2)) { System.out.println("\t r2 is killed"); mutations.remove("r2"); } if (! r1.equals(r3)) { System.out.println("\t r3 is killed"); mutations.remove("r3"); } if (! r1.equals(r4)) { System.out.println("\t r4 is killed"); mutations.remove("r4"); } System.out.printf("Test data: %d, %d, %d\t", a, b, c); System.out.printf("The alive mutations are: %s\n", mutations); } } ``` 結果: ![截圖 2024-11-27 下午5.28.37](https://hackmd.io/_uploads/HJou8wNm1x.png =45%x) r2永遠都殺不掉是因為 原程式 `if (a <= 0 || b <= 0 || c <= 0 || a + b <= c || a + c <= b || b + c <= a)` R2 `if (a < 0 || b <= 0 || c <= 0 || a + b <= c || a + c <= b || b + c <= a)` 他們差在 a<=0 或 a<0 能區別他們的測資是`a==0`的時候。`a==0`時原程式會進去,r2不會對,但r2其他條件兩邊和沒有大於第三邊一定會進去。 因為邏輯等價,所以沒有測資可以殺掉r2。 #### 自動化測試變異體 (pitest) 會依照test檔測試給的資料去測變異體 test檔案 ```java= public class TriangleTest { @Test public void testValidTriangle() { assertEquals("Not a valid triangle", Triangle.getTriangleType(-1, -2, -1)); } @Test public void testEquilateral() { assertEquals("Equilateral", Triangle.getTriangleType(2,2,2)); } } ``` pom檔: java版本不能用太新,差不多17 pitest版本用1.17.1 ```xml= <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>DemoMutation4</groupId> <artifactId>DemoMutation4</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- JUnit 4 Dependency --> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <!-- JaCoCo Plugin (Optional for Coverage Analysis) --> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.10</version> <executions> <execution> <id>prepare-agent</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>verify</phase> <goals> <goal>report</goal> </goals> </execution> </executions> </plugin> <!-- Pitest Plugin Configuration for JUnit 4 --> <plugin> <groupId>org.pitest</groupId> <artifactId>pitest-maven</artifactId> <version>1.17.1</version> <configuration> <targetClasses> <param>demo.*</param> </targetClasses> <targetTests> <param>demo.*Test</param> </targetTests> <mutators>STRONGER</mutators> <threads>4</threads> <!-- <timeoutConst>4000</timeoutConst>--> <outputFormats> <param>HTML</param> </outputFormats> </configuration> </plugin> </plugins> </build> </project> ``` 先按verify再按coverage,不能直接按coverage ![image](https://hackmd.io/_uploads/SJJQJdE7yg.png =70%x) ![截圖 2024-11-27 下午6.14.25](https://hackmd.io/_uploads/BkNQWOEmJe.png =50%x) ![image](https://hackmd.io/_uploads/rkq0xONXkl.png) ## 整合測試 整合測試是上而下,先測試高階的模組,逐步的往下測試到低階的模組。 ![image](https://hackmd.io/_uploads/Sk-G2y0Vyl.png) 下面範例中,要測試allPrime是不是正確的時可以先寫一個假的isPrime(stub),因為這個isPrime可能是給別人寫的,所以先用假的isPrime模擬預期輸入輸出。等測完再用真的isPrime去試。 ```java= int[] allPrime(int n) { String s = ""; for (int i=1; i<=n; i++) { if (isPrime(i)) s = s + i + " "; } return transformToArray(s); } // stub 假的isPrime boolean isPrime(int n) { if (n==2) return true; if (n==3) return true; return false; } ``` ### mokito ```xml= <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>5.7.0</version> <scope>test</scope> </dependency> ``` 用interface去取代寫stub的麻煩。 #### Mock and Spy 除了 Mock 物件外,還有 Spy 物件。若沒有 when…thenReturn 來設定值,則 spy 會呼叫真正的物件來運算(如果是 mock 的話,沒有定義的就是回傳預設值,通常是 0,或是 null)。 ```java= App a = mock(App.class); when(a.add(1,1)).thenRetuen(2); assertEquals(2, a.add(1,1)); assertEquals(4, a.add(2,2)); ``` 上述程式中,第四行的檢驗是對的,因為有第二行的 when thenReturn 宣告,第五行卻會產生錯誤,因為 a.add(2,2) 沒有定義,會回傳預設的 0。 如果我們改成 spy(如下,第一行)。a.add(2,2) 沒有在測試碼中定義,就會執行真實的程式碼,所以第五行不會產生錯誤。 ```java= App a = spy(App.class); when(a.add(1,1)).thenRetuen(2); assertEquals(2, a.add(1,1)); assertEquals(4, a.add(2,2)); Verify ``` 有時候我們要檢驗某個方法是否有被呼叫,且參數正確,就可以使用 Verify 物件。 ```java= Prime p = new spy(Prime.class); when(p.isPrime(2)).thenReturn(true); when(p.isPrime(3)).thenReturn(true); when(p.isPrime(5)).thenReturn(true); int[] expected = {2,3,5}; assertArrayEquals(expected, p.allPrime(5)); verify(p).isPrime(2); 也可以用來檢驗次數 mockedList.add("1"); mockedList.add("2"); mockedList.add("2"); mockedList.add("3"); mockedList.add("3"); mockedList.add("3"); //times(1) 是預設值 verify(mockedList).add("1"); verify(mockedList, times(1)).add("1"); //恰好次數 verify(mockedList, times(2)).add("2"); verify(mockedList, times(3)).add("3"); //never() 表示從來沒有用過,也可以用 times(0) verify(mockedList, never()).add("4"); //至少或是最多的次數 verify(mockedList, atLeastOnce()).add("3"); verify(mockedList, atLeast(2)).add("3"); verify(mockedList, atMost(5)).add("3"); ``` **套件生成(好像沒有很好用)** ```java= @ExtendWith(MockitoExtension.class) // 告訴 JUnit 使用 MockitoExtension 來啟用 Mockito 的功能。 // MockitoExtension 負責初始化 @Mock 和 @InjectMocks 註解的物件,並確保 Mock 行為正確。 public class CalculatorTest { @InjectMocks Calculator calculator; // 1. @InjectMocks:負責將 @Mock 註解的物件自動注入到被測試的類別(Calculator)的相應屬性或建構子中。 // 2. 當使用建構子注入(如 Calculator(CalculatorService service)),Mockito 會嘗試匹配並注入所需的 mock。 @Mock CalculatorService calculatorService; // 1. @Mock:建立一個假的(mock)的 CalculatorService 物件。 // 2. 這個物件是模擬的,所有方法默認返回 null、基本類型的默認值(例如 0)、空集合等,除非有特別定義行為。 // 3. 這允許我們控制 `calculatorService` 的行為,而不依賴其實際實現。 @BeforeEach void setUp() { calculator = new Calculator(calculatorService); // 1. 這一行手動完成了 mock 物件的注入,但通常可以省略,因為 @InjectMocks 已經自動完成了這部分工作。 } @Test public void testAdd() { when(calculatorService.add(2, 3)).thenReturn(5); // 1. when-thenReturn:指定當 mock 物件 `calculatorService` 的 `add(2, 3)` 方法被調用時,返回值為 5。 // 2. 我們不需要關心 `CalculatorService` 的實際邏輯,只需模擬所需行為。 assertEquals(5, calculator.add(2, 3)); // 測試 `calculator` 是否正確調用了 `calculatorService.add` 並返回結果。 verify(calculatorService).add(2, 3); // verify:驗證 `calculatorService.add(2, 3)` 是否被調用,確認方法行為是否符合預期。 } } ``` **範例:** prime.java ```java= public class Prime { private final PrimeChecker primeChecker; // 透過建構函數注入依賴 public Prime(PrimeChecker primeChecker) { this.primeChecker = primeChecker; } public int[] allPrime(int n) { ArrayList<Integer> primes = new ArrayList<>(); for (int i = 2; i <= n; i++) { if (primeChecker.isPrime(i)) { primes.add(i); } } return primes.stream().mapToInt(Integer::intValue).toArray(); } } ``` PrimeChecker.java 用這個介面以實作mock ```java= public interface PrimeChecker { boolean isPrime(int x); } ``` test檔案 ```java= @Test public void testAllPrimeWithMockedIsPrime() { // 建立 mock 的 PrimeChecker PrimeChecker mockChecker = mock(PrimeChecker.class); // 設置模擬行為 when(mockChecker.isPrime(2)).thenReturn(true); when(mockChecker.isPrime(3)).thenReturn(true); when(mockChecker.isPrime(4)).thenReturn(false); when(mockChecker.isPrime(5)).thenReturn(true); when(mockChecker.isPrime(7)).thenReturn(true); // 注入 mock 的依賴 Prime prime = new Prime(mockChecker); // 測試結果 int[] result = prime.allPrime(4); assertArrayEquals(new int[]{2, 3}, result); // 驗證 mock 的方法是否被呼叫過1次 //verify(mockChecker, times(1)).isPrime(1); //因為上面沒有設定到when(mockChecker.isPrime(1)所以會報錯 verify(mockChecker, times(1)).isPrime(2); verify(mockChecker, times(1)).isPrime(3); verify(mockChecker, times(1)).isPrime(4); //verify(mockChecker, times(1)).isPrime(5); //因為上面int[] result = prime.allPrime(4);不會執行到isPrime(5) 所以報錯 } ``` verify:times(n)代表檢驗某個方法是否有被呼叫過n次,且參數正確。 ## 系統測試 主要測試使用者的使用流程是否順暢 ### cucumber 用java去測試網頁內容 #### 使用方法 要先下載套件 ![image](https://hackmd.io/_uploads/BkE7BeANJe.png) 然後開網頁也要先用在vscode下載這個 ![image](https://hackmd.io/_uploads/B1VQwgAEkg.png) 用這個開 ![image](https://hackmd.io/_uploads/HyWDDe0Nyg.png) 再複製網址 ![image](https://hackmd.io/_uploads/ryVoPxCVJl.png) #### 例子: ![image](https://hackmd.io/_uploads/S1k79lR41x.png) 先在test的java檔之下創建StepDefs的檔案定義每句話對應到什麼test。 Cucumber 不強制檔名的規範,只要你的步驟定義檔案包含了正確的 @Given、@When 和 @Then 註解,並被 Cucumber 正確掃描,就可以運作。 BMICalculatorStepDefs.java ```java= package bmi.cucumber; import io.cucumber.java.After; import io.cucumber.java.Before; import io.cucumber.java.en.*; import org.openqa.selenium.By; import org.openqa.selenium.WebDriver; import org.openqa.selenium.WebElement; import org.openqa.selenium.chrome.ChromeDriver; import org.openqa.selenium.chrome.ChromeOptions; import java.time.Duration; import static org.junit.jupiter.api.Assertions.*; public class BMICalculatorStepDefs { private WebDriver driver; private final String testedURL = "http://127.0.0.1:5500/bmi.html"; @Before public void setUp() { ChromeOptions options = new ChromeOptions(); options.addArguments("--headless"); options.addArguments("--remote-allow-origins=*"); driver = new ChromeDriver(options); driver.manage().timeouts().implicitlyWait(Duration.ofSeconds(4)); } @After public void tearDown() { if (driver != null) { driver.quit(); } } @Given("I am on the BMI calculator page") public void i_am_on_the_bmi_calculator_page() { driver.get(testedURL); } @When("I enter {string} in the name field") public void i_enter_in_the_name_field(String name) { WebElement nameInput = driver.findElement(By.id("name")); nameInput.clear(); nameInput.sendKeys(name); } @When("I enter {string} in the height field") public void i_enter_in_the_height_field(String height) { WebElement heightInput = driver.findElement(By.id("height")); heightInput.clear(); heightInput.sendKeys(height); } @When("I enter {string} in the weight field") public void i_enter_in_the_weight_field(String weight) { WebElement weightInput = driver.findElement(By.id("weight")); weightInput.clear(); weightInput.sendKeys(weight); } @When("I click the calculate button") public void i_click_the_calculate_button() { WebElement submitButton = driver.findElement(By.xpath("//button[@type='submit']")); submitButton.click(); } @Then("I should see the greeting {string}") public void i_should_see_the_greeting(String expectedGreeting) { WebElement greeting = driver.findElement(By.id("greeting")); assertEquals(expectedGreeting, greeting.getText()); } @Then("I should see the BMI value {string}") public void i_should_see_the_bmi_value(String expectedBMI) { WebElement bmiValue = driver.findElement(By.id("bmiValue")); assertEquals(expectedBMI, bmiValue.getText()); } @Then("I should see the BMI status {string}") public void i_should_see_the_bmi_status(String expectedStatus) { WebElement bmiStatus = driver.findElement(By.id("bmiStatus")); assertEquals(expectedStatus, bmiStatus.getText()); } @When("I click the clear button") public void i_click_the_clear_button() { WebElement clearButton = driver.findElement(By.xpath("//button[text()='清除資料']")); clearButton.click(); } @Then("the name field should be empty") public void the_name_field_should_be_empty() { WebElement nameInput = driver.findElement(By.id("name")); assertEquals("", nameInput.getAttribute("value")); } @Then("the height field should be empty") public void the_height_field_should_be_empty() { WebElement heightInput = driver.findElement(By.id("height")); assertEquals("", heightInput.getAttribute("value")); } @Then("the weight field should be empty") public void the_weight_field_should_be_empty() { WebElement weightInput = driver.findElement(By.id("weight")); assertEquals("", weightInput.getAttribute("value")); } @Then("the result section should be hidden") public void the_result_section_should_be_hidden() { WebElement resultSection = driver.findElement(By.id("result")); assertEquals("hidden", resultSection.getAttribute("class")); } } ``` RunCucumberTest.java ```java= package bmi.cucumber; import io.cucumber.junit.platform.engine.Cucumber; /** * 這個類別 `RunCucumberTest` 使用了 @Cucumber 註解,作為執行 Cucumber 測試的入口點。 * 當使用 JUnit 5 作為測試框架時,Cucumber 會透過這個類別啟動所有符合條件的 `.feature` 測試檔案。 * 雖然這個類別看起來是「空的」,但它扮演著關鍵角色,讓 Cucumber 可以與 JUnit 5 無縫整合並正常執行測試。 * @Cucumber 註解會自動掃描位於 resources 資料夾內的 feature 檔案, * 並執行與這些檔案對應的步驟定義 (Step Definitions)。 */ @Cucumber public class RunCucumberTest { } ``` bmi_calculation.feature 用語言的方式去測試,看起來比較直觀。但實際上背後工程龐大。 ```feature= Feature: BMI Calculator To help users calculate their BMI and understand their health status Scenario: Calculate BMI successfully Given I am on the BMI calculator page When I enter "Nick" in the name field And I enter "172" in the height field And I enter "67" in the weight field And I click the calculate button Then I should see the greeting "你好,Nick!" And I should see the BMI value "你的 BMI 值為:22.65" And I should see the BMI status "你的健康狀態:適中" Scenario: Clear all form fields Given I am on the BMI calculator page When I enter "測試者" in the name field And I enter "170" in the height field And I enter "65" in the weight field And I click the calculate button Then I should see the BMI value "你的 BMI 值為:22.49" And I click the clear button Then the name field should be empty And the height field should be empty And the weight field should be empty And the result section should be hidden Scenario: Clear form when incomplete input Given I am on the BMI calculator page When I enter "John" in the name field And I enter "172" in the height field And I click the clear button Then the name field should be empty And the height field should be empty And the weight field should be empty And the result section should be hidden ``` #### Behavior-Driven Development (BDD) 為 Feature File 的每一步驟撰寫對應的程式碼,使用 Cucumber 提供的標籤,例如 `@Given`, `@When`, `@Then`。 - 範例 (Java): ```java @Given("I am on the login page") public void iAmOnTheLoginPage() { driver.get("https://example.com/login"); } @When("I enter valid credentials") public void iEnterValidCredentials() { driver.findElement(By.id("username")).sendKeys("user"); driver.findElement(By.id("password")).sendKeys("password"); driver.findElement(By.id("login")).click(); } @Then("I should see the dashboard") public void iShouldSeeTheDashboard() { String title = driver.getTitle(); assertEquals("Dashboard", title); } ``` ## 程式複雜度 :::spoiler 使用方法 ![image](https://hackmd.io/_uploads/ry4_kDEQyx.png) 載matrics ![image](https://hackmd.io/_uploads/BJcHeDNmyl.png) ![image](https://hackmd.io/_uploads/BJZdgwNQ1l.png) ![image](https://hackmd.io/_uploads/HymKlwNQ1g.png) ![image](https://hackmd.io/_uploads/SkqV-vNQJe.png) ::: :::spoiler Metrics ### v(G) (Cyclomatic Complexity) 方法複雜度 - **Definition**: Cyclomatic complexity (\( v(G) \)) is a measure of the **number of linearly independent paths** through a program’s source code. It is calculated for each method and gives an indication of its structural complexity. - **Purpose**: - Indicates how complex a method is in terms of its control flow. - Helps identify methods that are prone to bugs or difficult to test. - **Typical Values**: - \( v(G) = 1 \): Straight-line code, no branches. - \( v(G) > 10 \): Code is complex and may require refactoring. ### WMC (Weighted Methods per Class) 類別方法複雜度總和 - **Definition**: **WMC** is the sum of the cyclomatic complexities of all methods in a class. - **Purpose**: Measures the total complexity of a class by aggregating the complexity of its methods. It reflects the overall effort required to understand or modify the class. - **Implication**: - A high WMC indicates a potentially bloated or overly complex class. - Classes with high WMC values may benefit from refactoring to reduce complexity. --- ### OCavg 類別平均方法複雜度 OCavg (Average Cyclomatic Complexity of Operations) - **Definition**: OCavg stands for the **average cyclomatic complexity of all methods** (or operations) within a class. - **Purpose**: This metric gives an indication of how complex the methods in a class are, on average. - **Implication**: - Higher values suggest a class contains methods that are overly complex, which may be harder to maintain or test. --- ### CogC (Cognitive Complexity) 認知複雜度 **Cognitive Complexity** (\( CogC \)) is a newer metric designed to measure how **difficult code is to understand**. Unlike cyclomatic complexity, which focuses on control flow, cognitive complexity emphasizes **human comprehension** and assigns weights to different types of code structures based on how mentally taxing they are. Key Principles 1. **Structural Complexity**: - Adds complexity for each nested structure (e.g., `if`, `for`, `while`). - Penalizes deeply nested constructs more heavily. 2. **Incremental Cost**: - Adds incremental complexity for certain constructs like `switch-case`, `try-catch`, etc. 3. **Readability Matters**: - Avoids penalizing straightforward, linear code. - Encourages refactoring of deeply nested or hard-to-follow logic. ### LOC (line of code) * CLOC: Comment LOC 註解的行數 * JLOC: Java Comment LOC Java 註解的行數 * CLOC 包含 JLOC * NCLOC: Non-Comment LOC 不含註解的行數 * RLOC: Real LOC 不含註解予宣告,真正可以執行的行數 In the package view * LOC(rec): LOC recursive 包含所有子 package 的行數 * LOCp: LOC per class 該 package 下平均每個 class 的行數 ::: ## 終極pom檔案 沒有codereview的功能 要額外加 ```xml= <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>DemoMutation4</groupId> <artifactId>DemoMutation4</artifactId> <version>1.0-SNAPSHOT</version> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <dependencies> <!-- JUnit 4 Dependency--> <!-- <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> --> <!-- JUnit 5 Dependency--> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-api</artifactId> <version>5.9.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter-engine</artifactId> <version>5.9.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>5.12.0</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-junit-jupiter</artifactId> <version>5.12.0</version> <!-- Use the same version as mockito-core --> </dependency> <!-- Selenium WebDriver --> <dependency> <groupId>org.seleniumhq.selenium</groupId> <artifactId>selenium-java</artifactId> <version>4.12.0</version> </dependency> <!-- cucumber --> <dependency> <groupId>io.cucumber</groupId> <artifactId>cucumber-java</artifactId> <version>7.13.0</version> <scope>test</scope> </dependency> <dependency> <groupId>io.cucumber</groupId> <artifactId>cucumber-junit-platform-engine</artifactId> <version>7.13.0</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <!-- JaCoCo Plugin (Optional for Coverage Analysis) --> <plugin> <groupId>org.jacoco</groupId> <artifactId>jacoco-maven-plugin</artifactId> <version>0.8.10</version> <executions> <execution> <id>prepare-agent</id> <goals> <goal>prepare-agent</goal> </goals> </execution> <execution> <id>report</id> <phase>verify</phase> <goals> <goal>report</goal> </goals> </execution> </executions> </plugin> <!-- Pitest Plugin Configuration for JUnit 4 --> <plugin> <groupId>org.pitest</groupId> <artifactId>pitest-maven</artifactId> <version>1.17.1</version> <configuration> <targetClasses> <param>demo.*</param> </targetClasses> <targetTests> <param>demo.*Test</param> </targetTests> <mutators>STRONGER</mutators> <threads>4</threads> <!-- <timeoutConst>4000</timeoutConst>--> <outputFormats> <param>HTML</param> </outputFormats> </configuration> </plugin> </plugins> </build> </project> ```