To write a test in Go, first design the interface

If you end up writing test code in Go

When you are instructed by your superior to "write a test code" at work Is it designed to write tests?

In this article, for the use case of ** getting a file from S3 and reading it ** Think about writing tests.

Cases where the test is difficult to write

main.go


var awssess = newSession()
var s3Client = newS3Client()

const defaultRegion = "ap-northeast-1"

func newSession() *session.Session {
	return session.Must(session.NewSessionWithOptions(session.Options{
		SharedConfigState: session.SharedConfigEnable,
	}))
}

func newS3Client() *s3.S3 {
	return s3.New(awssess, &aws.Config{
		Region: aws.String(defaultRegion),
	})
}

func readObject(bucket, key string) ([]byte, error) {
	obj, err := s3Client.GetObject(&s3.GetObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	})
	if err != nil {
		return nil, err
	}

	defer obj.Body.Close()
	res, err := ioutil.ReadAll(obj.Body)
	if err != nil {
		return nil, err
	}

	return res, nil
}

func main() {
	res, err := readObject("{Your Bucket}", "{Your Key}")
	if err != nil {
		log.Println(err)
	}

	log.Println(string(res))

	return
}

The files that exist in the S3 bucket are displayed by log.Println (). Write test code for readObject (). However, readObject () contains processing to access S3 directly and is completely dependent on external processing. At this rate, you have to access S3 every time you run a test, which is not realistic.

The first thing to think about here is to make a ** mock **.

Separate functions

First, the problem with the current readObject () is that the following two points exist in the same function.

--Getting files from S3 --Reading a file

Therefore, these two processes are separated by another function.

func getObject(bucket, key string) (*s3.GetObjectOutput, error) {
	obj, err := s3Client.GetObject(&s3.GetObjectInput{
		Bucket: aws.String(bucket),
		Key:    aws.String(key),
	})
	if err != nil {
		return nil, err
	}

	return obj, nil
}

func readObject(bucket, key string) ([]byte, error) {
	obj, err := getObject(bucket, key)
	if err != nil {
		return nil, err
	}

	defer obj.Body.Close()
	res, err := ioutil.ReadAll(obj.Body)
	if err != nil {
		return nil, err
	}

	return res, nil
}

This completes the separation. However, as it is, the above-mentioned dependency on S3 access cannot be resolved.

This is where ** interface ** comes into play.

interface implementation

In conclusion, the function you want to mock should be implemented as a function of interface. In this article, getObject () is a function of interface.

By doing so, the processing inside getObject () can be passed from the outside. You can pass access to S3 during normal times and mock during tests.

Below is the whole code that implements interface.

main.go


package main

import (
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/s3"
	"io/ioutil"
	"log"
)

var awssess = newSession()
var s3Client = newS3Client()

const defaultRegion = "ap-northeast-1"

func newSession() *session.Session {
	return session.Must(session.NewSessionWithOptions(session.Options{
		SharedConfigState: session.SharedConfigEnable,
	}))
}

func newS3Client() *s3.S3 {
	return s3.New(awssess, &aws.Config{
		Region: aws.String(defaultRegion),
	})
}

type objectGetterInterface interface {
	getObject() (*s3.GetObjectOutput, error)
}

type objectGetter struct {
	Bucket string
	Key    string
}

func newObjectGetter(bucket, key string) *objectGetter {
	return &objectGetter{
		Bucket: bucket,
		Key:    key,
	}
}

func (getter *objectGetter) getObject() (*s3.GetObjectOutput, error) {
	obj, err := s3Client.GetObject(&s3.GetObjectInput{
		Bucket: aws.String(getter.Bucket),
		Key:    aws.String(getter.Key),
	})
	if err != nil {
		return nil, err
	}

	return obj, nil
}

func readObject(t objectGetterInterface) ([]byte, error) {
	obj, err := t.getObject()
	if err != nil {
		return nil, err
	}

	defer obj.Body.Close()
	res, err := ioutil.ReadAll(obj.Body)
	if err != nil {
		return nil, err
	}

	return res, nil
}

func main() {
	t := newObjectGetter("{Your Bucket}", "{Your Key}")
	res, err := readObject(t)
	if err != nil {
		log.Println(err)
	}

	log.Println(string(res))

	return
}

Notice the argument of readObject (). We are passing objectGetterInterface as an argument.

Also, getObject () uses the objectGetter structure as a receiver and makes it a method.

objectGetterInterface is subject to the following methods.

getObject() (*s3.GetObjectOutput, error)

In other words, the objectGetter structure satisfies the condition of objectGetterInterface.

Now let's think about the mock method. In the same way, let's implement it so that it satisfies getObject () (* s3.GetObjectOutput, error).

type objectGetterMock struct{}

func (m objectGetterMock) getObject() (*s3.GetObjectOutput, error) {
	b := ioutil.NopCloser(strings.NewReader("hoge"))
	return &s3.GetObjectOutput{
		Body: b,
	}, nil
}

It is assumed that a file containing the string hoge is stored.

Now the objectGetterMock structure can meet the objectGetterInterface condition as well.

Let's take a look at the actual test code.

Test code

main_test.go


package main

import (
	"github.com/aws/aws-sdk-go/service/s3"
	"github.com/stretchr/testify/assert"
	"github.com/stretchr/testify/suite"
	"io/ioutil"
	"strings"
	"testing"
)

type testSuite struct {
	suite.Suite
	service *objectGetter
}

func (s *testSuite) SetUpTest() {
	s.service.Bucket = "dummy"
	s.service.Key = "dummy"
}

func TestExecution(t *testing.T) {
	suite.Run(t, new(testSuite))
}

type objectGetterMock struct{}

func (m objectGetterMock) getObject() (*s3.GetObjectOutput, error) {
	b := ioutil.NopCloser(strings.NewReader("hoge"))
	return &s3.GetObjectOutput{
		Body: b,
	}, nil
}

func (s *testSuite) Test() {
	mock := objectGetterMock{}
	res, _ := readObject(mock)
	assert.Equal(s.T(), "hoge", string(res))
}

Please note the following.

func (s *testSuite) Test() {
    mock := objectGetterMock{}
    res, _ := readObject(mock)
    assert.Equal(s.T(), "hoge", string(res))
}

Passing objectGetterMock toreadObject (). So I didn't have to worry about ** getting files from S3 ** in my tests, I was able to write tests that focused on ** reading files **.

At the end

Designing the interface makes the test code very easy to write. By all means, please aim for a high quality product by writing test code.

Recommended Posts

To write a test in Go, first design the interface
Write the test in a python docstring
I want to write in Python! (2) Let's write a test
Feel free to write a test with nose (in the case of + gevent)
Write a table-driven test in C
Create a function to get the contents of the database in Go
Various comments to write in the program
The trick to write flatten concisely in python
Write Kikagaku-style algorithm theory in Go Primality test
How to write a named tuple document in 2020
[Go] How to write or call a function
Function to extract the maximum and minimum values ​​in a slice with Go
The first thing to check when a No Reverse Match occurs in Django
I didn't have to write a decorator in the class Thank you contextmanager
Write Pulumi in Go
How to write a GUI using the maya command
Register a task in cron for the first time
I want to write in Python! (3) Utilize the mock
Write a log-scale histogram on the x-axis in python
Write code to Unit Test a Python web app
How to unit test a function containing the current time using freezegun in python
How to test the current time with Go (I made a very thin library)
How to find the first element that matches your criteria in a Python list
Define a task to set the fabric env in YAML
How to write custom validations in the Django REST Framework
A memorandum to register the library written in Hy in PyPI
It's hard to write a very simple algorithm in php
Change the standard output destination to a file in Python
Write a python program to find the editing distance [python] [Levenshtein distance]
Write a program to solve the 4x4x4 Rubik's Cube! 1. Overview
The first step to creating a serverless application with Zappa
Put the lists together in pandas to make a DataFrame
Recursively get the Excel list in a specific folder with python and write it to Excel.
How to generate a query using the IN operator in Django
How to write a test for processing that uses BigQuery
How to get the last (last) value in a list in Python
I wrote it in Go to understand the SOLID principle
[Python] The first step to making a game with Pyxel
The story I was addicted to when I specified nil as a function argument in Go
I didn't want to write the AWS key in the program
If you write go table driven test in python, it may be better to use subTest
[sh] How to store the command execution result in a variable
How to determine the existence of a selenium element in Python
Let's write a program to solve the 4x4x4 Rubik's Cube! 2. Algorithm
Created a package to support AWS Lambda development in Go language
Let's write a program to solve the 4x4x4 Rubik's Cube! 3. Implementation
How to get all the possible values in a regular expression
A game to go on an adventure in python interactive mode
The 15th offline real-time how to write reference problem in Python
How to check the memory size of a variable in Python
First python ② Try to write code while examining the features of python
[Go] Create a CLI command to change the extension of the image
I wrote the code to write the code of Brainf * ck in python
[Introduction to Python] How to use the in operator in a for statement?
How to check the memory size of a dictionary in Python
Try to write a program that abuses the program and sends 100 emails
How to get the vertex coordinates of a feature in ArcPy
How to create a large amount of test data in MySQL? ??
Write a script to calculate the distance with Elasticsearch 5 system painless
Raspberry Pi --1 --First time (Connect a temperature sensor to display the temperature)
Read the config file in Go language! Introducing a simple sample