Kubernetes에서 Go 테스팅 배우기
Takashi Yamamoto
Infrastructure Engineer · Leapcell

왜 테스팅을 해야 하는가
좋은 단위 테스팅은 더 세련된 코드 설계를 가능하게 하여 코드 이해도, 재사용성, 유지보수성을 향상시킵니다. 변경 사항을 도입할 때, 전체 프로그램을 다시 테스트할 필요 없이 수정된 부분의 입력과 출력이 일관되게 유지되는지 확인하기만 하면 되며, 프로그램에 문제가 있는지 신속하게 확인할 수 있습니다.
또한, 버그가 발생할 때마다 버그의 입력을 테스트 케이스로 추가할 수 있습니다. 이렇게 하면 동일한 실수를 다시 하지 않게 되며, 매번 테스트를 한 번만 실행하여 과거의 유사한 문제가 새로운 변경 사항에 다시 도입되었는지 확인할 수 있습니다. 이는 소프트웨어 품질을 크게 향상시킵니다.
모의(Mocking)를 용이하게 하기 위해 메서드를 매개변수로 전달하기
Kubernetes의 정상 종료 로직에서 핸들러 매개변수를 직접 호출하는 대신 메서드로 선언함으로써 핸들러 자체의 정확성에 대해 걱정하지 않고 flushList
의 로직만 테스트할 수 있습니다.
그러나 gomonkey의 리플렉션을 사용하여 메서드의 반환 값을 직접 모의하여 동일한 효과를 얻을 수도 있습니다.
레이스 컨디션을 테스트해야 하는 경우, 고루틴을 실행하여 그렇게 할 수 있습니다.
type gracefulTerminationManager struct { rsList graceTerminateRSList } func newGracefulTerminationManager() *gracefulTerminationManager { return &gracefulTerminationManager{ rsList: graceTerminateRSList{ list: make(map[string]*item), }, } } type item struct { VirtualServer string RealServer string } type graceTerminateRSList struct { lock sync.Mutex list map[string]*item } func (g *graceTerminateRSList) flushList(handler func(rsToDelete *item) (bool, error)) bool { g.lock.Lock() defer g.lock.Unlock() success := true for _, rs := range g.list { if ok, err := handler(rs); !ok || err != nil { success = false } } return success } func (g *graceTerminateRSList) add(rs *item) { g.lock.Lock() defer g.lock.Unlock() g.list[rs.RealServer] = rs } func (g *graceTerminateRSList) len() int { g.lock.Lock() defer g.lock.Unlock() return len(g.list) }
여기서 우리는 레이스 컨디션 하에서 flushList
와 add
를 테스트해야 합니다.
func Test_raceGraceTerminateRSList_flushList(t *testing.T) { manager := newGracefulTerminationManager() go func() { for i := 0; i < 100; i++ { manager.rsList.add(&item{ VirtualServer: "virtualServer", RealServer: fmt.Sprint(i), }) } }() // Wait until a certain number of elements are added before proceeding for manager.rsList.len() < 20 { } // Pass in the handler for mocking success := manager.rsList.flushList(func(rsToDelete *item) (bool, error) { return true, nil }) assert.True(t, success) }
https://github.com/agiledragon/gomonkey를 사용하여 프로그램의 일부를 모의함으로써 테스트 중인 메서드를 외부 호출의 영향으로부터 격리할 수 있습니다.
개인 메서드를 스텁해야 하는 경우, 더 높은 버전의 gomonkey를 사용하여 테스트해야 하는 메서드에 더 집중할 수 있습니다.
테스트 파일 내에서 일부 통합 테스트를 수행하려는 경우, 먼저 데이터베이스 및 캐시와 같은 많은 리소스를 초기화해야 하는 문제에 직면할 수 있습니다. 이 경우 각 모듈 디렉토리 아래에 이러한 리소스를 초기화하는 메서드를 추가할 수 있습니다. 예를 들어:
func InitTestSuite(opts ...TestSuiteConfigOpt) { config := &TestSuiteConfig{} for _, opt := range opts { opt(config) } dsn := config.GetDSN() err := NewOrmClient(&Config{ Config: &gorm.Config{ //Logger: logger.Default.LogMode(logger.Info), }, SourceConfig: &SourceDBConfig{}, Dial: postgres.Open(dsn), }) }
그런 다음, 이를 사용해야 하는 테스트 파일에서 TestMain
메서드를 통해 초기화합니다.
이것의 또 다른 이점은 모듈이 깔끔하게 분리되었는지 여부를 조기에 발견할 수 있다는 것입니다. 예를 들어, 테스트 스위트를 설정할 때 많은 구성 요소를 초기화하는 것을 발견하면 모듈 설계가 올바른지 또는 필요한지 검토할 가치가 있습니다.
동시성 문제를 테스트하는 방법
동시성 프로그램을 위한 테스트를 작성하는 방법?
분산 시스템에서 가장 흔한 문제는 많은 레이스 조건입니다. 많은 경우가 매우 낮은 확률로 발생하지만, 일단 발생하면 심각한 사고로 이어질 수 있습니다. 따라서 가능한 한 많은 동시 레이스 시나리오를 시뮬레이션하고 모든 작업이 완료된 후 결과를 확인해야 합니다. 그러나 때로는 테스트가 한 번의 실행에서 성공적으로 통과될 수 있으므로 여러 번 실행한 후에도 결과가 여전히 일관적인지 확인해야 합니다. 이를 위해서는 다음 샘플 코드에 표시된 것처럼 코드를 여러 번 실행해야 합니다.
var ( counter int ) func increment() { counter++ } func TestIncrement(t *testing.T) { count := 100 var wg sync.WaitGroup for i := 0; i < count; i++ { wg.Add(1) go func() { increment() wg.Done() }() } assert.Equal(t, count, counter) }
메서드에서 작동하도록 여러 개의 고루틴을 실행하면 결과가 예상과 일치하지 않을 수 있습니다. 이 시점에서 코드를 검토하고 수정해야 합니다.
TDD (테스트 주도 개발)
테스트를 작성한 후에는 매번 테스트를 통과하는 데 필요한 최소한의 코드만 작성합니다. 상태 머신 코드를 구현하는 것을 예로 들어 보겠습니다. 먼저 메서드를 정의합니다. 더 쉽게 읽을 수 있도록 간단한 구현을 제공합니다.
func GetOrder(orderId string) Order { return Order{} } func UpdateOrder(originalOrder, order Order) error { return nil } func UpdateOrderStateByEvent(ctx context.Context, orderId string, event Event) (err error) { order := GetOrder(orderId) stateMap, ok := orderEventStateMap[event] if !ok { return errors.New("event not exists") } if !stateMap.currentStateSet.Contains(order.OrderState) { return errors.New("current OrderState error") } updateOrder := Order{ OrderId: order.OrderId, OrderState: order.OrderState, } err = UpdateOrder(order, updateOrder) if err != nil { return err } return nil }
그런 다음 UpdateOrderStateByEvent
를 테스트합니다. 단위 테스트는 이 메서드를 격리하여 테스트하기 위한 것입니다. 다른 메서드는 gomonkey로 모의하여 테스트의 반복성을 보장할 수 있습니다.
func TestOrderStateByEvent(t *testing.T) { type args struct { ctx context.Context orderId string event Event } tests := []struct { name string args args wantErr error initStubs func() (reset func()) }{{ name: "", args: args{ ctx: context.Background(), orderId: "orderId1", event: onHoldEvent, }, wantErr: nil, initStubs: func() (reset func()) { patches := gomonkey.ApplyFunc(GetOrder, func(orderId string) Order { return Order{ OrderId: orderId, OrderState: delivering, } }) return func() { patches.Reset() } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // 1. Mock the required methods reset := tt.initStubs() defer reset() // 2. Call the method to be tested err := UpdateOrderStateByEvent(tt.args.ctx, tt.args.orderId, tt.args.event) assert.Nil(t, err) }) } }
테스트 주도 개발의 개념은 1990년대 초에 제안되었습니다. 이 예제에서는 Go를 사용하지만 TDD는 다른 언어에서 처음 사용되었습니다. 작성자는 테스트를 작성한 다음 테스트를 통과하는 데 필요한 최소한의 코드를 작성하여 이러한 단계를 번갈아 수행합니다. 프로그램이 완료되면 이미 테스트 가능한 상태입니다.
테스트 코드로 시작하면 실제 코드를 작성한 후 주요 변경을 주저하는 것을 피할 수 있습니다. 이렇게 하면 함수가 너무 길어지는 것을 방지하여 향후 수정 및 재테스트가 훨씬 더 쉬워집니다. 비즈니스 로직을 개발할 때 비즈니스 로직을 미리 분해한 다음 접착 코드와 함께 구성 요소를 결합하면 모든 코드를 한 번에 작성하고 나중에 테스트하는 것보다 버그가 적게 발생합니다.
일부 사람들은 테스트를 작성하는 데 너무 많은 시간이 걸린다고 생각할 수 있지만 도구를 사용하여 테스트 효율성을 향상시킬 수 있습니다. 예를 들어 IDE를 사용하여 테스트 스켈레톤을 생성합니다. AI 코파일럿의 등장으로 테스트 케이스의 반복적인 작업은 더 이상 수동으로 수행할 필요가 없습니다. 이제 케이스 하나를 작성하고 테스트 메서드의 ло직을 구현하기만 하면 AI가 많은 엣지 케이스 예제를 생성하는 데 도움을 줄 수 있으며 때로는 자신보다 더 철저하게 생각할 수도 있습니다. 또한 메서드 이름을 잘 선택하면 생성된 샘플을 매우 유용하게 사용할 수 있습니다. AI 생성 테스트 케이스가 적합하지 않으면 메서드 이름 자체가 문제가 있는지 반성하고 코드를 계속 개선할 수 있습니다.
결론
처음에 우아한 코드를 작성할 필요는 없지만 항상 더 나은 코드를 작성하고 자신의 작업을 지속적으로 반영하며 도구를 사용하여 끊임없이 자신을 개선하는 것을 목표로 해야 합니다. 이런 식으로 우리가 생산하는 결과도 더욱 뛰어날 것입니다.
Leapcell은 Go 프로젝트 호스팅을 위한 최고의 선택입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발하십시오.
무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 비용을 지불하십시오. 요청이 없고 요금이 부과되지 않습니다.
타의 추종을 불허하는 비용 효율성
- 유휴 요금 없이 사용한 만큼만 지불하십시오.
- 예: $25는 평균 응답 시간 60ms에서 694만 개의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
손쉬운 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 확장됩니다.
- 운영 오버헤드가 없습니다. 구축에만 집중하십시오.
설명서에서 자세히 알아보십시오!
X에서 우리를 팔로우하십시오: @LeapcellHQ