This repository has been archived by the owner on Mar 20, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2
Demo script
Andy Kuszyk edited this page Feb 23, 2022
·
18 revisions
This page documents some steps for demonstrating the contents of this repo in a talk or meet-up setting.
We want to set-up an environment against which to run load tests. In this environment, we want:
- A mock message queue, for which we'll use
goaws
to mock SQS.- A demo application that we can send HTTP requests to, and consume SQS messages from.
- First, let's create a
docker-compose.yml
file to run a local SQS mock, along with a demo application to test:
cat <<EOF > docker-compose.yml
services:
goaws:
image: pafortin/goaws
ports:
- "4100:4100"
volumes:
- ./goaws.yaml:/conf/goaws.yaml
service:
build: .
ports:
- "8080:8080"
EOF
- Now, let's create a config file for
goaws
:
cat <<EOF > goaws.yaml
Local:
Host: goaws
Port: 4100
Region: eu-west-1
AccountId: "100010001000"
Queues:
- Name: test-queue
EOF
- Next, let's create a
Dockerfile
to run a demo service:
cat <<EOF > Dockerfile
FROM golang:1.17
COPY ./ /f1-example
WORKDIR /f1-example
CMD go run ./cmd/service/main.go
EOF
- Finally, let's stub out the demo application itself:
mkdir -p cmd/service
cat <<EOF > cmd/service/main.go
package main
func main() {
}
EOF
go mod init github.com/form3tech-oss/f1-example
Now we want to add functionality to our demo application that will:
- Expose an HTTP endpoint that we can use in our load tests.
- Publish an asynchronous notification when an HTTP request is received, which we can consume from our load test.
- First, let's setup an HTTP listener:
func main() {
http.HandleFunc("/payments", paymentsHandler)
http.ListenAndServe(":8080", nil)
}
func paymentsHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusAccepted)
}
- Now, let's add an SQS client to our demo application (some global variables, and additional code at the start of
main()
):
var (
sqsClient *sqs.SQS
queueUrl *string
)
goawsEndpoint := "http://goaws:4100"
region := "eu-west-1"
s, err := session.NewSession(&aws.Config{
Endpoint: &goawsEndpoint,
Region: ®ion,
Credentials: credentials.NewStaticCredentials("foo", "bar", ""),
})
if err != nil {
panic(err)
}
sqsClient = sqs.New(s)
queueName := "test-queue"
queueUrlResponse, err := sqsClient.GetQueueUrl(&sqs.GetQueueUrlInput{
QueueName: &queueName,
})
if err != nil {
panic(err)
}
queueUrl = queueUrlResponse.QueueUrl
- Finally, let's publish an SQS message whenever we handle an HTTP request (by adding the following to the start of
paymentsHandler()
):
if r.Method != http.MethodPost {
log.Printf("request received with invalid http method: %s", r.Method)
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
log.Println("http POST request received on /payments")
message := "test message"
_, err := sqsClient.SendMessage(&sqs.SendMessageInput{
MessageBody: &message,
QueueUrl: queueUrl,
})
if err != nil {
log.Printf("error: %s", err)
w.WriteHeader(http.StatusInternalServerError)
return
}
At this stage, we should be able to run our demo application and see it working.
- Run the demo application:
go mod vendor
docker-compose up -d
- Make a test web request:
curl -X POST http://localhost:8080/payments -v
- See the logs from the test service:
docker logs f1-example-service-1
- See if there's any SQS messages published to the queue:
aws configure
aws --endpoint-url http://localhost:4100 sqs list-queues
aws --endpoint-url http://localhost:4100 sqs get-queue-attributes --queue-url "http://eu-west-1.goaws:4100/100010001000/test-queue"
Now, let's write a load test which will:
- Send HTTP requests to our demo application.
- Consume the SQS messages that our demo application produces, in order to verify that each web request was successful.
- First, let's stub out an
f1
test binary:
mkdir -p cmd/f1
cat <<EOF > cmd/f1/main.go
package main
func main() {
}
EOF
- Now, let's turn this binary into a basic
f1
test runner (by importingf1
and starting the CLI inmain()
):
import "github.com/form3tech-oss/f1/v2/pkg/f1"
f := f1.New()
f.Execute()
- Now, let's set-up our Go project and try running our test runner:
go mod vendor
go run ./cmd/f1/main.go --help
- Next, let's create a new test scenario and register it with
f1
:
f.Add("testScenario", testScenario)
func testScenario(t *testing.T) testing.RunFn {
// Our test set-up code goes here.
runFn := func(t *testing.T) {
// Our test iteration code goes here.
}
return runFn
}
- We can see this new scenario in the
f1
CLI:
go run ./cmd/f1/main.go scenarios ls
- Now, let's set-up an SQS client for our test iterations to run. Since this code is outside of the
runFn
, it is only executed once (we add it to the start oftestScenario()
):
// Configure an SQS client
goawsEndpoint := "http://localhost:4100"
region := "eu-west-1"
s, err := session.NewSession(&aws.Config{
Endpoint: &goawsEndpoint,
Region: ®ion,
Credentials: credentials.NewStaticCredentials("foo", "bar", ""),
})
if err != nil {
t.Require().NoError(err)
}
sqsClient := sqs.New(s)
queueName := "test-queue"
queueUrlResponse, err := sqsClient.GetQueueUrl(&sqs.GetQueueUrlInput{
QueueName: &queueName,
})
if err != nil {
t.Require().NoError(err)
}
queueUrl := queueUrlResponse.QueueUrl
- Next, let's add some code to consume messages from SQS. This is where writing the test case in Go starts to become really beneficial:
// Consume SQS messages from the queue and put
// them in a channel.
messagesChan := make(chan string, 100)
stopChan := make(chan bool)
go func() {
for {
select {
case <-stopChan:
return
default:
messages, err := sqsClient.ReceiveMessage(&sqs.ReceiveMessageInput{
QueueUrl: queueUrl,
})
t.Require().NoError(err)
for _, message := range messages.Messages {
if message.Body != nil {
messagesChan <- *message.Body
}
}
}
}
}()
t.Cleanup(func() {
stopChan <- true
})
- Now we can add an implementation for our test iterations. We want to send an HTTP request, and wait for a corresponding SQS message to be received:
runFn := func(t *testing.T) {
// Our test iteration code goes here.
res, err := http.Post("http://localhost:8080/payments", "application/json", nil)
t.Require().NoError(err)
t.Require().Equal(http.StatusAccepted, res.StatusCode)
timer := time.NewTimer(10 * time.Second)
for {
select {
case <-timer.C:
t.Require().Fail("no message received after timeout")
return
case <-messagesChan:
t.Logger().Info("message received, iteration success")
return
}
}
}
Now that we've written our load test, we an run it using any of the built-in
f1
runner modes.
- Let's just compile the binary to make running it a bit more ergonomic:
go build -o f1 ./cmd/f1/main.go
- We can run the test using a constant rate:
./f1 run constant testScenario -r 1/s -d 10s
- We can run the test using a fixed pool of virtual users (like
k6
):
./f1 run users testScenario -c 10 -d 10s