Tutorial: Repository pattern in Golang with Test Driven Development

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 id

  • Get: Fetches an invoices from the database using its id

  • GetLastInvoiceForYear: 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:

github invoice-repo 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

💡
What challenges have you faced while implementing the repository pattern in your projects?