Skip to content

Commit

Permalink
Add tests for the storage package
Browse files Browse the repository at this point in the history
- Tests for all storage implementations
- Tests basic functionality (read and write) as well as concurrency

For #10
  • Loading branch information
philippgille committed Sep 7, 2018
1 parent 2b0872b commit 16dcdfc
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 6 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
sudo: required

services:
- redis-server
- docker

git:
Expand Down
56 changes: 54 additions & 2 deletions storage/bolt_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package storage_test

import (
"math/rand"
"os"
"strconv"
"sync"
"testing"

"github.com/philippgille/ln-paywall/ln"
"github.com/philippgille/ln-paywall/storage"
"github.com/philippgille/ln-paywall/wall"
)

// TestBoltClient tests if the BoltClient struct implements the StorageClient interface.
// TestBoltClientImpl tests if the BoltClient struct implements the StorageClient interface.
// This doesn't happen at runtime, but at compile time.
func TestBoltClient(t *testing.T) {
func TestBoltClientImpl(t *testing.T) {
t.SkipNow()
invoiceOptions := wall.InvoiceOptions{}
lnClient := ln.LNDclient{}
Expand All @@ -20,3 +24,51 @@ func TestBoltClient(t *testing.T) {
wall.NewGinMiddleware(invoiceOptions, lnClient, boltClient)
wall.NewEchoMiddleware(invoiceOptions, lnClient, boltClient, nil)
}

// TestBoltClient tests if reading and writing to the storage works properly.
func TestBoltClient(t *testing.T) {
boltOptions := storage.BoltOptions{
Path: generateRandomTempDbPath(),
}
boltClient, err := storage.NewBoltClient(boltOptions)
if err != nil {
t.Error(err)
}

testStorageClient(boltClient, t)
}

// TestBoltClientConcurrent launches a bunch of goroutines that concurrently work with one BoltClient.
// The BoltClient works with a single file, so everything should be locked properly.
// The locking is implemented in the bbolt package, but test it nonetheless.
func TestBoltClientConcurrent(t *testing.T) {
boltOptions := storage.BoltOptions{
Path: generateRandomTempDbPath(),
}
boltClient, err := storage.NewBoltClient(boltOptions)
if err != nil {
t.Error(err)
}

goroutineCount := 1000

waitGroup := sync.WaitGroup{}
waitGroup.Add(goroutineCount) // Must be called before any goroutine is started
for i := 0; i < goroutineCount; i++ {
go interactWithStorage(boltClient, strconv.Itoa(i), t, &waitGroup)
}
waitGroup.Wait()

// Now make sure that all values are in the storage
expected := true
for i := 0; i < goroutineCount; i++ {
actual, _ := boltClient.WasUsed(strconv.Itoa(i))
if actual != expected {
t.Errorf("Expected: %v, but was: %v", expected, actual)
}
}
}

func generateRandomTempDbPath() string {
return os.TempDir() + "/" + strconv.FormatInt(rand.Int63(), 10) + ".db"
}
37 changes: 35 additions & 2 deletions storage/map_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package storage_test

import (
"strconv"
"sync"
"testing"

"github.com/philippgille/ln-paywall/ln"
"github.com/philippgille/ln-paywall/storage"
"github.com/philippgille/ln-paywall/wall"
)

// TestGoMap tests if the GoMap struct implements the StorageClient interface.
// TestGoMapImpl tests if the GoMap struct implements the StorageClient interface.
// This doesn't happen at runtime, but at compile time.
func TestGoMap(t *testing.T) {
func TestGoMapImpl(t *testing.T) {
t.SkipNow()
invoiceOptions := wall.InvoiceOptions{}
lnClient := ln.LNDclient{}
Expand All @@ -19,3 +21,34 @@ func TestGoMap(t *testing.T) {
wall.NewHandlerMiddleware(invoiceOptions, lnClient, goMap)
wall.NewGinMiddleware(invoiceOptions, lnClient, goMap)
}

// TestGoMap tests if reading and writing to the storage works properly.
func TestGoMap(t *testing.T) {
goMap := storage.NewGoMap()

testStorageClient(goMap, t)
}

// TestGoMapConcurrent launches a bunch of goroutines that concurrently work with one GoMap.
// The GoMap is a sync.Map, so the concurrency should be supported by the used package.
func TestGoMapConcurrent(t *testing.T) {
goMap := storage.NewGoMap()

goroutineCount := 1000

waitGroup := sync.WaitGroup{}
waitGroup.Add(goroutineCount) // Must be called before any goroutine is started
for i := 0; i < goroutineCount; i++ {
go interactWithStorage(goMap, strconv.Itoa(i), t, &waitGroup)
}
waitGroup.Wait()

// Now make sure that all values are in the storage
expected := true
for i := 0; i < goroutineCount; i++ {
actual, _ := goMap.WasUsed(strconv.Itoa(i))
if actual != expected {
t.Errorf("Expected: %v, but was: %v", expected, actual)
}
}
}
86 changes: 84 additions & 2 deletions storage/redis_test.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,25 @@
package storage_test

import (
"log"
"strconv"
"sync"
"testing"

"github.com/go-redis/redis"

"github.com/philippgille/ln-paywall/ln"
"github.com/philippgille/ln-paywall/storage"
"github.com/philippgille/ln-paywall/wall"
)

// TestRedisClient tests if the RedisClient struct implements the StorageClient interface.
// Don't use the default number ("0"),
// which could lead to valuable data being deleted when a developer accidentally runs the test with valuable data in DB 0.
var testDbNumber = 15 // 16 DBs by default (unchanged config), starting with 0

// TestRedisClientImpl tests if the RedisClient struct implements the StorageClient interface.
// This doesn't happen at runtime, but at compile time.
func TestRedisClient(t *testing.T) {
func TestRedisClientImpl(t *testing.T) {
t.SkipNow()
invoiceOptions := wall.InvoiceOptions{}
lnClient := ln.LNDclient{}
Expand All @@ -19,3 +28,76 @@ func TestRedisClient(t *testing.T) {
wall.NewHandlerMiddleware(invoiceOptions, lnClient, redisClient)
wall.NewGinMiddleware(invoiceOptions, lnClient, redisClient)
}

// TestRedisClient tests if reading and writing to the storage works properly.
//
// Note: This test is only executed if the initial connection to Redis works.
func TestRedisClient(t *testing.T) {
if !checkRedisConnection(testDbNumber) {
t.Skip("No connection to Redis could be established. Probably not running in a proper test environment.")
}

deleteRedisDb(testDbNumber) // Prep for previous test runs
redisOptions := storage.RedisOptions{
DB: testDbNumber,
}
redisClient := storage.NewRedisClient(redisOptions)

testStorageClient(redisClient, t)
}

// TestRedisClientConcurrent launches a bunch of goroutines that concurrently work with the Redis client.
func TestRedisClientConcurrent(t *testing.T) {
if !checkRedisConnection(testDbNumber) {
t.Skip("No connection to Redis could be established. Probably not running in a proper test environment.")
}

deleteRedisDb(testDbNumber) // Prep for previous test runs
redisOptions := storage.RedisOptions{
DB: testDbNumber,
}
redisClient := storage.NewRedisClient(redisOptions)

goroutineCount := 1000

waitGroup := sync.WaitGroup{}
waitGroup.Add(goroutineCount) // Must be called before any goroutine is started
for i := 0; i < goroutineCount; i++ {
go interactWithStorage(redisClient, strconv.Itoa(i), t, &waitGroup)
}
waitGroup.Wait()

// Now make sure that all values are in the storage
expected := true
for i := 0; i < goroutineCount; i++ {
actual, _ := redisClient.WasUsed(strconv.Itoa(i))
if actual != expected {
t.Errorf("Expected: %v, but was: %v", expected, actual)
}
}
}

// checkRedisConnection returns true if a connection could be made, false otherwise.
func checkRedisConnection(number int) bool {
redisClient := redis.NewClient(&redis.Options{
Addr: storage.DefaultRedisOptions.Address,
Password: storage.DefaultRedisOptions.Password,
DB: number,
})
err := redisClient.Ping().Err()
if err != nil {
log.Printf("An error occurred during testing the connection to Redis: %v\n", err)
return false
}
return true
}

// deleteRedisDb deletes all entries of the given DB
func deleteRedisDb(number int) error {
redisClient := redis.NewClient(&redis.Options{
Addr: storage.DefaultRedisOptions.Address,
Password: storage.DefaultRedisOptions.Password,
DB: number,
})
return redisClient.FlushDB().Err()
}
63 changes: 63 additions & 0 deletions storage/storage_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package storage_test

import (
"math/rand"
"strconv"
"sync"
"testing"

"github.com/philippgille/ln-paywall/wall"
)

// testStorageClient tests if reading and writing to the storage works properly.
func testStorageClient(storageClient wall.StorageClient, t *testing.T) {
key := strconv.FormatInt(rand.Int63(), 10)

// Initially the key shouldn't exist, thus "was used" should be false
expected := false
actual, err := storageClient.WasUsed(key)
if err != nil {
t.Error(err)
}
if actual != expected {
t.Errorf("Expected: %v, but was: %v", expected, actual)
}

// Set the "key" to be used
err = storageClient.SetUsed(key)
if err != nil {
t.Error(err)
}

// Check usage again, this time "was used" should be true
expected = true
actual, err = storageClient.WasUsed(key)
if err != nil {
t.Error(err)
}
if actual != expected {
t.Errorf("Expected: %v, but was: %v", expected, actual)
}
}

// interactWithStorage reads from and writes to the DB. Meant to be executed in a goroutine.
// Does NOT check if the DB works correctly (that's done by another test), only checks for errors
func interactWithStorage(storageClient wall.StorageClient, key string, t *testing.T, waitGroup *sync.WaitGroup) {
defer waitGroup.Done()

// Read
_, err := storageClient.WasUsed(key)
if err != nil {
t.Error(err)
}
// Write
err = storageClient.SetUsed(key)
if err != nil {
t.Error(err)
}
// Read
_, err = storageClient.WasUsed(key)
if err != nil {
t.Error(err)
}
}

0 comments on commit 16dcdfc

Please sign in to comment.