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