Skip to content

Commit

Permalink
Use Hashicorp testutil for testing vs Consul API where required
Browse files Browse the repository at this point in the history
The behaviors we're testing in the consul package are very thin wrappers
around the upstream Consul API library, but that library doesn't have a
interfaces (in the golang sense) that we can mock. Using httptest with a
test server could give us the HTTP server but not the stateful behaviors
without having to implement a lot of Consul's own behavior and we'd have
to build code generation in order to validate it.

Here we'll use Hashi's own test infrastructure around Consul to run the
Consul binary and tear it down after tests. This lets us run locally
without Docker and makes it clear where the boundary of tests that
depend on the server lies. In future work we can try to minimize this
test region and try to eliminate the requirement or move these pieces
into integration tests.
  • Loading branch information
tgross committed Feb 23, 2017
1 parent 9fa6dfe commit 812a4ff
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 81 deletions.
11 changes: 7 additions & 4 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
FROM golang:1.8

RUN go get github.com/golang/lint/golint \
&& curl -Lo /tmp/glide.tgz https://github.com/Masterminds/glide/releases/download/v0.12.3/glide-v0.12.3-linux-amd64.tar.gz \
&& tar -C /usr/bin -xzf /tmp/glide.tgz --strip=1 linux-amd64/glide

RUN apt-get update \
&& apt-get install -y unzip \
&& go get github.com/golang/lint/golint \
&& curl -Lo /tmp/glide.tgz "https://github.com/Masterminds/glide/releases/download/v0.12.3/glide-v0.12.3-linux-amd64.tar.gz" \
&& tar -C /usr/bin -xzf /tmp/glide.tgz --strip=1 linux-amd64/glide \
&& curl --fail -Lso consul.zip "https://releases.hashicorp.com/consul/0.7.5/consul_0.7.5_linux_amd64.zip" \
&& unzip consul.zip -d /usr/bin

ENV CGO_ENABLED 0
ENV GOPATH /go:/cp
112 changes: 46 additions & 66 deletions discovery/consul/consul_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,51 +2,12 @@ package consul

import (
"fmt"
"os"
"testing"
"time"

"github.com/hashicorp/consul/testutil"
"github.com/joyent/containerpilot/discovery"
)

func getTestConsulAddress() string {
addr := os.Getenv("CONSUL")
if addr == "" {
addr = "localhost:8500"
}
fmt.Println(addr)
return addr
}

func setupConsul(serviceName string) (*Consul, *discovery.ServiceDefinition) {
consul, _ := NewConsulConfig(getTestConsulAddress())
service := &discovery.ServiceDefinition{
ID: serviceName,
Name: serviceName,
IPAddress: "192.168.1.1",
TTL: 1,
Port: 9000,
}
return consul, service
}

func setupWaitForLeader(consul *Consul) error {
maxRetry := 30
retry := 0
var err error

// we need to wait for Consul to start and self-elect
for ; retry < maxRetry; retry++ {
if retry > 0 {
time.Sleep(1 * time.Second)
}
if leader, err := consul.Status().Leader(); err == nil && leader != "" {
break
}
}
return err
}

func TestConsulObjectParse(t *testing.T) {
rawCfg := map[string]interface{}{
"address": "consul:8501",
Expand Down Expand Up @@ -83,11 +44,28 @@ func runParseTest(t *testing.T, uri, expectedAddress, expectedScheme string) {
}
}

func TestConsulTTLPass(t *testing.T) {
consul, service := setupConsul("service-TestConsulTTLPass")
if err := setupWaitForLeader(consul); err != nil {
t.Errorf("Consul leader could not be elected.")
}
/*
The TestWithConsul suite of tests uses Hashicorp's own testutil for managing
a Consul server for testing. The 'consul' binary must be in the $PATH
ref https://github.com/hashicorp/consul/tree/master/testutil
*/

var testServer *testutil.TestServer

func TestWithConsul(t *testing.T) {
testServer = testutil.NewTestServerConfig(t, func(c *testutil.TestServerConfig) {
c.LogLevel = "err"
})
defer testServer.Stop()
t.Run("TestConsulTTLPass", testConsulTTLPass)
t.Run("TestConsulReregister", testConsulReregister)
t.Run("TestConsulCheckForChanges", testConsulCheckForChanges)
t.Run("TestConsulEnableTagOverride", testConsulEnableTagOverride)
}

func testConsulTTLPass(t *testing.T) {
consul, _ := NewConsulConfig(testServer.HTTPAddr)
service := generateServiceDefinition(fmt.Sprintf("service-TestConsulTTLPass"))
id := service.ID

consul.SendHeartbeat(service) // force registration and 1st heartbeat
Expand All @@ -98,11 +76,9 @@ func TestConsulTTLPass(t *testing.T) {
}
}

func TestConsulReregister(t *testing.T) {
consul, service := setupConsul("service-TestConsulReregister")
if err := setupWaitForLeader(consul); err != nil {
t.Errorf("Consul leader could not be elected.")
}
func testConsulReregister(t *testing.T) {
consul, _ := NewConsulConfig(testServer.HTTPAddr)
service := generateServiceDefinition(fmt.Sprintf("service-TestConsulReregister"))
id := service.ID
consul.SendHeartbeat(service) // force registration and 1st heartbeat
services, _ := consul.Agent().Services()
Expand All @@ -112,7 +88,7 @@ func TestConsulReregister(t *testing.T) {
}

// new Consul client (as though we've restarted)
consul, service = setupConsul("service-TestConsulReregister")
consul, _ = NewConsulConfig(testServer.HTTPAddr)
service.IPAddress = "192.168.1.2"
consul.SendHeartbeat(service) // force re-registration and 1st heartbeat

Expand All @@ -123,37 +99,31 @@ func TestConsulReregister(t *testing.T) {
}
}

func TestConsulCheckForChanges(t *testing.T) {
backend := "service-TestConsulCheckForChanges"
consul, service := setupConsul(backend)
if err := setupWaitForLeader(consul); err != nil {
t.Errorf("Consul leader could not be elected.")
}
func testConsulCheckForChanges(t *testing.T) {
backend := fmt.Sprintf("service-TestConsulCheckForChanges")
consul, _ := NewConsulConfig(testServer.HTTPAddr)
service := generateServiceDefinition(backend)
id := service.ID
if consul.CheckForUpstreamChanges(backend, "") {
t.Fatalf("First read of %s should show `false` for change", id)
}
consul.SendHeartbeat(service) // force registration
consul.SendHeartbeat(service) // write TTL
consul.SendHeartbeat(service) // force registration and 1st heartbeat

if !consul.CheckForUpstreamChanges(backend, "") {
t.Errorf("%v should have changed after first health check TTL", id)
}
if consul.CheckForUpstreamChanges(backend, "") {
t.Errorf("%v should not have changed without TTL expiring", id)
}
time.Sleep(2 * time.Second) // wait for TTL to expire
consul.Agent().UpdateTTL(id, "expired", "critical")
if !consul.CheckForUpstreamChanges(backend, "") {
t.Errorf("%v should have changed after TTL expired.", id)
}
}

func TestConsulEnableTagOverride(t *testing.T) {
backend := "service-TestConsulEnableTagOverride"
consul, _ := NewConsulConfig(getTestConsulAddress())
if err := setupWaitForLeader(consul); err != nil {
t.Errorf("Consul leader could not be elected.")
}
func testConsulEnableTagOverride(t *testing.T) {
backend := fmt.Sprintf("service-TestConsulEnableTagOverride")
consul, _ := NewConsulConfig(testServer.HTTPAddr)
service := &discovery.ServiceDefinition{
ID: backend,
Name: backend,
Expand All @@ -180,3 +150,13 @@ func TestConsulEnableTagOverride(t *testing.T) {
}
}
}

func generateServiceDefinition(serviceName string) *discovery.ServiceDefinition {
return &discovery.ServiceDefinition{
ID: serviceName,
Name: serviceName,
IPAddress: "192.168.1.1",
TTL: 5,
Port: 9000,
}
}
30 changes: 19 additions & 11 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,14 @@ LDFLAGS := -X ${IMPORT_PATH}/core.GitHash='$(shell git rev-parse --short HEAD)'
ROOT := $(shell pwd)
RUNNER := -v ${ROOT}:/go/src/${IMPORT_PATH} -w /go/src/${IMPORT_PATH} containerpilot_build
docker := docker run --rm -e LDFLAGS="${LDFLAGS}" $(RUNNER)

# TODO: remove once we've mocked-out Consul in unit tests
dockerTest := docker run --rm -e LDFLAGS="${LDFLAGS}" -e CONSUL="consul:8500" --link containerpilot_consul:consul $(RUNNER)
export PATH :=$(PATH):$(GOPATH)/bin

# flags for local development
GOOS := $(shell uname -s | tr A-Z a-iz)
GOARCH := amd64
CGO_ENABLED := 0
GOEXPERIMENT := framepointer


## display this help message
help:
@echo -e "\033[32m"
Expand Down Expand Up @@ -93,14 +90,18 @@ dep-install:
dep-add: build/containerpilot_build
$(docker) bash -c "DEP=$(DEP) ./scripts/add_dep.sh"

# run 'GOOS=darwin make tools' if you're installing on MacOS
## set up local dev environment
tools:
@go version | grep 1.8 || (echo 'go1.8 not installed'; exit 1)
@$(if $(value GOPATH),, $(error 'GOPATH not set'))
go get github.com/golang/lint/golint
curl --fail -Lso glide.tgz "https://github.com/Masterminds/glide/releases/download/v0.12.3/glide-v0.12.3-$(OS)-amd64.tar.gz"
tar -C "$(GOPATH)/bin" -xzf glide.tgz --strip=1 $(OS)-amd64/glide
curl --fail -Lso glide.tgz "https://github.com/Masterminds/glide/releases/download/v0.12.3/glide-v0.12.3-$(GOOS)-$(GOARCH).tar.gz"
tar -C "$(GOPATH)/bin" -xzf glide.tgz --strip=1 $(GOOS)-$(GOARCH)/glide
rm glide.tgz
curl --fail -Lso consul.zip "https://releases.hashicorp.com/consul/0.7.5/consul_0.7.5_$(GOOS)_$(GOARCH).zip"
unzip consul.zip -d "$(GOPATH)/bin"
rm consul.zip


# ----------------------------------------------
Expand All @@ -113,6 +114,7 @@ debug:
@echo VERSION=$(VERSION)
@echo ROOT=$(ROOT)
@echo GOPATH=$(GOPATH)
@echo PATH=$(PATH)
@echo GOOS=$(GOOS)
@echo GOARCH=$(GOARCH)
@echo CGO_ENABLED=$(CGO_ENABLED)
Expand All @@ -126,7 +128,6 @@ debug:
## prefix before other make targets to run in your local dev environment
local: | quiet
@$(eval docker= )
@$(eval dockerTest= )
quiet: # this is silly but shuts up 'Nothing to be done for `local`'
@:

Expand All @@ -135,13 +136,20 @@ lint:
$(docker) bash ./scripts/lint.sh

## run unit tests
test: build/containerpilot_build consul
$(dockerTest) bash ./scripts/unit_test.sh
test: build/containerpilot_build
$(docker) bash ./scripts/unit_test.sh

## run unit tests and write out HTML file of test coverage
cover: build/containerpilot_build consul
cover: build/containerpilot_build
mkdir -p cover
$(dockerTest) bash ./scripts/cover.sh
$(docker) bash ./scripts/cover.sh

## generate Consul server test code
generate: consul
go run integration_tests/generation/genconsul.go -- \
$(ROOT)/discovery/consul
@docker stop containerpilot_consul
@docker rm -f containerpilot_consul

TEST ?= "all"
## run integration tests; filter with `TEST=testname make integration`
Expand Down

0 comments on commit 812a4ff

Please sign in to comment.