Skip to content

Commit

Permalink
Detect custom OpenAPI spec file and start stripe-mock from test suite
Browse files Browse the repository at this point in the history
Very similar to the recent feature implemented in other languages (for
example [1]), this wraps test runs in a script which will detect a
custom OpenAPI spec and fixture in `testing/openapi/` and start a local
version of stripe-mock pointing to them. The test suite is then executed
against that stripe-mock and it's shut down afterwards.

If there isn't a custom OpenAPI spec, we continue to fall back to the
system stripe-mock to keep test runs as fast as possible.

This differs a little from implementations in other languages in that we
had to start stripe-mock from a script (as opposed to a `before` block
or the like) because Go's testing tools don't give us a place where we
can do anything before any test runs. Therefore, we start stripe-mock
then execute `go test ./...` as a subprocess.

[1] stripe/stripe-ruby#715
  • Loading branch information
brandur committed Jan 22, 2019
1 parent 6f42b48 commit f444775
Show file tree
Hide file tree
Showing 4 changed files with 186 additions and 2 deletions.
3 changes: 3 additions & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ before_install:
stripe-mock/stripe-mock_${STRIPE_MOCK_VERSION}/stripe-mock -https-port 12112 > /dev/null &
STRIPE_MOCK_PID=$!
# stripe-mock must be in PATH for `scripts/test_with_stripe_mock.go` to work.
- export PATH="${PATH}:${PWD}/stripe-mock/stripe-mock_${STRIPE_MOCK_VERSION}"

cache:
directories:
- stripe-mock
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ lint:
golint -set_exit_status ./...

test:
go test -race ./...
go run scripts/test_with_stripe_mock.go -race ./...

vet:
go vet ./...

coverage:
# go currently cannot create coverage profiles when testing multiple packages, so we test each package
# independently. This issue should be fixed in Go 1.10 (https://github.com/golang/go/issues/6909).
go list ./... | xargs -n1 -I {} -P 4 go test -covermode=count -coverprofile=../../../{}/profile.coverprofile {}
go list ./... | xargs -n1 -I {} -P 4 go run scripts/test_with_stripe_mock.go -covermode=count -coverprofile=../../../{}/profile.coverprofile {}

clean:
find . -name \*.coverprofile -delete
172 changes: 172 additions & 0 deletions scripts/test_with_stripe_mock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// A script that wraps the run of the project test suite and starts stripe-mock
// with a custom OpenAPI + fixtures bundle if one was found in the appropriate
// spot (see `pathSpec` below).
//
// The script passes all its arguments to a Go's test command, and defaults to
// `./...`. For example, both of the following are valid invocations:
//
// go run test_with_stripe_mock.go
// go run test_with_stripe_mock.go ./charge
//
// The reason that we need a separate script for this is because Go's testing
// infrastructure doesn't provide any kind of global hook that we can use to do
// this work in only one place before any tests are run.
package main

import (
"bytes"
"fmt"
"os"
"os/exec"
"regexp"
"time"
)

const (
defaultStripeMockPort = "12112"

pathSpec = "./testing/openapi/spec3.json"
pathFixtures = "./testing/openapi/fixtures3.json"
)

var portMatch = regexp.MustCompile(` port: (\d+)`)

func main() {
var port string
var stripeMockProcess *os.Process

//
// Maybe start stripe-mock
//

if _, err := os.Stat(pathSpec); os.IsNotExist(err) {
port = os.Getenv("STRIPE_MOCK_PORT")
if port == "" {
port = defaultStripeMockPort
}
fmt.Printf("No custom spec file found, assuming stripe-mock is already running on port %v\n", port)
} else {
var err error
port, stripeMockProcess, err = startStripeMock()
if err != nil {
exitWithError(err)
}
}

//
// Run tests
//

err := runTests(port)
if err != nil {
stopStripeMock(stripeMockProcess)
exitWithError(err)
}

//
// Stop stripe-mock
//

// Try to cleanly stop stripe-mock, but in case we don't, it'll die anyway
// because it's executing as a subprocess.
stopStripeMock(stripeMockProcess)
}

//
// Private functions
//

func exitWithError(err error) {
fmt.Fprintf(os.Stderr, "%v", err)
os.Exit(1)
}

func runTests(port string) error {
args := append([]string{"test"}, os.Args[1:]...)

// Defaults to `./...`, but also allows a specific package (or other CLI
// flags like `-test.v`) to be passed.
if len(args) == 1 {
args = append(args, "./...")
}

cmd := exec.Command("go", args...)

// Inherit this script's environment so that it's still possible to pass
// the test package flags like `GOCACHE=off`.
cmd.Env = append(
os.Environ(),
"STRIPE_MOCK_PORT="+port,
)

cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout

err := cmd.Run()
if err != nil {
return fmt.Errorf("Error running tests: %v", err)
}

return nil
}

func startStripeMock() (string, *os.Process, error) {
fmt.Printf("Starting stripe-mock...\n")

cmd := exec.Command(
"stripe-mock",
"-https-port", "0", // stripe-mock will select a port
"-spec", pathSpec,
"-fixtures", pathFixtures,
)

cmd.Stderr = os.Stderr

stdout, err := cmd.StdoutPipe()
if err != nil {
return "", nil, fmt.Errorf("Error starting stripe-mock: %v", err)
}

err = cmd.Start()
if err != nil {
return "", nil, fmt.Errorf("Error starting stripe-mock: %v", err)
}

b := make([]byte, 1024)
var port string

// We store the entire captured output because the string we're looking for
// may have appeared across a read boundary.
var stdoutBuffer bytes.Buffer

for {
n, err := stdout.Read(b)
if err != nil {
return "", nil, err
}
stdoutBuffer.Write(b[0:n])

// Look for port in "Listening for HTTP on port: 50602"
matches := portMatch.FindStringSubmatch(stdoutBuffer.String())
if len(matches) > 0 {
port = matches[1]
break
}

time.Sleep(100 * time.Millisecond)
}

fmt.Printf("Started stripe-mock; PID = %v, port = %v\n", cmd.Process.Pid, port)
return port, cmd.Process, nil
}

func stopStripeMock(process *os.Process) {
if process == nil {
return
}

fmt.Printf("Stopping stripe-mock...\n")
process.Signal(os.Interrupt)
process.Wait()
fmt.Printf("Stopped stripe-mock\n")
}
9 changes: 9 additions & 0 deletions testing/openapi/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
## Using custom OpenAPI specification and fixtures files

You can place custom OpenAPI specification and fixtures files in this
directory. The files must be in JSON format, and must be named `spec3.json`
and `fixtures3.json` respectively.

If those files are present, the test suite will start its own stripe-mock
process on a random available port. In order for this to work, `stripe-mock`
must be on the `PATH` in the environment used to run the test suite.

0 comments on commit f444775

Please sign in to comment.