Skip to content

Commit

Permalink
Add more unit test
Browse files Browse the repository at this point in the history
  • Loading branch information
pauluswi committed Nov 17, 2024
1 parent a67ca19 commit c482731
Show file tree
Hide file tree
Showing 2 changed files with 149 additions and 6 deletions.
42 changes: 36 additions & 6 deletions distributed/redis/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,41 @@ type Transaction struct {

// RedisLock represents a distributed lock
type RedisLock struct {
client *redis.Client
client RedisClient
key string
value string
}

// RedisClient defines the interface for a Redis client
type RedisClient interface {
SetNX(key, value string, ttl time.Duration) (bool, error)
Get(key string) (string, error)
Del(key string) (int64, error)
}

// RedisAdapter wraps *redis.Client to implement RedisClient
type RedisAdapter struct {
client *redis.Client
}

func (r *RedisAdapter) SetNX(key, value string, ttl time.Duration) (bool, error) {
result, err := r.client.SetNX(ctx, key, value, ttl).Result()
return result, err
}

func (r *RedisAdapter) Get(key string) (string, error) {
result, err := r.client.Get(ctx, key).Result()
return result, err
}

func (r *RedisAdapter) Del(key string) (int64, error) {
result, err := r.client.Del(ctx, key).Result()
return result, err
}

// AcquireLock tries to acquire the lock
func (lock *RedisLock) AcquireLock(ttl time.Duration) (bool, error) {
success, err := lock.client.SetNX(ctx, lock.key, lock.value, ttl).Result()
success, err := lock.client.SetNX(lock.key, lock.value, ttl)
if err != nil {
return false, err
}
Expand All @@ -41,7 +68,7 @@ func (lock *RedisLock) AcquireLock(ttl time.Duration) (bool, error) {

// ReleaseLock releases the lock
func (lock *RedisLock) ReleaseLock() error {
val, err := lock.client.Get(ctx, lock.key).Result()
val, err := lock.client.Get(lock.key)
if err == redis.Nil {
return fmt.Errorf("lock not found")
} else if err != nil {
Expand All @@ -50,7 +77,7 @@ func (lock *RedisLock) ReleaseLock() error {

// Ensure the lock is released by the process that acquired it
if val == lock.value {
_, err = lock.client.Del(ctx, lock.key).Result()
_, err = lock.client.Del(lock.key)
if err != nil {
return err
}
Expand All @@ -59,7 +86,7 @@ func (lock *RedisLock) ReleaseLock() error {
}

// ProcessTransaction processes a single transaction on an account with distributed locking
func ProcessTransaction(account *Account, transaction Transaction, rdb *redis.Client, wg *sync.WaitGroup) {
func ProcessTransaction(account *Account, transaction Transaction, rdb RedisClient, wg *sync.WaitGroup) {
defer wg.Done()

lock := RedisLock{
Expand Down Expand Up @@ -95,11 +122,14 @@ func ProcessTransaction(account *Account, transaction Transaction, rdb *redis.Cl

func main() {
// Initialize Redis client
rdb := redis.NewClient(&redis.Options{
redisClient := redis.NewClient(&redis.Options{
Addr: "localhost:6379", // Adjust based on your setup
DB: 0,
})

// Wrap Redis client with adapter
rdb := &RedisAdapter{client: redisClient}

// Create accounts
accounts := map[string]*Account{
"11111": {AccountNumber: "11111", Balance: 1000},
Expand Down
113 changes: 113 additions & 0 deletions distributed/redis/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
package main

import (
"sync"
"testing"
"time"
)

// MockRedisClient simulates a Redis client for testing
type MockRedisClient struct {
locks map[string]string
mu sync.Mutex
}

func NewMockRedisClient() *MockRedisClient {
return &MockRedisClient{locks: make(map[string]string)}
}

func (m *MockRedisClient) SetNX(key, value string, ttl time.Duration) (bool, error) {
m.mu.Lock()
defer m.mu.Unlock()

if _, exists := m.locks[key]; exists {
return false, nil
}

m.locks[key] = value
go func() {
time.Sleep(ttl)
m.mu.Lock()
defer m.mu.Unlock()
if m.locks[key] == value {
delete(m.locks, key)
}
}()

return true, nil
}

func (m *MockRedisClient) Get(key string) (string, error) {
m.mu.Lock()
defer m.mu.Unlock()

value, exists := m.locks[key]
if !exists {
return "", nil
}
return value, nil
}

func (m *MockRedisClient) Del(key string) (int64, error) {
m.mu.Lock()
defer m.mu.Unlock()

if _, exists := m.locks[key]; exists {
delete(m.locks, key)
return 1, nil
}
return 0, nil
}

func TestProcessTransactionWithMockRedis(t *testing.T) {
// Initialize mock Redis client
rdb := NewMockRedisClient()

// Setup accounts
accounts := map[string]*Account{
"11111": {AccountNumber: "11111", Balance: 1000},
"22222": {AccountNumber: "22222", Balance: 2000},
}

// List of transactions
transactions := []Transaction{
{"11111", -200}, // Debit
{"11111", 300}, // Credit
{"22222", -500}, // Debit
{"22222", -3000}, // Insufficient funds
{"11111", 100}, // Credit
}

// Expected final balances
expectedBalances := map[string]float64{
"11111": 1200,
"22222": 1500,
}

var wg sync.WaitGroup

// Process transactions concurrently
for _, tr := range transactions { // Rename loop variable to 'tr'
account, exists := accounts[tr.AccountNumber]
if !exists {
t.Errorf("Account %s not found", tr.AccountNumber) // Use correct testing object
continue
}

wg.Add(1)
go func(acc *Account, tr Transaction) {
defer wg.Done()
ProcessTransaction(acc, tr, rdb, &wg)
}(account, tr)
}

wg.Wait() // Wait for all transactions to complete

// Verify final balances
for accountNumber, account := range accounts {
if account.Balance != expectedBalances[accountNumber] {
t.Errorf("Account %s: expected balance %.2f, got %.2f",
account.AccountNumber, expectedBalances[accountNumber], account.Balance)
}
}
}

0 comments on commit c482731

Please sign in to comment.