tdd in go with ginkgo and gomega
DESCRIPTION
An overview of TDD concepts, and an exploration of these concepts in Go using the Ginkgo and Gomega libraries.TRANSCRIPT
About Me
Eddy Reyes. Cofounder of Tasqr
(www.tasqr.io)
● Automates application deployment /
configuration without code or config files.
Previously hacked lots of code at:
● IBM, Lifesize, Click Security
Overview of Presentation
● Explain what TDD iso Not necessarily proselytizing that you must do it.
o I dislike many TDD-related presentations, they don’t
match my experience, they don’t stress what’s
important IMO.
● What Tools Does Go Have to Help You Do
TDD?o Ginkgo and Gomega, many others
Further TDD Reading
Uncle Bob Martin● http://blog.8thlight.com/uncle-bob/archive.html
Kent Beck● Extreme Programming Explained (book)
Is TDD Dead? (video debates)● Kent Beck, Martin Fowler, DHH
● https://www.youtube.com/watch?v=z9quxZsLcfo (part 1)
What Isn’t TDD?
● NOT just having written tests
● NOT a way to feel like you’re going fast
● NOT the Silver Bullet that saves software
engineering
● NOT like Axe, which makes you irresistible
to the opposite sex :-)
What Is TDD?
● A process you follow when writing software.
● The process goes like this:o You write a test that defines an expected outcome in
your program.
o You watch the test fail.
o You change the program until your test passes.
● Adding tests to an already substantial
untested program isn’t necessarily TDD.
How Do You Do TDD?
● In order to effectively follow the TDD pattern,
you must first understand your design.o Tests are rigid. Learning is free-form.
o You learn by hacking and breaking things.
o Tests ensure things don’t break.
● TDD helps you SHIP, not LEARN
Learn vs. Ship
● Learning is free-form exploration.
Experiment, play, break things.o DO NOT SHIP THIS CODE!
o After you learn your design, step away from the
keyboard, and start anew.
● Shippingo Code ends up in a product used by customers.
o Quality is important!
Tests Are Design!
● If tests are comprehensive, they are a formal
specification of your program.
● Writing a test is expressing the design of
your program’s functionality.
● To truly serve as a spec, tests must be:o Clean, readable
o Follow some consistent notation
Test/Spec Notation
GIVEN
● Precondition 1...N
WHEN
● Call into your program
THEN
● Assert Condition 1...N
A Test Framework Needs...
● Clean, simple notation
● Clean setup/teardown of dependencieso Idempotent, independent tests!
● Clean separation from tested code
● Simple runner mechanism
● Good reportingo test results
o coverage maps
Testing in Go
● Test runnero go test
● Separates test code from producto *_test.go
● Comes with a test writing libraryo “testing” package
● Comes with coverage reporting toolso Beautiful HTML output
“testing” Package
● Allows your to write test functions.
● Nice benchmarking utility
● Tests are basically blocks of code :(func TestXXX(t *testing.T) {
// setup stuff
// call your code
if something.State != WhatIWant {
t.Fail()
}
}
Better Test-Writing Library
● go command has great tooling.
● Need a better notation for specifying tests.
…
Ginkgo and Gomega is a step forward!
… but not perfect!
Ginkgo and Gomega
● Ginkgoo Compatible with “go test” but also comes with its
own test runner
o BDD-style test notation
o Expressive structuring of tests
● Gomegao “Matching” library
o Allows for expressive assert-like statements
Ginkgo
● Tests expressed as English statementso In the BDD tradition of Cucumber, et. al.
var _ = Describe(“My component”, func() {
It(“Does something special”, func() {
MyFunction()
Expect(MyVar).To(Equal(10))
})
})
Gomega
● Assertion languageExpect(number).To(Equal(10))
Expect(value).To(BeNumerically(“<”, 20))
Expect(someMap).To(Equal(otherMap)) // does reflect.DeepEqual()
Expect(someMap).To(BeEquivalent(otherMap)) // allows different types
Expect(myPtr).ToNot(BeNil())
Expect(flag).To(BeFalse())
Expect(err).ToNot(HaveOccurred())
Expect(err).To(MatchError(“my error message”))
Ginkgo and Dependencies● setup/teardown:
var _ = Describe(“my config object”, func() {
var (
configObj *config.MyConfig
tempDir string
)
BeforeEach(func() {
var err error
tempDir, err = ioutil.TempDir(“”, “test-dir”)
Expect(err).ToNot(HaveOccurred())
configObj = config.NewConfig()
})
Ginkgo and DependenciesIt(“Saves itself to file”, func() {
configObj.Save(tempDir)
restoredConfig := config.FromFile(configObj.Filename)
Expect(*restoredConfig).To(Equal(*configObj))
})
AfterEach(func() {
os.RemoveAll(tempDir)
})
}) // end Describe()
Ginkgo and Dependencies
● Ginkgo heavily relies on closures and
closure variables to manage dependencies.o Resist the temptation to share state among tests!
● Nested dependencieso Within a Describe block, you can nest:
Context block
More It’s and Before/AfterEach’s
Nested Dependenciesvar _ = Describe(“My component”)
var configObj config.MyConfig
BeforeEach(func() {
configObj = config.NewConfig()
})
It(“Does test1”, func() {
// configObj is setup
})
Nested DependenciesContext(“With a fake HTTP server configured”, func() {
var testServer httptest.Server
BeforeEach(func() {
testServer = httptest.NewServer(<fake handler>)
})
It(“Does test2”, func() {
configObj.ServerUrl = testServer.URL
configObj.DoSomething() ...
})
})
Testing Notation + Ginkgo
● Obviously, Ginkgo was not designed with my
exact notation in mind…
● However, it maps to it without too much
difficulty.
Testing Notation + Ginkgovar _ = Describe(“my component”, func() {
// GIVEN
BeforeEach(func () {
})
It(“Does something useful”, func() {
// WHEN
CallSomething()
// THEN
Expect(stuff).To((Equal(something))
})
})
Alternate Notation Stylevar _ = Describe(“my component”, func() {
BeforeEach(func() {...}) //
GIVEN
It(“Does something useful”, func() { // WHEN
closureVar = CallSomething()
})
AfterEach(func() {
Expect(closureVar).To(Equal(something)) // THEN
})
})
Ginkgo Bootstrap
● Ginkgo integrates with go test
● You must use the ginkgo command to
generate boilerplate code:
$ cd mypackage/ # within $GOPATH/src
$ ginkgo bootstrap
…
$ ls
mypackage_suite_test.go
Ginkgo Bootstrappackage mypackage_test // NOT THE SAME AS mypackage!!!
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"testing"
)
func TestConfig(t *testing.T) { // go test integration boilerplate!
RegisterFailHandler(Fail)
RunSpecs(t, "Mypackage Suite")
}
Ginkgo Generate
● The generated suite calls the generated test
files:$ ginkgo generate
…
$ ls
mypackage_suite_test.go mypackage_test.go
…
$ cat mypackage_test.go
...
Ginkgo Generatepackage mypackage_test // NOT THE SAME AS mypackage!!
import (
. “mypackage”
. “github.com/onsi/ginkgo”
. “github.com/onsi/gomega”
)
var _ = Describe(“Mypackage test”, func() {
})
Assessing Ginkgo/Gomega
Test Notation
● The BDD-style english annotations help
readability a little bit...
● Closures are a slippery slope!o Too much code: declare closure vars, must also
initialize in BeforeEach to ensure test
idempotency!
Assessing Ginkgo/Gomega
● Complex dependencies are doableo Accomplished by nesting Describe/Context/
BeforeEach’s
o However, your tests must be inserted in ever-more
nested closures
Can get kinda complicated!
● Gomega assertionso Flow nicely when read aloud
o Too complex as a formal notation
Assessing Ginkgo/Gomega
● Overall test notation grade: C+/B-o Depends on my mood and how complicated my test
needs to get.
o Worst part about it is that your test file easily grows
way too long, and thus harder to comprehend as a
spec.
● Good enough to get our job done with high
quality at Tasqr.
Go TDD Roundup
Questions?