Related
I have a function which gets all PDF files in a directory and returns the files if there are some.
func GetPdfFiles(path string) ([]string, error) {
var files []string
err := filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
if strings.HasSuffix(path, ".pdf") {
files = append(files, path)
}
return nil
})
if err != nil {
return nil, err
}
if files == nil {
return nil, errors.New("No PdfFiles found.")
}
return files, nil
}
My Test function gets the error: nil, from the filepath.Walk function which requires a anonymous function that returns an error, but it should get the error from the if statements like in the case of the second testcase with no files it should return errors.New("No PdfFiles found.").
How can i test it correctly.
func TestGetPdfFiles(t *testing.T) {
type args struct {
path string
}
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
tests := []struct {
name string
args args
want []string
wantErr bool
}{
{name: "2PdfFiles", args: args{path: fmt.Sprintf("%s/testData/", cwd)}, want: []string{fmt.Sprintf("%s/testData/test-1.pdf", cwd), fmt.Sprintf("%s/testData/test-2.pdf", cwd)}, wantErr: false},
{name: "noPdfFiles", args: args{path: fmt.Sprintf("%s", cwd)}, want: nil, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetPdfFiles(tt.args.path)
if (err != nil) != tt.wantErr {
t.Errorf("GetPdfFiles() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetPdfFiles() got = %v, want %v", got, tt.want)
}
})
}
}
You use dependency injection, and modify your function to accept an implementation of fs.FS. That lets your tests pass it a mock file system.
https://bitfieldconsulting.com/golang/filesystems
https://www.gopherguides.com/articles/golang-1.16-io-fs-improve-test-performance
Or, perhaps simpler for your use case, modify your GetPdfFiles() to accept a directory walker function with the same signature as path.WalkDir():
package main
import (
"io/fs"
"path"
"path/filepath"
"strings"
)
func GetPdfFiles(root string) ([]string, error) {
return GetPdfFilesWithWalker(root, filepath.WalkDir)
}
type DirectoryWalker = func(string, fs.WalkDirFunc) error
func GetPdfFilesWithWalker(root string, walk DirectoryWalker) (fns []string, err error) {
collectPdfFiles := func(fn string, info fs.DirEntry, err error) error {
if ext := strings.ToLower(path.Ext(fn)); ext == ".pdf" {
fns = append(fns, fn)
}
return nil
}
err = walk(root, collectPdfFiles)
return fns, err
}
Now your GetPdfFiles() is a do-nothing wrapper that injects the default implementation (from path/filepath), and the core is in GetPdfFilesWithWalker(), against which you write your tests, passing in a suitable mock.
you can even construct a mock that will return errors, so you can test your error handling.
Your mock directory walker can be as simple as something like this (especially since you only use the path passed to the callback:
func MockDirectoryWalker(root string, visit fs.WalkDirFunc) (err error) {
paths := [][]string{
{root, "a"},
{root, "a", "a.pdf"},
{root, "a", "b.txt"},
{root,"a", "b"},
{root, "a", "b", "c.pdf"},
{root, "a", "b", "d.txt"},
}
for _, p := range paths {
fqn := path.Join(p...)
var di fs.DirEntry
visit(fqn, di, nil)
}
return err
}
How about using T.TempDir() for a unique empty directory every time. This will guarantee repeatable results. The testing package will also take care of cleaning that:
func TestGetPdfFiles(t *testing.T) {
type args struct {
path string
}
tests := []struct {
name string
args args
want []string
wantErr bool
}{
{name: "noPdfFiles", args: args{path: t.TempDir()}, want: nil, wantErr: true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := GetPdfFiles(tt.args.path)
if (err != nil) != tt.wantErr {
t.Errorf("GetPdfFiles() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetPdfFiles() got = %v, want %v", got, tt.want)
}
})
}
}
I guess, there isn't really a good way to do it.
so i removed the error checks and just return an empty array, it would've been too messy.
Also thanks to #Qasim Sarfraz for the T.TempDir idea.
func GetPdfFiles(path string) []string {
var files []string
filepath.Walk(path, func(path string, info fs.FileInfo, err error) error {
if strings.HasSuffix(path, ".pdf") {
files = append(files, path)
}
return nil
})
return files
}
func TestGetPdfFiles(t *testing.T) {
type args struct {
path string
}
cwd, err := os.Getwd()
if err != nil {
log.Fatal(err)
}
tests := []struct {
name string
args args
want []string
wantErr bool
}{
{name: "2PdfFiles", args: args{path: fmt.Sprintf("%s/testData/", cwd)}, want: []string{fmt.Sprintf("%s/testData/test-1.pdf", cwd), fmt.Sprintf("%s/testData/test-2.pdf", cwd)}},
{name: "noPdfFiles", args: args{path: t.TempDir()}, want: nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := GetPdfFiles(tt.args.path)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("GetPdfFiles() got = %v, want %v", got, tt.want)
}
})
}
}
I am building a simple function that calls an API that returns a Post using GraphQL (https://github.com/machinebox/graphql). I wrapped the logic in a service that looks like this:
type Client struct {
gcl graphqlClient
}
type graphqlClient interface {
Run(ctx context.Context, req *graphql.Request, resp interface{}) error
}
func (c *Client) GetPost(id string) (*Post, error) {
req := graphql.NewRequest(`
query($id: String!) {
getPost(id: $id) {
id
title
}
}
`)
req.Var("id", id)
var resp getPostResponse
if err := c.gcl.Run(ctx, req, &resp); err != nil {
return nil, err
}
return resp.Post, nil
}
Now I'd like to add test tables for the GetPost function with a fail case when id is set to empty string which causes an error in the downstream call c.gcl.Run.
What I am struggling with is the way the gcl client can be mocked and forced to return the error (when no real API call happens).
My test so far:
package apiClient
import (
"context"
"errors"
"github.com/aws/aws-sdk-go/aws"
"github.com/google/go-cmp/cmp"
"github.com/machinebox/graphql"
"testing"
)
type graphqlClientMock struct {
graphqlClient
HasError bool
Response interface{}
}
func (g graphqlClientMock) Run(_ context.Context, _ *graphql.Request, response interface{}) error {
if g.HasError {
return errors.New("")
}
response = g.Response
return nil
}
func newTestClient(hasError bool, response interface{}) *Client {
return &Client{
gcl: graphqlClientMock{
HasError: hasError,
Response: response,
},
}
}
func TestClient_GetPost(t *testing.T) {
tt := []struct{
name string
id string
post *Post
hasError bool
response getPostResponse
}{
{
name: "empty id",
id: "",
post: nil,
hasError: true,
},
{
name: "existing post",
id: "123",
post: &Post{id: aws.String("123")},
response: getPostResponse{
Post: &Post{id: aws.String("123")},
},
},
}
for _, tc := range tt {
t.Run(tc.name, func(t *testing.T) {
client := newTestClient(tc.hasError, tc.response)
post, err := client.GetPost(tc.id)
if err != nil {
if tc.hasError == false {
t.Error("unexpected error")
}
} else {
if tc.hasError == true {
t.Error("expected error")
}
if cmp.Equal(post, &tc.post) == false {
t.Errorf("Response data do not match: %s", cmp.Diff(post, tc.post))
}
}
})
}
}
I am not sure if passing the response to the mock like this is the right way to do it. Also, I'm struggling to set the right value to the response, since an interface{} type is passed and I don't know how to convert it to the getPostResponse and set the value to Post there.
Your test cases should not go beyond the implementation. I'm specifically referring to the empty-vs-nonempty input or any kind of input really.
Let's take a look at the code you want to test:
func (c *Client) GetPost(id string) (*Post, error) {
req := graphql.NewRequest(`
query($id: String!) {
getPost(id: $id) {
id
title
}
}
`)
req.Var("id", id)
var resp getPostResponse
if err := c.gcl.Run(ctx, req, &resp); err != nil {
return nil, err
}
return resp.Post, nil
}
Nothing in the implementation above is doing anything based on the id parameter value and therefore nothing in your tests for this piece of code should really care about what input is passed in, if it is irrelevant to the implementation it should also be irrelevant to the tests.
Your GetPost has basically two code branches that are taken based on a single factor, i.e. the "nilness" of the returned err variable. This means that as far as your implementation is concerned there are only two possible outcomes, based on what err value Run returns, and therefore there should only be two test cases, a 3rd or 4th test case would be just a variation, if not an outright copy, of the first two.
Your test client is also doing some unnecessary stuff, the main one being its name, i.e. what you have there is not a mock so calling it that is not helpful. Mocks usually do a lot more than just return predefined values, they ensure that methods are called, in the expected order and with the expected arguments, etc. And actually you don't need a mock here at all so it's a good thing it isn't one.
With that in mind, here's what I would suggest you do with your test client.
type testGraphqlClient struct {
resp interface{} // non-pointer value of the desired response, or nil
err error // the error to be returned by Run, or nil
}
func (g testGraphqlClient) Run(_ context.Context, _ *graphql.Request, resp interface{}) error {
if g.err != nil {
return g.err
}
if g.resp != nil {
// use reflection to set the passed in response value
// (i haven't tested this so there may be a bug or two)
reflect.ValueOf(resp).Elem().Set(reflect.ValueOf(g.resp))
}
return nil
}
... and here are the necessary test cases, all two of them:
func TestClient_GetPost(t *testing.T) {
tests := []struct {
name string
post *Post
err error
client testGraphqlClient
}{{
name: "return error from client",
err: errors.New("bad input"),
client: testGraphqlClient{err: errors.New("bad input")},
}, {
name: "return post from client",
post: &Post{id: aws.String("123")},
client: testGraphqlClient{resp: getPostResponse{Post: &Post{id: aws.String("123")}}},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := Client{gql: tt.client}
post, err := client.GetPost("whatever")
if !cmp.Equal(err, tt.err) {
t.Errorf("got error=%v want error=%v", err, tt.err)
}
if !cmp.Equal(post, tt.post) {
t.Errorf("got post=%v want post=%v", post, tt.post)
}
})
}
}
... there's a bit of repetition going on here, the need to spell out the post and err twice but that's a small price to pay when compared to a more sophisticated/complicated test setup that would populate the test client from the test case's expected output fields.
Addendum:
If you were to update GetPost in such a way that it checks for the empty id and returns an error before it sends a request to graphql then your initial setup would make much more sense:
func (c *Client) GetPost(id string) (*Post, error) {
if id == "" {
return nil, errors.New("empty id")
}
req := graphql.NewRequest(`
query($id: String!) {
getPost(id: $id) {
id
title
}
}
`)
req.Var("id", id)
var resp getPostResponse
if err := c.gcl.Run(ctx, req, &resp); err != nil {
return nil, err
}
return resp.Post, nil
}
... and updating the test cases accordingly:
func TestClient_GetPost(t *testing.T) {
tests := []struct {
name string
id string
post *Post
err error
client testGraphqlClient
}{{
name: "return empty id error",
id: "",
err: errors.New("empty id"),
client: testGraphqlClient{},
}, {
name: "return error from client",
id: "nonemptyid",
err: errors.New("bad input"),
client: testGraphqlClient{err: errors.New("bad input")},
}, {
name: "return post from client",
id: "nonemptyid",
post: &Post{id: aws.String("123")},
client: testGraphqlClient{resp: getPostResponse{Post: &Post{id: aws.String("123")}}},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
client := Client{gql: tt.client}
post, err := client.GetPost(tt.id)
if !cmp.Equal(err, tt.err) {
t.Errorf("got error=%v want error=%v", err, tt.err)
}
if !cmp.Equal(post, tt.post) {
t.Errorf("got post=%v want post=%v", post, tt.post)
}
})
}
}
Following the example at https://golang.org/pkg/os/exec/#Cmd.StdoutPipe, suppose I have a function getPerson() defined like so:
package stdoutexample
import (
"encoding/json"
"os/exec"
)
// Person represents a person
type Person struct {
Name string
Age int
}
func getPerson() (Person, error) {
person := Person{}
cmd := exec.Command("echo", "-n", `{"Name": "Bob", "Age": 32}`)
stdout, err := cmd.StdoutPipe()
if err != nil {
return person, err
}
if err := cmd.Start(); err != nil {
return person, err
}
if err := json.NewDecoder(stdout).Decode(&person); err != nil {
return person, err
}
if err := cmd.Wait(); err != nil {
return person, err
}
return person, nil
}
In my 'real' application, the command run can have different outputs, I'd like to write test cases for each of these scenarios. However, I'm not sure how to go about this.
So far all I have is a test case for one case:
package stdoutexample
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetPerson(t *testing.T) {
person, err := getPerson()
require.NoError(t, err)
assert.Equal(t, person.Name, "Bob")
assert.Equal(t, person.Age, 32)
}
Perhaps the way to go about this is to split this function into two parts, one which writes the output of the command to a string, and another which decodes the output of a string?
adding to https://stackoverflow.com/a/58107208/9353289,
Instead of writing separate Test functions for every test, I suggest you use a Table Driven Test approach instead.
Here is an example,
func Test_getPerson(t *testing.T) {
tests := []struct {
name string
commandOutput []byte
want Person
}{
{
name: "Get Bob",
commandOutput: []byte(`{"Name": "Bob", "Age": 32}`),
want: Person{
Name: "Bob",
Age: 32,
},
},
{
name: "Get Alice",
commandOutput: []byte(`{"Name": "Alice", "Age": 25}`),
want: Person{
Name: "Alice",
Age: 25,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := getPerson(tt.commandOutput)
require.NoError(t, err)
assert.Equal(t, tt.want.Name, got.Name)
assert.Equal(t, tt.want.Age, got.Age)
})
}
}
Simply adding test cases to the slice, will run all test cases.
I added unit tests by splitting the function into two parts: one which reads the output to a slice of bytes, and one which parses that output to a Person:
package stdoutexample
import (
"bytes"
"encoding/json"
"os/exec"
)
// Person represents a person
type Person struct {
Name string
Age int
}
func getCommandOutput() ([]byte, error) {
cmd := exec.Command("echo", "-n", `{"Name": "Bob", "Age": 32}`)
return cmd.Output()
}
func getPerson(commandOutput []byte) (Person, error) {
person := Person{}
if err := json.NewDecoder(bytes.NewReader(commandOutput)).Decode(&person); err != nil {
return person, err
}
return person, nil
}
The following test cases pass:
package stdoutexample
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestGetPerson(t *testing.T) {
commandOutput, err := getCommandOutput()
require.NoError(t, err)
person, err := getPerson(commandOutput)
require.NoError(t, err)
assert.Equal(t, person.Name, "Bob")
assert.Equal(t, person.Age, 32)
}
func TestGetPersonBob(t *testing.T) {
commandOutput := []byte(`{"Name": "Bob", "Age": 32}`)
person, err := getPerson(commandOutput)
require.NoError(t, err)
assert.Equal(t, person.Name, "Bob")
assert.Equal(t, person.Age, 32)
}
func TestGetPersonAlice(t *testing.T) {
commandOutput := []byte(`{"Name": "Alice", "Age": 25}`)
person, err := getPerson(commandOutput)
require.NoError(t, err)
assert.Equal(t, person.Name, "Alice")
assert.Equal(t, person.Age, 25)
}
where the Bob and Alice test cases simulate different output which can be generated by the command.
your implementation design actively rejects fine grain testing because it does not allow any injection.
However, given the example, besides using a TestTable, there is not much to improve.
Now, on a real workload, you might encounter unacceptable slowdown dues to call to the external binary. This might justify another approach involving a design refactoring to mock and the setup of multiple tests stubs.
To mock your implementation you make use of interface capabilities.
To stub your execution you create a mock that outputs stuff you want to check for.
package main
import (
"encoding/json"
"fmt"
"os/exec"
)
type Person struct{}
type PersonProvider struct {
Cmd outer
}
func (p PersonProvider) Get() (Person, error) {
person := Person{}
b, err := p.Cmd.Out()
if err != nil {
return person, err
}
err = json.Unmarshal(b, &person)
return person, err
}
type outer interface{ Out() ([]byte, error) }
type echo struct {
input string
}
func (e echo) Out() ([]byte, error) {
cmd := exec.Command("echo", "-n", e.input)
return cmd.Output()
}
type mockEcho struct {
output []byte
err error
}
func (m mockEcho) Out() ([]byte, error) {
return m.output, m.err
}
func main() {
fmt.Println(PersonProvider{Cmd: echo{input: `{"Name": "Bob", "Age": 32}`}}.Get())
fmt.Println(PersonProvider{Cmd: mockEcho{output: nil, err: fmt.Errorf("invalid json")}}.Get())
}
I got an example from #volker about table driven test like following
But currently I miss what I should put in the real test, this test is using byte, currently im not sure what to put in the args and the expected []byte,
e.g. I want to check that in the file there is 2 new line and then application entry, how can I do it without the need to create the real file and parse it?
type Models struct {
name string
vtype string
contentType string
}
func setFile(file io.Writer, appStr Models) {
fmt.Fprint(file, "1.0")
fmt.Fprint(file, "Created-By: application generation process")
for _, mod := range appStr.Modules {
fmt.Fprint(file, "\n")
fmt.Fprint(file, "\n")
fmt.Fprint(file, appStr.vtype) //"userApp"
fmt.Fprint(file, "\n")
fmt.Fprint(file, appStr.name) //"applicationValue"
fmt.Fprint(file, "\n")
fmt.Fprint(file, appStr.contentType)//"ContentType"
}
}
func Test_setFile(t *testing.T) {
type args struct {
appStr models.App
}
var tests []struct {
name string
args args
expected []byte
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := &bytes.Buffer{}
setFile(b, tt.args.AppStr)
if !bytes.Equal(b.Bytes(), tt.expected) {
t.Error("somewhat bad happen")
}
})
}
}
I read and understand the following example but not for byte and file
https://medium.com/#virup/how-to-write-concise-tests-table-driven-tests-ed672c502ae4
If you are only checking for the static content at the beginning, then you really only need one test. It would look something like this:
func Test_setFile(t *testing.T) {
type args struct {
appStr models.App
}
var tests []struct {
name string
args args
expected []byte
}{
name: 'Test Static Content',
args: args{appStr: 'Some String'},
expected: []byte(fmt.Sprintf("%s%s%s", NEW_LINE, NEW_LINE, "Application")),
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := &bytes.Buffer{}
setFile(b, tt.args.AppStr)
if !bytes.Equal(b.Bytes(), tt.expected) {
t.Error("somewhat bad happen")
}
})
}
}
Although, since you only have one case for this test, there really isn't a need to use table driven tests here. You could clean it up to look something like this:
func Test_setFile(t *testing.T) {
b := &bytes.Buffer{}
setFile(b, 'Some String')
want := []byte(fmt.Sprintf("%s%s%s", NEW_LINE, NEW_LINE, "Application"))
got := b.Bytes()
if !bytes.Equal(want, got) {
t.Errorf("want: %s got: %s", want, got)
}
}
I have the following Golang code:
func getConfigFile() string {
var configFile string
flag.StringVar(&configFile, "config", "", "File containing configuration")
flag.Parse()
return configFile
}
This function is used elsewhere in my code, and I'd like to unit test what happens here when the user provides different values for the config argument (the config file name is used else where).
Is there a way to tell the flag package to return different values for the config argument while under test?
I have found that for testing custom flags is better to create a custom flag set, in that way I can fully test the flags, including the -h option without exiting the tests. hope the attached code could give you and idea of how you could implement test on your code:
package main
import (
"flag"
"fmt"
"os"
"reflect"
"testing"
)
// Test Helper
func expect(t *testing.T, a interface{}, b interface{}) {
if a != b {
t.Errorf("Expected: %v (type %v) Got: %v (type %v)", a, reflect.TypeOf(a), b, reflect.TypeOf(b))
}
}
type Flags struct {
ConfigFile string
}
func (self *Flags) Parse(fs *flag.FlagSet) (*Flags, error) {
fs.StringVar(&self.ConfigFile, "config", "", "File containing configuration")
err := fs.Parse(os.Args[1:])
if err != nil {
return nil, err
}
return self, nil
}
func main() {
fs := flag.NewFlagSet("test", flag.ContinueOnError)
parser := Flags{}
flags, err := parser.Parse(fs)
if err != nil {
panic(err)
}
fmt.Println(flags)
}
func TestFlags(t *testing.T) {
oldArgs := os.Args
defer func() { os.Args = oldArgs }()
var flagTest = []struct {
flag []string
name string
expected interface{}
}{
{[]string{"cmd", "-config", "config.yaml"}, "ConfigFile", "config.yaml"},
{[]string{"cmd", "-config", "config.json"}, "ConfigFile", "config.json"},
{[]string{"cmd", "-v"}, "Version", true},
}
for _, f := range flagTest {
os.Args = f.flag
p := &Flags{}
fs := flag.NewFlagSet("test", flag.ContinueOnError)
flags, err := p.Parse(fs)
if err != nil {
t.Error(err)
}
refValue := reflect.ValueOf(flags).Elem().FieldByName(f.name)
switch refValue.Kind() {
case reflect.Bool:
expect(t, f.expected, refValue.Bool())
case reflect.String:
expect(t, f.expected, refValue.String())
}
}
}
I put it also here: https://play.golang.org/p/h1nok1UMLA hope it can give you an idea.
If you change it like the code below, go test will fail but go test -config testconfig will pass. Not that we don't need to call flag.Parse() in the init() since it is be called by the testing package (as Rob Pike mentions in https://groups.google.com/d/msg/golang-nuts/uSFM8jG7yn4/PIQfEWOZx4EJ).
package main
import (
"flag"
"testing"
)
var configFile = flag.String("config", "", "File containing configuration")
func getConfigFile() string {
return *configFile
}
func TestConfig(t *testing.T) {
want := "testconfig"
if s := getConfigFile(); s != want {
t.Errorf("Got %s, want %s", s, want)
}
}
Test runs:
$ go test
--- FAIL: TestConfig (0.00s)
flag_test.go:17: Got , want testconfig
FAIL
exit status 1
FAIL github.com/dmitris/soflagtest 0.013s
$ go test -config testconfig
PASS
ok github.com/dmitris/soflagtest 0.012s
You can also use
var configFile string declaration and an init() function to assign the flag value to the variable:
func init() {
flag.StringVar(&configFile, "config", "", "File containing configuration")
}
(then no pointer dereferencing in getConfigFile since configFile is a string)