1
\$\begingroup\$

I am writing a Go package that handles communication with BoltDB as a part of some larger project. I have 2 methods for interacting with DB.

func Lookup(path string) (string, bool) {}
func RegisterUrl(path, url string) error {}

Lookup makes a request to database to retrieve corresponding url, whereas RegisterUrl writes path:url key-value pair to DB.

I would like to ask for a review of my implementation and some advice on how to implement unit tests.

Unit Testing

I've included the skeleton for unit tests that I would like to have for above 2 functions. However I have trouble figuring what is the best way to implement them.

  • I wouldn't like to test methods using the "prod" database/bucket, rather then that I would prefer to swap prod DB to test DB in the database_test.go file.
  • I thought about making dbName and bucketName public variables in database.go and then change them in _test file for test db, but this seems like I would be breaking the encapsulation.
  • I also considered passing test database/bucket to functions as functional options, but this seems like it will make code more complicated and I am not sure if this is the best approach.

I would be happy to hear your advice.

database.go

// Package database handles communication with BoltDB.
package database

import (
    "log"
    "time"

    "github.com/boltdb/bolt"
)

var dbOptions *bolt.Options = &bolt.Options{
    Timeout: 1 * time.Second,
}

const (
    dbName     = "urls.db"
    bucketName = "UrlFromPath"
)

func init() {
    // Make sure that the database exists.
    db, err := bolt.Open(dbName, 0600, dbOptions)
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()
    // Make sure that UrlFromPath bucket exists.
    err = db.Update(func(tx *bolt.Tx) error {
        _, err := tx.CreateBucketIfNotExists([]byte(bucketName))
        if err != nil {
            return err
        }
        return nil
    })
    if err != nil {
        log.Fatal(err)
    }
}

// Lookup checks if `path` key exists in database and returns related `url` value if found. If not
// found second return value `ok` is set to false.
func Lookup(path string) (url string, ok bool) { // TODO: return
    // Open connection to DB.
    db, err := bolt.Open(dbName, 0600, dbOptions)
    if err != nil {
        log.Fatalf("Cannot connect to DB: %s", err)
        return "", false
    }
    defer db.Close()

    // Retreive url from DB.
    var url_bytes []byte
    err = db.View(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(bucketName))
        url_bytes = b.Get([]byte(path))
        return nil
    })
    if err != nil {
        return "", false
    }
    if url_bytes != nil {
        return string(url_bytes), true
    } else {
        return "", false
    }

}

// RegisterUrl saves provided key:value (path:url) pair to database.
func RegisterUrl(path, url string) error {
    // Open connection to DB.
    db, err := bolt.Open(dbName, 0600, dbOptions)
    if err != nil {
        return err
    }
    defer db.Close()

    // Save path and url.
    err = db.Update(func(tx *bolt.Tx) error {
        b := tx.Bucket([]byte(bucketName))
        err := b.Put([]byte(path), []byte(url))
        return err
    })
    if err != nil {
        return err
    }

    log.Printf("Database: %s, %s registered.", path, url)
    return nil
}

database_test.go

package database

import "testing"

func setup() {
    // Init test db.
    // Init test bucket.
}

func cleanup() {
    // Remove test db.
}

func TestLookup(t *testing.T) {
    // T1. Path exists in db. Correct url is retrieved.

    // T2. Path doesn't exist in db. false is returned.

}

func TestRegisterUrl(t *testing.T) {
    // T1. Add path:url to db. Correct path:url pair has been added to db.
}
```
\$\endgroup\$
1
\$\begingroup\$

I think the code could benefit a struct for the database instead of global state. You can use a New method instead of init() where you pass in the params, which would differet from test and production.

I also demonstrated that you can keep the connection open and reuse it within the struct. If you prefer a lazy approach, you can omit this and create a the connection in each call if you want.

type Database struct {
   bucketName string 
   boltDB *bolt.DB
}

func New(dbName, bucketName string, opts *bolt.Options) (db *Database, error) {
   db, err := bolt.Open(dbName, 0600, opts)
   return &Database{
      boltDb: db,
      bucketName: bucketName
   }, nil
}

func(db *Database) Lookup(path string) (url string, ok bool) { // lookup logic }
func(db *Database) RegisterUrl(path, url string) error { //register logic}
```
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.