I'm trying to do a test:
The test cases are stored in a slice of structs with input and expect.
The expect can be int or Error.
The goal is to expect an Error if the input will cause an invalid result. I've learnt that it's not recommended to return an error string in Go. How can I expect an error whilst doing unit testing in Go?
Here's the code:
func arrIdx(arr []int, idx int) (int, error) {
if idx >= 0 && idx < len(arr) {
return arr[idx], nil
}
return 0, fmt.Errorf("This is an error")
}
func TestArrIdx(t *testing.T) {
arr := []int{1, 2, 3, 4, 5}
tests := []struct {
arr []int
idx int
expect int // or an error
}{
{arr, len(arr) - 1, 5, nil},
{arr, 1, 2, nil},
// {arr, -1, 0, t.Error},
}
for _, test := range tests {
testName := fmt.Sprintf("%d", test.input)
t.Run(testName, func(t *testing.T) {
got := arr[test.input]
if got != test.expect {
t.Errorf("got %v, want %v", got, test.expect)
} else {
fmt.Printf("got %v expect %v\n", got, test.expect)
}
})
}
}
I am not entirely sure what you are trying to do. Usually, you write methods that you want to test. Those methods have expected and unexpected results. You write unit tests to inject expected and unexpected input to generate those expected and unexpected results so your unit tests can make sure your methods are doing what they are built to do.
That being said, I could modify your block of code to handle errors, but I am not sure that is what you want:
package so_test
import (
"fmt"
"testing"
)
func TestArrIdx(t *testing.T) {
arr := [5]int{1, 2, 3, 4, 5}
tests := []struct {
input int
expect int
err error
}{
{len(arr) - 1, 5, nil},
{1, 2, nil},
{0, 0, fmt.Errorf("this is an error")},
}
for _, test := range tests {
testName := fmt.Sprintf("%d", test.input)
t.Run(testName, func(t *testing.T) {
if test.err != nil {
t.Errorf("received error '%v'", test.err)
} else {
got := arr[test.input]
if got != test.expect {
t.Errorf("got %v, want %v", got, test.expect)
} else {
fmt.Printf("got %v expect %v\n", got, test.expect)
}
}
})
}
}
Or, maybe this is closer to what you were hoping to do?
package so_test
import (
"fmt"
"testing"
)
// our unit test of the DoSomething() function
func TestDoSomething(t *testing.T) {
tests := []struct {
input int
expect int
err error
}{
{1, 2, nil},
{2, 2, fmt.Errorf("this is an error")},
{0, 0, fmt.Errorf("this is a DIFFERENT error")}, // will always fail
}
for _, test := range tests {
result, err := DoSomething(test.input)
if test.err != nil {
if err == nil {
t.Errorf("expected error '%v', received none\n", test.err)
} else if err.Error() != test.err.Error() {
t.Errorf("expected error: '%v', received '%v'\n", test.err, err)
} else {
fmt.Printf("expected error '%v', received error: '%v'\n", test.err, err)
}
} else {
if result != test.expect {
t.Errorf("got %v, want %v", result, test.expect)
} else {
fmt.Printf("got %v expect %v\n", result, test.expect)
}
}
}
}
// our function to test
func DoSomething(something int) (int, error) {
if something == 1 {
return 2, nil;
}
return 0, fmt.Errorf("this is an error")
}
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 use Testify to create a unit test for my golang app. I need to create a unit test for this function where it calls a variadic function (function with trailing arguments). I encountered an error when I test it. I'm actually not sure if the error is because of the trailing argument itself or not, but I feel like there's something wrong with the mock.
// svc/callThisFunction.go
// data type of args is []sqkit.SelectOption
func CallThisFunction(ctx context.Context, args ...sqkit.SelectFunctiom) (result string, err error) {
return result, nil
}
// svc/functionToTest.go
// This is the function that I wanna test
func FunctionToTest(ctx context.Context, id int64) (result string, err error) {
args := []sqkit.SelectOption{
sqkit.Where{
fmt.Sprintf("id = %d", id),
},
}
newResult, err := callThisFunctionService.CallThisFunction(ctx, args)
if err != nil {
return newResult, err
}
return newResult, nil
}
// svc/functionToTest_test.go
func Test_FunctionToTest(t *testing.T) {
testCase := []struct {
name string
id int64
onCallThisFunctionMock func(callThisFunctionSvc *mocks.CallThisFunctionSvc)
expectedResult string
wantError bool
expectedError error
}{
{
name: "Success",
id: 1,
onCallThisFunctionMock: func(callThisFunctionSvc *mocks.CallThisFunctionSvc) {
// NOTE: I've created 2 different versions (used separately, not at the same), using mock.Anything() and using actual arguments
// Both of these give the same errors
// Using actual arguments
args := []sqkit.SelectOption{
sqkit.Where{
fmt.Sprintf("id = %d", 1},
},
}
callThisFunctionSvc.On("CallThisFunction", context.Background(), args).Return("Success", nil)
// Using mock.Anything
callThisFunctionSvc.On("CallThisFunction", context.Background(), mock.Anything).Return("Success", nil)
}
}
}
for _, tc := range testCases {
var callThisFunctionSvc = new(mocks.CallThisFunctionSvc)
tc.onCallThisFunctionMock(callThisFunctionSvc)
svc := &svc.FunctionToTest{
CallThisFunction: callThisFunctionSvc,
}
actualResult, actualError := svc.FunctionToTest(context.Background(), tc.id)
if tc.wantEror {
require.Error(t, actualError, tc.expectedError)
} else {
require.NoError(t, actualError)
}
require.Equal(t, tc.expectedResult, actualResult)
}
}
This is the error it gives
=== RUN Test_GenerateDocument
--- FAIL: Test_GenerateDocument (0.00s)
panic:
assert: mock: I don't know what to return because the method call was unexpected.
Either do Mock.On("CallThisFunction").Return(...) first, or remove the GetTemplates() call.
This method was unexpected:
CallThisFunction(*context.emptyCtx,sqkit.Where)
0: (*context.emptyCtx)(0xc0000a4010)
1: sqkit.Where{"id = 1"}
Usually, when I encountered an error like this, it's because I haven't defined the return values of the function calls inside the function I wanna test. But this time I've created it, but it somehow can't read the return. Any idea why?
The error indicates you called CallThisFuncion with params (context.Context, sqkit.Where), but your example is using and setting the expectation for (context.Context, []sqkit.Option). The example with mock.Anything should work, but I believe it's failing because of the context. You'll need to set the expectation with the same context you're passing down. If FunctionToTest is going to be altering the context, I believe you'll need to use mock.Anything instead.
func Test_FunctionToTest(t *testing.T) {
testCase := []struct {
name string
id int64
onCallThisFunctionMock func(context.Context, *mocks.CallThisFunctionSvc)
expectedResult string
wantError bool
expectedError error
}{
{
name: "Success",
id: 1,
onCallThisFunctionMock: func(ctx context.Context, callThisFunctionSvc *mocks.CallThisFunctionSvc) {
args := []sqkit.SelectOption{
sqkit.Where{
fmt.Sprintf("id = %d", 1},
},
}
callThisFunctionSvc.On("CallThisFunction", ctx, args).Return("Success", nil)
}
}
}
for _, tc := range testCases {
var callThisFunctionSvc = new(mocks.CallThisFunctionSvc)
var ctx = context.Background()
tc.onCallThisFunctionMock(ctx, callThisFunctionSvc)
svc := &svc.FunctionToTest{
CallThisFunction: callThisFunctionSvc,
}
actualResult, actualError := svc.FunctionToTest(ctx, tc.id)
if tc.wantEror {
require.Error(t, actualError, tc.expectedError)
} else {
require.NoError(t, actualError)
}
require.Equal(t, tc.expectedResult, actualResult)
}
}
If you want to ensure a context.Context was passed as the first parameter but don't care what context, you could use AnythingOfType.
callThisFunctionSvc.On("CallThisFunction", mock.AnythingOfType("context.Context"), args).Return("Success", nil)
I'm making a simple test, and test using Go: Toggle Test Coverage In Current Package in Visual Studio Code like this
In my code is using os.Getenv to read a variable from my own env.
It get FAIL when using os.Getenv, but SUCCESS when hardcoded
How to get SUCCESS even get data from env using Go: Toggle Test Coverage In Current Package?
Here is my code:
type MyStruct struct {
Name string `json:"name"`
}
func MyFunc() ([]MyStruct, error) {
str := os.Getenv("MyStruct") // Fail
// str := `[{"name": "one"}, {"name": "two"}]` // Success
var myData []MyStruct
err := json.Unmarshal([]byte(str), &myData)
if err != nil {
return nil, err
}
return myData, nil
}
My test code:
func TestMyFunc(t *testing.T) {
tests := []struct {
name string
want []MyStruct
wantErr error
}{
{
name: "success",
want: []MyStruct{
{
Name: "one",
},
{
Name: "two",
},
},
wantErr: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := MyFunc()
assert.Equal(t, err, tt.wantErr)
assert.Equal(t, got, tt.want)
})
}
}
My env:
export MyStruct='[{"name": "one"}, {"name": "two"}]'
Using
I found the solution
Just add this code to test code:
strEnv := `[{"name": "one"}, {"name": "two"}]`
err := os.Setenv("MyStruct", strEnv)
if err != nil {
return
}
So I'm making unit test in golang using mockery and testify
The test code goes like this:
const bufSize = 1024 * 1024
var lis *bufconn.Listener
var mockAccountService = &mocks.AccountService{}
func init() {
lis = bufconn.Listen(bufSize)
s := grpc.NewServer()
RegisterAccountManagementHandlerServer(s, &server{mockAccountService})
go func() {
if err := s.Serve(lis); err != nil {
log.Fatalf("Server exited with error: %v", err)
}
}()
}
func bufDialer(context.Context, string) (net.Conn, error) {
return lis.Dial()
}
func TestSayHello(t *testing.T) {
var a uint64 = 1
ctx := context.Background()
conn, err := grpc.DialContext(ctx, "bufnet", grpc.WithContextDialer(bufDialer), grpc.WithInsecure())
if err != nil {
t.Fatalf("Failed to dial bufnet: %v", err)
}
defer conn.Close()
client := NewAccountManagementHandlerClient(conn)
mockAccountService.On("GetSavingAccount", context.Background(), a, a, "Hello", 1).Return(&models.SavingAccount{
CustomerID: 1,
ID: 1,
CardNo: "Hello",
SavingProductID: 1,
Balance: 0,
Status: 1,
})
resp, err := client.GetSavingAccount(ctx, &GetSavingAccountDataRequest{
Id: 1,
CustomerId: 1,
CardNo: "Hello",
SavingProductId: 1,
})
if err != nil {
t.Fatalf("SayHello failed: %v", err)
}
fmt.Printf("Response: %+v", resp)
// Test for output here.
But I get the error like this:
panic:
mock: Unexpected Method Call
-----------------------------
GetSavingAccount(*context.valueCtx,uint64,uint64,string,int64)
0: &context.valueCtx{Context:(*context.valueCtx)(0xc000115260), key:grpc.streamKey{}, val:(*transport.Stream)(0xc0004a2200)}
1: 0x1
2: 0x1
3: "Hello"
4: 1
The closest call I have is:
GetSavingAccount(mock.AnythingOfTypeArgument,uint64,uint64,string,int)
0: "&context.ValueCtx"
1: 0x1
2: 0x1
3: "Hello"
4: 1
What value should I pass to mock context.Background()?
I tried mock.AnythingOfType("&context.emptyCtx"), mock.Anything, doesn't work
Thank you
EDIT:
I tried
mockAccountService.On("GetSavingAccount", context.Background(), a, a, "Hello", 1).Return(...})
And get:
GetSavingAccount(*context.valueCtx,uint64,uint64,string,int64)
0: &context.valueCtx{Context:(*context.valueCtx)(0xc000021290), key:grpc.streamKey{}, val:(*transport.Stream)(0xc000522100)}
...
The closest call I have is:
GetSavingAccount(*context.emptyCtx,uint64,uint64,string,int)
0: (*context.emptyCtx)(0xc00002a080)
...
The method definition for GetSavingAccount method is:
func (a *accountService) GetSavingAccount(ctx context.Context, customerID, id uint64, cardNo string, savingProductId int64) (*models.SavingAccount, error)
So, you have method:
GetSavingAccount(*context.Context,uint64,uint64,string,int64)
And you have mocked:
GetSavingAccount(*context.Context,uint64,uint64,string,int)
You have difference in the last parameter, your problem is int64 and int, so you need:
mockAccountService.On("GetSavingAccount", mock.Anything, a, a, "Hello", int64(1)).Return(...})
For context parameters always use mock.Anything, it is more easier than to mock context. Be careful with int/int64/int32 and other types, also for pointer/struct parameters.
I'm writing unit tests, and I want to write a unit test that asserts that a public method on the struct (Foo.Start) properly handles error responses from an internal method on the struct (Foo.internal).
Essentially I want to get test coverage on the this section of my code:
if err != nil {
return err
}
Here's an example of code and associated test that doesn't work (but would work in say, Python)
# example.go
package example
import "fmt"
type FooAPI interface {
Start() error
internal(string) (string, error)
}
type Foo struct {
FooAPI
}
func (foo Foo) Start() (err error) {
data, err := foo.internal("bar")
if err != nil {
return err
}
fmt.Println(data)
return err
}
func (foo Foo) internal(input string) (output string, err error) {
return output, err
}
# example_test.go
package example
import (
"testing"
"github.com/pkg/errors"
)
type MockFoo struct {
FooAPI
}
func (foo MockFoo) internal(input string) (output string, err error) {
return output, errors.New("big error")
}
func TestStart(t *testing.T) {
tdata := []struct {
testCase string
expectedAnError bool
foo FooAPI
}{
{
testCase: "standard_case",
expectedAnError: false,
foo: Foo{},
},
{
testCase: "error_case",
expectedAnError: true,
foo: MockFoo{},
},
}
for _, test := range tdata {
t.Run(test.testCase, func(t *testing.T) {
// The function under test
test.foo.Start = Foo.Start // <= this doesn't work
err := test.foo.Start()
// Assertion 1
if test.expectedAnError == false && err != nil {
t.Error(err.Error())
}
// Assertion 2
if test.expectedAnError == true && err == nil {
t.Error("expected an error, but there was none!")
}
})
}
}
I'm less interested in correcting the specific example, more-so my goal is to get test coverage on Foo.Start's error handling. I feel like there's some trick with interfaces or pointers that will get me across the finish line here?
I figured out one solution, inspired by https://stackoverflow.com/a/48206430/3055558
Using a internal{{ your struct }} struct and associated interface, and mocking that.
# example.go
package example
import "fmt"
type internalFooAPI interface {
internalBehavior(string) (string, error)
}
type Foo struct {
internal internalFooAPI
}
type internalFoo struct{}
func NewFoo(internal internalFooAPI) Foo {
return Foo{
internal: internal,
}
}
func (foo Foo) Start() (err error) {
data, err := foo.internal.internalBehavior("bar")
if err != nil {
return err
}
fmt.Println(data)
return err
}
func (foo internalFoo) internalBehavior(input string) (output string, err error) {
return output, err
}
# example_test.go
package example
import (
"testing"
"github.com/pkg/errors"
)
type mockInternalFoo struct{}
type mockInternalFooWithErrors struct{}
func (foo mockInternalFoo) internalBehavior(input string) (output string, err error) {
return output, err
}
func (foo mockInternalFooWithErrors) internalBehavior(input string) (output string, err error) {
return output, errors.New("big error")
}
func TestStart(t *testing.T) {
tdata := []struct {
testCase string
expectedAnError bool
foo Foo
}{
{
testCase: "standard_case",
expectedAnError: false,
foo: NewFoo(internalFoo{}),
},
{
testCase: "mock_case",
expectedAnError: false,
foo: NewFoo(mockInternalFoo{}),
},
{
testCase: "error_case",
expectedAnError: true,
foo: NewFoo(mockInternalFooWithErrors{}),
},
}
for _, test := range tdata {
t.Run(test.testCase, func(t *testing.T) {
// The function under test
err := test.foo.Start()
// Assertion 1
if test.expectedAnError == false && err != nil {
t.Error(err.Error())
}
// Assertion 2
if test.expectedAnError == true && err == nil {
t.Error("expected an error, but there was none!")
}
})
}
}