테스트 코드 걸음마 떼기 - 은행 계좌 클래스 만들기
다음 기능을 하는 은행계좌 클래스를 만드려 합니다.
클래스 이름은 Account!
기능
- 계좌 잔고 조회
- 입금/ 출금
- 예상 복리 이자
TDD는 항상 테스트를 먼저 작성합니다.
TDD에서 테스트의 최소 작성 단위를 최하위 모듈의 단위와 일치시킵니다.
Java 언어 기준으로 최하위 모듈이란 '메서드'입니다.
계좌 클래스의 이름은 Account라고 하겠습니다.
1. AccountTest.java 파일을 생성합니다.
계좌 잔고 조회에 대해 한번에 테스트를 작성하고 싶지만 한번 참아보겠습니다.
우선 계좌를 생성하고 그 계좌가 정상적으로 생성되었는지 확인해보겠습니다.
2. 계좌가 생성되었는지 확인하는 테스트 코드를 작성합니다.
package test;
public class AccountTest {
public void testAccount() {
Account account = new Account();
if( account == null) {
throw new Exception("계좌생성실패");
}
}
public static void main(String[] args) {
AccountTest test = new AccountTest();
test.testAccount();
}
}
Account 클래스를 생성한 적이 없으므로 당연히 빨간 줄이 나타날 겁니다.
IDE의 경고문들을 무시하고 실행을 시켜보면 당연히 에러 메시지가 출력됩니다.
3. 이제 계좌 생성 테스트를 통과하는 코드를 작성합니다.
package main;
public class Account {
}
객체만 생성하면 되므로 내용은 필요 없습니다.
빈 Account 클래스를 생성합니다.
4. 테스트 코드의 코드를 조금 수정합니다.
package test;
import main.Account;
public class AccountTest {
public void testAccount() throws Exception {
Account account = new Account();
if( account == null) {
throw new Exception("계좌생성실패");
}
}
public static void main(String[] args) {
AccountTest test = new AccountTest();
try {
test.testAccount();
} catch (Exception e) {
// TODO Auto-generated catch block
System.out.println("실패!");
e.printStackTrace();
}
System.out.println("성공!");
}
}
로직은 건드리지 않고 예외처리와 실패와 성공을 알아볼 수 있도록 실패와 성공을 출력하도록 하였습니다.
이제 테스트 코드를 실행해보면 "성공!" 메시지를 확인할 수 있습니다.
5. 테스트 끝? 이제 리팩터링 할 시간입니다.
테스트 코드 작성 -> 실제 코드 작성 -> 테스트 성공 -> 리팩터링 단계로 이루어집니다.
- 소스코드의 가독성이 적절한가?
- 중복된 코드는 없는가?
- 이름이 잘못 부여된 메서드나 변수명은 없는가?
- 구조의 개선이 필요한 부분은 없는가?
위의 내용에 대해 스스로에게 질문하며 고민할 시간을 가져봅니다.
Account 클래스의 내용이 워낙 없기 때문에 딱히 수정할 것은 없습니다.
여기서 잠깐!
일일이 테스트에 실패와 성공에 대해 메시지를 출력해야 하며, if 구문을 이용한 예외처리 구조 대신에 좀 더 단순화된 방법으로 예상 값과 결괏값 비교 등이 지원된다면 좀 더 구조적인 테스트 케이스 작성이 가능해집니다.
현재 코드에 JUnit 단위 테스트 프레임워크를 적용해보겠습니다.
testAccount() 메서드 위에 @Test 어노테이션을 작성해보겠습니다.
이후에 main 구문을 모두 지우고 실행합니다.
@Test
public void testAccount() throws Exception {
Account account = new Account();
if( account == null) {
throw new Exception("계좌생성실패");
}
}
JUnit과 함께 테스트 코드는 다음과 같이 간결해졌습니다.
여기서 계좌를 생성하는 부분에서 문제가 생긴다면 throw new Exception()을 하지 않아도 자동적으로 JUnit이 해당 상황을 처리하기 때문에 이 부분도 지워도 됩니다.
@Test
public void testAccount() throws Exception {
Account account = new Account();
}
테스트 코드 실행 시 결과입니다
글자로 보는 것보다 훨씬 성공과 실패를 한눈에 알아볼 수 있습니다.
만약 실패하게 된다면 빨간색 막대로 표시됩니다.
이제 테스트 코드의 한 주기가 끝이 났으며 다음으로 계좌 잔고 조회 기능에 대하여 TDD를 실행해보겠습니다.
10,000원으로 계좌가 생성되며 잔고 조회 결과 만원이 출력되어야 한다고 가정해보겠습니다.
6. 테스트 코드에서 testGetBalance() 메서드를 생성합니다.
@Test
public void testGetBalance() {
Account account = new Account(10000);
if (account.getBalance() != 10000) {
fail();
}
}
마찬가지로 @Test 어노테이션을 사용합니다.
이후에 계좌를 생성한 후 account.getBalance()를 호출하여 이 값이 10,000원인지 확인합니다.
여기서 fail()은 JUnit에서 제공하는 메서드로 fail 메서드가 호출되면 해당 테스트 케이스는 무조건 실패합니다.
7. Quick Fix 기능을 이용해 생성자와 getBalance() 메서드를 작성합니다.
8. 모든 에러를 수정한 이후 Account 클래스의 모습
package main;
public class Account {
public Account(int i) {
}
public Account() {
}
public int getBalance() {
return 0;
}
}
이때 이렇게 해도 되지만 만약 계좌에 무조건 초기 입금액을 설정해야 한다는 요구사항이 존재한다면?
Account() 생성자는 사용하지 않고 테스트코드에 Account 객체를 생성할 때 매개변수를 넣어주는 방식을 사용해도 됩니다.
9. 테스트 코드를 모두 작성하였으니 코드를 실행해 보겠습니다.
getBalance는 0을 반환하므로 우리가 기대하는 10,000과 다릅니다.
여기서 만약 하드 코딩으로 10,000을 반환하면 테스트 코드는 성공하겠지만 이런 식으로 테스트에 성공하는 것은 의미가 없습니다.
즉, 테스트 케이스를 엉성하게 만들면 테스트 자체를 신뢰할 수 없습니다.
10. 신뢰할 수 있는 테스트를 만들기 위해 몇 가지 항목을 추가해보겠습니다.
@Test
public void testGetBalance() {
Account account = new Account(10000);
if (account.getBalance() != 10000) {
fail();
}
account = new Account(1000);
if (account.getBalance() != 1000) {
fail();
}
account = new Account(0);
if (account.getBalance() != 0) {
fail();
}
}
11. 테스트가 정상적으로 동작할 수 있도록 Account 클래스를 작성합니다.
package main;
public class Account {
private int balance;
public Account(int balance) {
this.balance = balance;
}
public Account() {
}
public int getBalance() {
return balance;
}
}
12. Account 클래스를 수정하였으니 테스트 코드를 실행해봅니다.
13. 여기서 끝이 아니라 리팩터링을 할 시간입니다.
- 소스코드의 가독성이 적절한가?
- 중복된 코드는 없는가?
- 이름이 잘못 부여된 메서드나 변수명은 없는가?
- 구조의 개선이 필요한 부분은 없는가?
package main;
public class Account {
private int balance;
public Account(int money) {
this.balance = money;
}
public Account() {
}
public int getBalance() {
return balance;
}
}
이름이 잘못 부여된 변수명을 balance -> moeny로 수정해줍니다.
이제 계좌 잔고 조회 기능도 TDD를 기반으로 성공적으로 개발했습니다.
if (account.getBalance() != 10000) {}
예상 값과 실제값을 비교하는 조건을 만족하지 않으면 실패!라는 콘셉트인데 JUnit 테스트 프레임워크에서 제공하는 assertEquals()라는 메서드를 이용하면 좀 더 편리합니다.
assertEquals(예상값, 실제값)
assertEquals("설명", 예상값, 실제값)
@Test
public void testGetBalance() {
Account account = new Account(10000);
assertEquals(10000, account.getBalance());
account = new Account(1000);
assertEquals(1000, account.getBalance());
account = new Account(0);
assertEquals(0, account.getBalance());
}
물론 if문이나 print문을 사용하면 TDD가 아닌 건 아니지만 코드를 좀 더 편하게 작성할 수 있습니다.
14. 이제 입금/ 출금에 대한 테스트 코드를 먼저 작성해보겠습니다.
@Test
public void testDeposit() {
Account account = new Account(10000);
account.deposit(1000);
assertEquals(11000, account.getBalance());
}
@Test
public void testWithdraw() {
Account account = new Account(10000);
account.withdraw(1000);
assertEquals(9000, account.getBalance());
}
15. Quick Fix 기능을 이용해 메서드를 생성합니다.
public void deposit(int i) {
}
public void withdraw(int i) {
}
16. 이제 테스트 코드를 실행해 봅니다.
테스트는 당연히 실패합니다.
17. 이제 Account 클래스의 메서드를 구현합니다.
public void deposit(int i) {
balance += i;
}
public void withdraw(int i) {
balance -= i;
}
18. 테스트를 실행해 봅니다.
19. 리펙토링을 진행합니다.
public void deposit(int money) {
balance += money;
}
public void withdraw(int money) {
balance -= money;
}
중복되는 코드를 메서드로 추출합니다.
Account account = new Account(10000); -> setup();
Account 계정을 생성하는 부분이 반복적으로 나타납니다.
이 부분을 클래스의 Field로 옮겨봅니다.
private Account account;
또한 모든 메서드들은 실행하기 전 setup()을 호출하여 사용합니다.
Junit의 @Before 어노테이션을 사용하면 각 테스트 케이스가 실행되기 전에 항상 @Before라고 표시된 메서드가 먼저 실행됩니다.
리펙토링 완료된 코드
package test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import org.junit.Before;
import org.junit.jupiter.api.Test;
import main.Account;
public class AccountTest {
private Account account;
@Before
public void setup() {
account = new Account(10000);
}
@Test
public void testAccount() throws Exception {
}
@Test
public void testGetBalance() {
assertEquals(10000, account.getBalance());
account = new Account(1000);
assertEquals(1000, account.getBalance());
account = new Account(0);
assertEquals(0, account.getBalance());
}
@Test
public void testDeposit() {
account.deposit(1000);
assertEquals(11000, account.getBalance());
}
@Test
public void testWithdraw() {
account.withdraw(1000);
assertEquals(9000, account.getBalance());
}
}
출처
https://repo.yona.io/files/3920(1단원)
https://repo.yona.io/doortts/blog/issue/1