GregariousGainz
GregariousGainz

Reputation: 71

Writing Tests in Go by Receiving Interfaces and Returning Structs

I'm having some issues implementing interface substitution for the Go code. Previously the code was not written with unit tests in mind and I am going back and adding support for testing (Testify and Mockery) and writing all of the Go tests for my code base.

The function I am currently trying to test previously received a pointer struct to the QueryTableResult provided by the Influx package. I have swapped it with a local interface:

type QueryTableResult interface {
        Next() bool
    Record() FluxRecord
    Err() error
}

My main issue is the Record() provided by the influx package QueryTableResult struct returns a specific FluxRecord struct. I have added a local interface for this struct also:

type FluxRecord interface {
    Time() time.Time
    Value() interface{}
}

And have changed the QueryTableResult interface Record() method to return this interface instead of the specific struct from the influx package. When I do this I can no longer execute my influx query functions as I am getting this error:

cannot use <variable_name> (variable of type *api.QueryTableResult) as QueryTableResult value in argument to parseInfluxData: *api.QueryTableResult does not implement QueryTableResult (wrong type for method Record) have Record() *query.FluxRecord want Record() FluxRecord

I want the FluxRecord to satisfy the *query.FluxRecord type or vice versa. I'm not sure how else to go about changing this.

I am trying to test this function:

func parseInfluxData(queryResult QueryTableResult) (RecordResult, error) {

    var formattedRecordResult RecordResult

    for queryResult.Next() {
        record := queryResult.Record()

        timestamp := record.Time().UTC()
        value := record.Value()

        // Format the timestamp as desired
        formattedTime := timestamp.Format(time.RFC3339)

        // Convert the value to an integer
        formattedValue, err := strconv.Atoi(fmt.Sprintf("%v", value))
        if err != nil {
            return formattedRecordResult, err
        }

        formattedRecordResult.Time = formattedTime
        formattedRecordResult.Value = formattedValue
    }
    if queryResult.Err() != nil {
        fmt.Printf("Query error: %s\n", queryResult.Err().Error())
    }
    return formattedRecordResult, nil
}

This is my test:

func TestParseInfluxData(t *testing.T) {
    // Create a new instance of the QueryTableResult mock
    queryTableResultMock := mocks.NewQueryTableResult(t)

    // Create a new instance of the FluxRecord mock
    fluxRecordMock := mocks.NewFluxRecord(t)

    // Set up the Record method of the QueryTableResult mock to return the FluxRecord mock
    queryTableResultMock.On("Record").Return(fluxRecordMock)

    // Define the expected behavior of the mock's methods
    queryTableResultMock.On("Next").Return(true).Once()
    // queryTableResultMock.On("Next").Return(false).Once()

    // Define the expected behavior of FluxRecord's Time() method
    expectedTime := time.Date(2023, 7, 15, 12, 34, 56, 0, time.UTC)
    fluxRecordMock.On("Time").Return(expectedTime)

    // Define the expected behavior of FluxRecord's Value() method
    expectedValue := 42
    fluxRecordMock.On("Value").Return(expectedValue)

    // Call the function being tested
    result, err := parseInfluxData(queryTableResultMock)
    assert.Nil(t, err)

    // Assertions based on the expected result and error
    assert.Equal(t, expectedTime.Format(time.RFC3339), result.Time)
    assert.Equal(t, expectedValue, result.Value)

    // Assert that all expected methods of the mock have been called
    queryTableResultMock.AssertExpectations(t)
    fluxRecordMock.AssertExpectations(t)
}

The test fails with this error:

./parseInfluxData_test.go:34:33: cannot use queryTableResultMock (variable of type *mocks.QueryTableResult) as QueryTableResult value in argument to parseInfluxData: *mocks.QueryTableResult does not implement QueryTableResult (wrong type for method Record) have Record() *mocks.FluxRecord want Record() FluxRecord

Here are the mocks:

// Code generated by mockery v2.32.0. DO NOT EDIT.

package mocks

import (
    time "time"
    "fmt"

    mock "github.com/stretchr/testify/mock"
)

// FluxRecord is an autogenerated mock type for the FluxRecord type
type FluxRecord struct {
    mock.Mock
}

// Time provides a mock function with given fields:
func (_m *FluxRecord) Time() time.Time {
    ret := _m.Called()

    var r0 time.Time
    if rf, ok := ret.Get(0).(func() time.Time); ok {
        r0 = rf()
    } else {
        r0 = ret.Get(0).(time.Time)
    }

    return r0
}

// Value provides a mock function with given fields:
func (_m *FluxRecord) Value() interface{} {
    fmt.Println("Time() method called")
    ret := _m.Called()

    var r0 interface{}
    if rf, ok := ret.Get(0).(func() interface{}); ok {
        r0 = rf()
    } else {
        if ret.Get(0) != nil {
            r0 = ret.Get(0).(interface{})
        }
    }

    return r0
}

// NewFluxRecord creates a new instance of FluxRecord. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewFluxRecord(t interface {
    mock.TestingT
    Cleanup(func())
}) *FluxRecord {
    mock := &FluxRecord{}
    mock.Mock.Test(t)

    t.Cleanup(func() { mock.AssertExpectations(t) })

    return mock
}

// Code generated by mockery v2.32.0. DO NOT EDIT.

package mocks

import (
    mock "github.com/stretchr/testify/mock"
)

// QueryTableResult is an autogenerated mock type for the QueryTableResult type
type QueryTableResult struct {
    mock.Mock
}

// Err provides a mock function with given fields:
func (_m *QueryTableResult) Err() error {
    ret := _m.Called()

    var r0 error
    if rf, ok := ret.Get(0).(func() error); ok {
        r0 = rf()
    } else {
        r0 = ret.Error(0)
    }

    return r0
}

// Next provides a mock function with given fields:
func (_m *QueryTableResult) Next() bool {
    ret := _m.Called()

    var r0 bool
    if rf, ok := ret.Get(0).(func() bool); ok {
        r0 = rf()
    } else {
        r0 = ret.Get(0).(bool)
    }

    return r0
}

// Record provides a mock function with given fields:
func (_m *QueryTableResult) Record() *FluxRecord {
    ret := _m.Called()

    var r0 *FluxRecord
    if rf, ok := ret.Get(0).(func() *FluxRecord); ok {
        r0 = rf()
    } else {
        if ret.Get(0) != nil {
            r0 = ret.Get(0).(*FluxRecord)
        }
    }

    return r0
}

// NewQueryTableResult creates a new instance of QueryTableResult. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewQueryTableResult(t interface {
    mock.TestingT
    Cleanup(func())
}) *QueryTableResult {
    mock := &QueryTableResult{}
    mock.Mock.Test(t)

    t.Cleanup(func() { mock.AssertExpectations(t) })

    return mock
}

How can I implement interface substitution to achieve testability within my code? Or is there another way I should be doing this instead?

Upvotes: 4

Views: 322

Answers (1)

Victor Vieira
Victor Vieira

Reputation: 51

So I've tried to do it by myself here. What I discover is you may be facing some circular dependency problems.

So here is how I solve it

Folder structure:

|
|__querytable
  |__querytable.go // Here is where resides your interfaces FluxRecord and Query table
  |__mocks
    |__FluxRecord.go // this is generated by mock
    |__QueryTableResult.go // this is generated by mock
|__test
  |__test_test.go // The actual folder that you function parseInfluxData resides

Organizing that way it works. You may explore other alternatives for structuring your files. I didn't change your code, but I updated the mocked-generated structs. I hope that works for you.

// Code generated by mockery v2.32.2. DO NOT EDIT.

package mocks

import (
    mock "github.com/stretchr/testify/mock"
    test "your_path_to_the_query_table_package/querytable"

)

// QueryTableResult is an autogenerated mock type for the QueryTableResult type
type QueryTableResult struct {
    mock.Mock
}

// Err provides a mock function with given fields:
func (_m *QueryTableResult) Err() error {
    ret := _m.Called()

    var r0 error
    if rf, ok := ret.Get(0).(func() error); ok {
        r0 = rf()
    } else {
        r0 = ret.Error(0)
    }

    return r0
}

// Next provides a mock function with given fields:
func (_m *QueryTableResult) Next() bool {
    ret := _m.Called()

    var r0 bool
    if rf, ok := ret.Get(0).(func() bool); ok {
        r0 = rf()
    } else {
        r0 = ret.Get(0).(bool)
    }

    return r0
}

// Record provides a mock function with given fields:
func (_m *QueryTableResult) Record() test.FluxRecord {
    ret := _m.Called()

    var r0 test.FluxRecord
    if rf, ok := ret.Get(0).(func() test.FluxRecord); ok {
        r0 = rf()
    } else {
        if ret.Get(0) != nil {
            r0 = ret.Get(0).(test.FluxRecord)
        }
    }

    return r0
}

// NewQueryTableResult creates a new instance of QueryTableResult. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewQueryTableResult(t interface {
    mock.TestingT
    Cleanup(func())
}) *QueryTableResult {
    mock := &QueryTableResult{}
    mock.Mock.Test(t)

    t.Cleanup(func() { mock.AssertExpectations(t) })

    return mock
}
// Code generated by mockery v2.32.2. DO NOT EDIT.

package mocks

import (
    mock "github.com/stretchr/testify/mock"

    time "time"
)

// FluxRecord is an autogenerated mock type for the FluxRecord type
type FluxRecord struct {
    mock.Mock
}

// Time provides a mock function with given fields:
func (_m *FluxRecord) Time() time.Time {
    ret := _m.Called()

    var r0 time.Time
    if rf, ok := ret.Get(0).(func() time.Time); ok {
        r0 = rf()
    } else {
        r0 = ret.Get(0).(time.Time)
    }

    return r0
}

// Value provides a mock function with given fields:
func (_m *FluxRecord) Value() interface{} {
    ret := _m.Called()

    var r0 interface{}
    if rf, ok := ret.Get(0).(func() interface{}); ok {
        r0 = rf()
    } else {
        if ret.Get(0) != nil {
            r0 = ret.Get(0).(interface{})
        }
    }

    return r0
}

// NewFluxRecord creates a new instance of FluxRecord. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations.
// The first argument is typically a *testing.T value.
func NewFluxRecord(t interface {
    mock.TestingT
    Cleanup(func())
}) *FluxRecord {
    mock := &FluxRecord{}
    mock.Mock.Test(t)

    t.Cleanup(func() { mock.AssertExpectations(t) })

    return mock
}

Upvotes: 0

Related Questions