Testing: Interview & Big Picture
File này tổng hợp các câu hỏi phỏng vấn phổ biến về testing và cách trả lời chúng một cách có chiều sâu. Mục tiêu không phải là thuộc lòng câu trả lời, mà là hiểu why đằng sau mỗi quyết định về testing strategy.
💡 "In interviews, they're not testing if you know how to write tests. They're testing if you understand trade-offs."
Big Picture: Testing Philosophy
Câu hỏi cốt lõi luôn quay về 3 điều này:
- Confidence: Test này có thực sự catch được bugs không?
- Speed: Feedback loop có đủ nhanh để dev chạy thường xuyên không?
- Cost: Effort để viết & maintain test có xứng đáng với value không?
Value
│
│
High confidence, cheap to maintain
│
┌──────────────────┼──────────────────┐
│ │ │
│ Integration │ Unit Tests │ ← Sweet spot
│ Tests │ │
│ │ │
────┼──────────────────┼──────────────────┼────
│ │ │
│ E2E Tests │ Manual QA │ ← Use sparingly
│ │ │
└──────────────────┼──────────────────┘
│
Low confidence, expensive to maintain
│
Câu hỏi phỏng vấn phổ biến
Level 1: Fundamentals
Q1: Sự khác biệt giữa Unit test, Integration test, và E2E test?
Trả lời (structured):
Unit test:
- Scope: Test một function/method độc lập
- Dependencies: Mock/stub tất cả external dependencies (DB, API, file system)
- Speed: Rất nhanh (< 1ms)
- When: Test business logic thuần, edge cases, error handling
Integration test:
- Scope: Test nhiều components work together (code + DB, code + message queue)
- Dependencies: Real infrastructure (dùng testcontainers, in-memory DB)
- Speed: Chậm hơn (1-10s)
- When: Test database queries, API contracts, message handling
E2E test:
- Scope: Test full user journey từ UI đến backend
- Dependencies: Toàn bộ system running (frontend, backend, DB, third-party services)
- Speed: Rất chậm (10s-minutes)
- When: Critical user flows (signup, checkout, payment)
Ví dụ concrete (e-commerce checkout):
Unit test:
func calculateDiscount(price, couponCode) → test logic thuần
Integration test:
POST /orders → verify order inserted vào DB đúng
E2E test:
1. User add item to cart
2. Click checkout
3. Enter payment info
4. Verify order confirmation email sent
Trade-off insight:
"E2E tests cho confidence cao nhất nhưng slow & flaky. Unit tests nhanh nhưng có thể miss integration bugs. Cần balance — 70% unit, 20% integration, 10% E2E."
Q2: Mock vs Stub vs Fake vs Spy — khi nào dùng cái nào?
Trả lời:
| Test Double | Purpose | Example |
|---|---|---|
| Stub | Trả về canned response | when(userRepo.findByID(1)).thenReturn(alice) |
| Mock | Verify interaction xảy ra | verify(emailService).send(eq("alice@example.com")) |
| Fake | Real working implementation (in-memory) | In-memory database, fake Redis |
| Spy | Wrap real object, track calls | spy(realUserService) — dùng real logic nhưng verify calls |
Khi nào dùng gì?
// STUB: Khi bạn chỉ cần data để test logic
func TestCalculateTotal(t *testing.T) {
// Stub: OrderRepo trả về fake orders
repo := &StubOrderRepo{
orders: []Order{{Price: 100}, {Price: 50}},
}
total := CalculateTotal(repo)
assert.Equal(t, 150, total)
}
// MOCK: Khi bạn cần verify behavior (function có được gọi không?)
func TestPlaceOrder_SendsEmail(t *testing.T) {
mockEmail := &MockEmailService{}
PlaceOrder(order, mockEmail)
// Verify email.Send() được gọi với đúng params
mockEmail.AssertCalled(t, "Send", "alice@example.com", "Order Confirmation")
}
// FAKE: Khi cần real behavior nhưng không muốn external dependency
func TestUserService_Integration(t *testing.T) {
// Fake: In-memory SQLite thay vì Postgres
db := NewInMemoryDB()
svc := NewUserService(db)
svc.CreateUser("alice@example.com")
user := svc.GetUser("alice@example.com")
assert.NotNil(t, user)
}
Red flag: Mock quá nhiều → test chỉ verify implementation details, không test behavior.
// 🔥 BAD: Over-mocking
mock.ExpectQuery("SELECT").WillReturnRows(rows)
mock.ExpectExec("UPDATE").WillReturnResult(result)
mock.ExpectQuery("SELECT").WillReturnRows(rows2)
// Test này break mỗi khi refactor query order → brittle
Q3: Test coverage bao nhiêu % là đủ?
Trả lời nuanced:
"Coverage % là lagging indicator, không phải goal. 100% coverage không guarantee zero bugs."
Insight:
- 80% coverage: Reasonable target cho majority projects
- 90%+: Diminishing returns — effort tăng exponentially
- 100%: Thường không practical (testing getters/setters, framework code)
Quan trọng hơn coverage %:
- Critical path coverage: User registration, payment, data migration phải có tests
- Edge case coverage: Null inputs, empty arrays, max boundaries
- Error path coverage: Network failures, timeouts, invalid data
Anti-pattern:
// 🔥 Chasing coverage without value
func TestGetterSetter(t *testing.T) {
user := User{}
user.SetName("Alice")
assert.Equal(t, "Alice", user.GetName())
// Coverage tăng nhưng không catch bugs gì
}
Better approach:
// ✅ Test behavior, not implementation
func TestUserValidation_RejectsInvalidEmail(t *testing.T) {
user := User{Email: "not-an-email"}
err := user.Validate()
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid email")
}
Metric tốt hơn coverage %: Mutation testing score (dùng tools như go-mutesting) — thay đổi code, test có fail không?
Level 2: Practical Scenarios
Q4: Làm thế nào để test code có external dependencies (database, third-party APIs)?
Trả lời (multi-layered):
Approach 1: Dependency Injection + Interfaces
// Interface cho DB
type UserRepository interface {
GetUser(id int64) (*User, error)
CreateUser(user *User) error
}
// Service nhận interface
type UserService struct {
repo UserRepository
}
// Production: real DB
realRepo := &PostgresUserRepo{db: pgConn}
svc := &UserService{repo: realRepo}
// Testing: in-memory fake
fakeRepo := &InMemoryUserRepo{users: map[int64]*User{}}
svc := &UserService{repo: fakeRepo}
Approach 2: Testcontainers cho Integration Tests
import "github.com/testcontainers/testcontainers-go"
func setupTestDB(t *testing.T) *sql.DB {
ctx := context.Background()
// Spin up real Postgres container
pgContainer, err := testcontainers.GenericContainer(ctx, testcontainers.GenericContainerRequest{
ContainerRequest: testcontainers.ContainerRequest{
Image: "postgres:15",
ExposedPorts: []string{"5432/tcp"},
Env: map[string]string{
"POSTGRES_PASSWORD": "test",
"POSTGRES_DB": "testdb",
},
},
Started: true,
})
require.NoError(t, err)
// Connect to container
host, _ := pgContainer.Host(ctx)
port, _ := pgContainer.MappedPort(ctx, "5432")
dsn := fmt.Sprintf("postgres://postgres:test@%s:%s/testdb", host, port.Port())
db, _ := sql.Open("postgres", dsn)
return db
}
func TestUserService_CreateUser_Integration(t *testing.T) {
db := setupTestDB(t)
defer db.Close()
repo := NewPostgresUserRepo(db)
svc := NewUserService(repo)
err := svc.CreateUser(&User{Email: "alice@example.com"})
assert.NoError(t, err)
// Verify in DB
user, _ := svc.GetUser("alice@example.com")
assert.Equal(t, "alice@example.com", user.Email)
}
Approach 3: Stub External APIs (httptest)
func TestFetchUserFromAPI(t *testing.T) {
// Create fake HTTP server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(200)
json.NewEncoder(w).Encode(map[string]string{
"name": "Alice",
"email": "alice@example.com",
})
}))
defer server.Close()
// Point client to fake server
client := NewAPIClient(server.URL)
user, err := client.FetchUser(123)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
}
When to use what?
- Unit tests: Mock/stub external dependencies
- Integration tests: Real DB (testcontainers), fake/stub APIs
- E2E tests: Real everything (staging environment)
Q5: Test bị flaky (sometimes pass, sometimes fail) — debug như thế nào?
Trả lời (structured troubleshooting):
Common causes & fixes:
1. Race conditions (concurrency bugs)
// 🔥 FLAKY: goroutine chưa finish khi test kết thúc
func TestAsyncJob(t *testing.T) {
go processJob()
result := getResult() // Race: job chưa xong?
assert.Equal(t, "done", result)
}
// ✅ FIX: Use sync primitives
func TestAsyncJob(t *testing.T) {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
processJob()
}()
wg.Wait() // Đợi job finish
result := getResult()
assert.Equal(t, "done", result)
}
2. Time-dependent assertions
// 🔥 FLAKY: Depends on system time
func TestCache_Expiry(t *testing.T) {
cache.Set("key", "value", 1*time.Second)
time.Sleep(1 * time.Second) // Có thể < 1s do clock skew
val := cache.Get("key")
assert.Nil(t, val)
}
// ✅ FIX: Mock time or use logical clock
func TestCache_Expiry(t *testing.T) {
clock := &FakeClock{now: time.Now()}
cache := NewCache(clock)
cache.Set("key", "value", 1*time.Second)
clock.Advance(2 * time.Second) // Deterministic
val := cache.Get("key")
assert.Nil(t, val)
}
3. Test order dependency
// 🔥 FLAKY: TestB phụ thuộc state từ TestA
func TestA(t *testing.T) {
globalUser = &User{ID: 1}
}
func TestB(t *testing.T) {
// Assume globalUser được set bởi TestA
assert.NotNil(t, globalUser)
}
// ✅ FIX: Mỗi test setup riêng
func TestB(t *testing.T) {
user := &User{ID: 1} // Explicit setup
assert.NotNil(t, user)
}
4. External service flakiness
// 🔥 FLAKY: Real API sometimes down
func TestFetchUser(t *testing.T) {
user := api.FetchUser(123) // External call
assert.NotNil(t, user)
}
// ✅ FIX: Stub external APIs
func TestFetchUser(t *testing.T) {
server := httptest.NewServer(...)
defer server.Close()
client := NewClient(server.URL)
user := client.FetchUser(123)
assert.NotNil(t, user)
}
Debugging strategy:
- Run test 100 times:
go test -count=100 -run TestFlaky - Enable race detector:
go test -race - Add verbose logging
- Isolate test (disable others)
- Check for shared state (global variables, DB records)
Q6: Table-driven tests vs individual test functions — khi nào dùng gì?
Trả lời:
Table-driven tests: Dùng khi test same logic with different inputs
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
wantErr bool
}{
{"valid email", "alice@example.com", false},
{"missing @", "alice.example.com", true},
{"missing domain", "alice@", true},
{"empty", "", true},
{"valid with subdomain", "alice@mail.example.com", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := ValidateEmail(tt.email)
if tt.wantErr {
assert.Error(t, err)
} else {
assert.NoError(t, err)
}
})
}
}
Individual test functions: Dùng khi different scenarios cần different setup
func TestUserService_CreateUser_Success(t *testing.T) {
repo := setupRepo(t)
svc := NewUserService(repo)
err := svc.CreateUser(&User{Email: "alice@example.com"})
assert.NoError(t, err)
}
func TestUserService_CreateUser_DuplicateEmail(t *testing.T) {
repo := setupRepo(t)
repo.CreateUser(&User{Email: "alice@example.com"}) // Pre-existing user
svc := NewUserService(repo)
err := svc.CreateUser(&User{Email: "alice@example.com"})
assert.Error(t, err)
assert.Contains(t, err.Error(), "duplicate")
}
func TestUserService_CreateUser_InvalidEmail(t *testing.T) {
repo := setupRepo(t)
svc := NewUserService(repo)
err := svc.CreateUser(&User{Email: "not-an-email"})
assert.Error(t, err)
}
Rule of thumb:
- Table-driven: Input validation, parsers, formatters, calculators
- Individual: Complex scenarios với nhiều setup steps, mocks, assertions
Level 3: Advanced Topics
Q7: Làm sao test legacy code không có tests?
Trả lời (pragmatic approach):
Challenge: Legacy code thường tightly coupled, hard to test.
Strategy: "Sprout Method" & "Wrap Method"
// Legacy code: untestable (tightly coupled)
func ProcessOrder(orderID int) {
db := sql.Open("postgres", hardcodedDSN) // Hardcoded!
order := fetchOrderFromDB(db, orderID)
if order.Total > 100 {
sendEmail(order.CustomerEmail, "Big order!") // Hardcoded email!
}
updateInventory(db, order.Items)
}
Step 1: Extract testable logic (Sprout Method)
// New function: pure logic, testable
func shouldNotifyBigOrder(total float64) bool {
return total > 100
}
// Test it
func TestShouldNotifyBigOrder(t *testing.T) {
assert.True(t, shouldNotifyBigOrder(150))
assert.False(t, shouldNotifyBigOrder(50))
}
Step 2: Refactor incrementally
// Add interface for DB (don't change legacy code yet)
type OrderRepository interface {
GetOrder(id int) (*Order, error)
}
// New version with DI
func ProcessOrderV2(orderID int, repo OrderRepository, emailSvc EmailService) {
order, _ := repo.GetOrder(orderID)
if shouldNotifyBigOrder(order.Total) {
emailSvc.Send(order.CustomerEmail, "Big order!")
}
updateInventory(order.Items)
}
Step 3: Characterization tests (Golden tests)
// Capture current behavior (even if buggy) before refactoring
func TestProcessOrder_Golden(t *testing.T) {
// Run legacy code
output := captureProcessOrderOutput(123)
// Compare with golden file
golden := readFile("testdata/golden_output.json")
assert.Equal(t, golden, output)
// Nếu refactor làm thay đổi behavior → test sẽ fail
}
Tips:
- Don't try to test everything at once — focus on critical paths
- Add tests before refactoring (characterization tests)
- Refactor small pieces at a time
- Use IDE refactoring tools (extract method, extract interface)
Q8: Property-based testing là gì? Khi nào dùng?
Trả lời:
Property-based testing: Thay vì test với specific inputs, define properties mà code phải satisfy với bất kỳ input nào.
Example-based test (traditional):
func TestReverse(t *testing.T) {
assert.Equal(t, "cba", Reverse("abc"))
assert.Equal(t, "54321", Reverse("12345"))
}
Property-based test:
import "pgregory.net/rapid"
func TestReverseProperties(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
s := rapid.String().Draw(t, "input")
reversed := Reverse(s)
// Property 1: Reverse twice = original
assert.Equal(t, s, Reverse(reversed))
// Property 2: Length unchanged
assert.Equal(t, len(s), len(reversed))
})
}
Framework sẽ generate hàng trăm random inputs và verify properties → có thể catch edge cases bạn không nghĩ đến.
Khi nào dùng?
- Parsers/serializers:
parse(serialize(x)) == x - Encoders/decoders:
decode(encode(x)) == x - Math functions:
abs(x) >= 0,sort(sort(list)) == sort(list) - Data structures:
insertthencontainsreturns true
Real-world example: Sorting
func TestSortProperties(t *testing.T) {
rapid.Check(t, func(t *rapid.T) {
arr := rapid.SliceOf(rapid.Int()).Draw(t, "array")
sorted := Sort(arr)
// Property 1: Result is sorted
for i := 0; i < len(sorted)-1; i++ {
assert.LessOrEqual(t, sorted[i], sorted[i+1])
}
// Property 2: Same elements (no loss/gain)
assert.ElementsMatch(t, arr, sorted)
})
}
Q9: Contract testing vs Integration testing — sự khác biệt?
Trả lời:
Scenario: Service A gọi Service B qua API.
Integration test (traditional):
┌─────────┐ HTTP ┌─────────┐
│Service A│ ─────────────► │Service B│
└─────────┘ └─────────┘
│ │
└── Both services running ─┘
Test cần cả hai services running → slow, flaky.
Contract testing (Pact):
┌─────────┐ ┌─────────┐
│Service A│ │Service B│
└────┬────┘ └────┬────┘
│ │
│ 1. Define contract │
│ (Pact file) │
│ │
│ 2. A tests contract │
│ (consumer test) │
│ │
│ 3. B tests contract │
│ (provider test) │
Mỗi service test riêng — không cần cả hai running cùng lúc.
Example (Consumer side - Service A):
import "github.com/pact-foundation/pact-go/dsl"
func TestGetUser_Contract(t *testing.T) {
pact := &dsl.Pact{
Consumer: "ServiceA",
Provider: "ServiceB",
}
// Define expected interaction
pact.AddInteraction().
Given("User 123 exists").
UponReceiving("A request for user 123").
WithRequest(dsl.Request{
Method: "GET",
Path: dsl.String("/users/123"),
}).
WillRespondWith(dsl.Response{
Status: 200,
Body: map[string]interface{}{
"id": 123,
"name": "Alice",
"email": "alice@example.com",
},
})
// Test consumer code
test := func() error {
client := NewAPIClient(pact.Server.URL)
user, err := client.GetUser(123)
assert.NoError(t, err)
assert.Equal(t, "Alice", user.Name)
return nil
}
pact.Verify(test)
// Generates pact file: serviceA-serviceB.json
}
Example (Provider side - Service B):
func TestPactProvider(t *testing.T) {
// Start Service B
server := startServiceB()
// Verify against pact file
pact.VerifyProvider(t, types.VerifyRequest{
ProviderBaseURL: server.URL,
PactURLs: []string{"./pacts/serviceA-serviceB.json"},
})
}
Benefits:
- ✅ Fast (no need to start both services)
- ✅ Clear contract documentation
- ✅ Catch breaking changes early
When to use:
- Microservices communicating via APIs
- Consumer-driven API design
- Multiple teams owning different services
Q10: Làm sao để test distributed systems (eventual consistency, async processing)?
Trả lời (advanced patterns):
Challenge: Distributed systems có delays, retries, out-of-order messages.
Pattern 1: Eventually assertions
import "github.com/stretchr/testify/assert"
import "time"
func AssertEventually(t *testing.T, condition func() bool, timeout time.Duration) {
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if condition() {
return // Success
}
time.Sleep(100 * time.Millisecond)
}
t.Fatal("Condition not met within timeout")
}
func TestEventualConsistency(t *testing.T) {
// Publish event
publisher.Publish("user.created", UserCreatedEvent{ID: 123})
// Assert eventually (not immediately)
AssertEventually(t, func() bool {
user, _ := readReplica.GetUser(123)
return user != nil
}, 5*time.Second)
}
Pattern 2: Test idempotency
func TestProcessEvent_Idempotent(t *testing.T) {
event := OrderPlacedEvent{OrderID: 123}
// Process same event twice
handler.ProcessEvent(event)
handler.ProcessEvent(event) // Duplicate delivery
// Should only create one order
orders := db.GetOrders()
assert.Equal(t, 1, len(orders))
}
Pattern 3: Chaos testing (simulate failures)
func TestResilientToNodeFailure(t *testing.T) {
cluster := startCluster(3) // 3 nodes
// Write data
cluster.Write("key", "value")
// Kill a node
cluster.KillNode(0)
// Should still be able to read
val, err := cluster.Read("key")
assert.NoError(t, err)
assert.Equal(t, "value", val)
}
Tools:
- Testcontainers: Spin up Kafka, Redis, Postgres
- Toxiproxy: Simulate network latency, failures
- Chaos Mesh: Kubernetes chaos engineering
System Design Questions về Testing
Q: Thiết kế testing strategy cho hệ thống microservices với 20 services
Trả lời (comprehensive):
┌──────────────┐
│ E2E Tests │
│ (Critical flows)│
└───────┬──────┘
│
┌───────────────┼───────────────┐
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│Contract │ │Contract │ │Contract │
│ Tests │ │ Tests │ │ Tests │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
┌────▼────┐ ┌────▼────┐ ┌────▼────┐
│Service A│ │Service B│ │Service C│
│ Tests │ │ Tests │ │ Tests │
└─────────┘ └─────────┘ └─────────┘
Unit + Unit + Unit +
Integration Integration Integration
Strategy:
1. Unit tests (per service)
- Coverage: 80% minimum
- Run: On every PR, locally before commit
- Scope: Business logic, validation, edge cases
2. Integration tests (per service)
- Coverage: API endpoints + DB interactions
- Run: Pre-merge CI pipeline
- Tools: Testcontainers for real dependencies
- Isolation: Each test gets clean DB state
3. Contract tests (between services)
- Coverage: All service-to-service API calls
- Run: On API changes
- Tools: Pact
- Benefit: Catch breaking changes before deployment
4. E2E tests (critical paths only)
- Coverage: 5-10 critical user journeys
- Run: Post-deployment to staging
- Scope: Signup → Order → Payment → Notification
- Tools: Playwright, Cypress
5. Performance tests
- Coverage: High-traffic endpoints
- Run: Weekly, before major releases
- Tools: k6, Gatling
- Metrics: p50, p95, p99 latency; throughput
6. Chaos testing
- Coverage: Failure scenarios (node down, network partition)
- Run: Staging environment, quarterly
- Tools: Chaos Mesh, Gremlin
Test data strategy:
- Synthetic data for unit/integration tests
- Anonymized production data for performance tests
- Golden files for regression tests
CI/CD pipeline:
PR → Unit + Integration → Contract tests → Merge
→ Deploy to Staging → E2E tests → Deploy to Prod
Mental Models
Testing Pyramid
▲
/ E2E \ ← Slow, expensive, high confidence
/───────\
/ Integra-\ ← Medium speed, medium cost
/ tion \
/─────────────\
/ Unit Tests \ ← Fast, cheap, lower confidence
/_______________\
Testing Trophy (Kent C. Dodds)
▲
/E2E\
/─────\
/ Inte- \ ← Focus here (most value)
/ gration\
/───────────\
/ Unit \
/_____________/
Insight: Integration tests give best ROI cho web apps.
Tóm tắt: Testing Best Practices
✅ Write tests that test behavior, not implementation
✅ Fast feedback loop > exhaustive coverage
✅ Isolate tests (no shared state, no order dependency)
✅ Use real dependencies in integration tests (testcontainers)
✅ Mock external APIs, not your own code
✅ Test unhappy paths (errors, timeouts, edge cases)
✅ Descriptive test names: Test_UserService_CreateUser_DuplicateEmail_ReturnsError
✅ Run tests in CI on every PR
✅ Keep tests maintainable (DRY, helper functions)
✅ Property-based testing for complex logic
Resources
Books:
- "Working Effectively with Legacy Code" — Michael Feathers
- "Growing Object-Oriented Software, Guided by Tests" — Steve Freeman, Nat Pryce
- "Unit Testing Principles, Practices, and Patterns" — Vladimir Khorikov
Tools:
- Go testing:
testing,testify,gomock - Testcontainers: Real dependencies in tests
- Pact: Contract testing
- k6, Gatling: Load testing
- Playwright, Cypress: E2E testing
Further reading:
- Martin Fowler's testing articles: https://martinfowler.com/testing/
- Google Testing Blog: https://testing.googleblog.com/
💡 Final thought: "Tests are not about proving your code is correct. They're about giving you confidence to change it." Nếu test khiến bạn sợ refactor → test đó cần được refactor.