💁🏻♂️ 어떻게 테스트 해야 될 것인지에 대한 이야기들
의존성을 주입하고 작은 단위로 테스트하기 위해 책임을 나누고 테스트에 용이한 구조를 설계하다 보면 객체 간의 책임이 잘 나눠진다.
아래는 TDD에 대한 다른 사람들의 의견을 찾다가 더 나아가서 테스트 코드에 대한 의견 또한 보게 됐고 그 과정에서 찾은 토론입니다.
즉, TDD 에 대한 토론은 아니고 '테스트 코드'를 작성해야 하는가? 에 대한 이야기입니다.'테스트 코드는 정상 케이스에 대한 검증만 수행하여 오류를 잡지 못한다' 라는
테스트 코드에 대한 회의적인 의견그리고 그에 대한 답변
-> 테스트코드가 없다면 개발 당시 당연하다고 생각했던 정상 케이스들을 몇 개월 뒤에 기억할 수 없고 다른 사람들은 모름.
테스트 코드가 없으므로 간단한 수정이라고 생각했던 구현으로 인해 예상치 못한 버그가 발생할 가능성이 높고 배포까지 이어질 가능성도 있음.
그렇기에 개발 시점에서는 필요성을 못 느낄 수도 있겠지만 이후를 위해서라도 작성하는 것이 좋다.
테스트 데이터가 단순히 많은게 중요한게 아니라 개념적 차이를 가진 다양한 테스트 케이스를 가지는 것이 중요하다는 뜻으로 보면 될 것 같습니다.
Bank bank = new Bank();
bank.addRate("USD", "GBP", STANDARD_RATE);
bank.commission(STANDARD_COMMISSION);
Money result = bank.convert(new Note(100, "USD"), "GBP");
assertEquals(new Note(49.25, "GBP"), result);
위와 같이 표현할수도 있지만, 좀 더 명확하게
Bank bank = new Bank();
bank.addRate("USD", "GBP", 2);
bank.commission(0.015);
Money result = bank.convert(new Note(100, "USD"), "GBP");
assertEquals(new Note(100 / 2 * (1 - 0.015), "GBP"), result);
와 같이 표현할 수도 있다.
단언 부분에 수식을 써놓으면 다음으로 무엇을 해야 할지 쉽게 알게 된다.
예제의 경우 어떻게든 나눗셈과 곱셈을 수행할 프로그램을 만들어야 한다는 것을 알게 되는 것이다.
명백한 데이터는 코드에 매직넘버를 쓰지 말라는 것에 대한 예외적인 규칙일 수 있다.
상수를 매직넘버로 다시 바꾸는건 그렇게 공감이 안 가지만, 49.25와 같은 숫자를 수식으로 바꾸는건 맞다고 생각합니다.
💁🏻♂️ 테스트를 언제 어디서 작성할 것인지, 테스트 작성을 언제 멈출 것인지에 대한 것
여러 연산을 필요로 하는 테스트보다는 최대한 간단한 테스트부터 작성하기 시작하라
앞선 예제에서도 나왔듯이 여러 개의 연산이 전제되는 복잡한 기능부터 구현하기 보다는 간단한 연산 하나하나부터 구현하는게 좋은 것 같습니다.
💁🏻♂️ 더 상세한 테스트 작성법에 대한 내용
def testNotification(self):
result = TestResult()
listener = ResultListener()
result.addListener(listener)
WasRun("testMethod").run(result)
asser 1 == listener.count
그리고 이벤트 통보 횟수를 셀 객체가 필요하다.
class ResultListener:
def __init__(self):
self.count = 0
def startTest(self):
self.count = self.count + 1
그런데 이렇게 별도의 리스너 객체를 만들기 보다는 테스트 객체 자체를 모의 객체로 사용할 수 있다.
def testNotification(self):
self.count = 0
result = TestResult()
result.addListener(self)
WasRun("testMethod").run(result)
asser 1 == listener.count
def startTest(self):
self.count = self.count + 1
이렇게 셀프 션트를 사용하여 작성한 테스트가 그렇지 않은 테스트보다 읽기에 더 수월하다. 통보횟수가 0이었다가 1이 됐다. 이 순서를 테스트에서 바로 읽어낼 수 있다.
혼자서 프로그래밍할 때 프로그래밍 세션을 어떤 상태로 끝마치는게 좋을까? 마지막 테스트가 깨진 상태로 끝마치는게 좋다.
프로그래밍 세션을 끝낼 때 테스트 케이스를 작성하고 이것이 실패하는 것까지 확인하고 다음 날부터 이어서 코딩을 시작한다. 그러면 다음 날 어느 작업부터 시작할 것인지 명백히 알 수 있고 시간을 절약할 수 있다.
💁🏻♂️ 최대한 빨리 테스트를 성공시키기 위한 패턴들
여지껏 읽은 책 내용을 토대로 이해하자면, 테스트를 빨리 통과시켜야 한다는 이유로 모든 것을 가짜로 구현할 필요는 없고 한 번에 진행하기에 너무 큰 작업인 경우에 우선 가짜로 구현 후 테스트가 통과함을 확인하고 리팩토링을 진행하면 될 것 같다.
테스트 케이스를 2개 이상 만들어 가짜로 구현한 메서드를 추상화 하기 위한 방법.
두 정수의 합을 반환하는 함수를 작성하고 싶다고 가정할 때 다음과 같이 작성할 수 있다.
public void testSum() {
assertEquals(4, plus(3,1));
}
private int plus(int augend, int addend) {
return 4;
}
삼각 측량을 사용해서 바른 설계로 간다면 다음과 같이 테스트 케이스를 하나 더 추가해야 한다.
public void testSum() {
assertEquals(4, plus(3,1));
assertEquals(7, plus(3,4));
}
이렇게 되면 테스트가 실패하게 됨으로써 아래와 같이 어떻게 추상화 해야 하는지 실마리를 찾을 수 있다.
private int plus(int augend, int addend) {
return augend + addend;
}
그래서 어떻게 해야 가짜로 구현한 메서드를 올바르게 추상화할 수 있을지 감잡기 어려울 때만 삼각측량을 사용한다.
예제가 너무 간단해서 와닿지는 않지만 다양한 테스트 케이스를 추가함으로써 어떻게 추상화해야 하는지 알기 위한 기법 정도로 정리할 수 있을 것 같습니다.
덧셈, 뺄셈 정도의 간단한 연산의 경우 굳이 가짜 구현을 사용할 필요가 없다. 하지만 '제대로 동작하는' '깨끗한 코드' 라는 두 개의 문제를 한 번에 만족하기 어려운 경우 우선 가짜 구현을 통해 '제대로 동작하는'이라는 문제를 해결한 후 리팩토링을 통해 '깨끗한 코드' 문제를 해결하는 것이 좋다.