برنامه نویسی

تست های ادغام در Go با Cucumber، Testcontainers و HTTPMock – DEV Community

Summarize this content to 400 words in Persian Lang

تست یکپارچه سازی بخش مهمی از چرخه عمر توسعه نرم افزار است که ماژول های مختلف یک نرم افزار را تضمین می کندبرنامه به طور یکپارچه با هم کار می کند. در اکوسیستم Go (Golang) می توانیم از ابزارها و کتابخانه های مختلفی برای تسهیل استفاده کنیمتست ادغام در این پست نحوه استفاده از Cucumber، Testcontainers برای پایگاه داده PostgreSQL وHTTPMock برای نوشتن تست های یکپارچه سازی قوی در Go.

تست یکپارچه سازی تعاملات بین بخش های مختلف برنامه شما را تأیید می کند، مانند سرویس های خارجی،پایگاه داده ها و API ها بر خلاف تست های واحد، که اجزای جداگانه را به صورت مجزا آزمایش می کنند، تست های یکپارچه سازی تضمین می کننددر صورت ترکیب، کل سیستم همانطور که در نظر گرفته شده است عمل می کند.

خیار: ابزاری که از توسعه رفتار محور (BDD) پشتیبانی می کند. این امکان را به شما می دهد تا سناریوهای آزمایشی قابل خواندن توسط انسان را به زبان Gherkin بنویسید، که شکاف بین ذینفعان فنی و غیر فنی را پر می کند.

https://github.com/cucumber/godog

ظروف آزمایش: کتابخانه ای که ظروف سبک وزن و یکبار مصرف را برای آزمایش فراهم می کند. این به شما امکان می‌دهد تا نمونه‌های واقعی پایگاه‌های داده، کارگزاران پیام، یا هر سرویس دیگری که برنامه شما به آن وابسته است را بچرخانید و آزمایش‌های شما را قابل اعتمادتر و به سناریوهای دنیای واقعی نزدیک‌تر می‌کند. ما از آن برای راه اندازی پایگاه داده PostgresSQL خود استفاده خواهیم کرد.

https://github.com/testcontainers/testcontainers-go

HTTPMock: یک کتابخانه ساده HTTP تمسخر آمیز که به شما امکان می دهد تعاملات HTTP را در آزمایشات خود شبیه سازی کنید. زمانی مفید است که می‌خواهید پاسخ‌های سرویس‌های HTTP خارجی را بدون درخواست شبکه مورد تمسخر قرار دهید.

https://github.com/jarcoal/httpmock

ما 1 برنامه خواهیم داشت که وظیفه اجرای منطق زیر را بر عهده دارد:

یک API برای ایجاد یک کتاب پیاده سازی کنید.
بررسی کنید آیا ISBN کتاب در یک API شخص ثالث وجود دارد یا خیر.
کتاب را در پایگاه داده ذخیره کنید
با استفاده از یک API شخص ثالث به آدرس helloworld@gmail.com ایمیل ارسال کنید

ما با درخواست خود به عنوان یک رفتار خواهیم کرد جعبه سیاه. این بدان معنی است که ما باید برنامه را راه اندازی کنیم و از آن مانند سایر برنامه ها استفاده کنیمکاربر، بدون تمسخر بخشی از کد.

ساختار پوشه پروژه خواهد بود

– cmd/
– features/
– createBook.feature
– integration_test.go
– main.go
– step_definition_api.go
– step_definition_common.go
– step_definition_database.go
– step_definition_mock_server.go
– test_utils.go
– testcontainers_config.go
– go.mod

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بیایید روند راه‌اندازی تست‌های یکپارچه‌سازی در Go را با استفاده از Cucumber، Testcontainers و HTTPMock مرور کنیم.

1. نصب کتابخانه های مورد نیاز

بعد، کتابخانه های لازم را نصب کنید

go get -u github.com/cucumber/godog
go get -u github.com/testcontainers/testcontainers-go
go get -u github.com/jarcoal/httpmock
go get -u github.com/lib/pq
— Database conection
go get -u gorm.io/gorm
go get -u gorm.io/driver/postgres

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

2. main.go خود را به روز کنید تا اجرای برنامه را کنترل کنید

اکنون، ما باید مقداری main.go خود را تغییر دهیم. اصلی خود را در 3 عملکرد مختلف جدا کنید.

func getDatabaseConnection() *gorm.DB*

این تابع در step_definition_common.go استفاده می‌شود و به ما امکان دسترسی به پایگاه داده در step_definition_database.go را می‌دهد.

func mainHttpServerSetup(addr string, httpClient *http.Client) (*http.Server, func())

این تابع در testcontainers_config.go استفاده می شود و به ما امکان می دهد اجرای برنامه را در integration_test.go کنترل کنیم.

main()

نقطه ورود برنامه واقعی.

package main

import (
“fmt”
servicebook “github.com/joseboretto/golang-testcontainers-gherkin-setup/internal/application/services/books”
controller “github.com/joseboretto/golang-testcontainers-gherkin-setup/internal/infrastructure/controllers”
controllerbook “github.com/joseboretto/golang-testcontainers-gherkin-setup/internal/infrastructure/controllers/books”
“gorm.io/driver/postgres”
“gorm.io/gorm”
“gorm.io/gorm/schema”
“os”
“time”

“log”
“net/http”

clientsbook “github.com/joseboretto/golang-testcontainers-gherkin-setup/internal/infrastructure/clients/book”
persistancebook “github.com/joseboretto/golang-testcontainers-gherkin-setup/internal/infrastructure/persistance/book”
)

func main() {
addr := “:8000”
httpClient := &http.Client{
Timeout: 5 * time.Second,
}
server, deferFn := mainHttpServerSetup(addr, httpClient)
log.Println(“Listing for requests at http://localhost” + addr)
err := server.ListenAndServe()
if err != nil {
panic(“Error staring the server: ” + err.Error())
}
defer deferFn()
}

func mainHttpServerSetup(addr string, httpClient *http.Client) (*http.Server, func()) {
db := getDatabaseConnection()
// Migrate the schema
err := db.AutoMigrate(&persistancebook.BookEntity{})
if err != nil {
panic(“Error executing db.AutoMigrate” + err.Error())
}
// Clients
checkIsbnClientHost := os.Getenv(“CHECK_ISBN_CLIENT_HOST”)
checkIsbnClient := clientsbook.NewCheckIsbnClient(checkIsbnClientHost, httpClient)
emailClientHost := os.Getenv(“EMAIL_CLIENT_HOST”)
sendEmailClient := clientsbook.NewSendEmailClient(emailClientHost, httpClient)
// repositories
newCreateBookRepository := persistancebook.NewCreateBookRepository(db)
// services
createBookService := servicebook.NewCreateBookService(newCreateBookRepository, checkIsbnClient, sendEmailClient)
// controllers
bookController := controllerbook.NewBookController(createBookService)
// routes
controller.SetupRoutes(bookController)
// Server
server := &http.Server{Addr: addr, Handler: nil}
// Defer function
// Add all defer
deferFn := func() {
fmt.Println(“closing database connection”)
sqlDB, err := db.DB()
err = sqlDB.Close()
if err != nil {
panic(“Error closing database connection: ” + err.Error())
}

}
return server, deferFn
}

func getDatabaseConnection() *gorm.DB {
// Read database configuration from environment variables
databaseUser := os.Getenv(“DATABASE_USER”)
databasePassword := os.Getenv(“DATABASE_PASSWORD”)
databaseName := os.Getenv(“DATABASE_NAME”)
databaseHost := os.Getenv(“DATABASE_HOST”)
databasePort := os.Getenv(“DATABASE_PORT”)
databaseConnectionString := `host=` + databaseHost + ` user=` + databaseUser + ` password=` + databasePassword + ` dbname=` + databaseName + ` port=` + databasePort + ` sslmode=disable`
fmt.Println(“databaseConnectionString: “, databaseConnectionString)
db, err := gorm.Open(postgres.New(postgres.Config{
DSN: databaseConnectionString,
PreferSimpleProtocol: true, // disables implicit prepared statement usage
}), &gorm.Config{
NamingStrategy: schema.NamingStrategy{
TablePrefix: “myschema.”, // schema name
SingularTable: false,
}})
// Check if connection is successful
if err != nil {
panic(“failed to connect database with error: ” + err.Error() + “\n” + “Please check your database configuration: ” + databaseConnectionString)
}
return db
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

3. نوشتن فایل ویژگی (خیار)

ایجاد یک فایل ویژگی (createBook.feature) داخل features دایرکتوری با سناریوهایی که مورد انتظار را توصیف می کندرفتار برنامه شما

Feature: Create book

Background: Clean database
Given SQL command
“””
DELETE FROM myschema.books;
“””
And reset mock server

Scenario: Create a new book successfully
Given a mock server request with method: “GET” and url: “https://api.isbncheck.com/isbn/0-061-96436-1”
And a mock server response with status 200 and body
“””json
{
“id”: “0-061-96436-1”
}
“””
And a mock server request with method: “POST” and url: “https://api.gmail.com/send-email” and body
“””json
{
“email” : “helloworld@gmail.com”,
“book” : {
“isbn” : “0-061-96436-1”,
“title” : “The Art of Computer Programming”
}
}
“””
And a mock server response with status 200 and body
“””json
{
“status”: “OK”
}
“””
When API “POST” request is sent to “/api/v1/createBook” with payload
“””json
{
“isbn”: “0-061-96436-1”,
“title”: “The Art of Computer Programming”
}
“””
Then response status code is 200 and payload is
“””json
{
“isbn”: “0-061-96436-1”,
“title”: “The Art of Computer Programming”
}
“””

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

4. پیاده سازی تعاریف مرحله در Go

اکنون، فایل های Go را برای پیاده سازی تعاریف مرحله برای سناریوها ایجاد کنید. ما بر اساس آن تعاریف چند مرحله ای داریماهداف

مهمترین مفهومی که باید به خاطر بسپارید نوشتن تعاریف کلی مرحله برای استفاده مجدد از آنها در هر مورد استفاده است.

step_definition_common.go

داده ها را بین راه اندازی برنامه و تعاریف مرحله به اشتراک بگذارید.

package main

import (
“github.com/cucumber/godog”
“gorm.io/gorm”
“net/http”
)

type StepsContext struct {
// Main setup
mainHttpServerUrl string // http://localhost:8000
database *gorm.DB
// Mock server setup
stepMockServerRequestMethod *string
stepMockServerRequestUrl *string
stepMockServerRequestBody *string
stepResponse *http.Response
}

func NewStepsContext(mainHttpServerUrl string, sc *godog.ScenarioContext) *StepsContext {
db := getDatabaseConnection()
s := &StepsContext{
mainHttpServerUrl: mainHttpServerUrl,
database: db,
}
// Register all the step definition function
s.RegisterMockServerSteps(sc)
s.RegisterDatabaseSteps(sc)
s.RegisterApiSteps(sc)
return s
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

step_definition_api.go

درخواست api را انجام دهید و پاسخ را بررسی کنید.

package main

import (
“bytes”
“context”
“fmt”
“github.com/cucumber/godog”
“io”
“log”
“net/http”
“time”
)

func (s *StepsContext) RegisterApiSteps(sc *godog.ScenarioContext) {
sc.Step(`^API “([^”]*)” request is sent to “([^”]*)” without payload$`, s.apiRequestIsSendWithoutPayload)
sc.Step(`^API “([^”]*)” request is sent to “([^”]*)” with payload$`, s.apiRequestIsSendWithPayload)
sc.Step(`^response status code is (\d+) and payload is$`, s.apiResponseIs)
}

func (s *StepsContext) apiRequestIsSendWithoutPayload(method, url string) error {
return s.apiRequestIsSendWithPayload(method, url, “”)
}

func (s *StepsContext) apiRequestIsSendWithPayload(method, url, payloadJson string) error {
client := &http.Client{
Timeout: 5 * time.Second,
}

req, err := http.NewRequestWithContext(context.Background(), method, s.mainHttpServerUrl+url, bytes.NewBufferString(payloadJson))
if err != nil {
return fmt.Errorf(“failed to create a new HTTP request: %w”, err)
}

req.Header.Set(“Content-Type”, “application/json”)

response, err := client.Do(req)
if err != nil {
return fmt.Errorf(“failed to execute HTTP request: %w”, err)
}

s.stepResponse = response
return nil
}

func (s *StepsContext) apiResponseIs(expected int, expectedResponse string) error {
defer s.stepResponse.Body.Close()

if s.stepResponse.StatusCode != expected {
return fmt.Errorf(“expected status code %d but got %d”, expected, s.stepResponse.StatusCode)
}

actualJson, err := getBody(s.stepResponse)
if err != nil {
return err
}

if match, err := compareJSON(expectedResponse, actualJson); err != nil {
return fmt.Errorf(“error comparing JSON: %w”, err)
} else if !match {
log.Printf(“Actual response body: %s”, actualJson)
return fmt.Errorf(“response body does not match. Expected: %s, actual: %s”, expectedResponse, actualJson)
}

return nil
}

func getBody(response *http.Response) (string, error) {
body, err := io.ReadAll(response.Body)
if err != nil {
return “”, fmt.Errorf(“failed to read response body: %w”, err)
}
return string(body), nil
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

step_definition_database.go

ذخیره داده ها در پایگاه داده علاوه بر این، شما باید تعاریف مرحله را پیاده سازی کنید تا داده های ذخیره شده در را بررسی کنیدپایگاه داده

package main

import (
“github.com/cucumber/godog”
)

func (s *StepsContext) RegisterDatabaseSteps(sc *godog.ScenarioContext) {
sc.Step(`^SQL command`, s.executeSQL)
}

func (s *StepsContext) executeSQL(sqlCommand string) error {
tx := s.database.Exec(sqlCommand)
if tx.Error != nil {
return tx.Error
}
return nil
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

step_definition_mock_server.go

استفاده نکنید httpmock.Activate() زیرا شما http.client پیش فرض را مسخره می کنید و با خطای “noپاسخ دهنده پیدا شد” از

httpmock https://github.com/jarcoal/httpmock/blob/v1/internal/error.go#L10زیرا سرور http ما را خراب می کند.

package main

import (
“fmt”
“github.com/cucumber/godog”
“github.com/jarcoal/httpmock”
“io”
“log”
“net/http”
)

// RegisterMockServerSteps registers all the step definition functions related to the mock server
func (s *StepsContext) RegisterMockServerSteps(ctx *godog.ScenarioContext) {
ctx.Step(`^a mock server request with method: “([^”]*)” and url: “([^”]*)”$`, s.storeMockServerMethodAndUrlInStepContext)
ctx.Step(`^a mock server request with method: “([^”]*)” and url: “([^”]*)” and body$`, s.storeMockServerMethodAndUrlAndRequestBodyInStepContext)
ctx.Step(`^a mock server response with status (\d+) and body$`, s.setupRegisterResponder)
ctx.Step(`^reset mock server$`, s.resetMockServer)
}

func (s *StepsContext) storeMockServerMethodAndUrlInStepContext(method, url string) error {
s.stepMockServerRequestMethod = &method
s.stepMockServerRequestUrl = &url
s.stepMockServerRequestBody = nil
return nil
}

func (s *StepsContext) storeMockServerMethodAndUrlAndRequestBodyInStepContext(method, url, body string) error {
s.stepMockServerRequestMethod = &method
s.stepMockServerRequestUrl = &url
s.stepMockServerRequestBody = &body
return nil
}

func (s *StepsContext) setupRegisterResponder(statusCode int, responseBody string) error {
if s.stepMockServerRequestMethod == nil || s.stepMockServerRequestUrl == nil {
log.Fatal(“stepMockServerRequestMethod or stepMockServerRequestUrl is nil. You have to setup the storeMockServerMethodAndUrlInStepContext step first”)
}
if s.stepMockServerRequestBody != nil {
s.registerResponderWithBodyCheck(statusCode, responseBody)
} else {
httpmock.RegisterResponder(*s.stepMockServerRequestMethod, *s.stepMockServerRequestUrl, httpmock.NewStringResponder(statusCode, responseBody))
}
return nil
}

func (s *StepsContext) registerResponderWithBodyCheck(statusCode int, responseBody string) {
httpmock.RegisterResponder(*s.stepMockServerRequestMethod, *s.stepMockServerRequestUrl,
func(req *http.Request) (*http.Response, error) {
body, err := io.ReadAll(req.Body)
if err != nil {
return nil, fmt.Errorf(“failed to read request body: %w”, err)
}

if match, err := compareJSON(*s.stepMockServerRequestBody, string(body)); err != nil {
return nil, fmt.Errorf(“error comparing JSON: %w”, err)
} else if !match {
log.Printf(“Actual request body without escapes: %s”, body)
return nil, fmt.Errorf(“request body does not match for method: %s and url: %s. Expected: %s, actual: %s”,
*s.stepMockServerRequestMethod, *s.stepMockServerRequestUrl, *s.stepMockServerRequestBody, string(body))
}

return httpmock.NewStringResponse(statusCode, responseBody), nil
})
}

func (s *StepsContext) resetMockServer() error {
httpmock.Reset()
// Reset the step context
s.stepMockServerRequestMethod = nil
s.stepMockServerRequestUrl = nil
s.stepMockServerRequestBody = nil
return nil
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

test_utils.go

package main

import (
“encoding/json”
“fmt”
“reflect”
)

// compareJSON compares two JSON strings and returns true if they are equal.
func compareJSON(jsonStr1, jsonStr2 string) (bool, error) {
var obj1, obj2 map[string]interface{}

// Unmarshal the first JSON string
if err := json.Unmarshal([]byte(jsonStr1), &obj1); err != nil {
return false, fmt.Errorf(“error unmarshalling jsonStr1: %v”, err)
}

// Unmarshal the second JSON string
if err := json.Unmarshal([]byte(jsonStr2), &obj2); err != nil {
return false, fmt.Errorf(“error unmarshalling jsonStr2: %v”, err)
}
// Compare the two maps
return reflect.DeepEqual(obj1, obj2), nil
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

5. ظروف تست را پیکربندی کنید

testcontainers_config.go

پیکربندی کنید TestContainersParams برای مطابقت با envs و پیکربندی برنامه واقعی شما. علاوه بر این، httpmock.ActivateNonDefault(mockClient) کلید ساخت است httpmock کار می کند زیرا فقط سوم ما را مسخره می کندمهمانی http.client

package main

import (
“context”
“fmt”
“github.com/jarcoal/httpmock”
“github.com/testcontainers/testcontainers-go”
“github.com/testcontainers/testcontainers-go/modules/postgres”
“log”
“net/http”
“os”
“path/filepath”
“time”

“github.com/docker/go-connections/nat”

_ “github.com/lib/pq” // Import the postgres driver
“github.com/testcontainers/testcontainers-go/wait”
)

type TestContainersParams struct {
MainHttpServerAddress string
PostgresImage string
DatabaseName string
DatabaseHostEnvVar string
DatabasePortEnvVar string
DatabaseUserEnvVar string
DatabasePasswordEnvVar string
DatabaseNameEnvVar string
DatabaseInitScript string
EnvironmentVariables map[string]string
}

type TestContainersContext struct {
MainHttpServer *http.Server
Params *TestContainersParams
}

func NewTestContainersParams() *TestContainersParams {
return &TestContainersParams{
MainHttpServerAddress: “:8000”,
PostgresImage: “docker.io/postgres:16-alpine”,
DatabaseName: “db”,
DatabaseHostEnvVar: “DATABASE_HOST”,
DatabasePortEnvVar: “DATABASE_PORT”,
DatabaseUserEnvVar: “DATABASE_USER”,
DatabasePasswordEnvVar: “DATABASE_PASSWORD”,
DatabaseNameEnvVar: “DATABASE_NAME”,
DatabaseInitScript: filepath.Join(“.”, “testAssets”, “testdata”, “dev-db.sql”),
EnvironmentVariables: map[string]string{
“CHECK_ISBN_CLIENT_HOST”: “https://api.isbncheck.com”,
“EMAIL_CLIENT_HOST”: “https://api.gmail.com”,
},
}
}

func NewMainWithTestContainers(ctx context.Context) *TestContainersContext {
params := NewTestContainersParams()
// Set the environment variables
setEnvVars(params.EnvironmentVariables)
// Start the postgres container
initPostgresContainer(ctx, params)
// Mock the third-party API client
mockClient := &http.Client{}
httpmock.ActivateNonDefault(mockClient)
// Build the app
server, _ := mainHttpServerSetup(params.MainHttpServerAddress, mockClient)
return &TestContainersContext{
MainHttpServer: server,
Params: params,
}
}

// initPostgresContainer starts a postgres container and sets the required environment variables.
// Source: https://golang.testcontainers.org/modules/postgres/
func initPostgresContainer(ctx context.Context, params *TestContainersParams) *postgres.PostgresContainer {
logTimeout := 10
port := “5432/tcp”
dbURL := func(host string, port nat.Port) string {
return fmt.Sprintf(“postgres://postgres:postgres@%s:%s/%s?sslmode=disable”,
host, port.Port(), params.DatabaseName)
}

postgresContainer, err := postgres.Run(ctx, params.PostgresImage,
postgres.WithInitScripts(params.DatabaseInitScript),
postgres.WithDatabase(params.DatabaseName),
testcontainers.WithWaitStrategy(wait.ForSQL(nat.Port(port), “postgres”, dbURL).
WithStartupTimeout(time.Duration(logTimeout)*time.Second)),
)
if err != nil {
log.Fatalf(“failed to start postgresContainer: %s”, err)
}
postgresHost, _ := postgresContainer.Host(ctx) //nolint:errcheck // non-critical
// Set database environment variables
setEnvVars(map[string]string{
params.DatabaseHostEnvVar: postgresHost,
params.DatabasePortEnvVar: getPostgresPort(ctx, postgresContainer),
params.DatabaseUserEnvVar: “postgres”,
params.DatabasePasswordEnvVar: “postgres”,
params.DatabaseNameEnvVar: params.DatabaseName,
})

s, err := postgresContainer.ConnectionString(ctx)
log.Printf(“Postgres container started at: %s”, s)
if err != nil {
log.Fatalf(“error from initPostgresContainer – ConnectionString: %e”, err)
}

return postgresContainer

}

func getPostgresPort(ctx context.Context, postgresContainer *postgres.PostgresContainer) string {
port, e := postgresContainer.MappedPort(ctx, “5432/tcp”)
if e != nil {
log.Fatalf(“Failed to get postgresContainer.Ports: %s”, e)
}

return port.Port()
}

// setEnvVars sets environment variables from a map.
func setEnvVars(envs map[string]string) {
for key, value := range envs {
if err := os.Setenv(key, value); err != nil {
log.Fatalf(“Failed to set environment variable %s: %v”, key, err)
}
}
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

7. خیار را برای اجرای تست ادغام پیکربندی کنید

integration_test.go

package main

import (
“context”
“log”
“net/http”
“testing”
“time”

“github.com/cucumber/godog”
)

func TestFeatures(t *testing.T) {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()

// Initialize test containers configuration
testcontainersConfig := NewMainWithTestContainers(ctx)

// Channel to notify when the server is ready
serverReady := make(chan struct{})

// Start the HTTP server in a separate goroutine
go func() {
log.Println(“Listening for requests at http://localhost” + testcontainersConfig.Params.MainHttpServerAddress)
// Notify that the server is ready
close(serverReady)

if err := testcontainersConfig.MainHttpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf(“Error starting the server: %v”, err)
}
}()

// Wait for the server to start
serverReady

// Allow a brief moment for the server to initialize
time.Sleep(500 * time.Millisecond)

// Run the godog test suite
suite := godog.TestSuite{
ScenarioInitializer: func(sc *godog.ScenarioContext) {
// This address should match the address of the app in the testcontainers_config.go file
mainHttpServerUrl := “http://localhost” + testcontainersConfig.Params.MainHttpServerAddress
NewStepsContext(mainHttpServerUrl, sc)
},
Options: &godog.Options{
Format: “pretty”,
Paths: []string{“features”}, // Edit this path locally to execute only the feature files you want to test.
TestingT: t, // Testing instance that will run subtests.
},
}

if suite.Run() != 0 {
t.Fatal(“Non-zero status returned, failed to run feature tests”)
}

// Gracefully shutdown the server after tests are done
if err := testcontainersConfig.MainHttpServer.Shutdown(ctx); err != nil {
log.Fatalf(“Error shutting down the server: %v”, err)
}
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

8. آزمایش را اجرا کنید

go test -v
=== RUN TestFeatures
2024/09/12 00:20:36 github.com/testcontainers/testcontainers-go – Connected to docker:
Server Version: 24.0.7
API Version: 1.43
Operating System: Ubuntu 23.10
Total Memory: 5910 MB
Testcontainers for Go Version: v0.33.0
Resolved Docker Host: unix:////Users/jose.boretto/.colima/docker.sock
Resolved Docker Socket Path: /var/run/docker.sock
Test SessionID: 14627f17d941189cbe6e129e838c4129dd664b2fd4f37eae3728f604b16097cc
Test ProcessID: ca9ac30f-09a7-4763-9d66-e9b6c2750cdd
2024/09/12 00:20:36 🐳 Creating container for image testcontainers/ryuk:0.8.1
2024/09/12 00:20:36 ✅ Container created: 9279a5a36186
2024/09/12 00:20:36 🐳 Starting container: 9279a5a36186
2024/09/12 00:20:36 ✅ Container started: 9279a5a36186
2024/09/12 00:20:36 ⏳ Waiting for container id 9279a5a36186 image: testcontainers/ryuk:0.8.1. Waiting for: &{Port:8080/tcp timeout: PollInterval:100ms skipInternalCheck:false}
2024/09/12 00:20:37 🔔 Container is ready: 9279a5a36186
2024/09/12 00:20:37 🐳 Creating container for image docker.io/postgres:16-alpine
2024/09/12 00:20:37 ✅ Container created: 29ceb3e179a8
2024/09/12 00:20:37 🐳 Starting container: 29ceb3e179a8
2024/09/12 00:20:37 ✅ Container started: 29ceb3e179a8
2024/09/12 00:20:37 ⏳ Waiting for container id 29ceb3e179a8 image: docker.io/postgres:16-alpine. Waiting for: &{timeout: deadline:0x140000109d0 Strategies:[0x1400011a140]}
2024/09/12 00:20:40 🔔 Container is ready: 29ceb3e179a8
2024/09/12 00:20:40 Postgres container started at: postgres://postgres:postgres@localhost:32933/db?
databaseConnectionString: host=localhost user=postgres password=postgres dbname=db port=32933 sslmode=disable
2024/09/12 00:20:40 Listening for requests at http://localhost:8000
Feature: Create book
databaseConnectionString: host=localhost user=postgres password=postgres dbname=db port=32933 sslmode=disable
=== RUN TestFeatures/Create_a_new_book_successfully

Background: Clean database
Given SQL command # :1 -> *StepsContext
“””
DELETE FROM myschema.books;
“””
And reset mock server # :1 -> *StepsContext

Scenario: Create a new book successfully # features/createBook.feature:10
Given a mock server request with method: “GET” and url: “https://api.isbncheck.com/isbn/0-061-96436-1” # :1 -> *StepsContext
And a mock server response with status 200 and body # :1 -> *StepsContext
“”” json
{
“id”: “0-061-96436-1”
}
“””
And a mock server request with method: “POST” and url: “https://api.gmail.com/send-email” and body # :1 -> *StepsContext
“”” json
{
“email” : “helloworld@gmail.com”,
“book” : {
“isbn” : “0-061-96436-1”,
“title” : “The Art of Computer Programming”
}
}
“””
And a mock server response with status 200 and body # :1 -> *StepsContext
“”” json
{
“status”: “OK”
}
“””

2024/09/12 00:20:40 /Users/jose.boretto/Documents/jose/golang-testcontainers-gherkin-setup/internal/infrastructure/persistance/book/create_book_repository.go:40 record not found
[4.356ms] [rows:0] SELECT * FROM “myschema”.”books” WHERE isbn = ‘0-061-96436-1’ AND “books”.”deleted_at” IS NULL ORDER BY “books”.”id” LIMIT 1
When API “POST” request is sent to “/api/v1/createBook” with payload # :1 -> *StepsContext
“”” json
{
“isbn”: “0-061-96436-1”,
“title”: “The Art of Computer Programming”
}
“””
Then response status code is 200 and payload is # :1 -> *StepsContext
“”” json
{
“isbn”: “0-061-96436-1”,
“title”: “The Art of Computer Programming”
}
“””

1 scenarios (1 passed)
8 steps (8 passed)
40.368084ms
— PASS: TestFeatures (4.22s)
— PASS: TestFeatures/Create_a_new_book_successfully (0.02s)
PASS
ok github.com/joseboretto/golang-testcontainers-gherkin-setup/cmd 4.583s

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ترکیب Cucumber، Testcontainers و HTTPMock مجموعه ای قدرتمند از ابزارها را برای نوشتن تست های یکپارچه سازی دربرو با استفاده از این تنظیمات، می توانید مطمئن شوید که برنامه شما به خوبی آزمایش شده، قابل اعتماد و آماده برای استقرار تولید است.

با استفاده از نقاط قوت هر ابزار، می توانید به پوشش تست جامع تری دست پیدا کنید و اطمینان حاصل کنید که تمام قسمت های آنسیستم شما یکپارچه با هم کار می کنند.

با خیال راحت با این ابزارها آزمایش کنید و مثال را مطابق با نیازهای برنامه خود گسترش دهید!

در اینجا می توانید مخزن را باکد: https://github.com/joseboretto/golang-testcontainers-gherkin-setup

تست یکپارچه سازی بخش مهمی از چرخه عمر توسعه نرم افزار است که ماژول های مختلف یک نرم افزار را تضمین می کند
برنامه به طور یکپارچه با هم کار می کند. در اکوسیستم Go (Golang) می توانیم از ابزارها و کتابخانه های مختلفی برای تسهیل استفاده کنیم
تست ادغام در این پست نحوه استفاده از Cucumber، Testcontainers برای پایگاه داده PostgreSQL و
HTTPMock برای نوشتن تست های یکپارچه سازی قوی در Go.

تست یکپارچه سازی تعاملات بین بخش های مختلف برنامه شما را تأیید می کند، مانند سرویس های خارجی،
پایگاه داده ها و API ها بر خلاف تست های واحد، که اجزای جداگانه را به صورت مجزا آزمایش می کنند، تست های یکپارچه سازی تضمین می کنند
در صورت ترکیب، کل سیستم همانطور که در نظر گرفته شده است عمل می کند.

  1. خیار: ابزاری که از توسعه رفتار محور (BDD) پشتیبانی می کند. این امکان را به شما می دهد تا سناریوهای آزمایشی قابل خواندن توسط انسان را به زبان Gherkin بنویسید، که شکاف بین ذینفعان فنی و غیر فنی را پر می کند.

    1. https://github.com/cucumber/godog
  2. ظروف آزمایش: کتابخانه ای که ظروف سبک وزن و یکبار مصرف را برای آزمایش فراهم می کند. این به شما امکان می‌دهد تا نمونه‌های واقعی پایگاه‌های داده، کارگزاران پیام، یا هر سرویس دیگری که برنامه شما به آن وابسته است را بچرخانید و آزمایش‌های شما را قابل اعتمادتر و به سناریوهای دنیای واقعی نزدیک‌تر می‌کند. ما از آن برای راه اندازی پایگاه داده PostgresSQL خود استفاده خواهیم کرد.

    1. https://github.com/testcontainers/testcontainers-go
  3. HTTPMock: یک کتابخانه ساده HTTP تمسخر آمیز که به شما امکان می دهد تعاملات HTTP را در آزمایشات خود شبیه سازی کنید. زمانی مفید است که می‌خواهید پاسخ‌های سرویس‌های HTTP خارجی را بدون درخواست شبکه مورد تمسخر قرار دهید.

    1. https://github.com/jarcoal/httpmock

ما 1 برنامه خواهیم داشت که وظیفه اجرای منطق زیر را بر عهده دارد:

  1. یک API برای ایجاد یک کتاب پیاده سازی کنید.
  2. بررسی کنید آیا ISBN کتاب در یک API شخص ثالث وجود دارد یا خیر.
  3. کتاب را در پایگاه داده ذخیره کنید
  4. با استفاده از یک API شخص ثالث به آدرس helloworld@gmail.com ایمیل ارسال کنید

ما با درخواست خود به عنوان یک رفتار خواهیم کرد جعبه سیاه. این بدان معنی است که ما باید برنامه را راه اندازی کنیم و از آن مانند سایر برنامه ها استفاده کنیم
کاربر، بدون تمسخر بخشی از کد.

توضیحات تصویر

ساختار پوشه پروژه خواهد بود

- cmd/  
   - features/   
      - createBook.feature  
   - integration_test.go  
   - main.go  
   - step_definition_api.go  
   - step_definition_common.go  
   - step_definition_database.go  
   - step_definition_mock_server.go  
   - test_utils.go  
   - testcontainers_config.go  
- go.mod
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

بیایید روند راه‌اندازی تست‌های یکپارچه‌سازی در Go را با استفاده از Cucumber، Testcontainers و HTTPMock مرور کنیم.

1. نصب کتابخانه های مورد نیاز

بعد، کتابخانه های لازم را نصب کنید

go get -u github.com/cucumber/godog  
go get -u github.com/testcontainers/testcontainers-go  
go get -u github.com/jarcoal/httpmock  
go get -u github.com/lib/pq  
-- Database conection  
go get -u gorm.io/gorm  
go get -u gorm.io/driver/postgres
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

2. main.go خود را به روز کنید تا اجرای برنامه را کنترل کنید

اکنون، ما باید مقداری main.go خود را تغییر دهیم. اصلی خود را در 3 عملکرد مختلف جدا کنید.

  1. func getDatabaseConnection() *gorm.DB*

    • این تابع در step_definition_common.go استفاده می‌شود و به ما امکان دسترسی به پایگاه داده در step_definition_database.go را می‌دهد.
  2. func mainHttpServerSetup(addr string, httpClient *http.Client) (*http.Server, func())

    • این تابع در testcontainers_config.go استفاده می شود و به ما امکان می دهد اجرای برنامه را در integration_test.go کنترل کنیم.
  3. main()

    • نقطه ورود برنامه واقعی.
package main

import (
    "fmt"
    servicebook "github.com/joseboretto/golang-testcontainers-gherkin-setup/internal/application/services/books"
    controller "github.com/joseboretto/golang-testcontainers-gherkin-setup/internal/infrastructure/controllers"
    controllerbook "github.com/joseboretto/golang-testcontainers-gherkin-setup/internal/infrastructure/controllers/books"
    "gorm.io/driver/postgres"
    "gorm.io/gorm"
    "gorm.io/gorm/schema"
    "os"
    "time"

    "log"
    "net/http"

    clientsbook "github.com/joseboretto/golang-testcontainers-gherkin-setup/internal/infrastructure/clients/book"
    persistancebook "github.com/joseboretto/golang-testcontainers-gherkin-setup/internal/infrastructure/persistance/book"
)

func main() {
    addr := ":8000"
    httpClient := &http.Client{
        Timeout: 5 * time.Second,
    }
    server, deferFn := mainHttpServerSetup(addr, httpClient)
    log.Println("Listing for requests at http://localhost" + addr)
    err := server.ListenAndServe()
    if err != nil {
        panic("Error staring the server: " + err.Error())
    }
    defer deferFn()
}

func mainHttpServerSetup(addr string, httpClient *http.Client) (*http.Server, func()) {
    db := getDatabaseConnection()
    // Migrate the schema
    err := db.AutoMigrate(&persistancebook.BookEntity{})
    if err != nil {
        panic("Error executing db.AutoMigrate" + err.Error())
    }
    // Clients
    checkIsbnClientHost := os.Getenv("CHECK_ISBN_CLIENT_HOST")
    checkIsbnClient := clientsbook.NewCheckIsbnClient(checkIsbnClientHost, httpClient)
    emailClientHost := os.Getenv("EMAIL_CLIENT_HOST")
    sendEmailClient := clientsbook.NewSendEmailClient(emailClientHost, httpClient)
    // repositories
    newCreateBookRepository := persistancebook.NewCreateBookRepository(db)
    // services
    createBookService := servicebook.NewCreateBookService(newCreateBookRepository, checkIsbnClient, sendEmailClient)
    // controllers
    bookController := controllerbook.NewBookController(createBookService)
    // routes
    controller.SetupRoutes(bookController)
    // Server
    server := &http.Server{Addr: addr, Handler: nil}
    // Defer function
    // Add all defer
    deferFn := func() {
        fmt.Println("closing database connection")
        sqlDB, err := db.DB()
        err = sqlDB.Close()
        if err != nil {
            panic("Error closing database connection: " + err.Error())
        }

    }
    return server, deferFn
}

func getDatabaseConnection() *gorm.DB {
    // Read database configuration from environment variables
    databaseUser := os.Getenv("DATABASE_USER")
    databasePassword := os.Getenv("DATABASE_PASSWORD")
    databaseName := os.Getenv("DATABASE_NAME")
    databaseHost := os.Getenv("DATABASE_HOST")
    databasePort := os.Getenv("DATABASE_PORT")
    databaseConnectionString := `host=` + databaseHost + ` user=` + databaseUser + ` password=` + databasePassword + ` dbname=` + databaseName + ` port=` + databasePort + ` sslmode=disable`
    fmt.Println("databaseConnectionString: ", databaseConnectionString)
    db, err := gorm.Open(postgres.New(postgres.Config{
        DSN:                  databaseConnectionString,
        PreferSimpleProtocol: true, // disables implicit prepared statement usage
    }), &gorm.Config{
        NamingStrategy: schema.NamingStrategy{
            TablePrefix:   "myschema.", // schema name
            SingularTable: false,
        }})
    // Check if connection is successful
    if err != nil {
        panic("failed to connect database with error: " + err.Error() + "\n" + "Please check your database configuration: " + databaseConnectionString)
    }
    return db
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

3. نوشتن فایل ویژگی (خیار)

ایجاد یک فایل ویژگی (createBook.feature) داخل features دایرکتوری با سناریوهایی که مورد انتظار را توصیف می کند
رفتار برنامه شما

Feature: Create book

  Background: Clean database
    Given SQL command
    """
    DELETE FROM myschema.books;
    """
    And reset mock server

  Scenario: Create a new book successfully
    Given a mock server request with method: "GET" and url: "https://api.isbncheck.com/isbn/0-061-96436-1"
    And a mock server response with status 200 and body
    """json
    {
       "id": "0-061-96436-1"
    }
    """
    And a mock server request with method: "POST" and url: "https://api.gmail.com/send-email" and body
    """json
    {
      "email" : "helloworld@gmail.com",
      "book" : {
        "isbn" : "0-061-96436-1",
        "title" : "The Art of Computer Programming"
      }
    }
    """
    And a mock server response with status 200 and body
    """json
    {
       "status": "OK"
    }
    """
    When API "POST" request is sent to "/api/v1/createBook" with payload
    """json
    {
      "isbn": "0-061-96436-1",
      "title": "The Art of Computer Programming"
    }
    """
    Then response status code is 200 and payload is
    """json
    {
        "isbn": "0-061-96436-1",
        "title": "The Art of Computer Programming"
    }
    """
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

4. پیاده سازی تعاریف مرحله در Go

اکنون، فایل های Go را برای پیاده سازی تعاریف مرحله برای سناریوها ایجاد کنید. ما بر اساس آن تعاریف چند مرحله ای داریم
اهداف

مهمترین مفهومی که باید به خاطر بسپارید نوشتن تعاریف کلی مرحله برای استفاده مجدد از آنها در هر مورد استفاده است.

step_definition_common.go

داده ها را بین راه اندازی برنامه و تعاریف مرحله به اشتراک بگذارید.

package main

import (
    "github.com/cucumber/godog"
    "gorm.io/gorm"
    "net/http"
)

type StepsContext struct {
    // Main setup  
    mainHttpServerUrl string // http://localhost:8000  
    database          *gorm.DB
    // Mock server setup  
    stepMockServerRequestMethod *string
    stepMockServerRequestUrl    *string
    stepMockServerRequestBody   *string
    stepResponse                *http.Response
}

func NewStepsContext(mainHttpServerUrl string, sc *godog.ScenarioContext) *StepsContext {
    db := getDatabaseConnection()
    s := &StepsContext{
        mainHttpServerUrl: mainHttpServerUrl,
        database:          db,
    }
    // Register all the step definition function  
    s.RegisterMockServerSteps(sc)
    s.RegisterDatabaseSteps(sc)
    s.RegisterApiSteps(sc)
    return s
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

step_definition_api.go

درخواست api را انجام دهید و پاسخ را بررسی کنید.

package main

import (
    "bytes"
    "context"
    "fmt"
    "github.com/cucumber/godog"
    "io"
    "log"
    "net/http"
    "time"
)

func (s *StepsContext) RegisterApiSteps(sc *godog.ScenarioContext) {
    sc.Step(`^API "([^"]*)" request is sent to "([^"]*)" without payload$`, s.apiRequestIsSendWithoutPayload)
    sc.Step(`^API "([^"]*)" request is sent to "([^"]*)" with payload$`, s.apiRequestIsSendWithPayload)
    sc.Step(`^response status code is (\d+) and payload is$`, s.apiResponseIs)
}

func (s *StepsContext) apiRequestIsSendWithoutPayload(method, url string) error {
    return s.apiRequestIsSendWithPayload(method, url, "")
}

func (s *StepsContext) apiRequestIsSendWithPayload(method, url, payloadJson string) error {
    client := &http.Client{
        Timeout: 5 * time.Second,
    }

    req, err := http.NewRequestWithContext(context.Background(), method, s.mainHttpServerUrl+url, bytes.NewBufferString(payloadJson))
    if err != nil {
        return fmt.Errorf("failed to create a new HTTP request: %w", err)
    }

    req.Header.Set("Content-Type", "application/json")

    response, err := client.Do(req)
    if err != nil {
        return fmt.Errorf("failed to execute HTTP request: %w", err)
    }

    s.stepResponse = response
    return nil
}

func (s *StepsContext) apiResponseIs(expected int, expectedResponse string) error {
    defer s.stepResponse.Body.Close()

    if s.stepResponse.StatusCode != expected {
        return fmt.Errorf("expected status code %d but got %d", expected, s.stepResponse.StatusCode)
    }

    actualJson, err := getBody(s.stepResponse)
    if err != nil {
        return err
    }

    if match, err := compareJSON(expectedResponse, actualJson); err != nil {
        return fmt.Errorf("error comparing JSON: %w", err)
    } else if !match {
        log.Printf("Actual response body: %s", actualJson)
        return fmt.Errorf("response body does not match. Expected: %s, actual: %s", expectedResponse, actualJson)
    }

    return nil
}

func getBody(response *http.Response) (string, error) {
    body, err := io.ReadAll(response.Body)
    if err != nil {
        return "", fmt.Errorf("failed to read response body: %w", err)
    }
    return string(body), nil
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

step_definition_database.go

ذخیره داده ها در پایگاه داده علاوه بر این، شما باید تعاریف مرحله را پیاده سازی کنید تا داده های ذخیره شده در را بررسی کنید
پایگاه داده

package main

import (
    "github.com/cucumber/godog"
)

func (s *StepsContext) RegisterDatabaseSteps(sc *godog.ScenarioContext) {
    sc.Step(`^SQL command`, s.executeSQL)
}

func (s *StepsContext) executeSQL(sqlCommand string) error {
    tx := s.database.Exec(sqlCommand)
    if tx.Error != nil {
        return tx.Error
    }
    return nil
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

step_definition_mock_server.go

استفاده نکنید httpmock.Activate() زیرا شما http.client پیش فرض را مسخره می کنید و با خطای “no
پاسخ دهنده پیدا شد” از

httpmock https://github.com/jarcoal/httpmock/blob/v1/internal/error.go#L10
زیرا سرور http ما را خراب می کند.

package main

import (
    "fmt"
    "github.com/cucumber/godog"
    "github.com/jarcoal/httpmock"
    "io"
    "log"
    "net/http"
)

// RegisterMockServerSteps registers all the step definition functions related to the mock server  
func (s *StepsContext) RegisterMockServerSteps(ctx *godog.ScenarioContext) {
    ctx.Step(`^a mock server request with method: "([^"]*)" and url: "([^"]*)"$`, s.storeMockServerMethodAndUrlInStepContext)
    ctx.Step(`^a mock server request with method: "([^"]*)" and url: "([^"]*)" and body$`, s.storeMockServerMethodAndUrlAndRequestBodyInStepContext)
    ctx.Step(`^a mock server response with status (\d+) and body$`, s.setupRegisterResponder)
    ctx.Step(`^reset mock server$`, s.resetMockServer)
}

func (s *StepsContext) storeMockServerMethodAndUrlInStepContext(method, url string) error {
    s.stepMockServerRequestMethod = &method
    s.stepMockServerRequestUrl = &url
    s.stepMockServerRequestBody = nil
    return nil
}

func (s *StepsContext) storeMockServerMethodAndUrlAndRequestBodyInStepContext(method, url, body string) error {
    s.stepMockServerRequestMethod = &method
    s.stepMockServerRequestUrl = &url
    s.stepMockServerRequestBody = &body
    return nil
}

func (s *StepsContext) setupRegisterResponder(statusCode int, responseBody string) error {
    if s.stepMockServerRequestMethod == nil || s.stepMockServerRequestUrl == nil {
        log.Fatal("stepMockServerRequestMethod or stepMockServerRequestUrl is nil. You have to setup the storeMockServerMethodAndUrlInStepContext step first")
    }
    if s.stepMockServerRequestBody != nil {
        s.registerResponderWithBodyCheck(statusCode, responseBody)
    } else {
        httpmock.RegisterResponder(*s.stepMockServerRequestMethod, *s.stepMockServerRequestUrl, httpmock.NewStringResponder(statusCode, responseBody))
    }
    return nil
}

func (s *StepsContext) registerResponderWithBodyCheck(statusCode int, responseBody string) {
    httpmock.RegisterResponder(*s.stepMockServerRequestMethod, *s.stepMockServerRequestUrl,
        func(req *http.Request) (*http.Response, error) {
            body, err := io.ReadAll(req.Body)
            if err != nil {
                return nil, fmt.Errorf("failed to read request body: %w", err)
            }

            if match, err := compareJSON(*s.stepMockServerRequestBody, string(body)); err != nil {
                return nil, fmt.Errorf("error comparing JSON: %w", err)
            } else if !match {
                log.Printf("Actual request body without escapes: %s", body)
                return nil, fmt.Errorf("request body does not match for method: %s and url: %s. Expected: %s, actual: %s",
                    *s.stepMockServerRequestMethod, *s.stepMockServerRequestUrl, *s.stepMockServerRequestBody, string(body))
            }

            return httpmock.NewStringResponse(statusCode, responseBody), nil
        })
}

func (s *StepsContext) resetMockServer() error {
    httpmock.Reset()
    // Reset the step context  
    s.stepMockServerRequestMethod = nil
    s.stepMockServerRequestUrl = nil
    s.stepMockServerRequestBody = nil
    return nil
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

test_utils.go

package main

import (
    "encoding/json"
    "fmt"
    "reflect"
)

// compareJSON compares two JSON strings and returns true if they are equal.  
func compareJSON(jsonStr1, jsonStr2 string) (bool, error) {
    var obj1, obj2 map[string]interface{}

    // Unmarshal the first JSON string  
    if err := json.Unmarshal([]byte(jsonStr1), &obj1); err != nil {
        return false, fmt.Errorf("error unmarshalling jsonStr1: %v", err)
    }

    // Unmarshal the second JSON string  
    if err := json.Unmarshal([]byte(jsonStr2), &obj2); err != nil {
        return false, fmt.Errorf("error unmarshalling jsonStr2: %v", err)
    }
    // Compare the two maps  
    return reflect.DeepEqual(obj1, obj2), nil
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

5. ظروف تست را پیکربندی کنید

testcontainers_config.go

پیکربندی کنید TestContainersParams برای مطابقت با envs و پیکربندی برنامه واقعی شما. علاوه بر این، httpmock.ActivateNonDefault(mockClient) کلید ساخت است httpmock کار می کند زیرا فقط سوم ما را مسخره می کند
مهمانی http.client

package main

import (
    "context"
    "fmt"
    "github.com/jarcoal/httpmock"
    "github.com/testcontainers/testcontainers-go"
    "github.com/testcontainers/testcontainers-go/modules/postgres"
    "log"
    "net/http"
    "os"
    "path/filepath"
    "time"

    "github.com/docker/go-connections/nat"

    _ "github.com/lib/pq" // Import the postgres driver  
    "github.com/testcontainers/testcontainers-go/wait"
)

type TestContainersParams struct {
    MainHttpServerAddress  string
    PostgresImage          string
    DatabaseName           string
    DatabaseHostEnvVar     string
    DatabasePortEnvVar     string
    DatabaseUserEnvVar     string
    DatabasePasswordEnvVar string
    DatabaseNameEnvVar     string
    DatabaseInitScript     string
    EnvironmentVariables   map[string]string
}

type TestContainersContext struct {
    MainHttpServer *http.Server
    Params         *TestContainersParams
}

func NewTestContainersParams() *TestContainersParams {
    return &TestContainersParams{
        MainHttpServerAddress:  ":8000",
        PostgresImage:          "docker.io/postgres:16-alpine",
        DatabaseName:           "db",
        DatabaseHostEnvVar:     "DATABASE_HOST",
        DatabasePortEnvVar:     "DATABASE_PORT",
        DatabaseUserEnvVar:     "DATABASE_USER",
        DatabasePasswordEnvVar: "DATABASE_PASSWORD",
        DatabaseNameEnvVar:     "DATABASE_NAME",
        DatabaseInitScript:     filepath.Join(".", "testAssets", "testdata", "dev-db.sql"),
        EnvironmentVariables: map[string]string{
            "CHECK_ISBN_CLIENT_HOST": "https://api.isbncheck.com",
            "EMAIL_CLIENT_HOST":      "https://api.gmail.com",
        },
    }
}

func NewMainWithTestContainers(ctx context.Context) *TestContainersContext {
    params := NewTestContainersParams()
    // Set the environment variables  
    setEnvVars(params.EnvironmentVariables)
    // Start the postgres container  
    initPostgresContainer(ctx, params)
    // Mock the third-party API client  
    mockClient := &http.Client{}
    httpmock.ActivateNonDefault(mockClient)
    // Build the app  
    server, _ := mainHttpServerSetup(params.MainHttpServerAddress, mockClient)
    return &TestContainersContext{
        MainHttpServer: server,
        Params:         params,
    }
}

// initPostgresContainer starts a postgres container and sets the required environment variables.  
// Source: https://golang.testcontainers.org/modules/postgres/  
func initPostgresContainer(ctx context.Context, params *TestContainersParams) *postgres.PostgresContainer {
    logTimeout := 10
    port := "5432/tcp"
    dbURL := func(host string, port nat.Port) string {
        return fmt.Sprintf("postgres://postgres:postgres@%s:%s/%s?sslmode=disable",
            host, port.Port(), params.DatabaseName)
    }

    postgresContainer, err := postgres.Run(ctx, params.PostgresImage,
        postgres.WithInitScripts(params.DatabaseInitScript),
        postgres.WithDatabase(params.DatabaseName),
        testcontainers.WithWaitStrategy(wait.ForSQL(nat.Port(port), "postgres", dbURL).
            WithStartupTimeout(time.Duration(logTimeout)*time.Second)),
    )
    if err != nil {
        log.Fatalf("failed to start postgresContainer: %s", err)
    }
    postgresHost, _ := postgresContainer.Host(ctx) //nolint:errcheck // non-critical  
    // Set database environment variables  
    setEnvVars(map[string]string{
        params.DatabaseHostEnvVar:     postgresHost,
        params.DatabasePortEnvVar:     getPostgresPort(ctx, postgresContainer),
        params.DatabaseUserEnvVar:     "postgres",
        params.DatabasePasswordEnvVar: "postgres",
        params.DatabaseNameEnvVar:     params.DatabaseName,
    })

    s, err := postgresContainer.ConnectionString(ctx)
    log.Printf("Postgres container started at: %s", s)
    if err != nil {
        log.Fatalf("error from initPostgresContainer - ConnectionString: %e", err)
    }

    return postgresContainer

}

func getPostgresPort(ctx context.Context, postgresContainer *postgres.PostgresContainer) string {
    port, e := postgresContainer.MappedPort(ctx, "5432/tcp")
    if e != nil {
        log.Fatalf("Failed to get postgresContainer.Ports: %s", e)
    }

    return port.Port()
}

// setEnvVars sets environment variables from a map.  
func setEnvVars(envs map[string]string) {
    for key, value := range envs {
        if err := os.Setenv(key, value); err != nil {
            log.Fatalf("Failed to set environment variable %s: %v", key, err)
        }
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

7. خیار را برای اجرای تست ادغام پیکربندی کنید

integration_test.go

package main

import (
    "context"
    "log"
    "net/http"
    "testing"
    "time"

    "github.com/cucumber/godog"
)

func TestFeatures(t *testing.T) {
    ctx, cancel := context.WithCancel(context.Background())
    defer cancel()

    // Initialize test containers configuration  
    testcontainersConfig := NewMainWithTestContainers(ctx)

    // Channel to notify when the server is ready  
    serverReady := make(chan struct{})

    // Start the HTTP server in a separate goroutine  
    go func() {
        log.Println("Listening for requests at http://localhost" + testcontainersConfig.Params.MainHttpServerAddress)
        // Notify that the server is ready  
        close(serverReady)

        if err := testcontainersConfig.MainHttpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
            log.Fatalf("Error starting the server: %v", err)
        }
    }()

    // Wait for the server to start  
    serverReady

    // Allow a brief moment for the server to initialize  
    time.Sleep(500 * time.Millisecond)

    // Run the godog test suite  
    suite := godog.TestSuite{
        ScenarioInitializer: func(sc *godog.ScenarioContext) {
            // This address should match the address of the app in the testcontainers_config.go file  
            mainHttpServerUrl := "http://localhost" + testcontainersConfig.Params.MainHttpServerAddress
            NewStepsContext(mainHttpServerUrl, sc)
        },
        Options: &godog.Options{
            Format:   "pretty",
            Paths:    []string{"features"}, // Edit this path locally to execute only the feature files you want to test.  
            TestingT: t,                    // Testing instance that will run subtests.  
        },
    }

    if suite.Run() != 0 {
        t.Fatal("Non-zero status returned, failed to run feature tests")
    }

    // Gracefully shutdown the server after tests are done  
    if err := testcontainersConfig.MainHttpServer.Shutdown(ctx); err != nil {
        log.Fatalf("Error shutting down the server: %v", err)
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

8. آزمایش را اجرا کنید

go test -v
=== RUN   TestFeatures
2024/09/12 00:20:36 github.com/testcontainers/testcontainers-go - Connected to docker:
Server Version: 24.0.7
API Version: 1.43
Operating System: Ubuntu 23.10
Total Memory: 5910 MB
Testcontainers for Go Version: v0.33.0
Resolved Docker Host: unix:////Users/jose.boretto/.colima/docker.sock
Resolved Docker Socket Path: /var/run/docker.sock
Test SessionID: 14627f17d941189cbe6e129e838c4129dd664b2fd4f37eae3728f604b16097cc
Test ProcessID: ca9ac30f-09a7-4763-9d66-e9b6c2750cdd
2024/09/12 00:20:36 🐳 Creating container for image testcontainers/ryuk:0.8.1
2024/09/12 00:20:36 ✅ Container created: 9279a5a36186
2024/09/12 00:20:36 🐳 Starting container: 9279a5a36186
2024/09/12 00:20:36 ✅ Container started: 9279a5a36186
2024/09/12 00:20:36 ⏳ Waiting for container id 9279a5a36186 image: testcontainers/ryuk:0.8.1. Waiting for: &{Port:8080/tcp timeout: PollInterval:100ms skipInternalCheck:false}
2024/09/12 00:20:37 🔔 Container is ready: 9279a5a36186
2024/09/12 00:20:37 🐳 Creating container for image docker.io/postgres:16-alpine
2024/09/12 00:20:37 ✅ Container created: 29ceb3e179a8
2024/09/12 00:20:37 🐳 Starting container: 29ceb3e179a8
2024/09/12 00:20:37 ✅ Container started: 29ceb3e179a8
2024/09/12 00:20:37 ⏳ Waiting for container id 29ceb3e179a8 image: docker.io/postgres:16-alpine. Waiting for: &{timeout: deadline:0x140000109d0 Strategies:[0x1400011a140]}
2024/09/12 00:20:40 🔔 Container is ready: 29ceb3e179a8
2024/09/12 00:20:40 Postgres container started at: postgres://postgres:postgres@localhost:32933/db?
databaseConnectionString:  host=localhost user=postgres password=postgres dbname=db port=32933 sslmode=disable
2024/09/12 00:20:40 Listening for requests at http://localhost:8000
Feature: Create book
databaseConnectionString:  host=localhost user=postgres password=postgres dbname=db port=32933 sslmode=disable
=== RUN   TestFeatures/Create_a_new_book_successfully

Background: Clean database
Given SQL command                                                                                      # :1 -> *StepsContext
"""
DELETE FROM myschema.books;
"""
And reset mock server                                                                                  # :1 -> *StepsContext

Scenario: Create a new book successfully                                                                 # features/createBook.feature:10
Given a mock server request with method: "GET" and url: "https://api.isbncheck.com/isbn/0-061-96436-1" # :1 -> *StepsContext
And a mock server response with status 200 and body                                                    # :1 -> *StepsContext
""" json
{
"id": "0-061-96436-1"
}
"""
And a mock server request with method: "POST" and url: "https://api.gmail.com/send-email" and body     # :1 -> *StepsContext
""" json
{
"email" : "helloworld@gmail.com",
"book" : {
"isbn" : "0-061-96436-1",
"title" : "The Art of Computer Programming"
}
}
"""
And a mock server response with status 200 and body                                                    # :1 -> *StepsContext
""" json
{
"status": "OK"
}
"""

2024/09/12 00:20:40 /Users/jose.boretto/Documents/jose/golang-testcontainers-gherkin-setup/internal/infrastructure/persistance/book/create_book_repository.go:40 record not found
[4.356ms] [rows:0] SELECT * FROM "myschema"."books" WHERE isbn = '0-061-96436-1' AND "books"."deleted_at" IS NULL ORDER BY "books"."id" LIMIT 1
When API "POST" request is sent to "/api/v1/createBook" with payload                                   # :1 -> *StepsContext
""" json
{
"isbn": "0-061-96436-1",
"title": "The Art of Computer Programming"
}
"""
Then response status code is 200 and payload is                                                        # :1 -> *StepsContext
""" json
{
"isbn": "0-061-96436-1",
"title": "The Art of Computer Programming"
}
"""

1 scenarios (1 passed)
8 steps (8 passed)
40.368084ms
--- PASS: TestFeatures (4.22s)
--- PASS: TestFeatures/Create_a_new_book_successfully (0.02s)
PASS
ok      github.com/joseboretto/golang-testcontainers-gherkin-setup/cmd  4.583s
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

ترکیب Cucumber، Testcontainers و HTTPMock مجموعه ای قدرتمند از ابزارها را برای نوشتن تست های یکپارچه سازی در
برو با استفاده از این تنظیمات، می توانید مطمئن شوید که برنامه شما به خوبی آزمایش شده، قابل اعتماد و آماده برای استقرار تولید است.

با استفاده از نقاط قوت هر ابزار، می توانید به پوشش تست جامع تری دست پیدا کنید و اطمینان حاصل کنید که تمام قسمت های آن
سیستم شما یکپارچه با هم کار می کنند.

با خیال راحت با این ابزارها آزمایش کنید و مثال را مطابق با نیازهای برنامه خود گسترش دهید!

در اینجا می توانید مخزن را با
کد: https://github.com/joseboretto/golang-testcontainers-gherkin-setup

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا