Java 學習記錄107 — Introduction to Unit Testing with JUnit

張小雄
7 min readJan 18, 2022

--

今天要學的是新篇章,測試功能

Tim老師教的是 Junit4,所以我練習也是用這版本

BankAccount.java

package bankAcoount;public class BankAccount {
private String firstName;
private String lastName;
private double balance;
public BankAccount(String firstName, String lastName, double balance) {
this.firstName = firstName;
this.lastName = lastName;
this.balance = balance;
}
// The branch argument is true if the customer is performing the transaction
// at a branch, with a teller, otherwise is at an ATM
public double deposit(double amount, boolean branch) {
balance += amount;
return balance;
}
// The branch argument is true if the customer is performing the transaction
// at a branch, with a teller, otherwise is at an ATM
public double withdraw(double amount, boolean branch) {
balance -= amount;
return balance;
}
public double getBalance() {
return balance;
}
// More methods that use firstName, lastName, and perform other functions
}

接著對著 class name 點右下 More actions

點第一個 Create Test

本課學的是 Junit 4,還要記得把下面的 Member 都打勾

BankAccountTest.java

package bankAcoount;import org.junit.Test;import static org.junit.Assert.*;public class BankAccountTest {    @Test
public void deposit() {
}
@Test
public void withdraw() {
}
@Test
public void getBalance() {
}
}

系統會新增一個 class,一開始都是空的

修改 BankAccountTest.java

package bankAcoount;import org.junit.Test;import static org.junit.Assert.*;public class BankAccountTest {    @Test
public void deposit() {
fail("Not yet implement");
}
@Test
public void withdraw() {
}
@Test
public void getBalance() {
}
@Test
public void dummyTest() {
assertEquals(20, 50);
}
}

接著右鍵點 Run

輸出結果:

第一個測試直接寫 fail 所以不會通過

最後一個測試寫,希望得到 20,但拿到的是 50,所以也沒通過

中間兩個測試都是空的,直接算通過

所以就是 2 個通過、2 個失敗

修改 BankAccountTest.java

@Test
public void deposit() {
BankAccount account = new BankAccount("Tim", "Jimmy", 1000.00);
double balance = account.deposit(200.00, true);
assertEquals(1200.00, balance, 0);
assertEquals(1200.00, account.getBalance(), 0);
}

測試結果:

通過

assertEquals()的參數分別為,希望得到的結果、實際給的數字,相差多少

不想一次運行所有測試可以只點行號旁邊的圖案,就可以單獨運行那個測試

修改 BankAccountTest.java

package bankAcoount;import org.junit.Test;import static org.junit.Assert.*;public class BankAccountTest {    @Test
public void deposit() {
BankAccount account = new BankAccount("Tim", "Jimmy", 1000.00);
double balance = account.deposit(200.00, true);
assertEquals(1200.00, balance, 0);
}
@Test
public void withdraw() {
fail("Not yet implement");
}
@Test
public void getBalance() {
BankAccount account = new BankAccount("Tim", "Jimmy", 1000.00);
account.deposit(200.00, true);
assertEquals(1200.00, account.getBalance(), 0);
}
}

測試結果:

通過,除了 withdraw()

主要是測試 getBalance()

Tim老師提醒說,所有的測試案例應該要可以分別單獨運行

所以 deposit() 也一起修改了

修改 BankAccountTest.java

package bankAcoount;import org.junit.Test;import static org.junit.Assert.*;public class BankAccountTest {    @Test
public void deposit() {
BankAccount account = new BankAccount("Tim", "Jimmy", 1000.00);
double balance = account.deposit(200.00, true);
assertEquals(1200.00, balance, 0);
}
@Test
public void withdraw() {
fail("Not yet implement");
}
@Test
public void getBalance_deposit() {
BankAccount account = new BankAccount("Tim", "Jimmy", 1000.00);
account.deposit(200.00, true);
assertEquals(1200.00, account.getBalance(), 0);
}
@Test
public void getBalance_withdraw() {
BankAccount account = new BankAccount("Tim", "Jimmy", 1000.00);
account.withdraw(200.00, true);
assertEquals(800.00, account.getBalance(), 0);
}
}

測試結果:

通過,除了 withdraw()

Tim老師又提醒說,測試名要有相關,就改成能直接看懂的名稱了

一個存款,一個提款,都符合希望得到的結果(1000 + 200=1200,1000–200=800)

BankAccount.java

package bankAcoount;public class BankAccount {
private String firstName;
private String lastName;
private double balance;
public static final int CHECKING = 1;
public static final int SAVING = 1;
private int typeOfAccount; public BankAccount(String firstName, String lastName, double balance, int typeOfAccount) {
this.firstName = firstName;
this.lastName = lastName;
this.balance = balance;
this.typeOfAccount = typeOfAccount;
}
// The branch argument is true if the customer is performing the transaction
// at a branch, with a teller, otherwise is at an ATM
public double deposit(double amount, boolean branch) {
balance += amount;
return balance;
}
// The branch argument is true if the customer is performing the transaction
// at a branch, with a teller, otherwise is at an ATM
public double withdraw(double amount, boolean branch) {
balance -= amount;
return balance;
}
public double getBalance() {
return balance;
}
public boolean isChecking() {
return typeOfAccount == CHECKING;
}
// More methods that use firstName, lastName, and perform other functions
}

修改 BankAccountTest.java

package bankAcoount;import org.junit.Test;import static org.junit.Assert.*;public class BankAccountTest {    @Test
public void deposit() {
BankAccount account = new BankAccount("Tim", "Jimmy", 1000.00, BankAccount.CHECKING);
double balance = account.deposit(200.00, true);
assertEquals(1200.00, balance, 0);
}
@Test
public void withdraw() {
fail("Not yet implement");
}
@Test
public void getBalance_deposit() {
BankAccount account = new BankAccount("Tim", "Jimmy", 1000.00, BankAccount.CHECKING);
account.deposit(200.00, true);
assertEquals(1200.00, account.getBalance(), 0);
}
@Test
public void getBalance_withdraw() {
BankAccount account = new BankAccount("Tim", "Jimmy", 1000.00, BankAccount.CHECKING);
account.withdraw(200.00, true);
assertEquals(800.00, account.getBalance(), 0);
}
@Test
public void isChecking_true() {
BankAccount account = new BankAccount("Tim", "Jimmy", 1000.00, BankAccount.CHECKING);
assertTrue(account.isChecking());
}
}

測試結果:

通過,除了 withdraw()

新增一個測試,帳戶類型是否為支票戶

修改 BankAccountTest.java

@Test
public void isChecking_true() {
BankAccount account = new BankAccount("Tim", "Jimmy", 1000.00, BankAccount.SAVING);
assertTrue("The account is NOT a checking account", account.isChecking());
}

測試結果:

java.lang.AssertionError: The account is NOT a checking account

at org.junit.Assert.fail(Assert.java:89)

at org.junit.Assert.assertTrue(Assert.java:42)

at bankAcoount.BankAccountTest.isChecking_true(BankAccountTest.java:38)

(以下省略)

改成存戶,測試就沒通過,還可自訂錯誤提示

修改 BankAccountTest.java

package bankAcoount;import org.junit.Before;
import org.junit.Test;
import static org.junit.Assert.*;public class BankAccountTest { private BankAccount account; @Before
public void setup() {
account = new BankAccount("Tim", "Jimmy", 1000.00, BankAccount.CHECKING);
System.out.println("Running a test...");
}
@Test
public void deposit() {
double balance = account.deposit(200.00, true);
assertEquals(1200.00, balance, 0);
}
@Test
public void withdraw() {
fail("Not yet implement");
}
@Test
public void getBalance_deposit() {
account.deposit(200.00, true);
assertEquals(1200.00, account.getBalance(), 0);
}
@Test
public void getBalance_withdraw() {
account.withdraw(200.00, true);
assertEquals(800.00, account.getBalance(), 0);
}
@Test
public void isChecking_true() {
assertTrue("The account is NOT a checking account", account.isChecking());
}
}

測試結果:

Running a test…

java.lang.AssertionError: Not yet implement

at org.junit.Assert.fail(Assert.java:89)

at bankAcoount.BankAccountTest.withdraw(BankAccountTest.java:26)

(省略部份)

Running a test…

Running a test…

Running a test…

Running a test…

把之前測試都會使用的部份,抽出來放到 setup() 裡面

跑每個測試的時候,都會運行一次

修改 BankAccountTest.java

package bankAcoount;import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import static org.junit.Assert.*;public class BankAccountTest { private BankAccount account; @BeforeClass
public static void beforeClass(){
System.out.println("This executes before any test cases");
}
@Before
public void setup() {
account = new BankAccount("Tim", "Jimmy", 1000.00, BankAccount.CHECKING);
System.out.println("Running a test...");
}
@Test
public void deposit() {
double balance = account.deposit(200.00, true);
assertEquals(1200.00, balance, 0);
}
@Test
public void withdraw() {
fail("Not yet implement");
}
@Test
public void getBalance_deposit() {
account.deposit(200.00, true);
assertEquals(1200.00, account.getBalance(), 0);
}
@Test
public void getBalance_withdraw() {
account.withdraw(200.00, true);
assertEquals(800.00, account.getBalance(), 0);
}
@Test
public void isChecking_true() {
assertTrue("The account is NOT a checking account", account.isChecking());
}
@AfterClass
public static void afterClass(){
System.out.println("This executes after any test cases.");
}
}

測試結果:

This executes before any test cases

Running a test…

java.lang.AssertionError: Not yet implement

at org.junit.Assert.fail(Assert.java:89)

at bankAcoount.BankAccountTest.withdraw(BankAccountTest.java:33)

(省略部份)

Running a test…

Running a test…

Running a test…

Running a test…

若不想每一次都執行,只想在測試案例之前或之後執行一次就好,則可以使用 @BeforeClass@AfterClass

Tim 老師提醒說,若是在測試結果中,This executes before any test cases 這句若跑到下面,並不代表執行順序就是那樣

修改 BankAccountTest.java

package bankAcoount;import org.junit.*;import static org.junit.Assert.*;public class BankAccountTest {    private BankAccount account;
private static int count;
@BeforeClass
public static void beforeClass(){
System.out.println("This executes before any test cases. Count = " + count++);
}
@Before
public void setup() {
account = new BankAccount("Tim", "Jimmy", 1000.00, BankAccount.CHECKING);
System.out.println("Running a test...");
}
@Test
public void deposit() {
double balance = account.deposit(200.00, true);
assertEquals(1200.00, balance, 0);
}
@Test
public void withdraw() {
fail("Not yet implement");
}
@Test
public void getBalance_deposit() {
account.deposit(200.00, true);
assertEquals(1200.00, account.getBalance(), 0);
}
@Test
public void getBalance_withdraw() {
account.withdraw(200.00, true);
assertEquals(800.00, account.getBalance(), 0);
}
@Test
public void isChecking_true() {
assertTrue("The account is NOT a checking account", account.isChecking());
}
@After
public void teardown(){
System.out.println("Count = " + count++);
}
@AfterClass
public static void afterClass(){
System.out.println("This executes after any test cases. Count = " + count++);
}
}

測試結果:

This executes before any test cases. Count = 0

Running a test…

Count = 1

java.lang.AssertionError: Not yet implement

at org.junit.Assert.fail(Assert.java:89)

at bankAcoount.BankAccountTest.withdraw(BankAccountTest.java:31)

Running a test…

Count = 2

Running a test…

Count = 3

Running a test…

Count = 4

Running a test…

Count = 5

This executes after any test cases. Count = 6

新增了一個 @After,每個測試案例都會執行一次

還加入了計數器,看著數字變化就能知道執行的順序為何

好處是就算測試結果的順序顯示亂掉了,也能從計數器看出順序

修改 BankAccount.java

// The branch argument is true if the customer is performing the transaction
// at a branch, with a teller, otherwise is at an ATM
public double withdraw(double amount, boolean branch) {
if ((amount > 500.00) && !branch) {
throw new IllegalArgumentException();
}
balance -= amount;
return balance;
}

修改 BankAccountTest.java

@Test
public void withdraw() {
double balance = account.withdraw(600.00, true);
assertEquals(400.00, balance, 0);
}

測試結果:

全通過

修改了 withdraw() 跟測試案例

若從 ATM 提款大於 500 元就會報錯

修改 BankAccountTest.java

@Test
public void withdraw_branch() {
double balance = account.withdraw(600.00, true);
assertEquals(400.00, balance, 0);
}
@Test
public void withdraw_notBranch() {
double balance = account.withdraw(600.00, false);
assertEquals(400.00, balance, 0);
}

測試結果:

This executes before any test cases. Count = 0

Running a test…

Count = 1

java.lang.IllegalArgumentException

at bankAcoount.BankAccount.withdraw(BankAccount.java:32)

at bankAcoount.BankAccountTest.withdraw_notBranch(BankAccountTest.java:37)

Running a test…

Count = 2

Running a test…

Count = 3

Running a test…

Count = 4

Running a test…

Count = 5

Running a test…

Count = 6

This executes after any test cases. Count = 7

拆成兩隻測試

跟我們預期的一樣,第二次測試沒有通過

但我們這邊本來就是設計成能拋錯,代表有通過才是

不應該歸類在沒通過

修改 BankAccountTest.java

@Test(expected = IllegalArgumentException.class)
public void withdraw_notBranch() {
double balance = account.withdraw(600.00, false);
assertEquals(400.00, balance, 0);
}

測試結果:

全通過

修改的方法,在 @Test 旁邊加上參數即可

若照我們預期拋出來這個錯誤,那代表測試成功

@Test
public void withdraw_branch() {
account.withdraw(600.00, true);
}
@Test(expected = IllegalArgumentException.class)
public void withdraw_notBranch() {
account.withdraw(600.00, false);
}

其實 asserEquals() 在這裡沒用到,直接移除也行

不會影響到運行結果

@Test //(expected = IllegalArgumentException.class)
public void withdraw_notBranch() {
try {
account.withdraw(600.00, false);
} catch (IllegalArgumentException ignored) {

}
}

注意 @Test 後面註釋掉了

這種寫法是 Junit4 之前的寫法

@Test //(expected = IllegalArgumentException.class)
public void withdraw_notBranch() {
try {
account.withdraw(600.00, true);
fail("Should have thrown and IllegalArgumentException");
} catch (IllegalArgumentException ignored) {
}
}

測試結果:

This executes before any test cases. Count = 0

Running a test…

Count = 1

java.lang.AssertionError: Should have thrown and IllegalArgumentException

at org.junit.Assert.fail(Assert.java:89)

at bankAcoount.BankAccountTest.withdraw_notBranch(BankAccountTest.java:38)

This executes after any test cases. Count = 2

加上自訂錯誤提示,就這樣寫

上面代碼全都紀錄在我的 Github

--

--