Skip to content

Latest commit

 

History

History
 
 

tdd

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

출처

1. 엔지니어링

어제까지 알고 있던 모든것들이 언제까지 사실이고 진실이지 않다.

  • 어떤 것들은 오래된 것들이 지금까지 계속 도움이 되는 것들이 있는 반면
  • 어떤 것들은 옳다고 믿었던 것들인데, 지금와서는 잘못된 것으로 판명되어서 좋은 영향보다는 나쁜 영향을 줄 수도 있다.
  • 원랜 좋았던 도구들도 시대가 흐르면서 안좋은 도구로 변하기도 한다
  • 믿고 있던것들을 계속 믿기 보다는 더 좋은 방법이 없을까를 고민해야 된다.

과학과 엔지니어링

  • 과학은 밝혀내고
  • 엔지니어링은 해결한다 -> 문제를 해결한다
    • 대부분의 개발은 거의 엔지니어링에 가깝다
    • 엔지니어링은 과학을 이용하되, 주어진 자원을 고려해서 수행해야 한다.

엔지니어는 이론을 이용하되, 매몰되지는 말아야 한다.

  • 클린코드라는 용어가 되게 핫한데,
  • 지나치게 클린코드에 빠져들고 있는 프로그래머들의 언행들은 좋게 보이지 않는다.
  • 종종 클린코드에 빠져 드는 사람의 말을 들어보면, 마치 어디에나 통용될 수 있는 보편적인 코드 기준을 이야기하는 것 같은 느낌이 든다.
    • 그럼, 코드를 평가하는 보편적인 기준을 얻음으로써 얻는게 무엇이 있을까?
    • 왜 클린 코드를 추구해야 하는 것일까?
    • 이런 질문에 명확하게 답변하는 사람들은 많지 않다.
  • 클린 코드가 제시하고 있는 조건들이 나쁘진 않지만, 엔지니어링을 하는 엔지니어로써 보편적인 높은 기준을 가지고 일을 하는 사람인가를 되돌아 봐야한다.
  • 주어진 임무가 제시하는 코드의 기준은 매번 달랐다.
    • 어떤 때에는 클린코드가 제시하는 기준과 일치하기도 하고,
    • 어떤 때에는 굉장히 많은 차이가 있기도 한다.

좋은 엔지니어는

  • 보편적인 기준을 다루기 보다는, 그때 그때 적절한 코드를 다루는 것이 중요하다.

패턴이란

  • 알려진 문제의 일반적이고 재사용할 수 있는 해결법
  • 프로그래머는 고유한 문제를 풀어야 한다
  • 고유한 문제는 좀 더 작은 하위 기술 문제를 가진다
  • 이 작은 기술 문제들 중 많은 일부는 과거 어디에 선가 여러 번 반복되고 해결된 적이 있다.

패턴은 해독제와 비슷하다.

  • 적절한 병에 맞는 해독제를 먹어야 병이 낫는 것 처럼
  • 패턴도 적절한 곳에 쓰여야 문제를 적절하게 해결할 수 있다.
  • 패턴을 어떻게 쓰는 것 인지 보다는 언제 이 패턴을 써야하는지를 익혀야 한다.
  • 어디에나 통하는 만병통치약과 같은 패턴은 없다.

은탄환은 없다

  • 프로그래밍은 협업이다.
    • 내가 상대방 코드를 봤을 때 쓰레기라고 느낄 수 있지만, 상대방도 그럴 수 있다.
    • 하지만, 중요한 것은 서로의 기준에서 좋은 코드를 작성하는 것 보다는 우리가 어떤 목표를 공유하고 있느냐가 중요하다.
  • 시스템은 연결된다
    • 다른회사 혹은 옆팀의 코드는 왜 저따구일까 라는 생각을 할 수도 있다.
    • 하지만, 그따위의 시스템도 내막을 알게 되면 다 사정이 있어서 아픈 역사를 가져서 그렇게 설계되었을 수도 있고,
    • 우리가 모르는 어떤 문제를 해결하기 위해 생각하고 있는 거랑 다른 방식으로 해결 되었을 수 있다.
  • 도구 상자에 다양한 도구를 채우자
    • 한가지, 어디에나 들어맞는 도구를 준비 하기 보다는
    • 다양한 도구를 준비해서 우리에게 주어진 상황에 가장 적합한 도구를 꺼내서 쓸 수 있어야 한다.
  • 엔지니어링은 거래(trade-off)이다.
    • 우리가 가지고 있는 도구가 완벽하게 동작하는 환경만 마주한다면 거래할 필요가 없겠지만, 그런일은 거의 발생하지 않는다.
    • 우리는 일반적으로 엔지니어링을 통해서 우리 입장의 아주 심각한 문제를 덜 심각한 문제랑 맞 바꾸는 일을 자주 한다

TDD도 모든 문제를 해결해주지 않는다.

  • TDD도 어떤 환경에서는 적합해서 잘 동작하고 여러가지 도움을 주지만,
  • 어떤 환경에서는 무리하게 도입하면 해가되는 경우도 있다.

근육 기억

  • 반복되는 문제 해결에 비용이 많이 쓰인다면
  • 연습하고, 연습하고, 연습해서 생각의 비용을 줄더들도록 뇌에 새겨라
  • 그러면, 새로운 문제에 더 많은 시간을 쓸 수 있다.

이규원님의 경험) 반복이 완벽함을 만든다.

  • 테스트 관련해서 나쁜 습관이 있다는 것을 깨닫고
  • 2주 정도 동안 나쁜 버릇을 사용해서 작성한 테스트케이스 1000개를 더 좋은 방식으로 개선했던 경험이 있다.
  • 그 결과 테스트 코드와 운영 코드의 결합을 맞춰서 유연한 설계를 얻을 수 있게 되었고
  • 이 경험은 지금까지 근육 기억으로 남아 있다.

2. 테스트 주도 개발 기초

1. 코드 기능 명세

도메인

  • 소프트웨어는 문제를 푸는 도구
  • 도메인은 소프트웨어가 풀어야 할 문제가 정의되는 공간
    • 비즈니스 시스템의 도메인은 비즈니스
  • 문제를 충분히 이해하지 못하면 문제를 푸는 도구를 잘 만들 수 없다
    • 틱택토 게임을 이해하지 못하면 틱택토 컴퓨터 게임을 만들 수 없다

비즈니스 시스템의 도메인 지식 흐름

graph LR;

비즈니스전문가 --> 분석가 --> 프로그래머 --> 컴퓨터
Loading
  • <-- : 목적/추상적
  • --> : 수단/구체적
  • 분석가: 제품 관리자 / 서비스 기획자 / 프로그래머

비즈니스 전문가란

  • 문제를 가장 잘 이해한다.
    • 시스템이 투영해야 할 핵심 지식의 원천
  • 문제 설명력 부족 (프로그래머 입장에서 봤을 때)
    • 지식의 저주
  • 풀이도 가장 잘 이해한다고 착각
    • 문제를 말해야 할 때 풀이를 말하려는 경향을 가짐
    • 의사한태 가서 비염때문에 왔어요 라고 하면 의사는 증상을 말하라고 한다. (의사: 프로그래머, 비염환자: 비즈니스 전문가)

분석가란

  • 비즈니스 전문가로부터 시스템 요구사항을 발굴한다
  • 발굴된 요구사항의 오류 탐색
  • 발견된 문제점을 구현 작업 전에 협업을 통해 해결
  • 요구사항이 기존의 시스템에 충돌되는 것은 없는지 검증을 찾아내야 하므로 시스템의 이해도도 필요하다.

프로그래머란

  • 정제된 기능 명세를 아키텍처와 코드로 번역
    • 제품 제작 과정 중 비용이 가장 큰 직업
  • 끊임없는 설계 결정
  • 지식 흐름 과정의 마지막 인간

컴퓨터

  • 코드를 통해 프로그래머로부터 지식을 전달받음
  • 철저히 수동적
  • 융통성 없음

프로그래머와 기능 명세

  • 컴퓨터는 스스로 설계를 결정하지 않기 떄문에
    • 프로그래머가 도메인 지식을 컴퓨터에 전달 할 때에는
    • 모든 요소들이 명확히 결정될 수 밖에 없다
  • 충분히 명확한 도메인 지식을 확보하지 못한 프로그래머는
    • 지식 흐름 상류에 지식 보강을 요청해야함 (분석가 혹은 비즈니스 전문가)
  • 하지만 어떤 프로그래머는 스스로 결정을 내림
    • 도메인 지식 투영에 오차 발생
    • 무책임하고 위험한 도박

무책임하고 위험한 도박의 예시

  • 배경
    • 어떤 회사가 분산 값을 통계값을 계산하는 제품을 만들려고 한다.
    • 프로그래머는 분산을 계산하는 공식을 전달 받았고,
    • 샘플 예제를 하나 받았다. 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개 이상의 데이터를 입력하세요. 
"""
  • 이제서야 오류 없는 프로그램이 완성되었지만,
  • 프로그램을 이전 버전에서 사용한 사용자들은 이미 이탈을 했을 것이다.

2. 테스트 기법

수등 테스트

  • 품질 담당자(QA)가 UI를 사용해 기능을 검증
  • 최종 사용자의 사용 경험과 가장 비슷하게 검증
  • 실행 비용이 높고 결과의 변동이 큼
    • 어떤 테스트 대상 기능이 동작 하는데 필요한 모든 코드가 동작한다.
    • 일부 코드가 빠지면 동작하지 않는다.
  • 가장 온전한 코드 실행
  • 인수 테스트
    • 소프트웨어를 요청한 클라이언트가 제작된 소프트웨어를 인수 받아도 되는지 하는 테스트

소프트 웨어 회귀

  • 소프트웨어의 기능이 원래 동작했는데, 어떤 시점 이후 로는 동작하지 않는것
  • 새로운 기능이 추가되고나서, 기존 기능이 오작동 하는 케이스

회귀테스트 대상

  • 시간이 지날수록 회귀테스트의 대상은 늘어난다
  • 새로운 기능을 추가한다고 해서 새로운 기능에 대해서만 테스트를 해서는 안된다.
    • 시스템이 정상적으로 동작한다는 것을 보장 하지 못하고,
    • 기존의 기능들까지 테스트해야 된다.
  • 수동 테스트만으로는 이 늘어나는 비용을 감당하기가 힘들다.
    • 수동 테스트만 운영하는 팀에서는 많은 경우
    • 새로운 영역에 대해서만 테스트하고
    • 기존의 영역에 대해서는 테스트하지 않는 경우도 있다.

테스트 자동화

  1. 기능을 검증하는 코드를 작성
  2. 테스트 코드 작성 비용이 소비되지만, 실행 비용이 낮고 결과의 신뢰도가 높음
    • 수동테스트는 사람이 직접 테스트해야 되기 때문에 비용이 높지만
    • 테스트 코드는 한번 만들어두면 실행 할 자동으로 처리할 수 있다.
  3. 테스트코드 작성과 관리가 프로그래머 역량에 크게 영향 받음

인수 테스트

  1. 배치된 시스템을 대상으로 검증
  2. 전체 시스템 이상 여부 신뢰도가 높음
  3. 높은 비용
    • 작성 비용
    • 관리 비용
    • 실행 비용
  4. 피드백 품질이 낮음
    • 현상은 드러나지만 원인은 숨겨짐

단위 테스트

  1. 시스템의 일부(하위시스템)을 대상으로 검증
  2. 낮은 비용
    • 작성 비용
    • 관리 비용
    • 실행 비용
  3. 높은 피드백 품질
  4. 전체 시스템 이상 여부 신뢰도가 낮음
    • 단위 시스템에 성공한 여러 하위 시스템들이 협력하는 과정에서 실패하는 케이스도 있다.

3. 코드 분해

문제의 크기

  • 프로그래머가 한번에 다룰 수 있는 문제의 크기는 한계가 있음
  • 프로그래머는 더 큰 문제를 자주 마주함
  • 시스템의 크기는 점점 커짐
  • 큰 문제는 작은 문제로 분해할 수 있음
  • 작은 문제의 일부는 반복됨
    • 코드의 가독성이 프로그래머 생산성에 많은 영향을 미친다.

코드 재사용

  • 반복되는 문제의 풀이는 재사용 가능
  • 소프트웨어 개발 비용 절감

모듈화

  • 분해
    • 큰 시스템은 더 작은 하위 시스템으로 분해 가능
    • 교체 가능
  • 조립
    • 작은 시스템은 더 큰 상위 시스템으로 조립 가능
    • 모듈 재사용
    • 라이브러리
  • 단위 테스트
    • 모듈을 제공하는 입장에서는 모듈을 잘 테스트해서 제공해줘야 한다.
    • 이러한 경우 수동으로 테스트하기 보다는 자동화된 테스트케이스를 테스트한다.
    • 클라이언트입장에서는 단위 테스트로 신뢰감을 얻을 수 있다.

4. 단위 테스트

단위 테스트 예제

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
    • 위 상황이 발생하지 않도록 해주는 것.
    • 즉, 동일한 테스트 코드를 여러개의 파라미터로 테스트 할 수 있도록 해주는 것

Parameteried 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)
  • 위 상황이 발생하지 않도록 해주는 것.
  • 즉, 동일한 테스트 코드를 여러개의 파라미터로 테스트 할 수 있도록 해주는 것

5. 테스트 우선 개발

테스트 코드의 특징

  • 가시적이고 구체적인 목표
  • 자가 검증
  • 반복 실행
  • 운영 코드 API의 클라이언트가 된다.

운영 코드 보다 테스트 코드를 먼저 작성

  1. 명확하고 검증 가능한 목표를 설정한 후 목표 달성
  2. 프로세스가 코딩에 앞선 목표 설정을 강요
  3. 프로그래머는 자신이 풀어야 할 문제를 구체적으로 이해해야 함
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)

위 테스트코드를 통해서 또 실패 한다는 것을 알 수 있다.

  • 하지만, 테스트를 통해서 작성한 실수르 빠른 타이밍에 피드백을 받을 수 있다.
  • 금지어를 마스킹하기위해서 새로운 코드를 추가한다면, 그 기능으로 인해 성공했던 테스트가 실패 할 수 있다.

6. 정리된 코드

작업 환경 정리

  • 생산성
    • 정리된 환경과 어지럽혀진 환경에서의 작업 생산성 차이
  • 지속성
    • 작업 환경의 생산성이 일정 수준 미만으로 떨어지면
    • 더 이상 그 환경에서 작업 진행은 불가능
  • 코드 작업은 환경이자 작업 결과물이다.

리팩터링란

  • 의미를 유지하며 코드베이스를 정리하는 것
  • 의미 유지를 어떻게 확인할 것인가?
    • 수동 테스트 혹은 자동 테스트

7. 테스트 주도 개발

테스트 주도 개발 절차

  • RED -> GREEN -> REFACTOR -> RED 를 반복.
  • RED: 실패하는 테스트 추가
  • GREEN: 테스트 통과, 최소한의 코딩
  • REFACTOR: 구현 설계 개선, 테스트 통과 유지

1. 테스트 실패(RED)

  1. 구체적인 하나의 요구사항을 검증하는 하나의 테스트를 추가
  2. 추가된 테스트가 실패하는지 확인
    • 실패하는 것을 확인해야 테스트가 동작함을 믿을 수 있다.
    • 운영 코드 변경이 진행되지 않았기 때문에 실패했는지 확인해야 한다.

2. 테스트 성공 (GREEN)

  1. 추가된 테스트를 비롯해 모든 테스트가 성공하도록 운영 코드를 변경
  2. 테스트 성공은 요구사항 만족을 의미 (코딩의 가장 중요한 입무)
  3. 테스트 성공을 위한 최소한의 변경 (가장 중요한 입무를 가장 빠르게 완수)

3. 리팩터링 (REFACTOR)

  • 코드 베이스 정리
  • 구현 설계 개선
    • 가독성
    • 적응성
    • 성능
  • 모든 테스트 성공을 전제해야 한다.

테스트 주도 개발 세부 흐름

  1. 실패하는 단위 테스트 작성 (성공 하면, 잘못 작성한 것이므로 다시 작성)
  2. 운영 코드 작성
  3. 단위 테스트 실행 (실패시에는 성공할 때 까지 2번으로 돌아가서 재작성)
  4. 설계 개선(리팩토링)
  5. 단위 테스트 실행 (성공 시에는 1번으로 돌아감, 실패 시에는 4번으로 돌아감)

테스트 주도 개발 비용

  • 기능 하나 추가 할 때의 비용
    • TDD(X): 시간이 지남에 따라 비용 증가
    • TDD(O): TDD(X)보다는 초기 비용은 높지만, 시간이 지나도 비용은 동일하게 유지 (나중엔 TDD 안했을 때보다 적은 시간 소요)

8. 프로그래머 피드백

기대 출력 피드백

  • 사용자 피드백: 사용자가 직접 코드를 사용한 후 경험한 버그나 불만을 제보
  • Quality Assurance(QA): 전문 인적 자원에 의한 인수 테스트
  • 프로그래머 테스트: 프로그래머가 직접 피드백 장치를 준비 (테스트 자동화)
  • 도구 피드백: 컴파일 오류, 정적 검사 등 프로그래머가 사용하는 도구가 제공하는 피드백

오버 엔지니어링

  • 프로그래머는 요구사항 명세에 명확히 지정되지 않은 성능 달성이나 구현 설계 품질 개선에 빠져드는 경향을 가짐
  • 이런 목표는 그 자체로 나쁜 것은 아니지만 지나치면 더 중요한 목적, 기능 요구사항에 써야 할 자원을 불필요하게 낭비하곤 한따.
  • 테스트 주도 개발은 가장 중요한 목표를 우선 달성하도록 유도하며 오버엔지니어링에 빠졌음을 느낄 때 안심하고 다음으로 나아갈 수 있또록 피드백을 제공한다.

테스트 주도 개발의 핵심은 피드백이다.

  • 테스트 주도 개발의 핵심은 정해진 절차가 아니라 짧은 주기로 지속되는 피드백
  • 피드백에 기반해 안정적으로 지식과 코드를 늘려 나가는 것이 목적이다.

9. 게임 설계 예시

게임 설계

  • 1부터 100까지 임의의 정수를 맞추는 게임
  • 플에이어가 숫자를 입력하면
    • 입력한 숫자가 정답보다 작으면 작다고 출력
    • 입력한 숫자가 정답보다 크면 크다고 출력
    • 입력한 숫자가 정답과 일치하면 라운드 종료
  • 단일 플레이어 모드와 다중 플레이어 모드 지원
    • 단일 플레이어 모드 라운드가 종료되면 총 시도를 출력
    • 다중 플레이어 모드 라운드가 종료되면 승자를 출력

사용자 입장에서의 게임 흐름

image

아키텍처

image

다이어 그램

image

3. 테스트 주도 개발의 깊은 곳

1. 인터페이스와 구현

협력과 계약

  • 대부분의 코드는 다른 코드와 협력
  • 협력에 필요한 것은 어떻게 가 아닌 무엇
  • 인터페이스
    • 무엇을 표현
    • 클라이언트 코드에게 반드시 필요한 정보
    • 협력하는 코드 사이의 계약
    • 추상화 결과

인터페이스의 정보를 숨김이란

  • 효과적인 모듈화
    • 조직간 의사소통 최소화
    • 변경 여파 최소화
    • 시스템 이해 도움
  • 공개된 설계 결정과 숨겨진 설계 결정
    • 어려운 설계 결정과
    • 변경될 것 같은 설계 결정을 숨겨라

정보 숨김

  • 대부분의 시스템 정보는 대부분의 개발자에게 숨겨지는 것이 도움 된다
  • 대신 어려운 설계 결정이나 변경 될 가능성이 있는 설계 결정 목록으로 시작하는 것이 좋다.
  • 그런 다음 각 모듈은 이러한 결정을 다른 모듈로부터 숨기도록 설계된다.

2. 환경 변화의 적응력

개방 폐쇄 원칙

  • 소프트웨어 엔터티(클래스, 모듈, 함수 등)는 확장에 열리고 수정에 닫혀야 한다.
  • 확장 가능한 경우 모듈은 열려 있다고 말한다
  • 다른 모듈에 의해 사용될 수 있을 때 모듈은 닫혀 있다고 말한다
  • 상속을 염두 한 정의

3. 입력과 출력

인터페이스의 입력과 출력

  • 직접 입력과 직접 출려
    • 공개된 인터페이스를 통한 입력과 출력
    • 다루기 간단함
  • 간접 입력과 간접 출력
    • 입력된 인터페이스를 통한 입력과 출력
    • 다루기 복잡함

인터페이스의 부작용

  • 인터페이스 설계에 드러나지 않은 출력
    • 반환 값 외 출력
  • 자주 사용되는 부작용
    • 실패
    • 지연
    • 간접 출력

4. 테스트 대역

테스트 대역과 가정

  • DOC 준비 비용이 큰 경우
    • 구동에 많은 자원이 필요
    • 환경 제어가 어려움
    • DOC: Depend on Component (테스트하려는 대상 코드가 의존하는 코드)
    • SUT: 테스트하려는 대상 코드
  • DOC가 SUT에 제공하는 계약(인터페이스)을 준수하는 대역 코드를 사용
  • 대역 코드가 계약을 DOC와 동일하게 준수할 것이라고 가정

Dummy

  • 테스트 대역 중에서 가장 단순한 형태
  • SUT 준비를 위해 해결되어야 하는 의존성이 테스트 대상 논리에 의해 사용되지 않는 경우에 의존 요소를 대신하는 테스트 대역

Stub

  • 간접 입력 대역
  • 미리 준비된 답을 출력
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);
}

Spy

  • 간접 출력 대역
  • 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();
}

Mock

  • 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);
}

테스트 대역 유형 - Fake

  • 의존성 계약을 준수하는 가벼운 구현체
  • DOC보다 적은 부작용
  • 인메모리 데이터베이스 등

단위 테스트가 어려울 때, 테스트 대역은 굉장히 좋은 도구다.

5. Mockists vs Classics

Sociable 테스트 vs Solitary 테스트

  • Sociable: 단위 테스트 -> 시스템 -> 의존대상
  • Solitary: 단위 테스트 -> 시스템 -> 테스트 대역

가정의 안정도

  • 테스트 대역 사용으로 인해 생기는 가정을 얼마나 믿을 수 있을까?
  • 테스트 대역이 구현하는 인터페이스가 단순할수록 1에 가까워짐 (안정)
  • 테스트 대역이 구현하는 인터페이스가 복잡할수록 0에 가까워짐 (불안정)

Mock의 위험

  • 상태 검증 보다는 행위 검증에 가깝다
  • 정보 숨김을 위배할 확률이 높다
  • 테스트가 SUT 구현에 의존
  • 고통스럽고 불안한 리팩터링

6. Should I test private methods

비공개 모듈 테스트

  • 비공개 모듈의 작성과 사용은 공개 모듈의 구현 영역
  • 비공개 모듈 테스트는 공개 모듈 구현 노출
    • 테스트가 공개 모듈 구현에 의존
    • 정보 숨김 위배
    • SUT와 테스트 사이의 높은 결합

비공개 모듈 동작에 대한 불안함

  • 공개 모듈 동작을 고민하자.
  • 켄트 벡의 설계 규칙
    • Passes the tests
    • Reveals intention
    • No duplication
    • Fewest elements

7. 테스트 주도 설계

설계가 단위 테스트에 미치는 영향

  • 테스트는 인터페이스 설계에 의존
  • 인터페이스 설계 품질이 낮으면 테스트 작성이 불편함

단위 테스트가 설계에 미치는 영향

  • 테스트가 있기 때문에 리팩터링 가능
  • 두려움 없이 구현 설계를 과감하게 개선

단위 테스트에 의지하는 인터페이스 설계

  • 단위 테스트는
    • 낮은 응집에 대한 피드백을 주지 않는다
    • 일관된 설계를 강요하지 않는다
    • 의도 노출을 요구하지 않는다

단위 테스트에 의지하는 구현 설계

  • 단위 테스트는 책임 분산을 유도하지 않는다
  • Mockists
  • 비공개 운영 코드 테스트

8. 테스트 주도 개발의 한계

은탄환은 없다

  • 테스트 주도 개발은 유용하고 매력적인 도구
  • 하지만 테스트 주도 개발 역시 남용을 주의해야 한다

불안정한 목표

  • 모든 코드의 목표가 안정적이지 않다
  • 탐색적으로 목표를 찾아내야 한다면 테스트 코드 작성 비용 부담이 큼

환경 제어

  • 하위 시스템이 협력하는 다른 하위 시스템, 의존성 중 일부는 출력의 예측과 제어가 어려움 -> 단위 테스트는 결정적 일수록 유용
  • 의존성 중 일부는 동작이 매우 느림 -> 단위 테스트는 빠를수록 유용
  • 의존성의 단점을 보안할 대역을 도입하면 가정이 발생 -> 가정으로 인한 안정감 감소

낮은 코드 적응력

  • 코드 적응력이 낮으면 단위 테스트하기 매우 어렵다
  • 이미 코드 적응력이 낮은 코드 기반에 테스트 주도 개발을 적용하기 어렵다
  • 코드 기반의 적응력을 높이는 것도 어렵다
    • 설계 변경의 어려움
    • 변경된 설계 검증 필요

9. 인터페이스와 테스트

인터페이스

  • 한 개체가 상호작용하는 다른 개체에 제공하는 상호작용 지점

API

  • 한 시스템이 협력 시스템에 제공한다
  • 코드 친화적 소통 수단이다.
  • 테스트 자동화 비용이 낮다

UI

  • 한 시스템이 시스템 사용자에게 제공한다
  • 인간 친화적 소통 수단이다.
  • 변경이 잦지만, 테스트 자동화 비용이 높다

10. 인수 테스트 주도 개발

  • 배치된 코드를 대상으로 최종 클라이언트 관점으로 테스트
  • UI로 할수도 있고, API로도 할 수 있다.
  • UI 응용 프로그램
    • 사용자가 클라이언트
    • 테스트 자동화 작성과 운영 비용이 높다
  • API 응용 프로그램
    • 외부 시스템이 클라이언트
    • 테스트 자동화 작성과 운영 비용이 낮음

인수 테스트 vs 단위 테스트

비교 대상 인수 테스트 단위 테스트
관점 최종 클라이언트 개발자
안정감 높음 낮음
피드백 낮음 높음
실행 속도 느림 매우 빠름
결정성 비결정적 결정적

인수 테스트 + 단위 테스트 주도 개발

  1. 인수테스트 작성
  2. 인수테스트 실행 (성공시 1번으로 돌아감)
  3. 단위 테스트 작성
  4. 단위 테스트 실행 (실패시 5번, 성공시 6번)
  5. 운영코드 작성
  6. 설계 개선
  7. 단위 테스트 실행 (실패시 6번, 성공시 2번으로 돌아감)

11. 코딩 계획

개발 작업 설계 - 목표를 정확히 기술

  • 어떤 가치를 구현하기 위해 코드를 쓰고 연결할 것인가?
  • 코드를 쓰는 일은 개발자가 하위 문제에 집중하게 만듦
  • 명확한 이정표가 없다면 개발자는 쉽게 길을 잃어 소중한 자원을 낭비한다
  • 사용자 스토리나 테스트 케이스는 목표 기술의 좋은 수단

개발 작업 설계 - 작업 분리

  • 전체 작업을 하위 작업으로 분리 - 얼마나 작게 분리할 것인가는 맥락에 따라 다르다
  • 하위 작업 역시 목표를 명확히 기술

개발 작업 설계의 장점 - 업무 가시성

  • 투명한 작업자의 업무 내용과 진행도
  • 위험 요소를 더 빨리 발견할 가능성이 높아진다.

개발 작업 설계의 장점 - 협업

  • 밀도 있고 입체적인 협업 계획