Tutorial: Repository pattern in Golang with Test Driven Development
Building a real world application in Go
In the last blog post I showed you how you can implement the repository pattern in Golang. We used SQLite and GORM to implement the CompanyRepository
.
Today I am going to show you how to implement the InvoiceRepository
.
The implementation will be very similar but today we are going to start first by writing unit tests and then the actual implementation.
Create a new branch
We will work in a new git branch. Let's create this:
git checkout -b invoice-repo
Our interface
We have to implement a struct that implements these three methods:
Create
: Inserts an invoice in the database and returns it's idGet
: Fetches an invoices from the database using its idGetLastInvoiceForYear
: Returns the last invoice from the provided year.
Writing a test first
In which package are we going to write our tests?
In our project layout we have a package sqlite
. This is where we are going to write our code.
Now create a new file named invoice_test.go
.
Let's write our tests:
package sqlite_test
import (
"testing"
)
func Test_invoiceRepository(t *testing.T) {
}
We don't test anything so far but let's verify that we can run our test:
go test -v ./... -run=Test_invoiceRepository
the output should be similar to:
Provide a dummy implementation of the interface
Create a file sqlite/invoice.go
with contents:
package sqlite
import (
"context"
"invoicehub"
"gorm.io/gorm"
)
var _ invoicehub.InvoiceRepository = &invoiceRepository{}
type invoiceRepository struct {
db *gorm.DB
}
func NewInvoiceRepository(db *gorm.DB) invoicehub.InvoiceRepository {
return &invoiceRepository{db}
}
func (r *invoiceRepository) Create(ctx context.Context, invoice *invoicehub.Invoice) (int, error) {
return 0, nil
}
func (r *invoiceRepository) Get(ctx context.Context, id int) (invoicehub.Invoice, error) {
return invoicehub.Invoice{}, nil
}
func (r *invoiceRepository) GetLastInvoiceForYear(ctx context.Context, year int) (invoicehub.Invoice, error) {
return invoicehub.Invoice{}, nil
}
We only wrote the required methods but they are empty.
We now create our tests:
func compareInvoice(t *testing.T, a, b invoicehub.Invoice) {
require.Equal(t, a.ID, b.ID, "ID mismatch")
require.Equal(t, a.InvoiceNumber, b.InvoiceNumber, "InvoiceNumber mismatch")
require.Equal(t, a.IssueDate, b.IssueDate, "IssueDate mismatch")
require.Equal(t, a.SellerID, b.SellerID, "SellerID mismatch")
require.Equal(t, a.BuyerID, b.BuyerID, "BuyerID mismatch")
require.Equal(t, a.DaysToPay, b.DaysToPay, "DaysToPay mismatch")
require.Len(t, a.LineItems, len(b.LineItems), "LineItems length mismatch")
for i := range a.LineItems {
require.Equal(t, a.LineItems[i].Description, b.LineItems[i].Description, "Description mismatch")
require.Equal(t, a.LineItems[i].Amount.Currency, b.LineItems[i].Amount.Currency, "Currency mismatch")
require.Equal(t, a.LineItems[i].Amount.Value.String(), b.LineItems[i].Amount.Value.String(), "Amount value mismatch")
}
}
func Test_invoiceRepository(t *testing.T) {
db, err := sqlite.SetupDB(":memory:")
require.NoError(t, err)
repo := sqlite.NewInvoiceRepository(db)
require.NotNil(t, repo)
ctx := context.Background()
invoice := invoicehub.Invoice{
InvoiceNumber: "1001/24",
IssueDate: time.Date(2024, 1, 2, 0, 0, 0, 0, time.UTC),
SellerID: 1,
BuyerID: 2,
DaysToPay: 14,
LineItems: []invoicehub.LineItem{
{
Description: "My awesome services",
Amount: invoicehub.Amount{
Value: decimal.NewFromFloat(100.00),
Currency: "EUR",
},
VatRate: decimal.NewFromFloat(0.19),
},
},
}
// Create a new invoice
id, err := repo.Create(ctx, &invoice)
require.NoError(t, err)
require.NotZero(t, id)
require.Equal(t, id, invoice.ID)
// fetch the invoice
invoice2, err := repo.Get(ctx, id)
require.NoError(t, err)
compareInvoice(t, invoice, invoice2)
// fetch a non-existing invoice
_, err = repo.Get(ctx, 999)
require.Error(t, err)
// add one more invoice
invoice3 := invoicehub.Invoice{
InvoiceNumber: "1002/24",
IssueDate: time.Date(2024, 2, 2, 0, 0, 0, 0, time.UTC),
SellerID: 1,
BuyerID: 2,
DaysToPay: 14,
LineItems: []invoicehub.LineItem{
{
Description: "web development",
Amount: invoicehub.Amount{
Value: decimal.NewFromFloat(100.00),
Currency: "EUR",
},
VatRate: decimal.NewFromFloat(0.19),
},
{
Description: "web scraping",
Amount: invoicehub.Amount{
Value: decimal.NewFromFloat(150.00),
Currency: "EUR",
},
VatRate: decimal.NewFromFloat(0.19),
},
},
}
id3, err := repo.Create(ctx, &invoice3)
require.NoError(t, err)
require.NotZero(t, id3)
require.Equal(t, id3, invoice3.ID)
// fetch the last invoice for the year 2024
lastInvoice, err := repo.GetLastInvoiceForYear(ctx, 2024)
require.NoError(t, err)
compareInvoice(t, invoice3, lastInvoice)
}
In the above test:
create an invoice
assert that our function returns a newly generated id
assert that the id generated is also set in our Invoice struct
try to fetch the invoice with that id
try to fetch a non existing invoice
create another invoice for a later date
assert that the last invoice of the year is the last created
We are using the package decimal.Decimal and testify package does not play well with it (bacause the way it uses reflection I think) . Thus, we created a custom compareInvoice function that performs the assertions
now run the test again:
As expected our test fails . We don't have any implementation yet.
Separate our domain entity (invoicehub.Invoice) from the database models
In sqlite/invoice.go
let's define the required struct that reassembles our database table:
type dbinvoice struct {
ID int `gorm:"primaryKey"`
InvoiceNumber string `gorm:"type:text"`
IssueDate time.Time `gorm:"type:datetime"`
BuyerID int
SellerID int
DaysToPay int
LineItems datatypes.JSONSlice[invoicehub.LineItem]
}
above we defined the db model (the table will be generated from that).
In order for Gorm to take care of the table creation modify the sqlite/sqlite.go
.
an attentive reader will have already noticed that the sellerID and buyerID fields do not have foreign keys that reference the dbcompanies table.
We will leave these out for now. However this might lead to inconsistencies in our data at some point.
Implement the Create method
func (r *invoiceRepository) Create(ctx context.Context, invoice *invoicehub.Invoice) (int, error) {
dbInvoice := dbinvoice{
InvoiceNumber: invoice.InvoiceNumber,
IssueDate: invoice.IssueDate,
BuyerID: invoice.BuyerID,
SellerID: invoice.SellerID,
DaysToPay: invoice.DaysToPay,
LineItems: datatypes.NewJSONSlice(invoice.LineItems),
}
if err := r.db.WithContext(ctx).Create(&dbInvoice).Error; err != nil {
return 0, err
}
invoice.ID = dbInvoice.ID
return invoice.ID, nil
}
Implement the Get method
func (r *invoiceRepository) Get(ctx context.Context, id int) (invoicehub.Invoice, error) {
var dbitem dbinvoice
if err := r.db.WithContext(ctx).First(&dbitem, id).Error; err != nil {
return invoicehub.Invoice{}, err
}
invoice := invoicehub.Invoice{
ID: dbitem.ID,
InvoiceNumber: dbitem.InvoiceNumber,
IssueDate: dbitem.IssueDate,
BuyerID: dbitem.BuyerID,
SellerID: dbitem.SellerID,
DaysToPay: dbitem.DaysToPay,
LineItems: dbitem.LineItems,
}
return invoice, nil
}
Implement the GetLastInvoiceForYear
func (r *invoiceRepository) GetLastInvoiceForYear(ctx context.Context, year int) (invoicehub.Invoice, error) {
query := r.db.WithContext(ctx).
Where("strftime('%Y', issue_date) = ?", strconv.Itoa(year)).
Order("issue_date desc").
Limit(1)
var dbitem dbinvoice
if err := query.First(&dbitem).Error; err != nil {
return invoicehub.Invoice{}, err
}
invoice := invoicehub.Invoice{
ID: dbitem.ID,
InvoiceNumber: dbitem.InvoiceNumber,
IssueDate: dbitem.IssueDate,
BuyerID: dbitem.BuyerID,
SellerID: dbitem.SellerID,
DaysToPay: dbitem.DaysToPay,
LineItems: dbitem.LineItems,
}
return invoice, nil
}
This one is a little bit more tricky.
We utilize the strftime
of SQLite in order to fetch all invoices with issue date in the provided year.
We don't have an index in the IssueDate column. This query might be slow when we have a lot of data.
But, since this web application will be only for one user we will skip creating index beforehand. We will do this when it's required. But it's good to keep that in mind.
Now we run our tests again:
It looks we are good so let's commit
git add .
git commit -m "implements invoice repository"
You can find today's version in the github branch:
Conclusion
This blog posted shows the reader how to implement the repository pattern in Go using GORM and SQLite.
We seen how we can write unittests first for our database related code and then complete the implementation.
We once again flagged the need to separate your database models with your domain entities.
❤️ If you read the article until here and liked it please hit the like button.
👉 I would love you to follow me on X or LinkedIn