- 출처
- 1. 엔지니어링
- 2. 테스트 주도 개발 기초
- 3. 테스트 주도 개발의 깊은 곳
- 강의: https://fastcampus.co.kr/dev_red_ygw
- 강사: 이규원님
- 어떤 것들은 오래된 것들이 지금까지 계속 도움이 되는 것들이 있는 반면
- 어떤 것들은 옳다고 믿었던 것들인데, 지금와서는 잘못된 것으로 판명되어서 좋은 영향보다는 나쁜 영향을 줄 수도 있다.
- 원랜 좋았던 도구들도 시대가 흐르면서 안좋은 도구로 변하기도 한다
- 믿고 있던것들을 계속 믿기 보다는 더 좋은 방법이 없을까를 고민해야 된다.
- 과학은 밝혀내고
- 엔지니어링은 해결한다 -> 문제를 해결한다
- 대부분의 개발은 거의 엔지니어링에 가깝다
- 엔지니어링은 과학을 이용하되, 주어진 자원을 고려해서 수행해야 한다.
- 클린코드라는 용어가 되게 핫한데,
- 지나치게 클린코드에 빠져들고 있는 프로그래머들의 언행들은 좋게 보이지 않는다.
- 종종 클린코드에 빠져 드는 사람의 말을 들어보면, 마치 어디에나 통용될 수 있는 보편적인 코드 기준을 이야기하는 것 같은 느낌이 든다.
- 그럼, 코드를 평가하는 보편적인 기준을 얻음으로써 얻는게 무엇이 있을까?
- 왜 클린 코드를 추구해야 하는 것일까?
- 이런 질문에 명확하게 답변하는 사람들은 많지 않다.
- 클린 코드가 제시하고 있는 조건들이 나쁘진 않지만, 엔지니어링을 하는 엔지니어로써 보편적인 높은 기준을 가지고 일을 하는 사람인가를 되돌아 봐야한다.
- 주어진 임무가 제시하는 코드의 기준은 매번 달랐다.
- 어떤 때에는 클린코드가 제시하는 기준과 일치하기도 하고,
- 어떤 때에는 굉장히 많은 차이가 있기도 한다.
- 보편적인 기준을 다루기 보다는, 그때 그때 적절한 코드를 다루는 것이 중요하다.
- 알려진 문제의 일반적이고 재사용할 수 있는 해결법
- 프로그래머는 고유한 문제를 풀어야 한다
- 고유한 문제는 좀 더 작은 하위 기술 문제를 가진다
- 이 작은 기술 문제들 중 많은 일부는 과거 어디에 선가 여러 번 반복되고 해결된 적이 있다.
- 적절한 병에 맞는 해독제를 먹어야 병이 낫는 것 처럼
- 패턴도 적절한 곳에 쓰여야 문제를 적절하게 해결할 수 있다.
- 패턴을 어떻게 쓰는 것 인지 보다는 언제 이 패턴을 써야하는지를 익혀야 한다.
- 어디에나 통하는 만병통치약과 같은 패턴은 없다.
- 프로그래밍은 협업이다.
- 내가 상대방 코드를 봤을 때 쓰레기라고 느낄 수 있지만, 상대방도 그럴 수 있다.
- 하지만, 중요한 것은 서로의 기준에서 좋은 코드를 작성하는 것 보다는 우리가 어떤 목표를 공유하고 있느냐가 중요하다.
- 시스템은 연결된다
- 다른회사 혹은 옆팀의 코드는 왜 저따구일까 라는 생각을 할 수도 있다.
- 하지만, 그따위의 시스템도 내막을 알게 되면 다 사정이 있어서 아픈 역사를 가져서 그렇게 설계되었을 수도 있고,
- 우리가 모르는 어떤 문제를 해결하기 위해 생각하고 있는 거랑 다른 방식으로 해결 되었을 수 있다.
- 도구 상자에 다양한 도구를 채우자
- 한가지, 어디에나 들어맞는 도구를 준비 하기 보다는
- 다양한 도구를 준비해서 우리에게 주어진 상황에 가장 적합한 도구를 꺼내서 쓸 수 있어야 한다.
- 엔지니어링은 거래(trade-off)이다.
- 우리가 가지고 있는 도구가 완벽하게 동작하는 환경만 마주한다면 거래할 필요가 없겠지만, 그런일은 거의 발생하지 않는다.
- 우리는 일반적으로 엔지니어링을 통해서 우리 입장의 아주 심각한 문제를 덜 심각한 문제랑 맞 바꾸는 일을 자주 한다
- TDD도 어떤 환경에서는 적합해서 잘 동작하고 여러가지 도움을 주지만,
- 어떤 환경에서는 무리하게 도입하면 해가되는 경우도 있다.
- 반복되는 문제 해결에 비용이 많이 쓰인다면
- 연습하고, 연습하고, 연습해서 생각의 비용을 줄더들도록 뇌에 새겨라
- 그러면, 새로운 문제에 더 많은 시간을 쓸 수 있다.
- 테스트 관련해서 나쁜 습관이 있다는 것을 깨닫고
- 2주 정도 동안 나쁜 버릇을 사용해서 작성한 테스트케이스 1000개를 더 좋은 방식으로 개선했던 경험이 있다.
- 그 결과 테스트 코드와 운영 코드의 결합을 맞춰서 유연한 설계를 얻을 수 있게 되었고
- 이 경험은 지금까지 근육 기억으로 남아 있다.
- 소프트웨어는 문제를 푸는 도구
- 도메인은 소프트웨어가 풀어야 할 문제가 정의되는 공간
- 비즈니스 시스템의 도메인은 비즈니스
- 문제를 충분히 이해하지 못하면 문제를 푸는 도구를 잘 만들 수 없다
- 틱택토 게임을 이해하지 못하면 틱택토 컴퓨터 게임을 만들 수 없다
graph LR;
비즈니스전문가 --> 분석가 --> 프로그래머 --> 컴퓨터
- <-- : 목적/추상적
- --> : 수단/구체적
- 분석가: 제품 관리자 / 서비스 기획자 / 프로그래머
- 문제를 가장 잘 이해한다.
- 시스템이 투영해야 할 핵심 지식의 원천
- 문제 설명력 부족 (프로그래머 입장에서 봤을 때)
- 지식의 저주
- 풀이도 가장 잘 이해한다고 착각
- 문제를 말해야 할 때 풀이를 말하려는 경향을 가짐
- 의사한태 가서 비염때문에 왔어요 라고 하면 의사는 증상을 말하라고 한다. (의사: 프로그래머, 비염환자: 비즈니스 전문가)
- 비즈니스 전문가로부터 시스템 요구사항을 발굴한다
- 발굴된 요구사항의 오류 탐색
- 발견된 문제점을 구현 작업 전에 협업을 통해 해결
- 요구사항이 기존의 시스템에 충돌되는 것은 없는지 검증을 찾아내야 하므로 시스템의 이해도도 필요하다.
- 정제된 기능 명세를 아키텍처와 코드로 번역
- 제품 제작 과정 중 비용이 가장 큰 직업
- 끊임없는 설계 결정
- 지식 흐름 과정의 마지막 인간
- 코드를 통해 프로그래머로부터 지식을 전달받음
- 철저히 수동적
- 융통성 없음
- 컴퓨터는 스스로 설계를 결정하지 않기 떄문에
- 프로그래머가 도메인 지식을 컴퓨터에 전달 할 때에는
- 모든 요소들이 명확히 결정될 수 밖에 없다
- 충분히 명확한 도메인 지식을 확보하지 못한 프로그래머는
- 지식 흐름 상류에 지식 보강을 요청해야함 (분석가 혹은 비즈니스 전문가)
- 하지만 어떤 프로그래머는 스스로 결정을 내림
- 도메인 지식 투영에 오차 발생
- 무책임하고 위험한 도박
- 배경
- 어떤 회사가 분산 값을 통계값을 계산하는 제품을 만들려고 한다.
- 프로그래머는 분산을 계산하는 공식을 전달 받았고,
- 샘플 예제를 하나 받았다.
1+2+3+4+5+6 = 3.5
S = list(map(float, input().split()))
sum = sum(S)
mean = sum / len(S)
sumOfSquares = 0.0
for s in S:
sumOfSquares += (s - mean) ** 2
variance = sumOfSquares / (len(S) - 1)
print("분산", variance)
"""
INPUT1
1.0 2.0 3.0 4.0 5.0 6.0
RESULT
분산 3.5
"""
"""
INPUT2 - BUG REPORT
RESULT
분산 -0.0 혹은 ERROR
"""
- INPUT1만 테스트해보고 서비스 오픈을 했는데
- INPUT2 케이스 에 대한 유저 컴플레인이 들어 왔다.
- 프로그래머는 그냥 이렇게 되는거 아니야? 라고 생각했지만, 통계전문가는 당연히 데이터가 없으면 분산은 계산할 수 없고 오류 메시지가 출력 되야지 라고 답을 한다.
- 프로그래머는 도메인지식을 프로그램에 투영 시켰지만, 저번에 전달 받았던 지식에서 누락된 지식을 깨달은 것이다.
S = list(map(float, input().split()))
if len(S) == 0: # 추가
print("데이터가 입력되지 않았습니다")
quit()
sum = sum(S)
mean = sum / len(S)
sumOfSquares = 0.0
for s in S:
sumOfSquares += (s - mean) ** 2
variance = sumOfSquares / (len(S) - 1)
print("분산", variance)
"""
INPUT1
1.0 2.0 3.0 4.0 5.0 6.0
RESULT
분산 3.5
"""
"""
INPUT2
RESULT
데이터가 입력되지 않았습니다.
"""
"""
INPUT3 - BUG REPORT
1
RESULT
분산 NaN 또는 ERROR
"""
- 통계 전문가: 분산을 계산하려면 적어도 2개이상은 입력해줘야 해
S = list(map(float, input().split()))
if len(S) == 0:
print("데이터가 입력되지 않았습니다")
quit()
if len(S) == 1:
print("2개 이상의 데이터를 입력하세요.")
quit()
sum = sum(S)
mean = sum / len(S)
sumOfSquares = 0.0
for s in S:
sumOfSquares += (s - mean) ** 2
variance = sumOfSquares / (len(S) - 1)
print("분산", variance)
"""
INPUT1
1.0 2.0 3.0 4.0 5.0 6.0
RESULT
분산 3.5
"""
"""
INPUT2
RESULT
데이터가 입력되지 않았습니다.
"""
"""
INPUT3
1
RESULT
2개 이상의 데이터를 입력하세요.
"""
- 이제서야 오류 없는 프로그램이 완성되었지만,
- 프로그램을 이전 버전에서 사용한 사용자들은 이미 이탈을 했을 것이다.
- 품질 담당자(QA)가 UI를 사용해 기능을 검증
- 최종 사용자의 사용 경험과 가장 비슷하게 검증
- 실행 비용이 높고 결과의 변동이 큼
- 어떤 테스트 대상 기능이 동작 하는데 필요한 모든 코드가 동작한다.
- 일부 코드가 빠지면 동작하지 않는다.
- 가장 온전한 코드 실행
- 인수 테스트
- 소프트웨어를 요청한 클라이언트가 제작된 소프트웨어를 인수 받아도 되는지 하는 테스트
- 소프트웨어의 기능이 원래 동작했는데, 어떤 시점 이후 로는 동작하지 않는것
- 새로운 기능이 추가되고나서, 기존 기능이 오작동 하는 케이스
- 시간이 지날수록 회귀테스트의 대상은 늘어난다
- 새로운 기능을 추가한다고 해서 새로운 기능에 대해서만 테스트를 해서는 안된다.
- 시스템이 정상적으로 동작한다는 것을 보장 하지 못하고,
- 기존의 기능들까지 테스트해야 된다.
- 수동 테스트만으로는 이 늘어나는 비용을 감당하기가 힘들다.
- 수동 테스트만 운영하는 팀에서는 많은 경우
- 새로운 영역에 대해서만 테스트하고
- 기존의 영역에 대해서는 테스트하지 않는 경우도 있다.
- 기능을 검증하는 코드를 작성
- 테스트 코드 작성 비용이 소비되지만, 실행 비용이 낮고 결과의 신뢰도가 높음
- 수동테스트는 사람이 직접 테스트해야 되기 때문에 비용이 높지만
- 테스트 코드는 한번 만들어두면 실행 할 자동으로 처리할 수 있다.
- 테스트코드 작성과 관리가 프로그래머 역량에 크게 영향 받음
- 배치된 시스템을 대상으로 검증
- 전체 시스템 이상 여부 신뢰도가 높음
- 높은 비용
- 작성 비용
- 관리 비용
- 실행 비용
- 피드백 품질이 낮음
- 현상은 드러나지만 원인은 숨겨짐
- 시스템의 일부(하위시스템)을 대상으로 검증
- 낮은 비용
- 작성 비용
- 관리 비용
- 실행 비용
- 높은 피드백 품질
- 전체 시스템 이상 여부 신뢰도가 낮음
- 단위 시스템에 성공한 여러 하위 시스템들이 협력하는 과정에서 실패하는 케이스도 있다.
- 프로그래머가 한번에 다룰 수 있는 문제의 크기는 한계가 있음
- 프로그래머는 더 큰 문제를 자주 마주함
- 시스템의 크기는 점점 커짐
- 큰 문제는 작은 문제로 분해할 수 있음
- 작은 문제의 일부는 반복됨
- 코드의 가독성이 프로그래머 생산성에 많은 영향을 미친다.
- 반복되는 문제의 풀이는 재사용 가능
- 소프트웨어 개발 비용 절감
- 분해
- 큰 시스템은 더 작은 하위 시스템으로 분해 가능
- 교체 가능
- 조립
- 작은 시스템은 더 큰 상위 시스템으로 조립 가능
- 모듈 재사용
- 라이브러리
- 단위 테스트
- 모듈을 제공하는 입장에서는 모듈을 잘 테스트해서 제공해줘야 한다.
- 이러한 경우 수동으로 테스트하기 보다는 자동화된 테스트케이스를 테스트한다.
- 클라이언트입장에서는 단위 테스트로 신뢰감을 얻을 수 있다.
from unittest import TestCase
def refineText(str):
return str.replace(" ", " ").replace(" ", " ")
class MyTests(TestCase):
def test_refineText1(self):
res = refineText("hello world")
self.assertEqual(res, "hello world")
def test_refineText2(self):
res = refineText("hello world")
self.assertEqual(res, "hello world")
def test_refineText3(self): # 실패 케이스
res = refineText("hello world")
self.assertEqual(res, "hello world")
- 위 테스트코드 3개를 단순하게 for문으로 코드양을 줄이면, 어디에서 테스트코드가 실패 했는지 찾기가 쉽지 않다 (피드백이 떨어진다)
- 코드양을 줄인다고 무조건 좋은 것이 아니다.
- Parameterized Test
- 위 상황이 발생하지 않도록 해주는 것.
- 즉, 동일한 테스트 코드를 여러개의 파라미터로 테스트 할 수 있도록 해주는 것
import unittest
from nose.tools import assert_equal
from parameterized import parameterized
def refineText(str):
return str.replace(" ", " ").replace(" ", " ")
class MyTests(unittest.TestCase):
@parameterized.expand([
("hello world", "hello world"),
("hello world", "hello world"),
("hello world", "hello world"),
])
def test_refineText(self, input, exptected):
assert_equal(refineText(input), exptected)
- 위 상황이 발생하지 않도록 해주는 것.
- 즉, 동일한 테스트 코드를 여러개의 파라미터로 테스트 할 수 있도록 해주는 것
- 가시적이고 구체적인 목표
- 자가 검증
- 반복 실행
- 운영 코드 API의 클라이언트가 된다.
- 명확하고 검증 가능한 목표를 설정한 후 목표 달성
- 프로세스가 코딩에 앞선 목표 설정을 강요
- 프로그래머는 자신이 풀어야 할 문제를 구체적으로 이해해야 함
def refineText(str):
return str.replace(" ", " ").replace(" ", " ").replace(" ", " ")
위 코드로 고치면 지금까지의 테스트 코드는 성공한다.
def refineText(str):
return str.replace(" ", " ").replace(" ", " ").replace(" ", " ").replace(" ", " ")
class MyTests(unittest.TestCase):
@parameterized.expand([
("hello world", "hello world"),
("hello world", "hello world"),
("hello world", "hello world"),
("hello world", "hello world"),
("hello world", "hello world"),
("hello world", "hello world"),
("hello world", "hello world"), # 실패
("hello world", "hello world"), # 실패
])
def test_refineText(self, input, exptected):
assert_equal(refineText(input), exptected)
위 테스트코드를 통해서 또 실패 한다는 것을 알 수 있다.
- 하지만, 테스트를 통해서 작성한 실수르 빠른 타이밍에 피드백을 받을 수 있다.
- 금지어를 마스킹하기위해서 새로운 코드를 추가한다면, 그 기능으로 인해 성공했던 테스트가 실패 할 수 있다.
- 생산성
- 정리된 환경과 어지럽혀진 환경에서의 작업 생산성 차이
- 지속성
- 작업 환경의 생산성이 일정 수준 미만으로 떨어지면
- 더 이상 그 환경에서 작업 진행은 불가능
- 코드 작업은 환경이자 작업 결과물이다.
- 의미를 유지하며 코드베이스를 정리하는 것
- 의미 유지를 어떻게 확인할 것인가?
- 수동 테스트 혹은 자동 테스트
- RED -> GREEN -> REFACTOR -> RED 를 반복.
- RED: 실패하는 테스트 추가
- GREEN: 테스트 통과, 최소한의 코딩
- REFACTOR: 구현 설계 개선, 테스트 통과 유지
- 구체적인 하나의 요구사항을 검증하는 하나의 테스트를 추가
- 추가된 테스트가 실패하는지 확인
- 실패하는 것을 확인해야 테스트가 동작함을 믿을 수 있다.
- 운영 코드 변경이 진행되지 않았기 때문에 실패했는지 확인해야 한다.
- 추가된 테스트를 비롯해 모든 테스트가 성공하도록 운영 코드를 변경
- 테스트 성공은 요구사항 만족을 의미 (코딩의 가장 중요한 입무)
- 테스트 성공을 위한 최소한의 변경 (가장 중요한 입무를 가장 빠르게 완수)
- 코드 베이스 정리
- 구현 설계 개선
- 가독성
- 적응성
- 성능
- 모든 테스트 성공을 전제해야 한다.
- 실패하는 단위 테스트 작성 (성공 하면, 잘못 작성한 것이므로 다시 작성)
- 운영 코드 작성
- 단위 테스트 실행 (실패시에는 성공할 때 까지 2번으로 돌아가서 재작성)
- 설계 개선(리팩토링)
- 단위 테스트 실행 (성공 시에는 1번으로 돌아감, 실패 시에는 4번으로 돌아감)
- 기능 하나 추가 할 때의 비용
- TDD(X): 시간이 지남에 따라 비용 증가
- TDD(O): TDD(X)보다는 초기 비용은 높지만, 시간이 지나도 비용은 동일하게 유지 (나중엔 TDD 안했을 때보다 적은 시간 소요)
- 사용자 피드백: 사용자가 직접 코드를 사용한 후 경험한 버그나 불만을 제보
- Quality Assurance(QA): 전문 인적 자원에 의한 인수 테스트
- 프로그래머 테스트: 프로그래머가 직접 피드백 장치를 준비 (테스트 자동화)
- 도구 피드백: 컴파일 오류, 정적 검사 등 프로그래머가 사용하는 도구가 제공하는 피드백
- 프로그래머는 요구사항 명세에 명확히 지정되지 않은 성능 달성이나 구현 설계 품질 개선에 빠져드는 경향을 가짐
- 이런 목표는 그 자체로 나쁜 것은 아니지만 지나치면 더 중요한 목적, 기능 요구사항에 써야 할 자원을 불필요하게 낭비하곤 한따.
- 테스트 주도 개발은 가장 중요한 목표를 우선 달성하도록 유도하며 오버엔지니어링에 빠졌음을 느낄 때 안심하고 다음으로 나아갈 수 있또록 피드백을 제공한다.
- 테스트 주도 개발의 핵심은 정해진 절차가 아니라 짧은 주기로 지속되는 피드백
- 피드백에 기반해 안정적으로 지식과 코드를 늘려 나가는 것이 목적이다.
- 1부터 100까지 임의의 정수를 맞추는 게임
- 플에이어가 숫자를 입력하면
- 입력한 숫자가 정답보다 작으면 작다고 출력
- 입력한 숫자가 정답보다 크면 크다고 출력
- 입력한 숫자가 정답과 일치하면 라운드 종료
- 단일 플레이어 모드와 다중 플레이어 모드 지원
- 단일 플레이어 모드 라운드가 종료되면 총 시도를 출력
- 다중 플레이어 모드 라운드가 종료되면 승자를 출력
- 대부분의 코드는 다른 코드와 협력
- 협력에 필요한 것은
어떻게
가 아닌무엇
- 인터페이스
- 무엇을 표현
- 클라이언트 코드에게 반드시 필요한 정보
- 협력하는 코드 사이의 계약
- 추상화 결과
- 효과적인 모듈화
- 조직간 의사소통 최소화
- 변경 여파 최소화
- 시스템 이해 도움
- 공개된 설계 결정과 숨겨진 설계 결정
- 어려운 설계 결정과
- 변경될 것 같은 설계 결정을 숨겨라
- 대부분의 시스템 정보는 대부분의 개발자에게 숨겨지는 것이 도움 된다
- 대신 어려운 설계 결정이나 변경 될 가능성이 있는 설계 결정 목록으로 시작하는 것이 좋다.
- 그런 다음 각 모듈은 이러한 결정을 다른 모듈로부터 숨기도록 설계된다.
- 소프트웨어 엔터티(클래스, 모듈, 함수 등)는 확장에 열리고 수정에 닫혀야 한다.
- 확장 가능한 경우 모듈은 열려 있다고 말한다
- 다른 모듈에 의해 사용될 수 있을 때 모듈은 닫혀 있다고 말한다
- 상속을 염두 한 정의
- 직접 입력과 직접 출려
- 공개된 인터페이스를 통한 입력과 출력
- 다루기 간단함
- 간접 입력과 간접 출력
- 입력된 인터페이스를 통한 입력과 출력
- 다루기 복잡함
- 인터페이스 설계에 드러나지 않은 출력
- 반환 값 외 출력
- 자주 사용되는 부작용
- 실패
- 지연
- 간접 출력
- DOC 준비 비용이 큰 경우
- 구동에 많은 자원이 필요
- 환경 제어가 어려움
- DOC: Depend on Component (테스트하려는 대상 코드가 의존하는 코드)
- SUT: 테스트하려는 대상 코드
- DOC가 SUT에 제공하는 계약(인터페이스)을 준수하는 대역 코드를 사용
- 대역 코드가 계약을 DOC와 동일하게 준수할 것이라고 가정
- 테스트 대역 중에서 가장 단순한 형태
- SUT 준비를 위해 해결되어야 하는 의존성이 테스트 대상 논리에 의해 사용되지 않는 경우에 의존 요소를 대신하는 테스트 대역
- 간접 입력 대역
- 미리 준비된 답을 출력
public class WayneEnterprisesProductSourceStub implements WayneEnterprisesProductSource {
private final WayneEnterprisesProduct[] products;
public WayneEnterprisesProductSourceStub(WayneEnterprisesProduct... products) {
this.products = products;
}
@Override
public Iterable<WayneEnterprisesProduct> fetchProducts() {
return Arrays.asList(products);
}
}
@ParameterizedTest
@DomainArgumentsSource
void sut_projects_all_products(WayneEnterprisesProduct[] source) {
var stub = new WayneEnterprisesProductSourceStub(source);
var sut = new WayneEnterprisesProductImporter(stub);
Iterable<Product> actual = sut.fetchProducts();
assertThat(actual).hasSize(source.length);
}
- 간접 출력 대역
- SUT의 간접 출력을 기록
public final class ProductInventorySpy implements ProductInventory {
private final List<Product> log = new ArrayList<Product>();
@Override
public void upsertProduct(Product product) {
log.add(product);
}
public List<Product> getLog() {
return log;
}
}
@ParameterizedTest
@DomainArgumentsSource
void sut_does_not_save_invalid_product(WayneEnterprisesProduct product) {
// Arrange
var lowerBound = new BigDecimal(product.getListPrice() + 10000);
var validator = new ListPriceFilter(lowerBound);
var stub = new WayneEnterprisesProductSourceStub(product);
var importer = new WayneEnterprisesProductImporter(stub);
var spy = new ProductInventorySpy();
var sut = new ProductSynchronizer(importer, validator, spy);
// Act
sut.run();
// Assert
assertThat(spy.getLog()).isEmpty();
}
- SUT 내부의 행위(상호작용) 검증
@Test
void sut_really_does_not_save_invalid_product() {
// Arrange
var pricing = new Pricing(BigDecimal.TEN, BigDecimal.ONE);
var product = new Product("supplierName", "productCode", "productName", pricing);
ProductImporter importer = mock(ProductImporter.class);
when(importer.fetchProducts()).thenReturn(Arrays.asList(product));
ProductValidator validator = mock(ProductValidator.class);
when(validator.isValid(product)).thenReturn(false);
ProductInventory inventory = mock(ProductInventory.class);
var sut = new ProductSynchronizer(importer, validator, inventory);
// Act
sut.run();
// Assert
verify(inventory, never()).upsertProduct(product);
}
- 의존성 계약을 준수하는 가벼운 구현체
- DOC보다 적은 부작용
- 인메모리 데이터베이스 등
단위 테스트가 어려울 때, 테스트 대역은 굉장히 좋은 도구다.
- Sociable: 단위 테스트 -> 시스템 -> 의존대상
- Solitary: 단위 테스트 -> 시스템 -> 테스트 대역
- 테스트 대역 사용으로 인해 생기는 가정을 얼마나 믿을 수 있을까?
- 테스트 대역이 구현하는 인터페이스가 단순할수록 1에 가까워짐 (안정)
- 테스트 대역이 구현하는 인터페이스가 복잡할수록 0에 가까워짐 (불안정)
- 상태 검증 보다는 행위 검증에 가깝다
- 정보 숨김을 위배할 확률이 높다
- 테스트가 SUT 구현에 의존
- 고통스럽고 불안한 리팩터링
- 비공개 모듈의 작성과 사용은 공개 모듈의 구현 영역
- 비공개 모듈 테스트는 공개 모듈 구현 노출
- 테스트가 공개 모듈 구현에 의존
- 정보 숨김 위배
- SUT와 테스트 사이의 높은 결합
- 공개 모듈 동작을 고민하자.
- 켄트 벡의 설계 규칙
- Passes the tests
- Reveals intention
- No duplication
- Fewest elements
- 테스트는 인터페이스 설계에 의존
- 인터페이스 설계 품질이 낮으면 테스트 작성이 불편함
- 테스트가 있기 때문에 리팩터링 가능
- 두려움 없이 구현 설계를 과감하게 개선
- 단위 테스트는
- 낮은 응집에 대한 피드백을 주지 않는다
- 일관된 설계를 강요하지 않는다
- 의도 노출을 요구하지 않는다
- 단위 테스트는 책임 분산을 유도하지 않는다
- Mockists
- 비공개 운영 코드 테스트
- 테스트 주도 개발은 유용하고 매력적인 도구
- 하지만 테스트 주도 개발 역시 남용을 주의해야 한다
- 모든 코드의 목표가 안정적이지 않다
- 탐색적으로 목표를 찾아내야 한다면 테스트 코드 작성 비용 부담이 큼
- 하위 시스템이 협력하는 다른 하위 시스템, 의존성 중 일부는 출력의 예측과 제어가 어려움 -> 단위 테스트는 결정적 일수록 유용
- 의존성 중 일부는 동작이 매우 느림 -> 단위 테스트는 빠를수록 유용
- 의존성의 단점을 보안할 대역을 도입하면 가정이 발생 -> 가정으로 인한 안정감 감소
- 코드 적응력이 낮으면 단위 테스트하기 매우 어렵다
- 이미 코드 적응력이 낮은 코드 기반에 테스트 주도 개발을 적용하기 어렵다
- 코드 기반의 적응력을 높이는 것도 어렵다
- 설계 변경의 어려움
- 변경된 설계 검증 필요
- 한 개체가 상호작용하는 다른 개체에 제공하는 상호작용 지점
- 한 시스템이 협력 시스템에 제공한다
- 코드 친화적 소통 수단이다.
- 테스트 자동화 비용이 낮다
- 한 시스템이 시스템 사용자에게 제공한다
- 인간 친화적 소통 수단이다.
- 변경이 잦지만, 테스트 자동화 비용이 높다
- 배치된 코드를 대상으로 최종 클라이언트 관점으로 테스트
- UI로 할수도 있고, API로도 할 수 있다.
- UI 응용 프로그램
- 사용자가 클라이언트
- 테스트 자동화 작성과 운영 비용이 높다
- API 응용 프로그램
- 외부 시스템이 클라이언트
- 테스트 자동화 작성과 운영 비용이 낮음
비교 대상 | 인수 테스트 | 단위 테스트 |
---|---|---|
관점 | 최종 클라이언트 | 개발자 |
안정감 | 높음 | 낮음 |
피드백 | 낮음 | 높음 |
실행 속도 | 느림 | 매우 빠름 |
결정성 | 비결정적 | 결정적 |
- 인수테스트 작성
- 인수테스트 실행 (성공시 1번으로 돌아감)
- 단위 테스트 작성
- 단위 테스트 실행 (실패시 5번, 성공시 6번)
- 운영코드 작성
- 설계 개선
- 단위 테스트 실행 (실패시 6번, 성공시 2번으로 돌아감)
- 어떤 가치를 구현하기 위해 코드를 쓰고 연결할 것인가?
- 코드를 쓰는 일은 개발자가 하위 문제에 집중하게 만듦
- 명확한 이정표가 없다면 개발자는 쉽게 길을 잃어 소중한 자원을 낭비한다
- 사용자 스토리나 테스트 케이스는 목표 기술의 좋은 수단
- 전체 작업을 하위 작업으로 분리 - 얼마나 작게 분리할 것인가는 맥락에 따라 다르다
- 하위 작업 역시 목표를 명확히 기술
- 투명한 작업자의 업무 내용과 진행도
- 위험 요소를 더 빨리 발견할 가능성이 높아진다.
- 밀도 있고 입체적인 협업 계획