Home > Enterprise >  go test setting private field state
go test setting private field state

Time:08-10

  • A library contains a struct type that exports method A which is computed based on unexported field data a.
  • Since a is unexported, directly constructing the type will yield empty field data.
  • The only place a is set is when the library instantiates the type in ways not easilsy replicable using exported functions/fields (e.g. data is populated from network API calls)
type S struct {
  a []byte
}
func (s *S) A() ... {
  // produces a meaningful result using the blob in field a
}
...
type I interface {
  DoLibraryThing() (*S)
  ...
}
struct Client type {...}
func (c *Client) DoLibraryThing() (*S) {
  ...
}
...

The program I want to write tests for uses DoLibraryThing and notably depends on and manipulates the result of S.A()

func testMe(client I) {
  // do a bunch of things with the client interface
  s := client.DoLibraryThing()
  data := s.A()
  // do more things with client interface and data
}

I have mocked the library Client that returns this type, but am unable to return an appropriate instance such that calling A will yield useful data to be used in the remainder of the test. The function being tested makes a number of other calls to the Client/library.

Is there a way to construct the type with a seeded with useful data or otherwise substitute a mock S that can return useful data from A? Having the Client mock return a different struct that exports A with hardcoded behavior doesn't work since it's not a *S when being returned from the mock.

Alternatives considered:

  • Split the function at the point of this library call, testing the behavior before and after the call, and providing the results of A to the test(s) as test inputs when testing the latter portion of the function. Splitting the function here would only really be meaningful for test and would involve the otherwise needless complexity of passing state from first part of function implementation to second part of function implementation.
func testMe(client I) {
  kludge, data := testMe1(client)
  return testMe2(kludge, data)
}
func testMe1(client I) {
  // do a bunch of things with the client interface
  s := client.DoLibraryThing()
  data := s.A()
  return Kludge{ /* pack up all the state */ }, data
}
func testMe2(k Kludge, d data) {
  // unpack all the state
  // do more things with client interface and data
}

^ testMe1 and testMe2 can now be independently tested and predetermined test data can be passed in when testing testMe2.

CodePudding user response:

I think you need to go unsafe.

For this you will need the ability to read the source-code of your library.


// LIB

type useful struct {
    exported   bool
    unexported int
}

var s useful = useful{unexported: 3}

The unexported field is after a boolean value, which uses in my computer 8 bits.

What you have to do, would be to get a pointer to s, shift it by the 8 bits, and read the value at this moment.

// Your side
func main() {
    fmt.Println(unsafe.Offsetof(s.unexported)) // we get 8, it's try and error in your case
    point := (*int)(unsafe.Pointer(uintptr(unsafe.Pointer(&s))   8))
    fmt.Println(*point)
}

A bit of warning from a weary traveler. The package is called unsafe for a very good reason. Do not try this on anything that would cost more than a CRTL C to fix. The values you get may vary from computer to computer, especially if you use a machine that is for niche purposes. I will not be held liable for any black hole or time anomaly that unsaffe unleashes.

You can find my playground test here.

CodePudding user response:

Here is a function that sets the value of a structure field, including the unexported field. I took this as a basis https://stackoverflow.com/a/43918797/3201891

I do not recommend using this function anywhere other than tests. And only if there is no other way to set test data. For example, in the case where the dependency is a third party package that you cannot edit. Be aware that tests may break if the external package changes.

func setFieldValue(target any, fieldName string, value any) {
    rv := reflect.ValueOf(target)
    for rv.Kind() == reflect.Ptr && !rv.IsNil() {
        rv = rv.Elem()
    }
    if !rv.CanAddr() {
        panic("target must be addressable")
    }
    if rv.Kind() != reflect.Struct {
        panic(fmt.Sprintf(
            "unable to set the '%s' field value of the type %T, target must be a struct",
            fieldName,
            target,
        ))
    }
    rf := rv.FieldByName(fieldName)

    reflect.NewAt(rf.Type(), unsafe.Pointer(rf.UnsafeAddr())).Elem().Set(reflect.ValueOf(value))
}

Here is usage example https://go.dev/play/p/i9w0RPcmKHc

  • Related