Go언어

안전하게 개발하기 레츠GO! #6 TestMain, testing.M

stdhsw 2026. 4. 1. 20:19

TestMain과 testing.M은 Go 언어에서 패키지 단위의 테스트 실행 흐름을 통제하기 위해 짝을 이루어 사용되는 핵심 요소입니다. 일반 애플리케이션에서 main() 함수가 프로그램의 시작점 역할을 하듯, 테스트 환경에서는 TestMain이 그 역할을 담당합니다.

 

TestMain이란?

TestMain은 특정 패키지 내 테스트들의 전역 진입점(Entry Point) 역할을 하는 사용자 정의 함수입니다.

기본적으로 go test 명령어를 실행하면, Go의 내장 테스트 러너가 패키지 내의 모든 TestXxx 함수를 알아서 찾아 실행합니다. 하지만 패키지 내에 TestMain 함수가 정의되어 있다면, Go 러너는 개별 테스트들을 바로 실행하지 않고 오직 TestMain 함수만을 최초로 호출하여 실행 제어권을 넘깁니다.

 

testing.M이란?

testing.M은 TestMain 함수의 매개변수로 주입되는 테스트 실행 제어 객체입니다.

TestMain이 실행 제어권을 가져왔기 때문에, 개별 테스트들(TestXxx, BenchmarkXxx 등)을 언제 실행할지는 전적으로 TestMain 내부에 작성된 로직에 달려 있습니다. 이때 실제 테스트들을 구동시키는 역할을 하는 것이 바로 testing.M 구조체입니다.

 

핵심 메서드 m.Run()이 메서드를 호출하는 순간, 패키지 내에 작성된 모든 일반 테스트와 벤치마크 테스트들이 비로소 실행됩니다. 모든 테스트가 완료되면 m.Run()은 최종 테스트 결과에 따른 시스템 종료 코드(모두 성공 시 0, 하나라도 실패 시 1 등 0이 아닌 값)를 정수형으로 반환합니다.

 

정확한 상호작용 및 실행 흐름

TestMain과 testing.M이 상호작용하는 정확한 순서는 다음과 같습니다.

  1. 시작: 사용자가 go test 명령어를 실행합니다.
  2. 진입: 패키지 내에 func TestMain(m *testing.M)이 존재하므로 이 함수가 가장 먼저 호출됩니다.
  3. Setup (사전 작업): TestMain 내부 상단에 작성된 전역 초기화 코드들이 실행됩니다.
  4. 테스트 구동: 초기화가 완료된 후 m.Run()이 호출됩니다.
  5. 개별 테스트 실행: m.Run() 내부 로직에 의해 패키지 안의 모든 Test... 함수들이 실행되며 결과를 취합합니다.
  6. 결과 반환: 개별 테스트가 모두 끝나면, m.Run()은 합산된 결과에 따라 상태 코드를 반환합니다.
  7. Teardown (사후 작업): m.Run() 호출 이후에 작성된 리소스 정리 코드들이 실행됩니다.
  8. 종료: os.Exit(종료코드)를 호출하여 운영체제에 성공/실패 여부를 알리고 테스트 프로세스를 완전히 종료합니다.

 

TestMain과 testing.M 실습

새로운 파일 math_test.go를 생성하고 아래 코드를 작성해 보았습니다. 이번 실습은 단순하게 TestMain의 testing.M으로 m.Run()함수를 통해 TestXXX의 유닛 테스트들이 동작하는 모습을 통해 TestMain의 역할을 알아보겠습니다.

package math_test

import (
	"fmt"
	"os"
	"testing"
)

// 테스트할 대상인 간단한 함수입니다.
func Add(a, b int) int {
	return a + b
}

// 패키지의 테스트 진입점인 TestMain 함수입니다.
func TestMain(m *testing.M) {
	// 1. Setup (사전 작업)
	// 실제 개발에서는 여기에 데이터베이스 연결이나 외부 종속성 초기화 코드가 들어갑니다.
	fmt.Println(">> [Setup] 테스트 환경을 초기화합니다.")

	// 2. 테스트 구동
	// m.Run()을 호출하면 패키지 내의 모든 TestXxx 함수들이 실행됩니다.
	exitCode := m.Run()

	// 3. Teardown (사후 작업)
	// 테스트가 끝난 후 사용한 리소스를 정리합니다.
	fmt.Println(">> [Teardown] 테스트 환경을 정리합니다.")

	// 4. 프로그램 종료
	// m.Run()이 반환한 상태 코드를 운영체제에 전달합니다.
	os.Exit(exitCode)
}

// 실제 단위 테스트 함수입니다.
func TestAdd(t *testing.T) {
	fmt.Println("   -> TestAdd 함수가 실행되었습니다.")
	
	result := Add(2, 3)
	expected := 5

	if result != expected {
		t.Errorf("Add(2, 3) 결과: %d, 기댓값: %d", result, expected)
	}
}

// 두 번째 단위 테스트 함수입니다.
func TestAddZero(t *testing.T) {
	fmt.Println("   -> TestAddZero 함수가 실행되었습니다.")
	
	result := Add(0, 0)
	expected := 0

	if result != expected {
		t.Errorf("Add(0, 0) 결과: %d, 기댓값: %d", result, expected)
	}
}

 

실행 방법 및 결과 확인

터미널에서 상세 출력 옵션(-v)을 주어 테스트를 실행합니다.

go test -v

>> [Setup] 테스트 환경을 초기화합니다.
=== RUN   TestAdd
   -> TestAdd 함수가 실행되었습니다.
--- PASS: TestAdd (0.00s)
=== RUN   TestAddZero
   -> TestAddZero 함수가 실행되었습니다.
--- PASS: TestAddZero (0.00s)
PASS
>> [Teardown] 테스트 환경을 정리합니다.
ok  	example.com/test	0.435s

 

동작 요약

결과에서 볼 수 있듯이 실행 흐름은 완벽하게 통제됩니다.

  1. TestAdd나 TestAddZero가 먼저 실행되지 않고, 가장 먼저 [Setup] 메시지가 출력됩니다.
  2. m.Run() 내부 로직에 의해 개별 테스트 함수들(TestAdd, TestAddZero)이 순서대로 실행됩니다.
  3. 모든 개별 테스트가 끝난 후, os.Exit()가 호출되기 직전에 [Teardown] 메시지가 마지막으로 출력됩니다.