Recommanded Free YOUTUBE Lecture: <% selectedImage[1] %>

TDD를 하는 이유

품질의 일관성

 품질의 일관성

  • 코드는 매 빌드마다 테스트 코드를 중복해서 실행한다.
  • 빌드를 성공하기 위해서는 테스트를 통과해야만 한다.
  • 따라서 "테스트를 성공했다면" 이전의 품질목표는 달성했다는 것을 보장 할 수 있다.

테스트를 하지 않으면

  • 기능이 많아질 수록 문제를 찾기가 점점 힘들어진다.
  • 프러덕트화 될 수록 문제 해결이 힘들어진다 : 여러 개의 레이어가 연동되기 때문에 문제가 서로에게 영향을 미친다.
  • 품질을 예측 할 수가 없다.

테스트는 자주 해야 한다.

  • 빈번한 빌드와 통합
  • 빌드의 자동화
  • 통합의 저주에서 벗어나기
    • 테스트의 복잡도는 네트워크 효과를 따른다.
    • 테스트의 난이도는 네트워크 시간과의 함수이며, 기하급수적으로 늘어난다.

개발에 집중

  • 내가 개발한 코드가 기존 코드에 영향을 미치지 않을 것이라는 것을 믿을 수 있다.
  • 테스트를 문서(코드)화 할 수 있다. : 개발 과정과 함께 점진적으로 고도화 할 수 있다.
  • 테스트를 문서(코드)화 할 수 있다. : 문서로 남는 다는 것은 비판을 허용하겠다는 의미다. 리뷰문화로 정착 할 수 있다.

테스트할 수 있는 코드는 좋은 코드다.

  • 값을 예측 할 수 있음.
  • 테스트 코드를 작성 할 수 있음.
  • "소프트웨어를 개발 할 때 부터, 테스트 할 수 있도록 염두에 둬야 함"

단위 별 테스트 & 통합

  • 하드웨어와 소프트웨어의 테스트를 분리
  • 소프트웨어의 경우에도 코어, 백앤드 애플리케이션(비지니스로직을 포함하는)이 독립적인 테스트가 가능하도록 해야 한다.
  • 이들이 통합될 수 있는 DEV&QA(Staging)망이 필요하다.

Go 언어와 TDD

  • 어떤 언어든지 프로세스의 정착이 이슈이다. 언어별 적당한 툴을 사용하면 될 일이다.

C++과 Go 언어의 차이

  • Go는 C/C++을 대체하기 위해서 만들어진 언어가 아니다.
  • 구글의 네트워크 인프라스트럭처를 구성하던 C++의 엄청난 복잡도와 기나긴 빌드시간을 줄이기 위해서 만들어졌다.
  • 네트워크 인프라스트럭처의 요구사항을 만족할 만한 정도의 성능 + 생산성
  • 멀티코어 아키텍처의 효과적인 사용
  • C/C++ 보다는 느리고 Javscript, Python 보다는 빠르다. Java와는 비슷(하거나 약간 더 빠른 정도)하다.
  • Go의 개발툴은 언어와 통합되으며, 매우 훌륭하다. C++의 개발툴은 매우 훌륭하지만 분산돼 있다.
  • C++은 임베디드 환경(운영체제가 없는 하드웨어를 포함)에서 실행된다. Go는 상용하드웨어(ARM, RISC-V, x86)에서 실행된다.
  • Go는 GC, 스케쥴러, 리플렉션, 모니터링, 측정등을 포함하는 런타임을 생성한다.

Simple example

프로젝트 구성
.
├── go.mod
└── mymath
    ├── math.go
    └── math_test.go

math.go
package mymath

func Sum(nums ...int) int {
    total := 0
    for _, num := range nums {
        total += num
    }
    return total
}

func Div(a, b float64) (float64, error) {
    return a / b, nil
}

func StrRept(s string, count int) string {
    b := make([]byte, len(s)*count)
    bp := copy(b, s)
    for bp < len(b) {
        copy(b[bp:], b[bp:])
        bp *= 2
    }
    return string(b)
}

math_test.go
package mymath
                                
import (
    "testing"
)

func Test_Sum(t *testing.T) {
    v0 := Sum(1, 2, 3)
    if v0 != 6 {
        t.Fatal("1+2+3=6 but ", v0)
    }

    v1 := Sum(6, 5)
    if v1 != 11 {
        t.Fatal("6+5=11 but ", v1)
    }
}

func Test_Div(t *testing.T) {
    v2, err := Div(0, 2)
    if err == nil {
        t.Fatal("Divide Zero")
    }
    t.Log("0/2=", v2)
}

go는 test툴을 통합해서 제공한다.
 ✘ ⚙ yundream@yundream  ~/workstation/tdd/function/mymath  go test -v
=== RUN   Test_Sum
--- FAIL: Test_Sum (0.00s)
    math_test.go:16: 
        	Error Trace:	math_test.go:16
        	Error:      	Not equal: 
        	            	expected: 6
        	            	actual  : 11
        	Test:       	Test_Sum
        	Messages:   	They shuould be equal
=== RUN   Test_Div
--- FAIL: Test_Div (0.00s)
    math_test.go:24: 
        	Error Trace:	math_test.go:24
        	Error:      	Expected value not to be nil.
        	Test:       	Test_Div
        	Messages:   	Devided zero
    math_test.go:26: Divide Zero
FAIL
exit status 1
FAIL	github.com/yundream/tddtest/mymath	0.003s

Assert 패키지

테스트를 체계적으로 하기 위해서 asserting 툴들을 사용한다. 위 코드는 아래와 같이 수정 할 수 있다.
package mymath

import (
    "github.com/stretchr/testify/assert"
    "testing"
)

func Test_Sum(t *testing.T) {
    v0 := Sum(1, 2, 3)
    assert.Equal(t, 6, v0, "They shuould be equal")
    if v0 != 6 {
        t.Fatal("1+2+3=6 but ", v0)
    }

    v1 := Sum(6, 5)
    assert.Equal(t, 6, v1, "They shuould be equal")
    if v1 != 11 {
        t.Fatal("6+5=11 but ", v1)
    }
}

func Test_Div(t *testing.T) {
    v2, err := Div(0, 2)
    assert.NotNil(t, err, "Devided zero")
    if err == nil {
        t.Fatal("Divide Zero")
    }
    t.Log("0/2=", v2)
}

HTTP API 테스트

서버/클라이언트 모델을 따르는 애플리케이션의 경우 독립된 두 개의 컴포넌트들이 필요하기 때문에 테스트가 까다로울 수 있다.
  1. 목업을 이용 할 것인가 ?
  2. 서버 & 클라이언트 쌍을 구축 할 것인가 ?
  3. 통합된 테스트 툴이 있나 ?
GoLang은 통합된(커버리지 측정이 가능한) 테스트 툴을 제공한다.
  • net/http/httptest 패키지를 제공

CICD

CICD와 TDD의 통합