Skip to content

Commit 1b6ed57

Browse files
jasondbornemanraphael
authored andcommitted
Add example tester service to show API Integration Testing of a Goa 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
1 parent c78642b commit 1b6ed57

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

57 files changed

+6010
-473
lines changed

.github/workflows/integration.yaml

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
name: Integration Tests
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches: [main]
7+
pull_request:
8+
types: [opened, reopened, synchronize]
9+
10+
jobs:
11+
integration-tests:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-go@v4
16+
with:
17+
go-version: "1.21"
18+
- name: tests
19+
run: |
20+
check_service_health() {
21+
local health_url="$1"
22+
local start_time=$(date +%s)
23+
24+
while : ; do
25+
if curl -s --fail "$health_url" > /dev/null; then
26+
echo "Service is up!"
27+
return 0
28+
fi
29+
30+
local current_time=$(date +%s)
31+
if (( current_time - start_time >= 5 )); then
32+
echo "Timed out waiting for service to be up."
33+
return 1
34+
fi
35+
36+
sleep 0.2
37+
done
38+
}
39+
echo "stop/disable/kill mono"
40+
sudo systemctl stop mono-xsp4.service || true
41+
sudo systemctl disable mono-xsp4.service || true
42+
sudo pkill mono || true
43+
echo "change to weather example directory"
44+
cd example/weather
45+
echo "run setup script"
46+
./scripts/setup
47+
echo "run server"
48+
./bin/forecaster -monitoring-enabled=false &
49+
./bin/locator -monitoring-enabled=false &
50+
./bin/tester -monitoring-enabled=false &
51+
./bin/front -monitoring-enabled=false &
52+
check_service_health "http://localhost:8081/healthz" &
53+
check_service_health "http://localhost:8083/healthz" &
54+
check_service_health "http://localhost:8091/healthz" &
55+
check_service_health "http://localhost:8085/healthz" &
56+
wait -n
57+
echo "-----RUN TESTS-----"
58+
results=$(curl -X POST http://localhost:8084/tester/smoke)
59+
echo "-----RESULTS-----"
60+
echo $results
61+
echo "----------"
62+
if [ $(echo $results | jq '.fail_count') -gt 0 ];
63+
then
64+
echo "Test errors found."
65+
exit 1
66+
else
67+
echo "Tests passed."
68+
exit 0
69+
fi

example/weather/Procfile

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
forecaster: bin/forecaster
22
locator: bin/locator
33
front: bin/front
4+
tester: bin/tester

example/weather/go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ require (
1212
goa.design/clue v0.20.0
1313
goa.design/goa/v3 v3.14.2
1414
goa.design/model v1.9.1
15+
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc
1516
google.golang.org/grpc v1.60.1
1617
google.golang.org/protobuf v1.32.0
1718
)
@@ -36,7 +37,6 @@ require (
3637
github.com/sergi/go-diff v1.3.1 // indirect
3738
go.opentelemetry.io/otel v1.21.0 // indirect
3839
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.21.0 // indirect
39-
go.opentelemetry.io/otel/exporters/stdout/stdoutmetric v0.44.0 // indirect
4040
go.opentelemetry.io/otel/metric v1.21.0 // indirect
4141
go.opentelemetry.io/otel/sdk v1.21.0 // indirect
4242
go.opentelemetry.io/otel/sdk/metric v1.21.0 // indirect

example/weather/go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ goa.design/goa/v3 v3.14.2 h1:V8skSFWRUTddqZnRy65Mdx0SpGmYe9aQhgCQppyvCKA=
9797
goa.design/goa/v3 v3.14.2/go.mod h1:sq7F2y/2/eK/XCc8Ff7Was3u42fRmQXp1pTm8FfnGgo=
9898
goa.design/model v1.9.1 h1:vhY89pjUWW1firAHaccW9MZTiMEEVvtcwN9g8odyywQ=
9999
goa.design/model v1.9.1/go.mod h1:PlrrmVbrBm7gxqoWqburVUudaMd/d3Sdul7/Kr4Ap54=
100+
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM=
101+
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
100102
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
101103
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
102104
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=

example/weather/scripts/build

+1-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ echo "Rebuilding services..."
1010

1111
mkdir -p bin
1212

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

example/weather/scripts/gen

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ pushd ${GIT_ROOT}/example/weather
77

88
echo "Generating Goa code..."
99

10-
for svc in front forecaster locator; do
10+
for svc in front forecaster locator tester; do
1111
goa gen goa.design/clue/example/weather/services/${svc}/design -o services/${svc}
1212
cmg gen ./services/${svc}/clients/*/
1313
done

example/weather/scripts/setup

+11
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,21 @@ source ./scripts/utils/common.sh
99

1010
proto_pkg="protobuf-compiler"
1111

12+
# install protoc, update version as needed
13+
PROTO_VER=25.1
14+
1215
if is_mac; then
16+
PROTOC_ZIP=protoc-${PROTO_VER}-osx-universal_binary.zip
1317
proto_pkg="protobuf"
18+
else
19+
PROTOC_ZIP=protoc-${PROTO_VER}-linux-x86_64.zip
1420
fi
1521

22+
curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v${PROTO_VER}/${PROTOC_ZIP}
23+
sudo unzip -o $PROTOC_ZIP -d /usr/local bin/protoc
24+
sudo unzip -o $PROTOC_ZIP -d /usr/local 'include/*'
25+
rm -f $PROTOC_ZIP
26+
1627
check_required_cmd "protoc" $proto_pkg
1728

1829
if [[ "$CI" == "" ]]; then
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
package tester
2+
3+
import (
4+
"context"
5+
6+
"goa.design/clue/debug"
7+
"goa.design/clue/log"
8+
goa "goa.design/goa/v3/pkg"
9+
"google.golang.org/grpc"
10+
11+
genfront "goa.design/clue/example/weather/services/front/gen/front"
12+
genclient "goa.design/clue/example/weather/services/tester/gen/grpc/tester/client"
13+
gentester "goa.design/clue/example/weather/services/tester/gen/tester"
14+
)
15+
16+
type (
17+
Client interface {
18+
// Runs ALL API Integration Tests from the Tester service, allowing for filtering on included or excluded tests
19+
TestAll(ctx context.Context, included, excluded []string) (*genfront.TestResults, error)
20+
// Runs API Integration Tests' Smoke Tests ONLY from the Tester service
21+
TestSmoke(ctx context.Context) (*genfront.TestResults, error)
22+
}
23+
24+
TestAllPayload struct {
25+
Include []string
26+
Exclude []string
27+
}
28+
29+
client struct {
30+
testSmoke goa.Endpoint
31+
testAll goa.Endpoint
32+
}
33+
)
34+
35+
// Creates a new client for the Tester service.
36+
func New(cc *grpc.ClientConn) Client {
37+
c := genclient.NewClient(cc, grpc.WaitForReady(true))
38+
return &client{
39+
debug.LogPayloads(debug.WithClient())(c.TestSmoke()),
40+
debug.LogPayloads(debug.WithClient())(c.TestAll()),
41+
}
42+
}
43+
44+
// TestSmoke runs the Smoke collection as defined in func_map.go of the tester service
45+
func (c *client) TestSmoke(ctx context.Context) (*genfront.TestResults, error) {
46+
res, err := c.testSmoke(ctx, nil)
47+
if err != nil {
48+
log.Errorf(ctx, err, "failed to run smoke tests: %s", err)
49+
return nil, err
50+
}
51+
return testerTestResultsToFrontTestResults(res.(*gentester.TestResults)), nil
52+
}
53+
54+
// TestAll runs all tests in all collections. Obeys include and exclude filters.
55+
// include and exclude are mutually exclusive and cannot be used together (400 error, bad request)
56+
func (c *client) TestAll(ctx context.Context, included, excluded []string) (*genfront.TestResults, error) {
57+
gtPayload := &gentester.TesterPayload{
58+
Include: included,
59+
Exclude: excluded,
60+
}
61+
res, err := c.testAll(ctx, gtPayload)
62+
if err != nil {
63+
log.Errorf(ctx, err, "failed to run all tests: %s", err)
64+
return nil, err
65+
}
66+
return testerTestResultsToFrontTestResults(res.(*gentester.TestResults)), nil
67+
}
68+
69+
func testerTestResultsToFrontTestResults(testResults *gentester.TestResults) *genfront.TestResults {
70+
var res = &genfront.TestResults{}
71+
if testResults != nil {
72+
res.Collections = testerTestCollectionsArrToFrontTestCollectionsArr(testResults.Collections)
73+
res.Duration = testResults.Duration
74+
res.PassCount = testResults.PassCount
75+
res.FailCount = testResults.FailCount
76+
}
77+
return res
78+
}
79+
80+
func testerTestCollectionsArrToFrontTestCollectionsArr(testCollection []*gentester.TestCollection) []*genfront.TestCollection {
81+
var res []*genfront.TestCollection
82+
for _, v := range testCollection {
83+
res = append(res, testerTestCollectionToFrontTestCollection(v))
84+
}
85+
return res
86+
}
87+
88+
func testerTestCollectionToFrontTestCollection(testCollection *gentester.TestCollection) *genfront.TestCollection {
89+
var res = &genfront.TestCollection{}
90+
if testCollection != nil {
91+
res.Name = testCollection.Name
92+
res.Results = testerTestResultsArrToFrontTestResultsArr(testCollection.Results)
93+
res.Duration = testCollection.Duration
94+
res.PassCount = testCollection.PassCount
95+
res.FailCount = testCollection.FailCount
96+
}
97+
return res
98+
}
99+
100+
func testerTestResultsArrToFrontTestResultsArr(testResults []*gentester.TestResult) []*genfront.TestResult {
101+
var res []*genfront.TestResult
102+
for _, v := range testResults {
103+
res = append(res, (*genfront.TestResult)(v))
104+
}
105+
return res
106+
}

example/weather/services/front/clients/tester/mocks/client.go

+72
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

example/weather/services/front/cmd/front/main.go

+14-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"goa.design/clue/example/weather/services/front"
2727
"goa.design/clue/example/weather/services/front/clients/forecaster"
2828
"goa.design/clue/example/weather/services/front/clients/locator"
29+
"goa.design/clue/example/weather/services/front/clients/tester"
2930
genfront "goa.design/clue/example/weather/services/front/gen/front"
3031
genhttp "goa.design/clue/example/weather/services/front/gen/http/front/server"
3132
)
@@ -40,6 +41,7 @@ func main() {
4041
locatorHealthAddr = flag.String("locator-health-addr", ":8083", "Locator service health-check address")
4142
coladdr = flag.String("otel-addr", ":4317", "OpenTelemtry collector listen address")
4243
debugf = flag.Bool("debug", false, "Enable debug logs")
44+
testerAddr = flag.String("tester-addr", ":8090", "Tester service address")
4345
)
4446
flag.Parse()
4547

@@ -106,9 +108,18 @@ func main() {
106108
log.Fatalf(ctx, err, "failed to connect to forecast")
107109
}
108110
fc := forecaster.New(fcc)
111+
tcc, err := grpc.DialContext(ctx, *testerAddr,
112+
grpc.WithTransportCredentials(insecure.NewCredentials()),
113+
grpc.WithUnaryInterceptor(log.UnaryClientInterceptor()),
114+
grpc.WithStatsHandler(otelgrpc.NewClientHandler()))
115+
if err != nil {
116+
log.Errorf(ctx, err, "failed to connect to tester")
117+
os.Exit(1)
118+
}
119+
tc := tester.New(tcc)
109120

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

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

0 commit comments

Comments
 (0)