GoBDD

command
v0.0.0-...-b52f5de Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 9, 2017 License: MIT Imports: 5 Imported by: 0

README

Golang Best Practices — Behavior-driven development and Continuous Integration Build Status Coverage Status

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
  1. Basic knowledge of GO language
  2. IDE — Gogland by Jetbrains or Visual Studio Code by Microsoft or Atom
  3. 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.

  1. Create folder GoBDD folder inside GOROOT\src folder
  2. 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:

  1. Create Github client — github.NewClient()
  2. Service call to get repo information client.Repositories.Get(context, “Golang-Coach”, “Lessons”)
  3. 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.

  1. 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
}
  1. 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
}
  1. 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
  1. Login to Travis-CI

  2. Enable Github Repository

  3. 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
  1. Login to coveralls.io

  2. Add repository

  3. 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

The Go Gopher

There is no documentation for this package.

Directories

Path Synopsis
Code generated by mockery v1.0.0
Code generated by mockery v1.0.0

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL