Skip to content
This repository has been archived by the owner on Mar 20, 2024. It is now read-only.

Demo script

Andy Kuszyk edited this page Feb 23, 2022 · 18 revisions

Introduction

This page documents some steps for demonstrating the contents of this repo in a talk or meet-up setting.

docker-compose set-up

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.
  1. 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
  1. 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
  1. 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
  1. 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

Implement a demo application

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.
  1. 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)
}
  1. 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:      &region,
		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
  1. 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
	}

Testing the demo application

At this stage, we should be able to run our demo application and see it working.

  1. Run the demo application:
go mod vendor
docker-compose up -d
  1. Make a test web request:
curl -X POST http://localhost:8080/payments -v
  1. See the logs from the test service:
docker logs f1-example-service-1
  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"

Write a load test using f1

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.
  1. First, let's stub out an f1 test binary:
mkdir -p cmd/f1
cat <<EOF > cmd/f1/main.go
package main

func main() {

}
EOF
  1. Now, let's turn this binary into a basic f1 test runner (by importing f1 and starting the CLI in main()):
import "github.com/form3tech-oss/f1/v2/pkg/f1"
	f := f1.New()
	f.Execute()
  1. Now, let's set-up our Go project and try running our test runner:
go mod vendor
go run ./cmd/f1/main.go --help
  1. 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
}
  1. We can see this new scenario in the f1 CLI:
go run ./cmd/f1/main.go scenarios ls
  1. 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 of testScenario()):
    // Configure an SQS client
    goawsEndpoint := "http://localhost:4100"
    region := "eu-west-1"
    s, err := session.NewSession(&aws.Config{
        Endpoint:    &goawsEndpoint,
        Region:      &region,
        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
  1. 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
    })
  1. 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
            }
        }
    }

Running our load test

Now that we've written our load test, we an run it using any of the built-in f1 runner modes.

  1. Let's just compile the binary to make running it a bit more ergonomic:
go build -o f1 ./cmd/f1/main.go
  1. We can run the test using a constant rate:
./f1 run constant testScenario -r 1/s -d 10s
  1. We can run the test using a fixed pool of virtual users (like k6):
./f1 run users testScenario -c 10 -d 10s