Updated:

13 minute read

개요

Go는 테스팅을 언어 차원에서 지원하며, testing 패키지와 go test 도구를 제공합니다.

주요 특징:

  • 내장 테스팅 프레임워크: 별도 설치 불필요
  • go test 명령어: 자동 테스트 실행
  • 벤치마크: 성능 측정 기능
  • 커버리지: 코드 커버리지 분석
  • Examples: 실행 가능한 문서화
  • Table-driven tests: Go 스타일 테스트 패턴
  • Subtests: 계층적 테스트 구조
  • 병렬 실행: 테스트 병렬화 지원

테스트 기본

1. 테스트 파일 구조

// math.go
package math

func Add(a, b int) int {
    return a + b
}

func Multiply(a, b int) int {
    return a * b
}

func Divide(a, b int) (int, error) {
    if b == 0 {
        return 0, errors.New("division by zero")
    }
    return a / b, nil
}
// math_test.go
package math

import "testing"

func TestAdd(t *testing.T) {
    result := Add(2, 3)
    expected := 5
    
    if result != expected {
        t.Errorf("Add(2, 3) = %d; want %d", result, expected)
    }
}

func TestMultiply(t *testing.T) {
    result := Multiply(3, 4)
    expected := 12
    
    if result != expected {
        t.Errorf("Multiply(3, 4) = %d; want %d", result, expected)
    }
}

명명 규칙:

  • 파일: *_test.go
  • 함수: Test* (첫 글자 대문자)
  • 파라미터: t *testing.T

2. testing.T 메서드

func TestTMethods(t *testing.T) {
    // 에러 기록 (계속 실행)
    t.Error("error message")
    t.Errorf("formatted error: %s", "detail")
    
    // 테스트 실패 (즉시 중단)
    if false {
        t.Fatal("fatal error")
        t.Fatalf("fatal error: %v", "reason")
    }
    
    // 테스트 건너뛰기
    if testing.Short() {
        t.Skip("skipping in short mode")
    }
    
    // 헬퍼 함수 표시 (스택 트레이스에서 제외)
    t.Helper()
    
    // 로그 출력 (verbose 모드에서만)
    t.Log("log message")
    t.Logf("log: %s", "formatted")
    
    // 병렬 실행 표시
    t.Parallel()
    
    // 테스트 이름
    t.Name()
    
    // 임시 디렉토리
    tmpDir := t.TempDir()
    t.Logf("Temp dir: %s", tmpDir)
}

3. go test 플래그

# 기본 실행
go test

# 상세 출력
go test -v

# 특정 패키지
go test ./...          # 모든 하위 패키지
go test ./pkg/math     # 특정 패키지

# 특정 테스트만 실행
go test -run TestAdd
go test -run "TestAdd|TestMultiply"
go test -run TestDivide/zero  # 서브테스트

# 짧은 테스트만 실행
go test -short

# 병렬 실행 제한
go test -parallel 4

# 타임아웃
go test -timeout 30s

# 캐시 비활성화
go test -count=1

# 여러 번 실행
go test -count=10

Table-Driven Tests

1. 기본 패턴

func TestAddTableDriven(t *testing.T) {
    tests := []struct {
        name string
        a, b int
        want int
    }{
        {"positive numbers", 2, 3, 5},
        {"negative numbers", -2, -3, -5},
        {"mixed numbers", 2, -3, -1},
        {"zero", 0, 0, 0},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got := Add(tt.a, tt.b)
            if got != tt.want {
                t.Errorf("Add(%d, %d) = %d; want %d", 
                    tt.a, tt.b, got, tt.want)
            }
        })
    }
}

2. 에러 테스트

func TestDivide(t *testing.T) {
    tests := []struct {
        name    string
        a, b    int
        want    int
        wantErr bool
    }{
        {"valid division", 10, 2, 5, false},
        {"division by zero", 10, 0, 0, true},
        {"negative numbers", -10, -2, 5, false},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := Divide(tt.a, tt.b)
            
            if (err != nil) != tt.wantErr {
                t.Errorf("Divide() error = %v, wantErr %v", 
                    err, tt.wantErr)
                return
            }
            
            if got != tt.want {
                t.Errorf("Divide() = %d, want %d", got, tt.want)
            }
        })
    }
}

3. 복잡한 테이블

type User struct {
    Name  string
    Email string
    Age   int
}

func TestValidateUser(t *testing.T) {
    tests := []struct {
        name      string
        user      User
        wantValid bool
        wantErr   error
    }{
        {
            name:      "valid user",
            user:      User{"John", "john@example.com", 25},
            wantValid: true,
            wantErr:   nil,
        },
        {
            name:      "empty name",
            user:      User{"", "john@example.com", 25},
            wantValid: false,
            wantErr:   ErrEmptyName,
        },
        {
            name:      "invalid email",
            user:      User{"John", "invalid", 25},
            wantValid: false,
            wantErr:   ErrInvalidEmail,
        },
        {
            name:      "underage",
            user:      User{"John", "john@example.com", 17},
            wantValid: false,
            wantErr:   ErrUnderage,
        },
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            valid, err := ValidateUser(tt.user)
            
            if valid != tt.wantValid {
                t.Errorf("ValidateUser() valid = %v, want %v", 
                    valid, tt.wantValid)
            }
            
            if !errors.Is(err, tt.wantErr) {
                t.Errorf("ValidateUser() error = %v, want %v", 
                    err, tt.wantErr)
            }
        })
    }
}

Subtests

1. 계층적 구조

func TestUser(t *testing.T) {
    t.Run("Validation", func(t *testing.T) {
        t.Run("Valid", func(t *testing.T) {
            user := User{"John", "john@example.com", 25}
            if valid, _ := ValidateUser(user); !valid {
                t.Error("expected valid user")
            }
        })
        
        t.Run("Invalid", func(t *testing.T) {
            user := User{"", "", 0}
            if valid, _ := ValidateUser(user); valid {
                t.Error("expected invalid user")
            }
        })
    })
    
    t.Run("Creation", func(t *testing.T) {
        user := NewUser("John", "john@example.com", 25)
        if user.Name != "John" {
            t.Errorf("unexpected name: %s", user.Name)
        }
    })
}

// 실행: go test -run TestUser/Validation/Valid

2. 병렬 실행

func TestParallel(t *testing.T) {
    tests := []struct {
        name string
        val  int
    }{
        {"test1", 1},
        {"test2", 2},
        {"test3", 3},
    }
    
    for _, tt := range tests {
        tt := tt // 클로저 캡처 방지
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // 병렬 실행
            
            time.Sleep(1 * time.Second)
            if tt.val < 0 {
                t.Errorf("unexpected value: %d", tt.val)
            }
        })
    }
}

테스트 헬퍼

1. 헬퍼 함수

func assertEqual(t *testing.T, got, want interface{}) {
    t.Helper() // 스택 트레이스에서 이 함수 제외
    
    if got != want {
        t.Errorf("got %v, want %v", got, want)
    }
}

func assertError(t *testing.T, err error, wantErr bool) {
    t.Helper()
    
    if (err != nil) != wantErr {
        t.Errorf("error = %v, wantErr %v", err, wantErr)
    }
}

func TestWithHelpers(t *testing.T) {
    result := Add(2, 3)
    assertEqual(t, result, 5)
    
    _, err := Divide(10, 0)
    assertError(t, err, true)
}

2. 테스트 픽스처

func setup(t *testing.T) (*Database, func()) {
    t.Helper()
    
    db := NewDatabase()
    if err := db.Connect(); err != nil {
        t.Fatalf("setup failed: %v", err)
    }
    
    // 정리 함수 반환
    teardown := func() {
        db.Close()
    }
    
    return db, teardown
}

func TestDatabase(t *testing.T) {
    db, teardown := setup(t)
    defer teardown()
    
    // 테스트 수행
    err := db.Insert("key", "value")
    if err != nil {
        t.Fatalf("Insert failed: %v", err)
    }
}

3. Cleanup

func TestCleanup(t *testing.T) {
    // t.Cleanup은 defer와 유사하지만 서브테스트에서도 작동
    t.Cleanup(func() {
        fmt.Println("Cleanup 1")
    })
    
    t.Cleanup(func() {
        fmt.Println("Cleanup 2")
    })
    
    t.Run("subtest", func(t *testing.T) {
        t.Cleanup(func() {
            fmt.Println("Subtest cleanup")
        })
    })
    
    // 출력 순서:
    // Subtest cleanup
    // Cleanup 2
    // Cleanup 1
}

4. TempDir

func TestFileOperations(t *testing.T) {
    tmpDir := t.TempDir() // 자동으로 정리됨
    
    filePath := filepath.Join(tmpDir, "test.txt")
    err := os.WriteFile(filePath, []byte("test"), 0644)
    if err != nil {
        t.Fatalf("WriteFile failed: %v", err)
    }
    
    // 테스트...
}

벤치마크

1. 기본 벤치마크

func BenchmarkAdd(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Add(2, 3)
    }
}

func BenchmarkMultiply(b *testing.B) {
    for i := 0; i < b.N; i++ {
        Multiply(3, 4)
    }
}

// 실행: go test -bench=.
// 결과: BenchmarkAdd-8   1000000000   0.25 ns/op

2. 벤치마크 옵션

# 모든 벤치마크 실행
go test -bench=.

# 특정 벤치마크
go test -bench=BenchmarkAdd

# 실행 시간 지정
go test -bench=. -benchtime=10s

# 반복 횟수 지정
go test -bench=. -benchtime=1000000x

# 메모리 통계
go test -bench=. -benchmem

# CPU 프로파일
go test -bench=. -cpuprofile=cpu.prof

# 메모리 프로파일
go test -bench=. -memprofile=mem.prof

3. 고급 벤치마크

func BenchmarkStringConcat(b *testing.B) {
    b.Run("plus operator", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = "hello" + " " + "world"
        }
    })
    
    b.Run("fmt.Sprintf", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            _ = fmt.Sprintf("%s %s", "hello", "world")
        }
    })
    
    b.Run("strings.Builder", func(b *testing.B) {
        for i := 0; i < b.N; i++ {
            var sb strings.Builder
            sb.WriteString("hello")
            sb.WriteString(" ")
            sb.WriteString("world")
            _ = sb.String()
        }
    })
}

4. 타이머 제어

func BenchmarkWithSetup(b *testing.B) {
    // 셋업은 측정하지 않음
    data := make([]int, 1000)
    for i := range data {
        data[i] = rand.Intn(1000)
    }
    
    b.ResetTimer() // 타이머 리셋
    
    for i := 0; i < b.N; i++ {
        sort.Ints(data)
    }
}

func BenchmarkWithPause(b *testing.B) {
    for i := 0; i < b.N; i++ {
        // 측정할 코드
        DoWork()
        
        b.StopTimer()
        // 측정하지 않을 코드
        Setup()
        b.StartTimer()
    }
}

5. 메모리 벤치마크

func BenchmarkAllocations(b *testing.B) {
    b.ReportAllocs() // 메모리 할당 보고
    
    for i := 0; i < b.N; i++ {
        s := make([]int, 100)
        _ = s
    }
}

// 결과:
// BenchmarkAllocations-8   5000000   250 ns/op   800 B/op   1 allocs/op
//                                                  ^^^        ^^^
//                                              bytes/op   allocations/op

커버리지

1. 커버리지 측정

# 커버리지 측정
go test -cover

# 커버리지 프로파일 생성
go test -coverprofile=coverage.out

# HTML 리포트 생성
go tool cover -html=coverage.out

# 함수별 커버리지
go tool cover -func=coverage.out

# 특정 패키지만
go test -coverprofile=coverage.out ./pkg/math
go tool cover -html=coverage.out

2. 커버리지 모드

# set: 라인 실행 여부만 (기본)
go test -covermode=set -coverprofile=coverage.out

# count: 라인 실행 횟수
go test -covermode=count -coverprofile=coverage.out

# atomic: count + 동시성 안전
go test -covermode=atomic -coverprofile=coverage.out

3. 커버리지 패키지 선택

# 현재 패키지만
go test -cover

# 특정 패키지
go test -coverpkg=./pkg/math -coverprofile=coverage.out

# 모든 패키지
go test -coverpkg=./... -coverprofile=coverage.out ./...

Examples

1. Example 함수

func ExampleAdd() {
    result := Add(2, 3)
    fmt.Println(result)
    // Output: 5
}

func ExampleMultiply() {
    result := Multiply(3, 4)
    fmt.Println(result)
    // Output: 12
}

// go test 시 자동으로 실행 및 검증
// godoc에 자동으로 포함됨

2. Example with Unordered Output

func ExamplePrintMap() {
    m := map[string]int{"a": 1, "b": 2}
    for k, v := range m {
        fmt.Printf("%s=%d\n", k, v)
    }
    // Unordered output:
    // a=1
    // b=2
}

3. Example for Method

type Calculator struct{}

func (c Calculator) Add(a, b int) int {
    return a + b
}

func ExampleCalculator_Add() {
    calc := Calculator{}
    result := calc.Add(2, 3)
    fmt.Println(result)
    // Output: 5
}

4. Example for Package

func Example() {
    // 패키지 전체 예제
    result := Add(2, 3)
    fmt.Println(result)
    // Output: 5
}

func Example_second() {
    // 추가 패키지 예제
    result := Multiply(3, 4)
    fmt.Println(result)
    // Output: 12
}

모킹과 인터페이스

1. 인터페이스 기반 테스트

// production code
type UserRepository interface {
    GetUser(id int) (*User, error)
    SaveUser(user *User) error
}

type UserService struct {
    repo UserRepository
}

func (s *UserService) GetUserName(id int) (string, error) {
    user, err := s.repo.GetUser(id)
    if err != nil {
        return "", err
    }
    return user.Name, nil
}

// test code
type mockUserRepository struct {
    users map[int]*User
}

func (m *mockUserRepository) GetUser(id int) (*User, error) {
    user, ok := m.users[id]
    if !ok {
        return nil, errors.New("user not found")
    }
    return user, nil
}

func (m *mockUserRepository) SaveUser(user *User) error {
    m.users[user.ID] = user
    return nil
}

func TestUserService_GetUserName(t *testing.T) {
    repo := &mockUserRepository{
        users: map[int]*User{
            1: {ID: 1, Name: "John"},
        },
    }
    
    service := &UserService{repo: repo}
    
    name, err := service.GetUserName(1)
    if err != nil {
        t.Fatalf("unexpected error: %v", err)
    }
    
    if name != "John" {
        t.Errorf("got %s, want John", name)
    }
}

2. httptest 활용

import "net/http/httptest"

func HandleUser(w http.ResponseWriter, r *http.Request) {
    id := r.URL.Query().Get("id")
    if id == "" {
        http.Error(w, "id required", http.StatusBadRequest)
        return
    }
    
    fmt.Fprintf(w, `{"id": "%s", "name": "John"}`, id)
}

func TestHandleUser(t *testing.T) {
    req := httptest.NewRequest("GET", "/user?id=1", nil)
    w := httptest.NewRecorder()
    
    HandleUser(w, req)
    
    resp := w.Result()
    if resp.StatusCode != http.StatusOK {
        t.Errorf("status = %d, want %d", resp.StatusCode, http.StatusOK)
    }
    
    body, _ := io.ReadAll(resp.Body)
    expected := `{"id": "1", "name": "John"}`
    if string(body) != expected {
        t.Errorf("body = %s, want %s", body, expected)
    }
}

3. testify 라이브러리

import (
    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/mock"
    "github.com/stretchr/testify/require"
)

func TestWithTestify(t *testing.T) {
    // assert: 실패해도 계속
    assert.Equal(t, 5, Add(2, 3))
    assert.NotNil(t, &User{})
    
    // require: 실패하면 중단
    result, err := Divide(10, 2)
    require.NoError(t, err)
    require.Equal(t, 5, result)
}

// Mock
type MockRepository struct {
    mock.Mock
}

func (m *MockRepository) GetUser(id int) (*User, error) {
    args := m.Called(id)
    return args.Get(0).(*User), args.Error(1)
}

func TestWithMock(t *testing.T) {
    repo := new(MockRepository)
    repo.On("GetUser", 1).Return(&User{ID: 1, Name: "John"}, nil)
    
    service := &UserService{repo: repo}
    name, err := service.GetUserName(1)
    
    assert.NoError(t, err)
    assert.Equal(t, "John", name)
    repo.AssertExpectations(t)
}

통합 테스트

1. 빌드 태그

//go:build integration
// +build integration

package integration

func TestDatabaseIntegration(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping integration test")
    }
    
    // 실제 데이터베이스 테스트
}

// 실행: go test -tags=integration

2. 환경 변수 활용

func TestWithEnv(t *testing.T) {
    dbURL := os.Getenv("DATABASE_URL")
    if dbURL == "" {
        t.Skip("DATABASE_URL not set")
    }
    
    db, err := sql.Open("postgres", dbURL)
    require.NoError(t, err)
    defer db.Close()
    
    // 테스트...
}

3. Docker 컨테이너 사용

func TestWithDocker(t *testing.T) {
    if testing.Short() {
        t.Skip("skipping docker test")
    }
    
    // testcontainers-go 사용 예시
    ctx := context.Background()
    
    req := testcontainers.ContainerRequest{
        Image:        "postgres:14",
        ExposedPorts: []string{"5432/tcp"},
        Env: map[string]string{
            "POSTGRES_PASSWORD": "password",
        },
    }
    
    container, err := testcontainers.GenericContainer(ctx, 
        testcontainers.GenericContainerRequest{
            ContainerRequest: req,
            Started:          true,
        })
    require.NoError(t, err)
    defer container.Terminate(ctx)
    
    // 테스트...
}

일반적인 실수

1. 테스트 독립성 부족

var globalCounter int

func TestNonIsolated1(t *testing.T) {
    globalCounter++ // ❌ 전역 상태 변경
    if globalCounter != 1 {
        t.Error("expected 1")
    }
}

func TestNonIsolated2(t *testing.T) {
    globalCounter++
    if globalCounter != 1 { // 실행 순서에 따라 실패
        t.Error("expected 1")
    }
}

// ✅ 올바른 방법
func TestIsolated(t *testing.T) {
    counter := 0 // 로컬 변수 사용
    counter++
    if counter != 1 {
        t.Error("expected 1")
    }
}

2. 에러 검증 누락

func TestErrorMissing(t *testing.T) {
    result, _ := Divide(10, 0) // ❌ 에러 무시
    if result != 0 {
        t.Error("unexpected result")
    }
}

// ✅ 올바른 방법
func TestErrorCheck(t *testing.T) {
    _, err := Divide(10, 0)
    if err == nil {
        t.Error("expected error")
    }
}

3. Table-driven test에서 클로저 문제

func TestClosureBug(t *testing.T) {
    tests := []struct {
        name string
        val  int
    }{
        {"test1", 1},
        {"test2", 2},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel() // ❌ tt 변수 공유
            
            if tt.val < 0 {
                t.Error("negative")
            }
        })
    }
}

// ✅ 올바른 방법
func TestClosureFix(t *testing.T) {
    tests := []struct {
        name string
        val  int
    }{
        {"test1", 1},
        {"test2", 2},
    }
    
    for _, tt := range tests {
        tt := tt // 섀도잉으로 복사
        t.Run(tt.name, func(t *testing.T) {
            t.Parallel()
            
            if tt.val < 0 {
                t.Error("negative")
            }
        })
    }
}

4. 타이밍 의존적 테스트

func TestTimingDependentBad(t *testing.T) {
    go doAsync()
    time.Sleep(100 * time.Millisecond) // ❌ 불안정
    
    // 검증...
}

// ✅ 올바른 방법
func TestTimingDependentGood(t *testing.T) {
    done := make(chan bool)
    go func() {
        doAsync()
        done <- true
    }()
    
    select {
    case <-done:
        // 검증...
    case <-time.After(1 * time.Second):
        t.Fatal("timeout")
    }
}

5. 테스트 이름 불명확

func TestFunc(t *testing.T) { // ❌ 불명확
    // ...
}

func Test1(t *testing.T) { // ❌ 숫자만 사용
    // ...
}

// ✅ 올바른 방법
func TestUserValidation_EmptyName_ReturnsError(t *testing.T) {
    // ...
}

func TestDivideByZero_ReturnsError(t *testing.T) {
    // ...
}

6. Cleanup 누락

func TestCleanupMissing(t *testing.T) {
    file, _ := os.Create("/tmp/test.txt") // ❌ 정리 안함
    // 테스트...
}

// ✅ 올바른 방법
func TestCleanupProper(t *testing.T) {
    file, err := os.Create("/tmp/test.txt")
    require.NoError(t, err)
    t.Cleanup(func() {
        os.Remove("/tmp/test.txt")
    })
    
    // 테스트...
}

7. 과도한 모킹

// ❌ 모든 것을 모킹
type MockEverything struct {
    mock.Mock
}

// ✅ 필요한 것만 모킹, 실제 구현 사용
func TestWithRealDependencies(t *testing.T) {
    // 외부 의존성만 모킹
    // 내부 로직은 실제 코드 사용
}

베스트 프랙티스

1. 명확한 테스트 구조 (AAA)

func TestUserCreation(t *testing.T) {
    // Arrange (준비)
    name := "John"
    email := "john@example.com"
    age := 25
    
    // Act (실행)
    user := NewUser(name, email, age)
    
    // Assert (검증)
    assert.Equal(t, name, user.Name)
    assert.Equal(t, email, user.Email)
    assert.Equal(t, age, user.Age)
}

2. 테스트 이름 명명 규칙

// 패턴: Test<Function>_<Scenario>_<ExpectedResult>

func TestDivide_ByZero_ReturnsError(t *testing.T) {
    _, err := Divide(10, 0)
    assert.Error(t, err)
}

func TestDivide_ValidInput_ReturnsQuotient(t *testing.T) {
    result, err := Divide(10, 2)
    assert.NoError(t, err)
    assert.Equal(t, 5, result)
}

3. Table-driven tests 활용

func TestValidation(t *testing.T) {
    tests := []struct {
        name    string
        input   string
        wantErr bool
    }{
        {"valid email", "user@example.com", false},
        {"no @", "userexample.com", true},
        {"no domain", "user@", true},
        {"empty", "", true},
    }
    
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            err := ValidateEmail(tt.input)
            if (err != nil) != tt.wantErr {
                t.Errorf("got error %v, wantErr %v", err, tt.wantErr)
            }
        })
    }
}

4. Golden Files

func TestRenderHTML(t *testing.T) {
    data := &PageData{Title: "Test", Content: "Hello"}
    result := RenderHTML(data)
    
    goldenFile := "testdata/output.golden"
    
    if *update {
        os.WriteFile(goldenFile, []byte(result), 0644)
    }
    
    expected, err := os.ReadFile(goldenFile)
    require.NoError(t, err)
    
    assert.Equal(t, string(expected), result)
}

// 실행: go test -update

5. 테스트 데이터 분리

// testdata/users.json
[
    {"id": 1, "name": "John"},
    {"id": 2, "name": "Jane"}
]

func TestLoadUsers(t *testing.T) {
    data, err := os.ReadFile("testdata/users.json")
    require.NoError(t, err)
    
    var users []User
    err = json.Unmarshal(data, &users)
    require.NoError(t, err)
    
    assert.Len(t, users, 2)
}

6. 테스트 커버리지 목표

# 80% 이상 권장
go test -cover

# CI/CD에서 최소 커버리지 강제
go test -cover | grep -E 'coverage: [0-9]+' | \
    awk '{if ($2 < 80) exit 1}'

정리

  • 기본: *_test.go, Test* 함수, testing.T
  • go test: 자동 테스트 실행, 다양한 플래그
  • Table-driven: Go 스타일 테스트 패턴
  • Subtests: 계층적 구조, 병렬 실행
  • 헬퍼: t.Helper(), t.Cleanup(), t.TempDir()
  • 벤치마크: Benchmark*, -bench, -benchmem
  • 커버리지: -cover, -coverprofile, HTML 리포트
  • Examples: 실행 가능한 문서, // Output: 검증
  • 모킹: 인터페이스 기반, testify, httptest
  • 통합 테스트: 빌드 태그, 환경 변수, Docker
  • 실수: 독립성, 에러 검증, 클로저, 타이밍, 정리
  • 베스트: AAA 패턴, 명확한 이름, 데이터 분리
  • 원칙: 빠르고, 독립적이며, 반복 가능한 테스트