Skip to content

1. Getting Started With Grill

amit-shekhar edited this page Sep 27, 2023 · 5 revisions

We follow the standard principle for writing functional tests ->

establish your dependencies, execute the method, verify the output, and perform cleanup.

Setting Up Functional Tests with Grill

  1. Start Grills
  2. Setup Infra Components
  3. Set Environment Variables
  4. Run Application
  5. Run Tests
func TestFunctional(t *testing.T) {  
    if testing.Short() {  
       t.Skip("skipping functional test")  
    }  
  
    // setup grills
    grills := NewGrills()  
    if err := grills.StartAll(); err != nil {  
       t.Fatalf("error starting grills, error=%v", err)  
    }  
    defer grills.StopAll()  
    
    setupInfra(grills)
    
    setEnv(grills)  
    
    runServer(t)  

    runTests(grills) 
}

Start Grills

In this example we need DynamoDB and Kafka grills, the setup for it would look like ->

type Grills struct {
	Dynamo    *grilldynamo.Dynamo
	Kafka     *grillkafka.Kafka
}

func NewGrills() *Grills {
	return &Grills{
		Dynamo:    &grilldynamo.Dynamo{},
		Kafka:     &grillkafka.Kafka{},
	}
}

func (grills *Grills) StartAll() error {
	return grill.StartAll(context.Background(), grills.Dynamo, grills.Kafka)
}

func (grills *Grills) StopAll() {
	_ = grill.StopAll(context.Background(), grills.Dynamo, grills.Kafka)
}

Setup Infra Components

Create all Infra Dependencies required by your service like databases, tables, sqs queues, kafka topics etc.

func setupInfra(grills *Grills) error {
	if err := grills.Dynamo.CreateTable(testdata.OrderTableRequest()).Stub(); err != nil {
		return err
	}
	if err := grills.Kafka.CreateTopics("newOrderTopic").Stub(); err != nil {
		return err
	}
	return nil
}

Setup Environment Variables

Setup environment variables such that the service points to grills for all its dependencies.

func endpoint(host, port string) string {
	return fmt.Sprintf("http://%s:%s", host, port)
}

func setEnv(grills *Grills) {
	_ = os.Setenv("LOG_LEVEL", "DEBUG")

	_ = os.Setenv("DYNAMODB_TABLENAME", "test-table")
	_ = os.Setenv("DYNAMODB_REGION", grills.Dynamo.Region())
	_ = os.Setenv("DYNAMODB_ENDPOINT", endpoint(grills.Dynamo.Host(), grills.Dynamo.Port()))
	_ = os.Setenv("AWS_ACCESS_KEY_ID", grills.Dynamo.AccessKey())
	_ = os.Setenv("AWS_SECRET_ACCESS_KEY", grills.Dynamo.SecretKey())

	_ = os.Setenv("KAFKA_BOOTSTRAP_SERVERS", grills.Kafka.BootstrapServers())
}

Run Application

Start the application in a different go routine and wait for health check to pass

func runServer(t *testing.T) {
	go server.Run(config.New())
	if err := waitForHealthCheck(cfg.AppPort, time.Second*20); err != nil {
		t.Fatalf("health check failed, error=%v", err)
	}
}

func waitForHealthCheck(appPort int, maxTime time.Duration) error {
	maxTimeTicker := time.Tick(maxTime)
	ticker := time.Tick(time.Millisecond * 500)
	httpClient := http.Client{Timeout: time.Millisecond * 100}
	for {
		select {
		case <-maxTimeTicker:
			return fmt.Errorf("health check failed")
		case <-ticker:
			res, err := httpClient.Get(fmt.Sprintf("http://localhost:%d/health-check", appPort))
			if res != nil && res.Body != nil {
				res.Body.Close()
			}
			if err == nil && res.StatusCode == http.StatusOK {
				return nil
			}
		}
	}
}

Run Tests

Run your testscases with grill

func runTests(t *testing.T, grills *Grills) error {
    grill.Run(t, createOrderTests(grills))
    grill.Run(t, getOrderTests(grills))
}

Example Tests

func createOrderTests(grills *Grills) []grill.TestCase {
	return []grill.TestCase{
		{
			Name: "CreateOrder-Success",
			Stubs: []grill.Stub{},
			Action: func() interface{} {
				response, err := createOrder(testData.Order1)
				if err != nil {
					return grill.ActionOutput("", 0, err != nil)
				}
				defer response.Body.Close()
				responseBody, _ := io.ReadAll(response.Body)
				return grill.ActionOutput(string(responseBody), response.StatusCode, nil)
			},
			Assertions: []grill.Assertion{
				grills.AssertOutput("ok", 200, nil)
				grills.Dynamo.AssertItem(testData.Order1.DBItem()),
				grills.Kafka.AssertMessageCount("newOrderTopic", testData.Order1, 1)
			},
			Cleaners: []grill.Cleaner{},
		}
	}
}

func getOrderTests(grills *Grills) []grill.TestCase {
	return []grill.TestCase{
		{
			Name: "GetOrder-NotFound",
			Stubs: []grill.Stub{},
			Action: func() interface{} {
				response, err := getOrder("order_id_invalid")
				if err != nil {
					return grill.ActionOutput("", 0, err != nil)
				}
				defer response.Body.Close()
				responseBody, _ := io.ReadAll(response.Body)
				return grill.ActionOutput(string(responseBody), response.StatusCode, nil)
			},
			Assertions: []grill.Assertion{
				grill.AssertOutput("", 404, nil)
			},
			Cleaners: []grill.Cleaner{},
		},
		{
			Name: "GetOrder-FoundSuccess",
			Stubs: []grill.Stub{
				grills.Dynamo.PutItem(testdata.Order1.DBItem)
			},
			Action: func() interface{} {
				response, err := getOrder(testdata.Order1.ID)
				if err != nil {
					return grill.ActionOutput("", 0, err != nil)
				}
				defer response.Body.Close()
				responseBody, _ := io.ReadAll(response.Body)
				return grill.ActionOutput(string(responseBody), response.StatusCode, nil)
			},
			Assertions: []grill.Assertion{
				grill.AssertOutput(testdata.Order1.String(), 200, nil)
			},
			Cleaners: []grill.Cleaner{
				grills.EmptyDynamoDBTable()
			},
		}
	}
}