DEV Community

Cover image for golang unit testing
Lakshan Dissanayake
Lakshan Dissanayake

Posted on

golang unit testing

Introduction

Test driven development also known as TDD is a good software development practice followed by all over the software industry. With unit tests we can test a small pieces of our software independently what makes it "unit".

Problem

Let's assume you're making some changes to a function which was developed by someone else previously. with the developer test you might think, it is safe to deploy. But what will happen if you missed a specific test scenario? probably it will lead to a software crash if exceptions are not properly handled. panics in go's context.

Solution

If we have already developed unit tests, then we can run it to test whether we broke the code or not. It will execute a list of specific scenarios of the software and shows if some flows are broken. if there is not unit test, I highly recommend you to implement it first, then do the changes to the function.

Libraries we can use to write unit tests

I use gomock or mockery for mocking the interfaces and testify for evaluating tests

Let'a have a look how its implemented

Sms Provider

package dependency
//go:generate mockgen -package=dependency -destination=sms_provider_mocks.go . SmsProvider
type SmsProvider interface {
SendSMS(number string, content string) (string, error)
}
type smsProviderImpl struct {
}
func (svc *smsProviderImpl) SendSMS(number string, content string) (string, error) {
// send sms
return "message-id", nil
}
view raw sms_provider.go hosted with ❤ by GitHub

Email Provider

package dependency
//go:generate mockgen -package=dependency -destination=email_provider_mocks.go . EmailProvider
type EmailProvider interface {
SendEmail(from, to, subject, body string) (string, error)
}
type emailProviderImpl struct{}
func (e *emailProviderImpl) SendEmail(from, to, subject, body string) (string, error) {
//send email
return "message-id", nil
}

And suppose notification service, which validates phone numbers and email and calls sms provider and email provider to send actual email and test messages

package service
import (
"fmt"
"regexp"
"testing-demo/dependency"
)
type NotificationService interface {
SendTextNotification(number string, message string) (string, error)
SendEmailNotification(from, to, subject, body string) (string, error)
}
type notifySvc struct {
sms dependency.SmsProvider
email dependency.EmailProvider
}
func (client *notifySvc) SendTextNotification(number string, message string) (string, error) {
match, err := regexp.MatchString(`^(\+\d{1,2}\s)?\(?\d{3}\)?[\s.-]\d{3}[\s.-]\d{4}$`, number)
if err != nil {
return "", err
} else if !match {
return "", fmt.Errorf("%s is not a valid phone number", number)
}
return client.sms.SendSMS(number, message)
}
func (client *notifySvc) SendEmailNotification(from, to, subject, body string) (string, error) {
match, err := regexp.MatchString(`^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$`, to)
if err != nil {
return "", err
} else if !match {
return "", fmt.Errorf("%s is not a valid email address", to)
}
return client.email.SendEmail(from, to, subject, body)
}
func NewNotificationClient(sms dependency.SmsProvider, email dependency.EmailProvider) NotificationService {
return &notifySvc{sms: sms, email: email}
}

The unit tests of the notification service can be written as the following

package service
import (
"testing"
"testing-demo/dependency"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
func Test_Notification_Service(t *testing.T) {
ctrl := gomock.NewController(t)
defer ctrl.Finish()
t.Run("SendTextNotification", func(t *testing.T) {
mockSmsSvc := dependency.NewMockSmsProvider(ctrl)
notifyClient := NewNotificationClient(mockSmsSvc, nil)
t.Run("with invalid phone number", func(t *testing.T) {
number, message := "1233452345", "message"
messageID, err := notifyClient.SendTextNotification(number, message)
assert.Empty(t, messageID)
assert.NotNil(t, err)
})
t.Run("with valid phone number", func(t *testing.T) {
number, message := "012 234 2345", "message"
mockSmsSvc.EXPECT().SendSMS(number, message).Return("message-id", nil)
messageID, err := notifyClient.SendTextNotification(number, message)
assert.NotEmpty(t, messageID)
assert.Nil(t, err)
})
})
t.Run("SendEmailNotification", func(t *testing.T) {
mockEmailSvc := dependency.NewMockEmailProvider(ctrl)
notifyClient := NewNotificationClient(nil, mockEmailSvc)
t.Run("with invalid email address", func(t *testing.T) {
from, to, subject, body := "from", "to", "subject", "body"
messageID, err := notifyClient.SendEmailNotification(from, to, subject, body)
assert.Empty(t, messageID)
assert.NotNil(t, err)
})
t.Run("with valid email address", func(t *testing.T) {
from, to, subject, body := "from", "johndoe@example.com", "subject", "body"
mockEmailSvc.EXPECT().SendEmail(from, to, subject, body).Return("message-id", nil)
messageID, err := notifyClient.SendEmailNotification(from, to, subject, body)
assert.NotEmpty(t, messageID)
assert.Nil(t, err)
})
})
}

Billboard image

The Next Generation Developer Platform

Coherence is the first Platform-as-a-Service you can control. Unlike "black-box" platforms that are opinionated about the infra you can deploy, Coherence is powered by CNC, the open-source IaC framework, which offers limitless customization.

Learn more

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more