Skip to main content

End-to-End Testing

E2E testing = ทดสอบทั้ง flow ตั้งแต่ infrastructure ขึ้น → application พร้อม → user flow ทำงานได้

E2E vs Integration

Test TypeScopeSpeed
UnitSingle module logic⚡ Fast
IntegrationModule + cloud🐢 Slow
E2EFull system + app + user flow🐢🐢 Very slow

ตัวอย่าง E2E Scenario

Scenario: Web app deployment

  1. Terraform สร้าง: VPC, ALB, ASG, RDS, S3
  2. Application deploy ไปยัง EC2
  3. ทดสอบว่า:
    • HTTP endpoint ตอบ 200
    • Login flow ทำงาน
    • Database queries สำเร็จ
    • File upload ขึ้น S3

E2E ด้วย Terratest

test/e2e_test.go
package test

import (
"fmt"
"net/http"
"strings"
"testing"
"time"

"github.com/gruntwork-io/terratest/modules/http-helper"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/stretchr/testify/assert"
)

func TestE2EWebApp(t *testing.T) {
uniqueId := random.UniqueId()

opts := &terraform.Options{
TerraformDir: "../",
Vars: map[string]interface{}{
"environment": fmt.Sprintf("e2e-%s", uniqueId),
},
}

defer terraform.Destroy(t, opts)
terraform.InitAndApply(t, opts)

appURL := terraform.Output(t, opts, "app_url")

// 1. Wait for app to be ready
http_helper.HttpGetWithRetry(
t, appURL+"/health", nil, 200, "OK",
30, 10*time.Second,
)

// 2. Test login flow
t.Run("login_flow", func(t *testing.T) {
resp, err := http.PostForm(appURL+"/login",
map[string][]string{
"username": {"testuser"},
"password": {"testpass"},
},
)
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)

body, _ := io.ReadAll(resp.Body)
assert.True(t, strings.Contains(string(body), "Welcome"))
})

// 3. Test API endpoint
t.Run("api_endpoint", func(t *testing.T) {
resp, err := http.Get(appURL + "/api/users")
assert.NoError(t, err)
assert.Equal(t, 200, resp.StatusCode)
})

// 4. Test file upload
t.Run("file_upload", func(t *testing.T) {
// ... upload test ...
})
}

E2E ด้วย Cypress (UI Testing)

หลัง Terraform deploy → run Cypress:

cypress/e2e/smoke.cy.js
describe('Smoke Tests', () => {
const APP_URL = Cypress.env('APP_URL')

it('should load home page', () => {
cy.visit(APP_URL)
cy.contains('Welcome')
})

it('should login', () => {
cy.visit(APP_URL + '/login')
cy.get('[name=username]').type('testuser')
cy.get('[name=password]').type('testpass')
cy.get('[type=submit]').click()
cy.url().should('include', '/dashboard')
})

it('should fetch data', () => {
cy.request(APP_URL + '/api/health').should((response) => {
expect(response.status).to.eq(200)
expect(response.body.status).to.eq('healthy')
})
})
})

CI/CD Pipeline

.github/workflows/e2e.yml
on:
push:
branches: [main]

jobs:
e2e:
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- uses: actions/checkout@v4

- name: Setup Terraform
uses: hashicorp/setup-terraform@v3

- name: Setup Node (for Cypress)
uses: actions/setup-node@v4
with:
node-version: 20

- name: Configure AWS
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: ${{ vars.E2E_ROLE_ARN }}
aws-region: us-east-1

# Step 1: Deploy infrastructure
- name: Terraform Apply
run: |
terraform init
terraform apply -auto-approve

# Step 2: Get app URL
- name: Get App URL
id: tf
run: echo "url=$(terraform output -raw app_url)" >> $GITHUB_OUTPUT

# Step 3: Wait for app
- name: Wait for app
run: |
for i in {1..30}; do
if curl -f ${{ steps.tf.outputs.url }}/health; then
break
fi
sleep 10
done

# Step 4: Run Cypress
- name: Run Cypress
uses: cypress-io/github-action@v6
with:
config: baseUrl=${{ steps.tf.outputs.url }}

# Step 5: Always cleanup
- name: Terraform Destroy
if: always()
run: terraform destroy -auto-approve

Pattern: Ephemeral E2E Environment

PR ทุก PR สร้าง environment ของตัวเอง:

.github/workflows/pr-e2e.yml
on:
pull_request:
types: [opened, synchronize]

jobs:
pr-environment:
runs-on: ubuntu-latest
steps:
- name: Deploy PR env
run: |
terraform workspace new pr-${{ github.event.pull_request.number }} || \
terraform workspace select pr-${{ github.event.pull_request.number }}
terraform apply -auto-approve

- name: Comment URL on PR
uses: actions/github-script@v7
with:
script: |
const url = await exec.getExecOutput('terraform', ['output', '-raw', 'url'])
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `🚀 Preview environment: ${url.stdout}`
})

- name: Run E2E
run: npm run test:e2e -- --baseUrl=$(terraform output -raw url)

cleanup:
on:
pull_request:
types: [closed]
runs-on: ubuntu-latest
steps:
- name: Destroy PR env
run: |
terraform workspace select pr-${{ github.event.pull_request.number }}
terraform destroy -auto-approve
terraform workspace delete pr-${{ github.event.pull_request.number }}

Pattern: Smoke Tests

หลัง production apply — run smoke tests:

func TestProdSmoke(t *testing.T) {
prodURL := os.Getenv("PROD_URL")

// Don't deploy — just test existing prod
t.Run("home_page", func(t *testing.T) {
resp, _ := http.Get(prodURL)
assert.Equal(t, 200, resp.StatusCode)
})

t.Run("api_health", func(t *testing.T) {
resp, _ := http.Get(prodURL + "/api/health")
assert.Equal(t, 200, resp.StatusCode)
})

t.Run("database_connection", func(t *testing.T) {
resp, _ := http.Get(prodURL + "/api/db-check")
assert.Equal(t, 200, resp.StatusCode)
})
}
- name: Apply prod
run: terraform apply -auto-approve

- name: Run smoke tests
run: |
PROD_URL=$(terraform output -raw url) go test ./test -tags=smoke

Test Disaster Recovery

func TestDR(t *testing.T) {
// Deploy primary
primary := &terraform.Options{TerraformDir: "../primary"}
defer terraform.Destroy(t, primary)
terraform.InitAndApply(t, primary)

// Deploy DR
dr := &terraform.Options{TerraformDir: "../dr"}
defer terraform.Destroy(t, dr)
terraform.InitAndApply(t, dr)

// Simulate primary failure
// ... (mark primary unhealthy)

// Verify DR took over
drURL := terraform.Output(t, dr, "url")
http_helper.HttpGetWithRetry(t, drURL, nil, 200, "OK", 30, 10*time.Second)
}

Best Practices

✅ DO:
- Run E2E ใน dedicated test account
- ใช้ ephemeral environments per PR
- Cleanup เสมอ (defer destroy)
- Set timeout เพียงพอ (60min+)
- Test critical user flows + happy paths
- Run smoke tests หลัง prod deploy

❌ DON'T:
- ห้าม run E2E ใน prod account
- ห้าม share state file ระหว่าง tests
- ห้าม skip cleanup
- ห้ามทำให้ E2E flaky (retry logic + waits)
- ห้าม test ทุก edge case ใน E2E (ใช้ unit/integration)

E2E Test Pyramid

       /\
/E2E\ <- น้อยที่สุด (slow + costly)
/------\
/Integ. \ <- กลาง (moderate)
/----------\
/ Unit \ <- มากที่สุด (fast + cheap)
/--------------\
  • Many unit tests — fast, cheap, catch most bugs
  • Some integration tests — verify real cloud behavior
  • Few E2E tests — verify critical flows

ตัวอย่าง Real-World E2E

package e2e

import (
"context"
"database/sql"
"fmt"
"testing"
"time"

_ "github.com/lib/pq"
"github.com/gruntwork-io/terratest/modules/aws"
"github.com/gruntwork-io/terratest/modules/http-helper"
"github.com/gruntwork-io/terratest/modules/random"
"github.com/gruntwork-io/terratest/modules/terraform"
)

func TestFullStack(t *testing.T) {
uniqueId := random.UniqueId()
region := "us-east-1"

opts := &terraform.Options{
TerraformDir: "../examples/full-stack",
Vars: map[string]interface{}{
"environment": fmt.Sprintf("e2e-%s", uniqueId),
},
}

// Deploy
defer terraform.Destroy(t, opts)
terraform.InitAndApply(t, opts)

// Get outputs
appURL := terraform.Output(t, opts, "app_url")
dbHost := terraform.Output(t, opts, "db_host")
bucketName := terraform.Output(t, opts, "bucket_name")

// Test 1: Web app health
t.Run("web_health", func(t *testing.T) {
http_helper.HttpGetWithRetry(t, appURL+"/health", nil, 200, "OK",
30, 10*time.Second)
})

// Test 2: Database connectivity
t.Run("database", func(t *testing.T) {
db, err := sql.Open("postgres", fmt.Sprintf("host=%s ...", dbHost))
assert.NoError(t, err)
defer db.Close()

err = db.Ping()
assert.NoError(t, err)
})

// Test 3: S3 bucket usable
t.Run("s3_bucket", func(t *testing.T) {
aws.PutS3Object(t, region, bucketName, "test.txt", "content")
content := aws.GetS3ObjectContents(t, region, bucketName, "test.txt")
assert.Equal(t, "content", content)
})

// Test 4: End-to-end user flow
t.Run("user_flow", func(t *testing.T) {
// Sign up → Login → Upload file → Verify
// ...
})
}

สรุป

  • E2E = test ทั้ง flow infrastructure + app + user flow
  • ใช้ Terratest + Cypress / Playwright สำหรับ UI tests
  • Pattern: ephemeral PR env, smoke tests after prod, DR scenarios
  • น้อยที่สุดใน test pyramid — slow + expensive
  • Cleanup สำคัญที่สุด — defer destroy เสมอ

ต่อไป → Testing Modules