java測試驅動開發(TDD)之《井字遊戲》 頂 原

永久更新地址:https://my.oschina.net/bysu/blog/1632393

寫在前面若有侵權,請發郵件[email protected]告知。

本文主要是學習《Java測試驅動開發》過程中的記錄,除了工具有點不一致之外,其他都是摘抄自書本。

轉載者告知如果本文被轉載,但凡涉及到侵權相關事宜,轉載者需負責。請知悉!

個人覺得這書相當不錯,有興趣的可以買來看看。

圖書的相關鏈接:http://www.ituring.com.cn/book/1942

書中源碼下載:https://download.csdn.net/download/gdzjsubaoya/10286870

工具:eclipse(書中用的是IntelliJ IDEA)+gradle+Junit,整個項目目錄如下:

這個練習中,你將根據需求編寫測試,再編寫滿足測試期望的代碼。最後,如果有必要,將對代碼進行重構。也就是書中所說的:“紅燈-綠燈-重構”過程。

開發“開發井字遊戲”

遊戲規則:雙方輪流在一個3X3的網格中畫X和O,最先在水平、垂直或對角線上將自己的3個標記連起來的 玩家獲勝。

需求1

我們應該首先定義邊界,以及將棋子放在哪些地方非法。

可將棋子放在3X3棋盤上任何沒有棋子的地方。

可將這個需求分成三個測試:

1.如果棋子放在超出了X軸邊界的地方,就引發RuntimeException異常;

2.如果棋子放在超出了Y軸邊界的地方,就引發RuntimeException異常;

3.如果棋子放在已經有棋子的地方,就引發RuntimeException異常;

編寫第一個測試前,先簡單說說如何使用JUNIT測試異常。JUnit4.7引入了一項名爲規則(Rule)的功能,使用它可以做很多不同的事情,但在這裏我們感興趣的是規則Expected-Exception。

import org.junit.Test;
import org.junit.Rule;
import org.junit.rules.ExpectedException;


public class TicTacToeTest {
	
	@Rule
	public ExpectedException exception = ExpectedException.none();
	private TicTacToe ticTacToe;
    }

    @Test
    public void whenXOutsideBoardThenRuntimeException() {
    	exception.expect(RuntimeException.class);
    	ticTacToe.play(5,2);
    }
}

 

1.測試

首先檢查棋子是否放在3X3棋盤的邊界內:

棋子放在超出X軸邊界的地方時,將引發RuntimenException異常。

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;

public class TicTacToeTest {
	
	@Rule
	public ExpectedException exception = ExpectedException.none();
	private TicTacToe ticTacToe;
	
	@Before
	public final void before(){
		ticTacToe = new TicTacToe();
	}
	
    @Test public void whenXOutsideBoardThenRuntimeException() {
    	exception.expect(RuntimeException.class);
    	ticTacToe.play(5,2);
    }
}

這個測試中,我們指出調用方法ticTacToe.play(5,2)時,期望的結果是引發RuntimeException異常。這個測試既簡短又容易,要讓它通過應該也很容易:只需創建方法paly,並確保它在參數X小於1或大於3(棋盤是3X3的)時引發RuntimeException異常。你應運行這個測試3次:第一次運行時,它應該不能通過,因爲此時還沒有方法play;添加這個方法後,測試也應不能通過,因爲它沒有引發異常RuntimeException;第三次運行時應該通過,因爲實現了與這個測試相關聯的所有代碼。

2.實現

明確什麼情況下應引發異常後,實現代碼編寫起來應該很簡單:

public class TicTacToe {
    	public String play(int x,int y){

		    if(x < 1 || x > 3){
			    throw new RuntimeException("X is outside board");
		    }
	    }
}

 3.測試

這個測試與前一個測試幾乎相同,但驗證的是Y軸:

 @Test public void whenYOutsideBoardThenRuntimeException() {
    	exception.expect(RuntimeException.class);
    	ticTacToe.play(5,2);
    }

4.實現

這個規範的實現幾乎與前一個相同,只需在參數Y不在指定範圍內時引發異常即可:

public class TicTacToe {
    	public String play(int x,int y){

		    if(x < 1 || x > 3){
			    throw new RuntimeException("X is outside board");
		    }else if(y < 1 || y >3){
                throw new RuntimeException("Y is outside board");
            }
	    }
}

爲讓最後一個測試通過,添加一條“檢查參數Y是否在棋盤內”的else子句。

下面編寫當前需求涉及的最後一個測試。

5.測試

確定棋子在棋盤邊界內後,還需確保它放在未被別的棋子佔據的地方:

棋子放在被別的棋子佔據的地方時,將引發RuntimeException異常。

    @Test
    public void whenOccupiedThenRuntimeException() {
    	ticTacToe.play(2, 1);
    	exception.expect(RuntimeException.class);
    	ticTacToe.play(2, 1);
    }

這就是最後一個測試。編寫 實現後,即可認爲第一個需求完成了。

6.實現

爲實現最後一個測試,應將既有棋子的位置存儲在一個數組中。每當玩家放置新棋子時,都應確認棋子放在未佔用的位置,否則引發異常:

public class TicTacToe {

        private Character[][] board = {{'\0','\0','\0'},{'\0','\0','\0'},{'\0','\0','\0'}};

    	public String play(int x,int y){

		    if(x < 1 || x > 3){
			    throw new RuntimeException("X is outside board");
		    }else if(y < 1 || y >3){
                throw new RuntimeException("Y is outside board");
            }
            if(board[x -1][y - 1] != '\0'){
                throw new RuntimeException("Box is occupied");
            }else{
                board[x - 1][y - 1] = 'X';
            }
	    }
}

我們檢查要放置棋子的位置是否被佔用,如果未佔用,就將相應數組元素的值從空('\0')改爲被佔用的(X)。注意,我們還沒有記錄棋子是誰(X還是O)的。

7.重構

這些代碼雖然滿足了測試指定的需求,但有點令人迷惑。如果有人閱讀這些代碼,會搞不清方法play的母的。應重構這個方法,將其中的代碼放在多個方法中。重構後代碼如下:

	public String play(int x,int y){
		checkAxis(x);
		checkAxis(y);
		setBox(x,y,lastPlayer);
		
	}

	private void checkAxis(int axis){
		if(axis < 1 || axis > 3){
			throw new RuntimeException("X is outside board!");
		}
	}

    private void setBox(int x,int y,char lastPlayer){
		if(board[x-1][y-1] != '\0'){
			throw new RuntimeException("Box is occupied");
		}else{
			board[x-1][y-1] = 'X';
		}
	}

這個重構過程中,沒有改變方法play的功能,其行爲與以前完全相同,但代碼的可讀性更強了。由於我們有覆蓋了所有功能的測試,因此不用害怕重構時犯錯。只要確保所有測試都通過且重構時沒有引入新行爲,就可以放心大膽地修改代碼。

需求1.完整的源碼:

測試代碼:

 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 
 public class TicTacToeTest {
 
     @Rule
     public ExpectedException exception = ExpectedException.none();
     private TicTacToe ticTacToe;
 
     @Before
     public final void before() {
         ticTacToe = new TicTacToe();
     }
 
     @Test
     public void whenXOutsideBoardThenRuntimeException() {
         exception.expect(RuntimeException.class);
         ticTacToe.play(5, 2);
     }
 
     @Test
     public void whenYOutsideBoardThenRuntimeException() {
         exception.expect(RuntimeException.class);
         ticTacToe.play(2, 5);
     }
 
     @Test
     public void whenOccupiedThenRuntimeException() {
-        ticTacToe.play(1, 1);
         ticTacToe.play(2, 1);
         exception.expect(RuntimeException.class);
         ticTacToe.play(2, 1);
     }
 
 }

實現代碼


 public class TicTacToe {
 
     private Character[][] board = {{null, null, null}, {null, null, null}, {null, null, null}};
 
     public void play(int x, int y) {
         if (x < 1 || x > 3) {
             throw new RuntimeException("X is outside board");
         } else if (y < 1 || y > 3) {
             throw new RuntimeException("Y is outside board");
         }
         if (board[x - 1][y - 1] != null) {
             throw new RuntimeException("Y is outside board");
             throw new RuntimeException("Box is occupied");
         } else {
             board[x - 1][y - 1] = 'X';
         }
     }
 
 }

需求2

現在處理輪到哪個玩家落子的問題。

需要提供一種途徑,用於判斷接下來該誰落子。

可將這個需求分成三個 測試:

1.玩家X先下;

2.如果上一次是X下的,接下來將輪到O下;

3.如果上一次是O下的,接下來將輪到X下。

到目前爲止,我們還未使用過JUnit斷言。要使用斷言,需要導入org.junit.Assert類中的靜態(static)方法:

import static org.junit.Assert.*;

Assert類中的方法都非常簡單,它們大都以assert打頭。例如,assertEquals對兩個對象進行比較:assertNotEquals驗證兩個對象不同,而assertArrayEquals驗證兩個數組相同。這兩個斷言都有很多重載版本,因此幾乎能夠對任何類型的java對象進行比較。

在這裏,我們需要比較兩個字符,其中第一個是預期的字符,而第二個是方法nextPlayer返回的實際字符。

現在編寫這些測試及其實現。

先編寫測試,再編寫實現代碼

這樣做的好處是:可 確保編寫的代碼是可測試的,且每行代碼都有對應的測試。

通過先編寫或修改測試,開發人員可在編寫代碼前專注於需求。這是與完成實現後再編寫測試的主要差別所在。測試先行的另一個好處是,可避免原本應爲質量保證的測試淪爲質量檢查。

1.測試

玩家X先下:

 //X玩家先下
    @Test public void givenFirstTurnWhenNextPlayerThenX(){
    	assertEquals('X', ticTacToe.nextPlayer());
    }

應該是玩家X先下

這個測試應該是不言自明的:我們期望nextPlayer返回X。如果現在運行這個測試,將發現它都不能通過編輯,這是因爲還沒有方法player。我們的任務是編寫方法nextPlayer,並確保它返回正確的值。

2.實現

其實根本不需要檢查玩家X是否先下,因爲就目前而言,只需讓nextPlayer返回X就能讓這個測試通過。後面的測試將要求我們修改這個方法的代碼:

	public char nextPlayer(){

		return 'X';
	}

3.測試

現在需要確保讓玩家輪流下。玩家X下棋後,應輪到玩家O,然後再輪到玩家X,以此類推:

    //X玩家下完,到O玩家下
    @Test public void givenLastTurnWhenNextPlayerThenO(){
    	ticTacToe.play(1, 1);
    	assertEquals('O', ticTacToe.nextPlayer());
    }

如果前一次是玩家X下的,接下來應輪到玩家O。

4.實現

爲跟蹤接下來該誰下,需要存儲前一次下棋的玩家:

	private char lastPlayer = '\0';

	public String play(int x,int y){
		checkAxis(x);
		checkAxis(y);
		setBox(x,y,lastPlayer);
        lastPlayer = nextPlayer();
		
	}	

    public char nextPlayer(){
		if(lastPlayer == 'X'){
			return 'O';
		}
		return 'X';
	}

你很可能已經進入狀態。測試很小且易於編寫,有了足夠的經驗後,編寫一個測試只需一分鐘甚至幾秒鐘;而編寫實現所需的時間也差不多,甚至更短。

5.測試

我們終於可以檢查玩家O下後是不是輪到玩家X了。

如果前一次是玩家O下的,接下來應輪到玩家X下。

即使什麼都不用做,這個測試也能通過。因此它毫無用處,應當刪除。如果編寫這個測試,將發現它存在報錯問題:在沒有修改實現的情況下就能通過。你可以自己試一試。編寫測試後,如果它在沒有編寫任何實現代碼時就能通過,應將其刪除。

此時,測試的完整代碼

 import org.junit.Before;
 import org.junit.Rule;
 import org.junit.Test;
 import org.junit.rules.ExpectedException;
 import static org.junit.Assert.*;
 
 public class TicTacToeSpec {
 
     @Rule
     public ExpectedException exception = ExpectedException.none();
     private TicTacToe ticTacToe;
 
     @Before
     public final void before() {
         ticTacToe = new TicTacToe();
     }
 
     @Test
     public void whenXOutsideBoardThenRuntimeException() {
         exception.expect(RuntimeException.class);
         ticTacToe.play(5, 2);
     }
 
     @Test
     public void whenYOutsideBoardThenRuntimeException() {
         exception.expect(RuntimeException.class);
         ticTacToe.play(2, 5);
     }
 
     @Test
     public void whenOccupiedThenRuntimeException() {
         ticTacToe.play(1, 1);
         ticTacToe.play(2, 1);
         exception.expect(RuntimeException.class);
         ticTacToe.play(2, 1);
     }
 
     @Test
     public void givenFirstTurnWhenNextPlayerThenX() {
         assertEquals('X', ticTacToe.nextPlayer());
     }
 
     @Test
     public void givenLastTurnWasXWhenNextPlayerThenO() {
         ticTacToe.play(1, 1);
         assertEquals('O', ticTacToe.nextPlayer());
     }
 
 }

需求3

現在考慮這個遊戲的獲勝規則。相比於前面的代碼,這部分工作更繁瑣。我們必須檢查所有可能獲勝的情況,只要滿足其中一個,就宣佈相應玩家獲勝。

最先在水平、垂直或對角線上將自己的3個標記連起來的玩家獲勝。

要檢查同一玩家的3顆棋子是否連成了線,需要檢查水平方向、垂直方向和對角線。

1.測試

下面首先定義方法play的默認返回值:

     @Test
     public void whenPlayThenNoWinner() {
         String actual = ticTacToe.play(1, 1);
         assertEquals("No winner", actual);
     }

如果不滿足獲勝條件,則無人獲勝。

2.實現

默認返回值總是最容易實現的,這裏也不例外:

    public String play(int x, int y) {
         checkAxis(x);
         checkAxis(y);
         setBox(x, y, lastPlayer);
         lastPlayer = nextPlayer();
         return "No winner";
     }

3.測試

指定默認結果(沒有人獲勝)後,處理各種獲勝條件:

     @Test
     public void whenPlayAndWholeHorizontalLineThenWinner() {
         ticTacToe.play(1, 1); // X
         ticTacToe.play(1, 2); // O
         ticTacToe.play(2, 1); // X
         ticTacToe.play(2, 2); // O
         String actual = ticTacToe.play(3, 1); // X
         assertEquals("X is the winner", actual);
     }

一個玩家的棋子佔據整條水平線就贏了。

4.實現

爲讓這個測試通過,需要檢查是否有水平線全被當前玩家的棋子佔據。到目前爲止,我們根本不關心存儲到數組board中的值是什麼,但現在不但要記錄哪些棋盤格是空的,還需記錄各個棋盤格被哪個玩家佔據:

    public String play(int x, int y) {
        checkAxis(x);
        checkAxis(y);
        lastPlayer = nextPlayer();
        setBox(x, y, lastPlayer);
        for(int index = 0;index < 3;index++){
		    if(board[0][index] == lastPlayer && board[1][index] == lastPlayer && board[2][index] == lastPlayer){
				return lastPlayer + " is the winner";    
			}
		}
        return "No winner";
    }

    private void setBox(int x, int y, char lastPlayer) {
        if (board[x - 1][y - 1] != '\0') {
            throw new RuntimeException("Box is occupied");
        } else {
            board[x - 1][y - 1] = lastPlayer;
        }
    }

5.重構

前面的代碼能夠讓測試通過,完成了儘快讓測試通過的使命,但並非沒有改進的空間。現在我們有了確保預期行爲完整性的測試,可對代碼進行重構:

    private static final int SIZE = 3;

    public String play(int x, int y) {
        checkAxis(x);
        checkAxis(y);
        lastPlayer = nextPlayer();
        setBox(x, y, lastPlayer);
        if (isWin()) {
            return lastPlayer + " is the winner";
        }
        return "No winner";
    }

    private boolean isWin() {
       
        for (int i = 0; i < SIZE; i++) {
            if ((board[0][i] + board[1][i] + board[2][i]) == (lastPlayer * SIZE)) {
                return true;
            }
        }
        return false;
    }

重構後的解決方案看起來更好。play依然很短,很容易理解。將實現獲勝邏輯的代碼移到一個獨立的方法中,不僅讓方法play的目的變得清晰,還能讓我們獨立添加檢查獲勝條件的代碼。

6.測試

我們還需檢查是否有垂直線完全被某個玩家佔據:

    @Test
    public void whenPlayAndWholeVerticalLineThenWinner() {
        ticTacToe.play(2, 1); // X
        ticTacToe.play(1, 1); // O
        ticTacToe.play(3, 1); // X
        ticTacToe.play(1, 2); // O
        ticTacToe.play(2, 2); // X
        String actual = ticTacToe.play(1, 3); // O
        assertEquals("O is the winner", actual);
    }

一個玩家的棋子佔據整條垂直線就贏了。

7.實現

這個實現應該與前一個類似。前面在水平方向上做了檢查,現在需要在垂直方向上做同樣的檢查:

private boolean isWin() {
        int playerTotal = lastPlayer * SIZE;
        for (int i = 0; i < SIZE; i++) {
            if ((board[0][i] + board[1][i] + board[2][i]) == playerTotal) {
                return true;
            } else if ((board[i][0] + board[i][1] + board[i][2]) == playerTotal) {
                return true;
            }
        }
        return false;
    }

8.測試

水平線和垂直線都處理後,該將注意力轉向對角線了:

    @Test
    public void whenPlayAndTopBottomDiagonalLineThenWinner() {
        ticTacToe.play(1, 1); // X
        ticTacToe.play(1, 2); // O
        ticTacToe.play(2, 2); // X
        ticTacToe.play(1, 3); // O
        String actual = ticTacToe.play(3, 3); // O
        assertEquals("X is the winner", actual);
    }

一個玩家的棋子佔據從左上角到右下角的整條對角線就贏了。

9.實現

    private boolean isWin() {
        int playerTotal = lastPlayer * 3;

        for (int i = 0; i < SIZE; i++) {

            if ((board[0][i] + board[1][i] + board[2][i]) == playerTotal) {
                return true;
            } else if ((board[i][0] + board[i][1] + board[i][2]) == playerTotal) {
                return true;
            }
        }
        if (board[0][0] + board[1][1] + board[2][2] == playerTotal) {
            return true;
        }
        return false;
    }

10.測試

最後,還有最後一個可能的獲勝條件需要處理:

    @Test
    public void whenPlayAndBottomTopDiagonalLineThenWinner() {
        ticTacToe.play(1, 3); // X
        ticTacToe.play(1, 1); // O
        ticTacToe.play(2, 2); // X
        ticTacToe.play(1, 2); // O
        String actual = ticTacToe.play(3, 1); // O
        assertEquals("X is the winner", actual);
    }

一個玩家的棋子佔據從左下角到右上角的整條對角線就贏了。

11.實現

這個測試的實現應該與前一個幾乎完全相同:

    private boolean isWin() {
        int playerTotal = lastPlayer * 3;

        for (int i = 0; i < SIZE; i++) {

            if ((board[0][i] + board[1][i] + board[2][i]) == playerTotal) {
                return true;
            } else if ((board[i][0] + board[i][1] + board[i][2]) == playerTotal) {
                return true;
            }
        }
        if (board[0][0] + board[1][1] + board[2][2] == playerTotal) {
            return true;
        }else if (board[0][2] + board[1][1] + board[2][0] == playerTotal) {
            return true;
        }
        return false;
    }

12.重構

處理對角線時,所做的計算看起來不太好,也許重用既有的循環更合適:

    private boolean isWin(){
		int playerTotal = lastPlayer * 3;
		char diagonal1 = '\0';
		char diagonal2 = '\0';
		
		for(int index=0;index<SIZE;index ++){
			diagonal1 += board[index][index];//從左上到右下的對角線
			diagonal2 += board[index][SIZE-index-1];//從右下到左上的對角線
			
			if(board[0][index] + board[1][index] + board[2][index] == playerTotal){//水平線,贏
				return true;
			}else if(board[index][0] + board[index][1] + board[index][2] == playerTotal){//垂直線,贏
				return true;
			}
			
			if(diagonal1 == playerTotal || diagonal2 == playerTotal){//兩條對角線
				return true;
			}
		}
		return false;
	}

下面處理最後一個需求。

需求4

現在缺失的唯一一項內容是如何處理平局。

所有格子都佔滿則爲平局。

1.測試

可以通過填滿棋盤的所有格子測試平局結果:

    //平局,棋盤被佔滿
    @Test
    public void whenAllBoxesAreFilledThenDraw(){
    	ticTacToe.play(1, 1);
    	ticTacToe.play(1, 2);
    	ticTacToe.play(1, 3);
    	ticTacToe.play(2, 1);
    	ticTacToe.play(2, 3);
    	ticTacToe.play(2, 2);
    	ticTacToe.play(3, 1);
    	ticTacToe.play(3, 3);
    	String actual = ticTacToe.play(3, 2);
    	assertEquals("The result is draw", actual);
    }

2.實現

檢查是否爲平局非常簡單——只需檢查是否已佔滿整個棋盤。爲此,可遍歷數組board:

   public String play(int x,int y){
		checkAxis(x);
		checkAxis(y);
		lastPlayer = nextPlayer();
		setBox(x,y,lastPlayer);
		if(isWin()){
			return lastPlayer + " is the winner";
		}else if(isDraw()){
			return "The result is draw";
		}else{
			return "No winner";
		}
	}
	
	private boolean isDraw(){
		for(int x=0;x<SIZE;x++){
			for(int y=0;y<SIZE;y++){
				if(board[x][y] == '\0'){
					return false;
				}
			}
		}
		return true;
	}

3.重構

	private boolean isWin(int x,int y){
		int playerTotal = lastPlayer * SIZE;
		char horizontal='\0',vertical='\0',diagonal1='\0',diagonal2 = '\0';
		
		for(int index=0;index<SIZE;index ++){
			horizontal += board[index][y-1];
			vertical += board[x-1][index];
			diagonal1 += board[index][index];//從左上到右下的對角線
			diagonal2 += board[index][SIZE-index-1];//從右下到左上的對角線
			
			if(horizontal == playerTotal || vertical == playerTotal || diagonal1 == playerTotal || diagonal2 == playerTotal){//兩條對角線
				return true;
			}
		}
		return false;
	}

    //調用時isWin函數需要傳對應的參數
    public String play(int x,int y){
		checkAxis(x);
		checkAxis(y);
		lastPlayer = nextPlayer();
		setBox(x,y,lastPlayer);
		if(isWin(x,y)){
			return lastPlayer + " is the winner";
		}else if(isDraw()){
			return "The result is draw";
		}else{
			return "No winner";
		}
	}

Git倉庫中的源碼:https://bitbucket.org/vfarcic/tdd-java-ch03-tic-tac-toe/branch/04-draw

或下面源碼

測試源碼

import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import static org.junit.Assert.*;

public class TicTacToeTest{

    @Rule
    public ExpectedException exception = ExpectedException.none();
    private TicTacToe ticTacToe;

    @Before
    public final void before() {
        ticTacToe = new TicTacToe();
    }

    @Test
    public void whenXOutsideBoardThenRuntimeException() {
        exception.expect(RuntimeException.class);
        ticTacToe.play(5, 2);
    }

    @Test
    public void whenYOutsideBoardThenRuntimeException() {
        exception.expect(RuntimeException.class);
        ticTacToe.play(2, 5);
    }

    @Test
    public void whenOccupiedThenRuntimeException() {
        ticTacToe.play(2, 1);
        exception.expect(RuntimeException.class);
        ticTacToe.play(2, 1);
    }

    @Test
    public void givenFirstTurnWhenNextPlayerThenX() {
        assertEquals('X', ticTacToe.nextPlayer());
    }

    @Test
    public void givenLastTurnWasXWhenNextPlayerThenO() {
        ticTacToe.play(1, 1);
        assertEquals('O', ticTacToe.nextPlayer());
    }

    @Test
    public void whenPlayThenNoWinner() {
        String actual = ticTacToe.play(1, 1);
        assertEquals("No winner", actual);
    }

    @Test
    public void whenPlayAndWholeHorizontalLineThenWinner() {
        ticTacToe.play(1, 1); // X
        ticTacToe.play(1, 2); // O
        ticTacToe.play(2, 1); // X
        ticTacToe.play(2, 2); // O
        String actual = ticTacToe.play(3, 1); // X
        assertEquals("X is the winner", actual);
    }

    @Test
    public void whenPlayAndWholeVerticalLineThenWinner() {
        ticTacToe.play(2, 1); // X
        ticTacToe.play(1, 1); // O
        ticTacToe.play(3, 1); // X
        ticTacToe.play(1, 2); // O
        ticTacToe.play(2, 2); // X
        String actual = ticTacToe.play(1, 3); // O
        assertEquals("O is the winner", actual);
    }

    @Test
    public void whenPlayAndTopBottomDiagonalLineThenWinner() {
        ticTacToe.play(1, 1); // X
        ticTacToe.play(1, 2); // O
        ticTacToe.play(2, 2); // X
        ticTacToe.play(1, 3); // O
        String actual = ticTacToe.play(3, 3); // O
        assertEquals("X is the winner", actual);
    }

    @Test
    public void whenPlayAndBottomTopDiagonalLineThenWinner() {
        ticTacToe.play(1, 3); // X
        ticTacToe.play(1, 1); // O
        ticTacToe.play(2, 2); // X
        ticTacToe.play(1, 2); // O
        String actual = ticTacToe.play(3, 1); // O
        assertEquals("X is the winner", actual);
    }

    @Test
    public void whenAllBoxesAreFilledThenDraw() {
        ticTacToe.play(1, 1);
        ticTacToe.play(1, 2);
        ticTacToe.play(1, 3);
        ticTacToe.play(2, 1);
        ticTacToe.play(2, 3);
        ticTacToe.play(2, 2);
        ticTacToe.play(3, 1);
        ticTacToe.play(3, 3);
        String actual = ticTacToe.play(3, 2);
        assertEquals("The result is draw", actual);
    }

}

 

實現源碼:

    public class TicTacToe {

    private Character[][] board = {{'\0', '\0', '\0'}, {'\0', '\0', '\0'}, {'\0', '\0', '\0'}};
    private char lastPlayer = '\0';
    private static final int SIZE = 3;

    public String play(int x, int y) {
        checkAxis(x);
        checkAxis(y);
        lastPlayer = nextPlayer();
        setBox(x, y, lastPlayer);
        if (isWin(x, y)) {
            return lastPlayer + " is the winner";
        } else if (isDraw()) {
            return "The result is draw";
        } else {
            return "No winner";
        }
    }

    public char nextPlayer() {
        if (lastPlayer == 'X') {
            return 'O';
        }
        return 'X';
    }

    private void checkAxis(int axis) {
        if (axis < 1 || axis > SIZE) {
            throw new RuntimeException("X is outside board");
        }
    }

    private void setBox(int x, int y, char lastPlayer) {
        if (board[x - 1][y - 1] != '\0') {
            throw new RuntimeException("Box is occupied");
        } else {
            board[x - 1][y - 1] = lastPlayer;
        }
    }

    private boolean isWin(int x, int y) {
        int playerTotal = lastPlayer * SIZE;
        char horizontal, vertical, diagonal1, diagonal2;
        horizontal = vertical = diagonal1 = diagonal2 = '\0';
        for (int i = 0; i < SIZE; i++) {
            horizontal += board[i][y - 1];
            vertical += board[x - 1][i];
            diagonal1 += board[i][i];
            diagonal2 += board[i][SIZE - i - 1];
        }
        if (horizontal == playerTotal
                || vertical == playerTotal
                || diagonal1 == playerTotal
                || diagonal2 == playerTotal) {
            return true;
        }
        return false;
    }

    private boolean isDraw() {
        for (int x = 0; x < SIZE; x++) {
            for (int y = 0; y < SIZE; y++) {
                if (board[x][y] == '\0') {
                    return false;
                }
            }
        }
        return true;
    }

}

代碼覆蓋率

如果需要結合JaCoCo,參考我另一篇博文,不過跟本博文中抄錄的書籍無關。書中也說了JaCoCo的使用,感興趣的可以找來閱讀。

在eclipse中使用jacoco插件對測試覆蓋進行監控

小結

我們使用“紅燈-綠燈-重構”流程完成了“井字遊戲”,這些示例本身都很簡單,易於理解。

本章並非要深入探討複雜的東西,而是要讓你養成反覆使用“紅燈-綠燈-重構”流程的習慣。

你學習瞭如下內容:開發軟件的最簡單方式就是將其分成小塊;設計方案脫胎於測試,而不是預先採用複雜的方法進行制定;先編寫測試並確定未通過後,在着手編寫實現代碼;確定最後一個測試未通過後,就能肯定它是有效的(你一不小心就會犯錯,編寫總是能夠通過的測試),要實現的功能還不存在;測試未通過後,編寫其實現代碼;編寫實現時,力圖使其儘可能簡單,只要能讓測試通過就行,而不試圖提供完美的解決方案;不斷重複這個過程,直到認爲需要對代碼進行重構爲止;重構時不能引入任何新功能(即不改變應用程序的行爲),而只是對代碼進行改進,使其更容易理解和維護。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章