README ¶
Golang Best Practices — Behavior-driven development and Continuous Integration
In software engineering, behavior-driven development ( BDD) is a software development process that emerged from test-driven development (TDD). The behavior-driven-development combines the general techniques and principles of TDD with ideas from domain-driven design and object-oriented analysis and design to provide software development and management teams with shared tools and a shared process to collaborate on software development. ( Wiki link)
The focus of BDD is the language and interactions used in the process of software development. Behavior-driven developers use their native language in combination with the ubiquitous language of Domain Driven Design to describe the purpose and benefit of their code. This allows the developers to focus on why the code should be created, rather than the technical details and minimizes translation between the technical language in which the code is written and the domain language spoken by the business, users, stakeholders, project management etc.
This article will give you a quick introduction on how to get started with BDD (Behavior Driven Development) and Continuous integration in Golang.
For BDD, we will use GoConvey and for CI we will use Travis-CI
We’ll cover:
- Prerequisites for this tutorial
- Setting up your application
- Walkthrough of Sample Code
- Single Responsibility Principle
- Interface for loose coupling
- Mockery to generate mocks
- GoConvey for BDD
- Travis-CI setup
- Coveralls.io setup
Prerequisites for the Golang tutorial
- Basic knowledge of GO language
- IDE — Gogland by Jetbrains or Visual Studio Code by Microsoft or Atom
- Go through Playing with Github API with GO-GITHUB Golang library** **article.
Setting up your application
It’s time to make our hands dirty. Open your favorite editor (Gogland, VS Code or Atom). For this article, I will use Gogland editor.
- Create folder GoBDD folder inside GOROOT\src folder
- Get following Golang packages
go get github.com/google/go-github
go get github.com/stretchr/testify
go get github.com/smartystreets/goconvey
go get github.com/onsi/ginkgo/ginkgo
go get github.com/modocache/gover
go get github.com/vektra/mockery
Walk through of Sample Code
It is highly recommended to go through Playing with Github API with GO-GITHUB Golang library* *article. Below Code snippet call Github API and get repository information.
package main
import (
"github.com/google/go-github/github"
"context"
"fmt"
"os"
)
// Model
type Package struct {
FullName string
Description string
StarsCount int
ForksCount int
LastUpdatedBy string
}
func main() {
context := context.Background()
// Step 1 - create github client
client := github.NewClient(nil)
// step 2 - Service call to get repo information
repo, _, err := client.Repositories.Get(context, "Golang-Coach", "Lessons")
if err != nil {
fmt.Printf("Problem in getting repository information %v\n", err)
os.Exit(1)
}
// Step 3 - bind result to Package Model
pack := &Package{
FullName: *repo.FullName,
Description: *repo.Description,
ForksCount: *repo.ForksCount,
StarsCount: *repo.StargazersCount,
}
fmt.Printf("%+v\n", pack)
}
To get Github repo information, steps are very simple:
- Create Github client — github.NewClient()
- Service call to get repo information client.Repositories.Get(context, “Golang-Coach”, “Lessons”)
- Bind repo result to Package
The above code snippet has few drawbacks:
- The single main function is doing everything, it is calling services and binding result to Package model. This can be overcome by using Single Responsibility Principle
- Because of tight coupling between *the third party library *(github.com/google/go-github) and *main *function, the code is not testable and would be hard to maintain in long run. This can be overcome by using Interface based segregationDependency Injection
Single Responsibility Principle (SRP)
In SRP, each class will handle only one responsibility. In above code snippet, we can move Github API Call and Package Model code into the separate class or struct as shown below.
- Package.go
// File name - github.com/Golang-Coach/Lessons/GoBDD/models/package.go
package models
import "time"
// Package : here you tell us what Salutation is
type Package struct {
FullName string
Description string
StarsCount int
ForksCount int
UpdatedAt time.Time
LastUpdatedBy string
ReadMe string
Tags []string
Categories []string
}
- Github.go
// File name - github.com/Golang-Coach/Lessons/GoBDD/services/github.go
package services
import (
"context"
"github.com/Golang-Coach/Lessons/GoBDD/models"
"github.com/google/go-github/github"
)
// Github : This struct will be used to get Github related information
type Github struct {
repositoryServices *github.RepositoriesService
context context.Context
}
// NewGithub : It will intialized Github class
func NewGithub(context context.Context, repositoryServices *github.RepositoriesService) Github {
return Github{
repositoryServices: repositoryServices,
context: context,
}
}
// GetPackageRepoInfo : This receiver provide Github related repository information
func (service *Github) GetPackageRepoInfo(owner string, repositoryName string) (*models.Package, error) {
repo, _, err := service.repositoryServices.Get(service.context, owner, repositoryName)
if err != nil {
return nil, err
}
pack := &models.Package{
FullName: *repo.FullName,
Description: *repo.Description,
ForksCount: *repo.ForksCount,
StarsCount: *repo.StargazersCount,
}
return pack, nil
}
- Main.go
package main
import (
"github.com/google/go-github/github"
"github.com/Golang-Coach/Lessons/GoBDD/services"
"context"
"fmt"
)
func main() {
context := context.Background()
client := github.NewClient(nil)
// Step 1 - create github api client
githubAPI := services.NewGithub(context, client.Repositories)
// Step 1 - Get Repository Package Information
pack, err := githubAPI.GetPackageRepoInfo("Golang-Coach", "Lessons")
fmt.Printf("%+v\n", pack)
fmt.Printf("%+v\n", err)
}
Folder structure will be as follows:
Now we have segregated Github Service call and *package *binding logic from the main function.
When we write test cases against above code, it will make actual service call and will get the result from Github. To avoid the actual service call, we need to mock Github service.
Interface for loose coupling
In Go language, the function can be mocked by Interface approach. It is the nature of the Interfaces to provide many implementations, thus enable mocking. Instead of actually calling a dependent system or even a module, or a complicated and difficult to instantiate the type, you can provide the simplest interface implementation that will provide results needed **for the unit test to complete correctly. **Code **service.repositoryServices.Get(service.context, owner, repositoryName)**make service call. To mock this,we need to create interface as shown below:
// IRepositoryServices : This interface will be used to provide loose coupling between github.RepositoryServices and its consumer
type IRepositoryServices interface {
Get(ctx context.Context, owner, repo string) (*github.Repository, *github.Response, error)
}
Mockery to generate mocks
Interfaces are naturally super good integration points for tests since the implementation of an interface can easily be replaced by a mock implementation. However, writing mocks can be quite tedious and boring. To make life easier mockery provides the ability to easily generate mocks for Golang interfaces. It removes the boilerplate coding required to use mocks.
To mock IRepositoryServices interface, we need to run below command:
mockery -name=IRepositoryServices
It will create mocks folder at the root level and also create IRepositoryServices.go file.
// Code generated by mockery v1.0.0
package mocks
import context "context"
import github "github.com/google/go-github/github"
import mock "github.com/stretchr/testify/mock"
// IRepositoryServices is an autogenerated mock type for the IRepositoryServices type
type IRepositoryServices struct {
mock.Mock
}
// Get provides a mock function with given fields: ctx, owner, repo
func (_m *IRepositoryServices) Get(ctx context.Context, owner string, repo string) (*github.Repository, *github.Response, error) {
ret := _m.Called(ctx, owner, repo)
var r0 *github.Repository
if rf, ok := ret.Get(0).(func(context.Context, string, string) *github.Repository); ok {
r0 = rf(ctx, owner, repo)
} else {
if ret.Get(0) != nil {
r0 = ret.Get(0).(*github.Repository)
}
}
var r1 *github.Response
if rf, ok := ret.Get(1).(func(context.Context, string, string) *github.Response); ok {
r1 = rf(ctx, owner, repo)
} else {
if ret.Get(1) != nil {
r1 = ret.Get(1).(*github.Response)
}
}
var r2 error
if rf, ok := ret.Get(2).(func(context.Context, string, string) error); ok {
r2 = rf(ctx, owner, repo)
} else {
r2 = ret.Error(2)
}
return r0, r1, r2
}
How to use mocks:
We can create an instance of the mock struct IRepositoryServices,
repositoryServices := new(mocks.IRepositoryServices)
and we can mock function Get() as shown below:
repositoryServices.On("Get", backgroundContext, "golang-coach", "Lessons").Return(repo, nil, nil)
When unit test code request for *Get() function with mentioned parameters, it will return repo *object. The example will be:
backgroundContext := context.Background()
repositoryServices := new(mocks.IRepositoryServices)
github := NewGithub(backgroundContext, repositoryServices)
fullName := "ABC"
starCount := 10
repo := &Repository{
FullName: &fullName,
Description: &fullName,
ForksCount: &starCount,
StargazersCount: &starCount,
}
repositoryServices.On("Get", backgroundContext, "golang-coach", "Lessons").Return(repo, nil, nil)
GoConvey is an extension of the built-in Go test tool. It facilitates Behavior-driven Development (BDD) in Go, though this is not the only way to use it. Many people continue to write traditional Go tests but prefer GoConvey’s web UI for reporting test results.
Installation
go get github.com/smartystreets/goconvey
Start up the GoConvey web server at your project’s path:
// for linux or mac
$GOPATH/bin/goconvey
// for windows
%GOPATH%/bin/goconvey
Then watch the test results display in your browser at:
http:localhost:8080
GoConvey Composer
The goconvey composer will be useful to generate feature description to Convey Code Snippet. Click on edit icon on GoConvery browser window as shown below It will navigate to composer page as show below:
Left side is used to write feature set like :
TestGithubAPI
Should return repository information
Should return error when failed to retrieve repository information
and right side, it will generate *_test code snippet as shown below.
import (
"testing"
. "github.com/smartystreets/goconvey/convey"
)
func TestGithubAPI(t *testing.T) {
Convey("Should return repository information", t, nil)
Convey("Should return error when failed to retrieve repository information", t, nil)
}
Copy code into github_test.go and use mocks classes as shown below:
package services
import (
"github.com/Golang-Coach/Lessons/GoBDD/mocks"
"context"
. "github.com/smartystreets/goconvey/convey"
. "github.com/google/go-github/github"
"testing"
"errors"
)
func TestGithubAPI(t *testing.T) {
Convey("Should return repository information", t, func() {
backgroundContext := context.Background()
// create mock of IRepositoryServices interface
repositoryServices := new(mocks.IRepositoryServices)
// pass mocked object in NewGithub constructor/func
github := NewGithub(backgroundContext, repositoryServices)
fullName := "ABC"
starCount := 10
repo := &Repository{
FullName: &fullName,
Description: &fullName,
ForksCount: &starCount,
StargazersCount: &starCount,
}
// when code calls Get method of IRepositoryServices, it will return repo mocked object
repositoryServices.On("Get", backgroundContext, "golang-coach", "Lessons").Return(repo, nil, nil)
pack, _ := github.GetPackageRepoInfo("golang-coach", "Lessons")
// assert
So(pack.ForksCount, ShouldEqual, starCount)
})
Convey("Should return error when failed to retrieve repository information", t, func() {
backgroundContext := context.Background()
repositoryServices := new(mocks.IRepositoryServices)
github := NewGithub(backgroundContext, repositoryServices)
repositoryServices.On("Get", backgroundContext, "golang-coach", "Lessons").Return(nil, nil, errors.New("Error has been occurred"))
_, err := github.GetPackageRepoInfo("golang-coach", "Lessons")
So(err, ShouldNotBeEmpty)
})
}
Whenever you make any changes in the source file, GoConvey will run test cases and the result will be visible in GoConvey site ( localhost:8080). You can also set browser notification.
Let’s have quick look at how we can integrate build pipeline to this source code. For continuous integration, we will use Travis-CI
Travis-CI Setup
-
Login to Travis-CI
-
Enable Github Repository
-
Create .travis.yml and commit this file in Github repository.
language: go
go:
- tip
before_install:
- go get golang.org/x/tools/cmd/cover
- go get github.com/axw/gocov/gocov
- go get github.com/mattn/goveralls
- go get github.com/onsi/ginkgo/ginkgo
- go get github.com/modocache/gover
script:
- $HOME/gopath/bin/ginkgo -r -cover -coverpkg=./GoBDD/services/... -trace -race
- $HOME/gopath/bin/gover
- $HOME/gopath/bin/goveralls -coverprofile=gover.coverprofile -repotoken "Your Security Token"
GoConvey provides you coverage information during developer machine, but it is also important that during the build, we should also get build status and coverage information.
go test -c -coverpkg is only supported coverage of only one package. For multiple package coverage, we will use ginkgo -r -cover
Below snippet will collect coverage from different packages and generate package_name.coverprofile file.
$HOME/gopath/bin/ginkgo -r -cover -coverpkg=./GoBDD/services/... -trace -race
To collect coverages from all packages, below code snippet has been used
$HOME/gopath/bin/gover
Coveralls.io setup
-
Login to coveralls.io
-
Add repository
-
Click on details and get repo_token
Modify .travis.yml file and put repo_token, it will push coverage to coveralls.io site
$HOME/gopath/bin/goveralls -coverprofile=gover.coverprofile -repotoken "Your Security Token"
After build is successful, you will see status at travis-ci site
https://travis-ci.org/Golang-Coach/Lessons
and coverage report at
https://coveralls.io/repos/github/Golang-Coach/Lessons/
Get the complete Golang tutorial solution
Please have a look at the entire source code at GitHub.
Durgaprasad Budhwani
Documentation ¶
There is no documentation for this package.