Testcontainers for Go 시작하기
Testcontainers for Go 시작하기
이 가이드에서는 Testcontainers for Go를 이용해 실제 PostgreSQL 인스턴스로 Go 애플리케이션을 만들고 데이터베이스 상호작용을 테스트하는 방법을 배워요.
출처: 문서
본문
이 가이드를 통해 다음 내용을 배울 수 있어요.
- 모듈 지원이 있는 Go 애플리케이션 만들기
pgx드라이버를 사용해 PostgreSQL 데이터베이스에서 고객 데이터를 관리하는Repository구현하기testcontainers-go를 사용해 통합 테스트 작성하기- 테스트 스위트(suite)를 사용해 여러 테스트에서 컨테이너 재사용하기
사전 준비 (Prerequisites)
- Go 1.25 이상
- 선호하는 IDE (VS Code, GoLand)
- Testcontainers가 지원하는 Docker 환경. 자세한 내용은 testcontainers-go 시스템 요구사항을 확인해요.
참고: Testcontainers가 처음이라면 Testcontainers 개요를 방문해 Testcontainers가 무엇이고 어떤 장점이 있는지 알아보는 걸 권장해요.
Go 프로젝트 만들기
먼저 Go 프로젝트를 만들어요.
$ mkdir testcontainers-go-demo
$ cd testcontainers-go-demo
$ go mod init github.com/testcontainers/testcontainers-go-demo
이 가이드는 Postgres 데이터베이스와 상호작용하기 위해 jackc/pgx PostgreSQL 드라이버를 사용하고, 테스트용 Postgres Docker 인스턴스를 띄우기 위해 testcontainers-go의 Postgres 모듈을 사용해요. 또한 여러 테스트를 스위트로 실행하고 단언(assertion)을 작성하기 위해 testify도 사용해요.
이 의존성들을 설치해요.
$ go get github.com/jackc/pgx/v5
$ go get github.com/testcontainers/testcontainers-go
$ go get github.com/testcontainers/testcontainers-go/modules/postgres
$ go get github.com/stretchr/testify
Customer 구조체 만들기
customer 패키지에 types.go 파일을 만들고 고객 정보를 모델링할 Customer 구조체를 정의해요.
package customer
type Customer struct {
Id int
Name string
Email string
}
Repository 만들기
다음으로 customer/repo.go를 만들고 Repository 구조체를 정의한 뒤, 고객을 생성하고 이메일로 조회하는 메서드를 추가해요.
package customer
import (
"context"
"fmt"
"os"
"github.com/jackc/pgx/v5"
)
type Repository struct {
conn *pgx.Conn
}
func NewRepository(ctx context.Context, connStr string) (*Repository, error) {
conn, err := pgx.Connect(ctx, connStr)
if err != nil {
_, _ = fmt.Fprintf(os.Stderr, "Unable to connect to database: %v\n", err)
return nil, err
}
return &Repository{conn: conn}, nil
}
func (r Repository) CreateCustomer(ctx context.Context, customer Customer) (Customer, error) {
err := r.conn.QueryRow(ctx,
"INSERT INTO customers (name, email) VALUES ($1, $2) RETURNING id",
customer.Name, customer.Email).Scan(&customer.Id)
return customer, err
}
func (r Repository) GetCustomerByEmail(ctx context.Context, email string) (Customer, error) {
var customer Customer
query := "SELECT id, name, email FROM customers WHERE email = $1"
err := r.conn.QueryRow(ctx, query, email).Scan(&customer.Id, &customer.Name, &customer.Email)
if err != nil {
return Customer{}, err
}
return customer, nil
}
코드가 하는 일을 살펴보면,
Repository는 데이터베이스 연산을 수행하기 위한*pgx.Conn을 보관해요.NewRepository(connStr)은 데이터베이스 연결 문자열을 받아Repository를 초기화해요.CreateCustomer()와GetCustomerByEmail()은Repository리시버에 정의된 메서드로, 고객 레코드를 삽입하고 조회해요.
Testcontainers로 테스트 작성하기
Repository 구현은 준비됐지만 테스트하려면 PostgreSQL 데이터베이스가 필요해요. testcontainers-go를 사용하면 Docker 컨테이너에서 Postgres 데이터베이스를 띄우고 그 데이터베이스를 대상으로 테스트를 실행할 수 있어요.
테스트 데이터베이스 준비하기
실제 애플리케이션에서는 데이터베이스 마이그레이션 도구를 쓰겠지만, 이 가이드에서는 스크립트로 데이터베이스를 초기화해요. testdata/init-db.sql 파일을 만들어 CUSTOMERS 테이블을 생성하고 샘플 데이터를 넣어요.
CREATE TABLE IF NOT EXISTS customers (
id serial,
name varchar(255),
email varchar(255)
);
INSERT INTO customers (name, email) VALUES ('John', '[email protected]');
testcontainers-go API 이해하기
testcontainers-go 라이브러리는 어떤 컨테이너화된 서비스든 실행할 수 있는 범용 Container 추상화를 제공해요. 여기에 더해 testcontainers-go는 기술별 모듈을 제공해서 상용구 코드를 줄여주고, 컨테이너 인스턴스를 구성하기 위한 함수형 옵션 패턴을 제공해요. 예를 들어 PostgresContainer는 WithDatabase(), WithUsername(), WithPassword() 등의 함수로 Postgres 컨테이너의 여러 속성을 설정할 수 있게 해줘요.
테스트 작성하기
customer/repo_test.go 파일을 만들고 테스트를 구현해요.
package customer
import (
"context"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
)
func TestCustomerRepository(t *testing.T) {
ctx := context.Background()
ctr, err := postgres.Run(ctx,
"postgres:16-alpine",
postgres.WithInitScripts(filepath.Join("..", "testdata", "init-db.sql")),
postgres.WithDatabase("test-db"),
postgres.WithUsername("postgres"),
postgres.WithPassword("postgres"),
postgres.BasicWaitStrategies(),
)
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)
connStr, err := ctr.ConnectionString(ctx, "sslmode=disable")
require.NoError(t, err)
customerRepo, err := NewRepository(ctx, connStr)
require.NoError(t, err)
c, err := customerRepo.CreateCustomer(ctx, Customer{
Name: "Henry",
Email: "[email protected]",
})
assert.NoError(t, err)
assert.NotNil(t, c)
customer, err := customerRepo.GetCustomerByEmail(ctx, "[email protected]")
assert.NoError(t, err)
assert.NotNil(t, customer)
assert.Equal(t, "Henry", customer.Name)
assert.Equal(t, "[email protected]", customer.Email)
}
테스트가 하는 일을 살펴보면,
postgres.Run()을 첫 번째 인자로postgres:16-alpineDocker 이미지를 넘기며 호출해요. 이건 v0.41.0 API로, 이미지가 옵션이 아니라 필수 위치 인자예요.WithInitScripts(...)로 초기화 스크립트를 구성해서, 데이터베이스가 시작된 뒤CUSTOMERS테이블이 만들어지고 샘플 데이터가 삽입되게 해요.postgres.BasicWaitStrategies()를 사용해서 Postgres 로그 메시지 대기와 포트 준비 대기를 함께 처리해요. 수동으로 대기 전략을 구성할 필요가 없어요.postgres.Run()바로 다음에testcontainers.CleanupContainer(t, ctr)를 호출해서 테스트 프레임워크에 자동 정리를 등록해요. 수동t.Cleanup과Terminate패턴을 대체한답니다.- 컨테이너에서 데이터베이스
ConnectionString을 얻어Repository를 초기화해요. [email protected]이메일로 고객을 생성하고, 데이터베이스에 그 고객이 존재하는지 확인해요.
테스트 스위트로 컨테이너 재사용하기
이전 섹션에서는 단일 테스트를 위해 Postgres Docker 컨테이너를 띄웠어요. 하지만 한 파일에 테스트가 여러 개 있는 경우가 많고, 같은 Postgres Docker 컨테이너를 모두가 재사용하고 싶을 거예요. testify의 suite 패키지를 사용하면 공통 테스트 설정(setup)과 정리(teardown) 동작을 구현할 수 있어요.
컨테이너 설정 추출하기
먼저 PostgresContainer 생성 로직을 testhelpers/containers.go라는 별도 파일로 추출해요.
package testhelpers
import (
"context"
"path/filepath"
"testing"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
)
type PostgresContainer struct {
*postgres.PostgresContainer
ConnectionString string
}
func CreatePostgresContainer(t *testing.T, ctx context.Context) *PostgresContainer {
t.Helper()
ctr, err := postgres.Run(ctx,
"postgres:16-alpine",
postgres.WithInitScripts(filepath.Join("..", "testdata", "init-db.sql")),
postgres.WithDatabase("test-db"),
postgres.WithUsername("postgres"),
postgres.WithPassword("postgres"),
postgres.BasicWaitStrategies(),
)
testcontainers.CleanupContainer(t, ctr)
require.NoError(t, err)
connStr, err := ctr.ConnectionString(ctx, "sslmode=disable")
require.NoError(t, err)
return &PostgresContainer{
PostgresContainer: ctr,
ConnectionString: connStr,
}
}
containers.go에서 PostgresContainer는 testcontainers-go의 PostgresContainer를 확장해서 ConnectionString에 쉽게 접근할 수 있게 해줘요. CreatePostgresContainer() 함수는 첫 번째 인자로 *testing.T를 받고 t.Helper()를 호출해서 테스트 실패가 호출자 지점을 가리키게 하며, testcontainers.CleanupContainer()로 자동 정리를 등록해요.
테스트 스위트 작성하기
customer/repo_suite_test.go를 만들고 testify suite 패키지를 사용해 고객 생성 및 이메일 조회 테스트를 구현해요.
package customer
import (
"context"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/testcontainers/testcontainers-go-demo/testhelpers"
)
type CustomerRepoTestSuite struct {
suite.Suite
pgContainer *testhelpers.PostgresContainer
repository *Repository
ctx context.Context
}
func (suite *CustomerRepoTestSuite) SetupSuite() {
suite.ctx = context.Background()
suite.pgContainer = testhelpers.CreatePostgresContainer(suite.T(), suite.ctx)
repository, err := NewRepository(suite.ctx, suite.pgContainer.ConnectionString)
require.NoError(suite.T(), err)
suite.repository = repository
}
func (suite *CustomerRepoTestSuite) TestCreateCustomer() {
t := suite.T()
customer, err := suite.repository.CreateCustomer(suite.ctx, Customer{
Name: "Henry",
Email: "[email protected]",
})
require.NoError(t, err)
assert.NotNil(t, customer.Id)
}
func (suite *CustomerRepoTestSuite) TestGetCustomerByEmail() {
t := suite.T()
customer, err := suite.repository.GetCustomerByEmail(suite.ctx, "[email protected]")
require.NoError(t, err)
assert.Equal(t, "John", customer.Name)
assert.Equal(t, "[email protected]", customer.Email)
}
func TestCustomerRepoTestSuite(t *testing.T) {
suite.Run(t, new(CustomerRepoTestSuite))
}
코드가 하는 일을 살펴보면,
CustomerRepoTestSuite는suite.Suite를 확장하고 여러 테스트에서 공유할 필드를 담아요.SetupSuite()는 모든 테스트 전에 한 번 실행돼요.CreatePostgresContainer(suite.T(), ...)를 호출하며,CleanupContainer을 통해 정리 등록이 자동으로 처리되므로TearDownSuite()가 필요 없어요.TestCreateCustomer()는 생성 연산에require.NoError()(에러가 나면 즉시 실패)를, ID 검사에assert.NotNil()을 사용해요.TestGetCustomerByEmail()은require.NoError()후 반환된 값들에 대해 단언을 해요.TestCustomerRepoTestSuite(t *testing.T)는go test실행 시 테스트 스위트를 돌려줘요.
팁: 이 가이드에서는 테스트가 데이터베이스의 데이터를 리셋하지 않아요. 실제로는 각 테스트 실행 전에 데이터베이스를 알려진 상태로 리셋하는 게 좋아요.
테스트 실행과 다음 단계
go test ./...로 모든 테스트를 실행해요. 선택적으로 -v 플래그를 붙이면 상세 출력을 볼 수 있어요.
$ go test -v ./...
Postgres Docker 컨테이너가 두 개 자동으로 시작되는 걸 볼 수 있어요. 하나는 스위트와 그 안의 두 테스트용, 다른 하나는 앞서 만든 독립 테스트용이에요. 모든 테스트가 통과해야 해요. 테스트가 끝나면 컨테이너는 자동으로 중지되고 제거돼요.
요약 (Summary)
Testcontainers for Go 라이브러리는 목(mock) 대신 프로덕션에서 쓰는 것과 같은 종류의 데이터베이스(Postgres)를 사용해 통합 테스트를 작성할 수 있게 도와줘요. 목을 쓰지 않고 실제 서비스와 대화하기 때문에, 코드를 리팩터링해도 애플리케이션이 예상대로 동작하는지 검증할 수 있답니다.
Testcontainers에 대해 더 알아보고 싶다면 Testcontainers 개요를 방문해요.
더 읽어보기 (Further reading)
- Testcontainers for Go 문서
- Testcontainers for Go 빠른 시작(quickstart)
- Go용 Testcontainers Postgres 모듈