Integration and End-to-End Testing
While unit tests are valuable, they don’t verify how components work together. Integration and end-to-end tests fill this gap.
Integration Testing with Test Containers
Test containers allow you to run real dependencies like databases in isolated environments:
package integration
import (
"context"
"database/sql"
"fmt"
"testing"
"time"
"github.com/google/uuid"
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"myapp/internal/repository"
"myapp/internal/models"
)
// PostgresContainer represents a PostgreSQL container for testing
type PostgresContainer struct {
Container testcontainers.Container
URI string
DB *sqlx.DB
}
// SetupPostgresContainer creates a new PostgreSQL container for testing
func SetupPostgresContainer(ctx context.Context) (*PostgresContainer, error) {
// Define container request
req := testcontainers.ContainerRequest{
Image: "postgres:14-alpine",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_USER": "testuser",
"POSTGRES_PASSWORD": "testpass",
"POSTGRES_DB": "testdb",
},
WaitingFor: wait.ForLog("database system is ready to accept connections"),
}
// Start container
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
if err != nil {
return nil, fmt.Errorf("failed to start container: %w", err)
}
// Get host and port
host, err := container.Host(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get container host: %w", err)
}
port, err := container.MappedPort(ctx, "5432")
if err != nil {
return nil, fmt.Errorf("failed to get container port: %w", err)
}
// Construct connection URI
uri := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
"testuser", "testpass", host, port.Port(), "testdb")
// Connect to the database
db, err := sqlx.Connect("postgres", uri)
if err != nil {
return nil, fmt.Errorf("failed to connect to database: %w", err)
}
// Set connection pool settings
db.SetMaxOpenConns(5)
db.SetMaxIdleConns(5)
db.SetConnMaxLifetime(5 * time.Minute)
// Create a container wrapper
pgContainer := &PostgresContainer{
Container: container,
URI: uri,
DB: db,
}
// Run migrations
if err := runMigrations(db); err != nil {
return nil, fmt.Errorf("failed to run migrations: %w", err)
}
return pgContainer, nil
}
// runMigrations applies database migrations
func runMigrations(db *sqlx.DB) error {
// Create products table
_, err := db.Exec(`
CREATE TABLE IF NOT EXISTS products (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price DECIMAL(10, 2) NOT NULL,
category_id VARCHAR(36) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
deleted_at TIMESTAMP
)
`)
if err != nil {
return err
}
// Create categories table
_, err = db.Exec(`
CREATE TABLE IF NOT EXISTS categories (
id VARCHAR(36) PRIMARY KEY,
name VARCHAR(255) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW()
)
`)
return err
}
// TestProductRepositoryIntegration demonstrates integration testing with a real database
func TestProductRepositoryIntegration(t *testing.T) {
// Skip in short mode
if testing.Short() {
t.Skip("Skipping integration test in short mode")
}
// Set up test container
ctx := context.Background()
pgContainer, err := SetupPostgresContainer(ctx)
require.NoError(t, err)
defer func() {
// Clean up
if err := pgContainer.Container.Terminate(ctx); err != nil {
t.Fatalf("Failed to terminate container: %v", err)
}
}()
// Create repository
productRepo := repository.NewProductRepository(pgContainer.DB)
// Test case: create and retrieve a product
t.Run("create and retrieve product", func(t *testing.T) {
// Create a category first
categoryID := uuid.New().String()
_, err := pgContainer.DB.ExecContext(ctx,
"INSERT INTO categories (id, name) VALUES ($1, $2)",
categoryID, "Test Category")
require.NoError(t, err)
// Create a new product
product := &models.Product{
ID: uuid.New().String(),
Name: "Test Product",
Description: "A test product for integration testing",
Price: 99.99,
CategoryID: categoryID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Save the product
err = productRepo.Create(ctx, product)
require.NoError(t, err)
// Retrieve the product
retrieved, err := productRepo.FindByID(ctx, product.ID)
require.NoError(t, err)
require.NotNil(t, retrieved)
// Verify the retrieved product
assert.Equal(t, product.ID, retrieved.ID)
assert.Equal(t, product.Name, retrieved.Name)
assert.Equal(t, product.Description, retrieved.Description)
assert.Equal(t, product.Price, retrieved.Price)
assert.Equal(t, product.CategoryID, retrieved.CategoryID)
})
// Test case: update a product
t.Run("update product", func(t *testing.T) {
// Create a category first
categoryID := uuid.New().String()
_, err := pgContainer.DB.ExecContext(ctx,
"INSERT INTO categories (id, name) VALUES ($1, $2)",
categoryID, "Another Category")
require.NoError(t, err)
// Create a new product
product := &models.Product{
ID: uuid.New().String(),
Name: "Product to Update",
Description: "This product will be updated",
Price: 50.00,
CategoryID: categoryID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Save the product
err = productRepo.Create(ctx, product)
require.NoError(t, err)
// Update the product
product.Name = "Updated Product Name"
product.Price = 75.00
product.UpdatedAt = time.Now()
err = productRepo.Update(ctx, product)
require.NoError(t, err)
// Retrieve the updated product
retrieved, err := productRepo.FindByID(ctx, product.ID)
require.NoError(t, err)
require.NotNil(t, retrieved)
// Verify the updated fields
assert.Equal(t, "Updated Product Name", retrieved.Name)
assert.Equal(t, 75.00, retrieved.Price)
})
// Test case: delete a product
t.Run("delete product", func(t *testing.T) {
// Create a category first
categoryID := uuid.New().String()
_, err := pgContainer.DB.ExecContext(ctx,
"INSERT INTO categories (id, name) VALUES ($1, $2)",
categoryID, "Delete Test Category")
require.NoError(t, err)
// Create a new product
product := &models.Product{
ID: uuid.New().String(),
Name: "Product to Delete",
Description: "This product will be deleted",
Price: 25.00,
CategoryID: categoryID,
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
// Save the product
err = productRepo.Create(ctx, product)
require.NoError(t, err)
// Delete the product
err = productRepo.Delete(ctx, product.ID)
require.NoError(t, err)
// Try to retrieve the deleted product
retrieved, err := productRepo.FindByID(ctx, product.ID)
assert.Error(t, err)
assert.Nil(t, retrieved)
assert.ErrorIs(t, err, repository.ErrProductNotFound)
})
}
This integration test demonstrates:
- Test containers: Using Docker containers for real database testing
- Database setup: Creating schema and initial data
- Full CRUD operations: Testing create, read, update, and delete
- Proper cleanup: Ensuring containers are terminated after tests
- Test isolation: Each test case operates independently
API Testing with HTTP Handlers
Testing HTTP handlers ensures your API behaves correctly:
package api
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
"myapp/internal/models"
"myapp/internal/service"
)
// MockProductService is a mock implementation of the ProductService interface
type MockProductService struct {
mock.Mock
}
// GetProduct mocks the GetProduct method
func (m *MockProductService) GetProduct(ctx context.Context, id string) (*models.Product, error) {
args := m.Called(ctx, id)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*models.Product), args.Error(1)
}
// ListProducts mocks the ListProducts method
func (m *MockProductService) ListProducts(ctx context.Context, limit, offset int) ([]*models.Product, error) {
args := m.Called(ctx, limit, offset)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).([]*models.Product), args.Error(1)
}
// CreateProduct mocks the CreateProduct method
func (m *MockProductService) CreateProduct(ctx context.Context, product *models.Product) error {
args := m.Called(ctx, product)
return args.Error(0)
}
// Setup initializes the test environment
func Setup() *gin.Engine {
// Set Gin to test mode
gin.SetMode(gin.TestMode)
// Create a new Gin engine
r := gin.New()
r.Use(gin.Recovery())
return r
}
// TestGetProductHandler tests the GetProduct HTTP handler
func TestGetProductHandler(t *testing.T) {
// Setup
router := Setup()
mockService := new(MockProductService)
// Create handler with mock service
handler := NewProductHandler(mockService)
// Register routes
router.GET("/products/:id", handler.GetProduct)
// Test cases
testCases := []struct {
name string
productID string
setupMock func()
expectedStatus int
expectedBody map[string]interface{}
}{
{
name: "successful product retrieval",
productID: "prod-123",
setupMock: func() {
product := &models.Product{
ID: "prod-123",
Name: "Test Product",
Description: "A test product",
Price: 29.99,
CategoryID: "cat-456",
CreatedAt: time.Now(),
UpdatedAt: time.Now(),
}
mockService.On("GetProduct", mock.Anything, "prod-123").Return(product, nil)
},
expectedStatus: http.StatusOK,
expectedBody: map[string]interface{}{
"id": "prod-123",
"name": "Test Product",
"description": "A test product",
"price": 29.99,
"category_id": "cat-456",
},
},
{
name: "product not found",
productID: "non-existent",
setupMock: func() {
mockService.On("GetProduct", mock.Anything, "non-existent").Return(nil, service.ErrProductNotFound)
},
expectedStatus: http.StatusNotFound,
expectedBody: map[string]interface{}{
"error": "Product not found",
},
},
{
name: "internal server error",
productID: "error-id",
setupMock: func() {
mockService.On("GetProduct", mock.Anything, "error-id").Return(nil, errors.New("database connection failed"))
},
expectedStatus: http.StatusInternalServerError,
expectedBody: map[string]interface{}{
"error": "Internal server error",
},
},
}
// Run test cases
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Setup mock expectations
tc.setupMock()
// Create request
req, err := http.NewRequest(http.MethodGet, "/products/"+tc.productID, nil)
require.NoError(t, err)
// Create response recorder
w := httptest.NewRecorder()
// Serve the request
router.ServeHTTP(w, req)
// Check status code
assert.Equal(t, tc.expectedStatus, w.Code)
// Parse response body
var response map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
// Check response body
for key, expectedValue := range tc.expectedBody {
assert.Equal(t, expectedValue, response[key])
}
// Verify mock expectations
mockService.AssertExpectations(t)
})
}
}
// TestCreateProductHandler tests the CreateProduct HTTP handler
func TestCreateProductHandler(t *testing.T) {
// Setup
router := Setup()
mockService := new(MockProductService)
// Create handler with mock service
handler := NewProductHandler(mockService)
// Register routes
router.POST("/products", handler.CreateProduct)
// Test cases
testCases := []struct {
name string
requestBody map[string]interface{}
setupMock func(map[string]interface{})
expectedStatus int
expectedBody map[string]interface{}
}{
{
name: "successful product creation",
requestBody: map[string]interface{}{
"name": "New Product",
"description": "A new product",
"price": 39.99,
"category_id": "cat-789",
},
setupMock: func(body map[string]interface{}) {
mockService.On("CreateProduct", mock.Anything, mock.MatchedBy(func(p *models.Product) bool {
return p.Name == body["name"] &&
p.Description == body["description"] &&
p.Price == body["price"] &&
p.CategoryID == body["category_id"]
})).Return(nil)
},
expectedStatus: http.StatusCreated,
expectedBody: map[string]interface{}{
"message": "Product created successfully",
},
},
{
name: "invalid request body",
requestBody: map[string]interface{}{
"name": "",
"description": "Missing name",
"price": 39.99,
"category_id": "cat-789",
},
setupMock: func(body map[string]interface{}) {
// No mock setup needed as validation should fail before service is called
},
expectedStatus: http.StatusBadRequest,
expectedBody: map[string]interface{}{
"error": "Invalid request body",
},
},
{
name: "service error",
requestBody: map[string]interface{}{
"name": "Error Product",
"description": "A product that causes an error",
"price": 39.99,
"category_id": "cat-789",
},
setupMock: func(body map[string]interface{}) {
mockService.On("CreateProduct", mock.Anything, mock.Anything).Return(errors.New("database error"))
},
expectedStatus: http.StatusInternalServerError,
expectedBody: map[string]interface{}{
"error": "Internal server error",
},
},
}
// Run test cases
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Setup mock expectations
tc.setupMock(tc.requestBody)
// Create request body
jsonBody, err := json.Marshal(tc.requestBody)
require.NoError(t, err)
// Create request
req, err := http.NewRequest(http.MethodPost, "/products", bytes.NewBuffer(jsonBody))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
// Create response recorder
w := httptest.NewRecorder()
// Serve the request
router.ServeHTTP(w, req)
// Check status code
assert.Equal(t, tc.expectedStatus, w.Code)
// Parse response body
var response map[string]interface{}
err = json.Unmarshal(w.Body.Bytes(), &response)
require.NoError(t, err)
// Check response body
for key, expectedValue := range tc.expectedBody {
assert.Equal(t, expectedValue, response[key])
}
// Verify mock expectations
mockService.AssertExpectations(t)
})
}
}
This API testing approach demonstrates:
- HTTP handler testing: Using
httptest
to simulate HTTP requests - Response validation: Checking status codes and response bodies
- Multiple scenarios: Testing success, validation errors, and server errors
- Mock services: Isolating handlers from business logic
- Matcher functions: Using
mock.MatchedBy
for flexible argument matching
End-to-End Testing with API Clients
For true end-to-end testing, we can test the entire API as a client would:
package e2e
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"os"
"testing"
"time"
"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/wait"
"myapp/internal/api"
"myapp/internal/config"
"myapp/internal/db"
"myapp/internal/models"
"myapp/internal/repository"
"myapp/internal/service"
)
// TestEnvironment holds all the components needed for E2E testing
type TestEnvironment struct {
Router *gin.Engine
Server *httptest.Server
Config *config.Config
DB *db.Database
PgContainer testcontainers.Container
ProductRepo *repository.ProductRepository
ProductSvc *service.ProductService
CategoryRepo *repository.CategoryRepository
CategorySvc *service.CategoryService
}
// SetupTestEnvironment creates a complete test environment with real dependencies
func SetupTestEnvironment(t *testing.T) *TestEnvironment {
// Skip in short mode
if testing.Short() {
t.Skip("Skipping E2E test in short mode")
}
// Create context
ctx := context.Background()
// Start PostgreSQL container
pgContainer, pgURI := startPostgresContainer(t, ctx)
// Create configuration
cfg := &config.Config{
Database: config.DatabaseConfig{
URI: pgURI,
},
Server: config.ServerConfig{
Port: 8080,
},
}
// Initialize database
database, err := db.NewDatabase(cfg)
require.NoError(t, err)
// Run migrations
err = database.Migrate()
require.NoError(t, err)
// Create repositories
productRepo := repository.NewProductRepository(database.DB)
categoryRepo := repository.NewCategoryRepository(database.DB)
// Create services
productSvc := service.NewProductService(productRepo, categoryRepo)
categorySvc := service.NewCategoryService(categoryRepo)
// Create API handlers
productHandler := api.NewProductHandler(productSvc)
categoryHandler := api.NewCategoryHandler(categorySvc)
// Set up router
gin.SetMode(gin.TestMode)
router := gin.New()
router.Use(gin.Recovery())
// Register routes
api.RegisterRoutes(router, productHandler, categoryHandler)
// Create test server
server := httptest.NewServer(router)
return &TestEnvironment{
Router: router,
Server: server,
Config: cfg,
DB: database,
PgContainer: pgContainer,
ProductRepo: productRepo,
ProductSvc: productSvc,
CategoryRepo: categoryRepo,
CategorySvc: categorySvc,
}
}
// CleanupTestEnvironment tears down the test environment
func CleanupTestEnvironment(t *TestEnvironment) {
// Close test server
if t.Server != nil {
t.Server.Close()
}
// Close database connection
if t.DB != nil {
t.DB.Close()
}
// Stop container
if t.PgContainer != nil {
ctx := context.Background()
if err := t.PgContainer.Terminate(ctx); err != nil {
fmt.Fprintf(os.Stderr, "Failed to terminate container: %v\n", err)
}
}
}
// startPostgresContainer starts a PostgreSQL container for testing
func startPostgresContainer(t *testing.T, ctx context.Context) (testcontainers.Container, string) {
// Define container request
req := testcontainers.ContainerRequest{
Image: "postgres:14-alpine",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_USER": "testuser",
"POSTGRES_PASSWORD": "testpass",
"POSTGRES_DB": "testdb",
},
WaitingFor: wait.ForLog("database system is ready to accept connections"),
}
// Start container
container, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: req,
Started: true,
})
require.NoError(t, err)
// Get host and port
host, err := container.Host(ctx)
require.NoError(t, err)
port, err := container.MappedPort(ctx, "5432")
require.NoError(t, err)
// Construct connection URI
uri := fmt.Sprintf("postgres://%s:%s@%s:%s/%s?sslmode=disable",
"testuser", "testpass", host, port.Port(), "testdb")
return container, uri
}
// TestProductAPI performs end-to-end testing of the product API
func TestProductAPI(t *testing.T) {
// Set up test environment
env := SetupTestEnvironment(t)
defer CleanupTestEnvironment(env)
// Create a client for making requests
client := &http.Client{
Timeout: 5 * time.Second,
}
// Test case: create a category
t.Run("create category", func(t *testing.T) {
// Create request body
categoryData := map[string]interface{}{
"name": "Test Category",
}
jsonData, err := json.Marshal(categoryData)
require.NoError(t, err)
// Create request
req, err := http.NewRequest(http.MethodPost,
env.Server.URL+"/api/categories",
bytes.NewBuffer(jsonData))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
// Send request
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Check status code
assert.Equal(t, http.StatusCreated, resp.StatusCode)
// Parse response
var response map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&response)
require.NoError(t, err)
// Verify response
assert.Contains(t, response, "id")
assert.Contains(t, response, "message")
assert.Equal(t, "Category created successfully", response["message"])
// Store category ID for later use
categoryID := response["id"].(string)
assert.NotEmpty(t, categoryID)
// Test case: create a product in that category
t.Run("create product", func(t *testing.T) {
// Create request body
productData := map[string]interface{}{
"name": "Test Product",
"description": "A test product for E2E testing",
"price": 99.99,
"category_id": categoryID,
}
jsonData, err := json.Marshal(productData)
require.NoError(t, err)
// Create request
req, err := http.NewRequest(http.MethodPost,
env.Server.URL+"/api/products",
bytes.NewBuffer(jsonData))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
// Send request
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Check status code
assert.Equal(t, http.StatusCreated, resp.StatusCode)
// Parse response
var response map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&response)
require.NoError(t, err)
// Verify response
assert.Contains(t, response, "id")
assert.Contains(t, response, "message")
assert.Equal(t, "Product created successfully", response["message"])
// Store product ID for later use
productID := response["id"].(string)
assert.NotEmpty(t, productID)
// Test case: get the created product
t.Run("get product", func(t *testing.T) {
// Create request
req, err := http.NewRequest(http.MethodGet,
env.Server.URL+"/api/products/"+productID, nil)
require.NoError(t, err)
// Send request
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Check status code
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Parse response
var product map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&product)
require.NoError(t, err)
// Verify product data
assert.Equal(t, productID, product["id"])
assert.Equal(t, "Test Product", product["name"])
assert.Equal(t, "A test product for E2E testing", product["description"])
assert.Equal(t, 99.99, product["price"])
assert.Equal(t, categoryID, product["category_id"])
})
// Test case: list all products
t.Run("list products", func(t *testing.T) {
// Create request
req, err := http.NewRequest(http.MethodGet,
env.Server.URL+"/api/products", nil)
require.NoError(t, err)
// Send request
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Check status code
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Parse response
var response map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&response)
require.NoError(t, err)
// Verify response
assert.Contains(t, response, "products")
products := response["products"].([]interface{})
assert.GreaterOrEqual(t, len(products), 1)
// Find our product in the list
found := false
for _, p := range products {
product := p.(map[string]interface{})
if product["id"] == productID {
found = true
assert.Equal(t, "Test Product", product["name"])
break
}
}
assert.True(t, found, "Created product not found in product list")
})
// Test case: update the product
t.Run("update product", func(t *testing.T) {
// Create request body
updateData := map[string]interface{}{
"name": "Updated Product Name",
"description": "Updated product description",
"price": 129.99,
"category_id": categoryID,
}
jsonData, err := json.Marshal(updateData)
require.NoError(t, err)
// Create request
req, err := http.NewRequest(http.MethodPut,
env.Server.URL+"/api/products/"+productID,
bytes.NewBuffer(jsonData))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
// Send request
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Check status code
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Verify the update with a GET request
req, err = http.NewRequest(http.MethodGet,
env.Server.URL+"/api/products/"+productID, nil)
require.NoError(t, err)
resp, err = client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusOK, resp.StatusCode)
var product map[string]interface{}
err = json.NewDecoder(resp.Body).Decode(&product)
require.NoError(t, err)
assert.Equal(t, "Updated Product Name", product["name"])
assert.Equal(t, "Updated product description", product["description"])
assert.Equal(t, 129.99, product["price"])
})
// Test case: delete the product
t.Run("delete product", func(t *testing.T) {
// Create request
req, err := http.NewRequest(http.MethodDelete,
env.Server.URL+"/api/products/"+productID, nil)
require.NoError(t, err)
// Send request
resp, err := client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
// Check status code
assert.Equal(t, http.StatusOK, resp.StatusCode)
// Verify deletion with a GET request
req, err = http.NewRequest(http.MethodGet,
env.Server.URL+"/api/products/"+productID, nil)
require.NoError(t, err)
resp, err = client.Do(req)
require.NoError(t, err)
defer resp.Body.Close()
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
})
})
})
}
This end-to-end test demonstrates:
- Complete environment setup: Creating a full test environment with real dependencies
- HTTP client testing: Testing the API as a client would
- Test flow: Creating, reading, updating, and deleting resources
- Nested tests: Building a logical test flow with dependencies
- Proper cleanup: Ensuring all resources are released after testing