Skip to content

Commit

Permalink
Add example tester service to show API Integration Testing of a Goa…
Browse files Browse the repository at this point in the history
… System (#331)

* initial commit

* Empty-Commit

* add readme file (still need to fill out)

* tester readme update

* Trying out a workflow to run integration tests against local binaries

* Add proto install to /example/weather/scripts/setup

* fix integration.yaml

* disable mono on ubuntu

* move mono stop

* Try to find out what's running on 8084

* Trying to disable mono?

* trying to stop mono 2

* add a sleep after stop/disable?

* add some echo

* see if stop/disable mono fails

* add back in || true, change front ports to see if this even works at all

* oops forgot to change int test port

* try no send to dev null?

* sleep before testing

* take out netstat

* sleep longer? give time for docker to do work to get grafana?

* remove dev>null again... :\

* sleep 60 :(

* Implement review comment changes

* Fix runTests check for filtering if

* Empty-Commit

* try starting each service individually, check fo rmono

* just KILL mono?

* back to 8084, check all ports

* fix typo

* add flag to turn off grafana for services (for CI testing)

* clean up integration.yaml script

* review fixes

* Get Stack Trace to Err Log on Panic

* small fixes for style

* protoc -> 25.1, better wait in integration.yaml

* Add synchronous testing feature

* Make one of the synchronous as an example

* move func maps to service definition
  • Loading branch information
jasondborneman authored and raphael committed Jan 8, 2024
1 parent c78642b commit 1b6ed57
Show file tree
Hide file tree
Showing 57 changed files with 6,010 additions and 473 deletions.
69 changes: 69 additions & 0 deletions .github/workflows/integration.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
name: Integration Tests

on:
workflow_dispatch:
push:
branches: [main]
pull_request:
types: [opened, reopened, synchronize]

jobs:
integration-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v4
with:
go-version: "1.21"
- name: tests
run: |
check_service_health() {
local health_url="$1"
local start_time=$(date +%s)
while : ; do
if curl -s --fail "$health_url" > /dev/null; then
echo "Service is up!"
return 0
fi
local current_time=$(date +%s)
if (( current_time - start_time >= 5 )); then
echo "Timed out waiting for service to be up."
return 1
fi
sleep 0.2
done
}
echo "stop/disable/kill mono"
sudo systemctl stop mono-xsp4.service || true
sudo systemctl disable mono-xsp4.service || true
sudo pkill mono || true
echo "change to weather example directory"
cd example/weather
echo "run setup script"
./scripts/setup
echo "run server"
./bin/forecaster -monitoring-enabled=false &
./bin/locator -monitoring-enabled=false &
./bin/tester -monitoring-enabled=false &
./bin/front -monitoring-enabled=false &
check_service_health "http://localhost:8081/healthz" &
check_service_health "http://localhost:8083/healthz" &
check_service_health "http://localhost:8091/healthz" &
check_service_health "http://localhost:8085/healthz" &
wait -n
echo "-----RUN TESTS-----"
results=$(curl -X POST http://localhost:8084/tester/smoke)
echo "-----RESULTS-----"
echo $results
echo "----------"
if [ $(echo $results | jq '.fail_count') -gt 0 ];
then
echo "Test errors found."
exit 1
else
echo "Tests passed."
exit 0
fi
1 change: 1 addition & 0 deletions example/weather/Procfile
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
forecaster: bin/forecaster
locator: bin/locator
front: bin/front
tester: bin/tester
2 changes: 1 addition & 1 deletion example/weather/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ require (
goa.design/clue v0.20.0
goa.design/goa/v3 v3.14.2
goa.design/model v1.9.1
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc
google.golang.org/grpc v1.60.1
google.golang.org/protobuf v1.32.0
)
Expand All @@ -36,7 +37,6 @@ require (
github.com/sergi/go-diff v1.3.1 // indirect
go.opentelemetry.io/otel v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0 // indirect
go.opentelemetry.io/otel/metric v1.21.0 // indirect
go.opentelemetry.io/otel/sdk v1.21.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect
Expand Down
2 changes: 2 additions & 0 deletions example/weather/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ goa.design/goa/v3 v3.14.2 h1:V8skSFWRUTddqZnRy65Mdx0SpGmYe9aQhgCQppyvCKA=
goa.design/goa/v3 v3.14.2/go.mod h1:sq7F2y/2/eK/XCc8Ff7Was3u42fRmQXp1pTm8FfnGgo=
goa.design/model v1.9.1 h1:vhY89pjUWW1firAHaccW9MZTiMEEVvtcwN9g8odyywQ=
goa.design/model v1.9.1/go.mod h1:PlrrmVbrBm7gxqoWqburVUudaMd/d3Sdul7/Kr4Ap54=
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM=
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
Expand Down
2 changes: 1 addition & 1 deletion example/weather/scripts/build
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ echo "Rebuilding services..."

mkdir -p bin

for svc in forecaster locator front; do
for svc in forecaster locator front tester; do
go build -o bin/${svc} -ldflags "-X goa.design/clue/health.Version=$GIT_COMMIT" goa.design/clue/example/weather/services/${svc}/cmd/${svc}
done

Expand Down
2 changes: 1 addition & 1 deletion example/weather/scripts/gen
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ pushd ${GIT_ROOT}/example/weather

echo "Generating Goa code..."

for svc in front forecaster locator; do
for svc in front forecaster locator tester; do
goa gen goa.design/clue/example/weather/services/${svc}/design -o services/${svc}
cmg gen ./services/${svc}/clients/*/
done
Expand Down
11 changes: 11 additions & 0 deletions example/weather/scripts/setup
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,21 @@ source ./scripts/utils/common.sh

proto_pkg="protobuf-compiler"

# install protoc, update version as needed
PROTO_VER=25.1

if is_mac; then
PROTOC_ZIP=protoc-${PROTO_VER}-osx-universal_binary.zip
proto_pkg="protobuf"
else
PROTOC_ZIP=protoc-${PROTO_VER}-linux-x86_64.zip
fi

curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v${PROTO_VER}/${PROTOC_ZIP}
sudo unzip -o $PROTOC_ZIP -d /usr/local bin/protoc
sudo unzip -o $PROTOC_ZIP -d /usr/local 'include/*'
rm -f $PROTOC_ZIP

check_required_cmd "protoc" $proto_pkg

if [[ "$CI" == "" ]]; then
Expand Down
106 changes: 106 additions & 0 deletions example/weather/services/front/clients/tester/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package tester

import (
"context"

"goa.design/clue/debug"
"goa.design/clue/log"
goa "goa.design/goa/v3/pkg"
"google.golang.org/grpc"

genfront "goa.design/clue/example/weather/services/front/gen/front"
genclient "goa.design/clue/example/weather/services/tester/gen/grpc/tester/client"
gentester "goa.design/clue/example/weather/services/tester/gen/tester"
)

type (
Client interface {
// Runs ALL API Integration Tests from the Tester service, allowing for filtering on included or excluded tests
TestAll(ctx context.Context, included, excluded []string) (*genfront.TestResults, error)
// Runs API Integration Tests' Smoke Tests ONLY from the Tester service
TestSmoke(ctx context.Context) (*genfront.TestResults, error)
}

TestAllPayload struct {
Include []string
Exclude []string
}

client struct {
testSmoke goa.Endpoint
testAll goa.Endpoint
}
)

// Creates a new client for the Tester service.
func New(cc *grpc.ClientConn) Client {
c := genclient.NewClient(cc, grpc.WaitForReady(true))
return &client{
debug.LogPayloads(debug.WithClient())(c.TestSmoke()),
debug.LogPayloads(debug.WithClient())(c.TestAll()),
}
}

// TestSmoke runs the Smoke collection as defined in func_map.go of the tester service
func (c *client) TestSmoke(ctx context.Context) (*genfront.TestResults, error) {
res, err := c.testSmoke(ctx, nil)
if err != nil {
log.Errorf(ctx, err, "failed to run smoke tests: %s", err)
return nil, err
}
return testerTestResultsToFrontTestResults(res.(*gentester.TestResults)), nil
}

// TestAll runs all tests in all collections. Obeys include and exclude filters.
// include and exclude are mutually exclusive and cannot be used together (400 error, bad request)
func (c *client) TestAll(ctx context.Context, included, excluded []string) (*genfront.TestResults, error) {
gtPayload := &gentester.TesterPayload{
Include: included,
Exclude: excluded,
}
res, err := c.testAll(ctx, gtPayload)
if err != nil {
log.Errorf(ctx, err, "failed to run all tests: %s", err)
return nil, err
}
return testerTestResultsToFrontTestResults(res.(*gentester.TestResults)), nil
}

func testerTestResultsToFrontTestResults(testResults *gentester.TestResults) *genfront.TestResults {
var res = &genfront.TestResults{}
if testResults != nil {
res.Collections = testerTestCollectionsArrToFrontTestCollectionsArr(testResults.Collections)
res.Duration = testResults.Duration
res.PassCount = testResults.PassCount
res.FailCount = testResults.FailCount
}
return res
}

func testerTestCollectionsArrToFrontTestCollectionsArr(testCollection []*gentester.TestCollection) []*genfront.TestCollection {
var res []*genfront.TestCollection
for _, v := range testCollection {
res = append(res, testerTestCollectionToFrontTestCollection(v))
}
return res
}

func testerTestCollectionToFrontTestCollection(testCollection *gentester.TestCollection) *genfront.TestCollection {
var res = &genfront.TestCollection{}
if testCollection != nil {
res.Name = testCollection.Name
res.Results = testerTestResultsArrToFrontTestResultsArr(testCollection.Results)
res.Duration = testCollection.Duration
res.PassCount = testCollection.PassCount
res.FailCount = testCollection.FailCount
}
return res
}

func testerTestResultsArrToFrontTestResultsArr(testResults []*gentester.TestResult) []*genfront.TestResult {
var res []*genfront.TestResult
for _, v := range testResults {
res = append(res, (*genfront.TestResult)(v))
}
return res
}
72 changes: 72 additions & 0 deletions example/weather/services/front/clients/tester/mocks/client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 14 additions & 1 deletion example/weather/services/front/cmd/front/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"goa.design/clue/example/weather/services/front"
"goa.design/clue/example/weather/services/front/clients/forecaster"
"goa.design/clue/example/weather/services/front/clients/locator"
"goa.design/clue/example/weather/services/front/clients/tester"
genfront "goa.design/clue/example/weather/services/front/gen/front"
genhttp "goa.design/clue/example/weather/services/front/gen/http/front/server"
)
Expand All @@ -40,6 +41,7 @@ func main() {
locatorHealthAddr = flag.String("locator-health-addr", ":8083", "Locator service health-check address")
coladdr = flag.String("otel-addr", ":4317", "OpenTelemtry collector listen address")
debugf = flag.Bool("debug", false, "Enable debug logs")
testerAddr = flag.String("tester-addr", ":8090", "Tester service address")
)
flag.Parse()

Expand Down Expand Up @@ -106,9 +108,18 @@ func main() {
log.Fatalf(ctx, err, "failed to connect to forecast")
}
fc := forecaster.New(fcc)
tcc, err := grpc.DialContext(ctx, *testerAddr,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithUnaryInterceptor(log.UnaryClientInterceptor()),
grpc.WithStatsHandler(otelgrpc.NewClientHandler()))
if err != nil {
log.Errorf(ctx, err, "failed to connect to tester")
os.Exit(1)
}
tc := tester.New(tcc)

// 4. Create service & endpoints
svc := front.New(fc, lc)
svc := front.New(fc, lc, tc)
endpoints := genfront.NewEndpoints(svc)
endpoints.Use(debug.LogPayloads())
endpoints.Use(log.Endpoint)
Expand All @@ -128,6 +139,8 @@ func main() {
httpServer := &http.Server{Addr: *httpListenAddr, Handler: handler}

// 6. Mount health check & metrics on separate HTTP server (different listen port)
// No testerHealthAddr pinger because we don't want the whole system to die just because
// tester isn't healthy for some reason
check := health.Handler(health.NewChecker(
health.NewPinger("locator", *locatorHealthAddr),
health.NewPinger("forecaster", *forecasterHealthAddr)))
Expand Down
Loading

0 comments on commit 1b6ed57

Please sign in to comment.