Go and Test: Cucumber

This if the first post of a series of three:

  1. Cucumber in Go with Godog
  2. Testing with GoConvey solution
  3. Mocking.

Cucumber in Go with Godog

When you begin to work with a new programing language, after you know the syntax and structures of languages, the next step write code. To write code It is necessary to know the testing tools that you can use with the language. This post addresses the tools that I use now with go.

In Fexco we believe that first thing that you must do is to define the test and then to write the code. The reason is that we follow the paradigms BDD and TDD. We always define the features that our code must accomplish. The objective is for our code to have a good coverage and then it will do what it is designed for. That is the reason why we write in gherkin what we want the code to do.

Writing the test before has something that I specially like: when an bug is discovered in code, the problem is not the code, the problem is that the test case wasn’t correct or we didn’t design the correct case. This change in philosophy that every time that you work with you code, this one improves. I like to call this “always be better”. The effort to write code will be less in every interaction and adding new features will be more comfortable

The first approach that I used in Go was searching cucumber implementation for Go. Then I found godog.

https://github.com/DATA-DOG/godog

The explanation of use in Readme is very good and is easy to start working with it.

To install you only have to execute:

go get github.com/DATA-DOG/godog/cmd/godog

Then you add the godog command in path

#the executable is here after installation
#$GOPATH/bin/godog
export PATH=$PATH:$GOPATH/bin

Steps to use godog

The steps basically are:

  1. To make the features files. This file must be in folder ‘feature’ in your project.
  2. Execute godog command. Godog will find the features files and search the corresponding code, if the code doesn’t exist, godog will suggest the code to use. The code is searched in main package.
  3. Copy the suggested code in _test.go. Godog will detected now the code and it will be executed. This default code returns pending error.
  4. One time that the code it’s implemented and the scenarios passed, godog will return green

Let’s go with a simple example.

  1. To make the features file. Check this link to know all about gherkin
Feature: Gru manage minion

   This a feature example, you can describe 
   the context of features before scenarios

   Scenario: Gru need a new minion
   Given Task list
   When Gru is busy
   Then a new minion is created

   Scenario: Gru need destroy minion
   Given  A list of minions
   When There are more minion that tasks
   Then a minion is destroyed

located this in directory features of you project.

    $GOPATH/src/grutesgo
            /features/gruManageMinion.feature

2. Execute godog. In the ouput we can see that we have two pending escenario with 6 pending test. Godog suggested a pice of code that you can use to begin the implementation.

fsolana@fts:~/go/src/grutestgo$ godog
Feature: Gru manage minion
  This a feature example, you can describe
  the context of features before scenarios

  Scenario: Gru need a new minion # features/gruManageMinion.feature:6
    Given Task list
    When Gru is busy
    Then a new minion is created

  Scenario: Gru need destroy minion       # features/gruManageMinion.feature:11
    Given A list of minions
    When There are more minion that tasks
    Then a minion is destroyed

2 scenarios (2 undefined)
6 steps (6 undefined)
56.676µs

You can implement step definitions for undefined steps with these snippets:

func taskList() error {
 return godog.ErrPending
}

func gruIsBusy() error {
 return godog.ErrPending
}

func aNewMinionIsCreated() error {
 return godog.ErrPending
}

func aListOfMinions() error {
 return godog.ErrPending
}

func thereAreMoreMinionThatTasks() error {
 return godog.ErrPending
}

func aMinionIsDestroyed() error {
 return godog.ErrPending
}

func FeatureContext(s *godog.Suite) {
 s.Step(`^Task list$`, taskList)
 s.Step(`^Gru is busy$`, gruIsBusy)
 s.Step(`^a new minion is created$`, aNewMinionIsCreated)
 s.Step(`^A list of minions$`, aListOfMinions)
 s.Step(`^There are more minion that tasks$`, thereAreMoreMinionThatTasks)
 s.Step(`^a minion is destroyed$`, aMinionIsDestroyed)
}

3. Copy the autogenerated code in a go test file (the code must be in main package and finish with _test.go) and execute it again. Could you see the diference? Now the code is found by godog, and the execution shows that there are pending implementations.

 #The name of file of feature and test do not have to be necessary the same but that will help you 
    $GOPATH/src/grutesgo
            /features/gruManageMinion.feature
            gruManageMinion_test.go

A interesting thing is that godog writes a comment with the file and line where the step and feature are. It’s very useful

fsolana@fts:~/go/src/grutestgo$ godog
Feature: Gru manage minion
  This a feature example, you can describe
  the context of features before scenarios

  Scenario: Gru need a new minion # features/gruManageMinion.feature:6
    Given Task list               # gruManageMinion_test.go:6 -> taskList
      TODO: write pending definition
    When Gru is busy              # gruManageMinion_test.go:10 -> gruIsBusy
    Then a new minion is created  # gruManageMinion_test.go:14 -> aNewMinionIsCreated

  Scenario: Gru need destroy minion       # features/gruManageMinion.feature:11
    Given A list of minions               # gruManageMinion_test.go:18 -> aListOfMinions
      TODO: write pending definition
    When There are more minion that tasks # gruManageMinion_test.go:22 -> thereAreMoreMinionThatTasks
    Then a minion is destroyed            # gruManageMinion_test.go:26 -> aMinionIsDestroyed

2 scenarios (2 pending)
6 steps (2 pending, 4 skipped)
172.097µs

4. Now it’s the moment to code and you have to make the code do the feature description. After the code is written the execution will be on green

fsolana@fts:~/go/src/grutestgo$ godog
Feature: Gru manage minion
  This a feature example, you can describe
  the context of features before scenarios

  Scenario: Gru need a new minion # features/gruManageMinion.feature:6
    Given Task list               # gruManageMinion_test.go:6 -> taskList
    When Gru is busy              # gruManageMinion_test.go:10 -> gruIsBusy
    Then a new minion is created  # gruManageMinion_test.go:14 -> aNewMinionIsCreated

  Scenario: Gru need destroy minion       # features/gruManageMinion.feature:11
    Given A list of minions               # gruManageMinion_test.go:18 -> aListOfMinions
    When There are more minion that tasks # gruManageMinion_test.go:22 -> thereAreMoreMinionThatTasks
    Then a minion is destroyed            # gruManageMinion_test.go:26 -> aMinionIsDestroyed

2 scenarios (2 passed)
6 steps (6 passed)
149.525µs

A important thing about godog is how it registers the steps: The suite registers a function to execute for every text expression.

func FeatureContext(s *godog.Suite) {
	s.Step(`^Task list$`, taskList)
	s.Step(`^Gru is busy$`, gruIsBusy)
	s.Step(`^a new minion is created$`, aNewMinionIsCreated)
	s.Step(`^A list of minions$`, aListOfMinions)
	s.Step(`^There are more minion that tasks$`, thereAreMoreMinionThatTasks)
	s.Step(`^a minion is destroyed$`, aMinionIsDestroyed)
}

The order of step execution is the same order than the one defined in gherkin scenarios, and only one function with the same regular text expression text is registered. If a step has the same text  it means that it Will do exactly the same, then the steps are shared between scenarios. To be more clear, a step with the text “Give a user” has to do the same in every scenario where it is included, then the same code must be executed every time that it is included.

Included the code in Go Test execution

To execute test in go the command is

go test

To integrate go test with godog we only have to include the test Main package.

var opt = godog.Options{
	Output: colors.Colored(os.Stdout),
	Format: "progress", // can define default values
}

func init() {
	godog.BindFlags("godog.", flag.CommandLine, &opt)
}

func TestMain(m *testing.M) {
	flag.Parse()
	opt.Paths = flag.Args()

	status := godog.RunWithOptions("godogs", func(s *godog.Suite) {
		FeatureContext(s)
	}, opt)

	if st := m.Run(); st > status {
		status = st
	}
	os.Exit(status)
}

Execute test and coverage

go test -v --godog.format=pretty --godog.random -race -coverprofile=coverage.txt -covermode=atomic

fsolana@fts:~/go/src/grutestgo$ go test -v --godog.format=pretty --godog.random -race -coverprofile=coverage.txt -covermode=atomic
Feature: Gru manage minion
  This a feature example, you can describe
  the context of features before scenarios

  Scenario: Gru need destroy minion       # features/gruManageMinion.feature:11
    Given A list of minions               # gruManageMinion_test.go:18 -> aListOfMinions
    When There are more minion that tasks # gruManageMinion_test.go:22 -> thereAreMoreMinionThatTasks
    Then a minion is destroyed            # gruManageMinion_test.go:26 -> aMinionIsDestroyed

  Scenario: Gru need a new minion # features/gruManageMinion.feature:6
    Given Task list               # gruManageMinion_test.go:6 -> taskList
    When Gru is busy              # gruManageMinion_test.go:10 -> gruIsBusy
    Then a new minion is created  # gruManageMinion_test.go:14 -> aNewMinionIsCreated

2 scenarios (2 passed)
6 steps (6 passed)
950.278µs

Randomized with seed: 23532
testing: warning: no tests to run
PASS
coverage: 100.0% of statements
ok   grutestgo 1.034s

An important problem that I could find with godog is that the coverage only applies to the main package and the sub package is not included. If you don’t have a specific code to test, the coverage will be 0%. Then you could have total coverage  in all your project only with your BDD test, but go test coverage only knows about the tests included in golang, and not about all the execution with godog.

HTML Report

If you need that your clients can see the report of GoDog I recommend using Cucumber-html-report

Conclusions

You can use Cucumber in Go. But if you use this solution you won’t know the coverage that covers the cucumber test and you’ll need cover this with unitary tests, but it’s not a problem because we do TDD.

In the next Post I’ll talk about Convey to do BDD.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.