Skip to content

Latest commit

 

History

History
125 lines (92 loc) · 5.26 KB

Dev Guide.md

File metadata and controls

125 lines (92 loc) · 5.26 KB

Dev Guide

More advanced information needed to develop or build the docker provider will live here

Testing

Last updated 8/18/2021

To run all unit tests run the commands test/unit-tests/run_go_tests.sh and test/unit-tests/run_ruby_tests.sh

Conventions:

  1. Unit tests should go in their own file, but in the same folder as the source code their testing. For example, the tests for in_kube_nodes.rb are in in_kube_nodes_test.rb. Both files are in the folder source/plugin/ruby.

Ruby

Sample tests are provided in in_kube_nodes_test.rb. They are meant to demo the tooling used for unit tests (as opposed to being comprehensive tests). Basic techniques like mocking are demonstrated there.

Conventions:

  1. When modifying a fluentd plugin for unit testing, any mocked classes (like KubernetesApiClient, applicationInsightsUtility, env, etc.) should be passed in as optional arguments of initialize. For example:
    def initialize
      super

would be turned into

    def initialize (kubernetesApiClient=nil, applicationInsightsUtility=nil, extensionUtils=nil, env=nil)
      super()
  1. Having end-to-end tests of all fluentd plugins is a longshot. We care more about unit testing smaller blocks of functionality (like all the helper functions in KubeNodeInventory.rb). Unit tests for fluentd plugins are not expected.

Golang

Since golang is statically compiled, mocking requires a lot more work than in ruby. Sample tests are provided in utils_test.go and extension_test.go. Again, they are meant to demo the tooling used for unit tests (as opposed to being comprehensive tests). Basic techniques like mocking are demonstrated there.

Mocking:

Mocks are generated with gomock (mockgen).

  • Mock files should be called *_mock.go (socket_writer.go => socket_writer_mock.go)
  • Mocks should not be checked in to git. (they have been added to the .gitignore)
  • The command to generate mock files should go in a //go:generate comment at the top of the mocked file (see socket_writer.go for an example). This way mocks can be generated by the unit test script.
  • Mocks also go in the same folder as the mocked files. This is unfortunate, but necessary to avoid circular package dependencies (anyone else feel free to figure out how to move mocks to a separate folder)

Using mocks is also a little tricky. In order to mock functions in a package with gomock, they must be converted to reciever methods of a struct. This way the struct can be swapped out at runtime to change which implementaions of a method are called. See the example below:

// declare all functions to be mocked in this interface
type registrationPreCheckerInterface interface {
	FUT(string) bool
}

// Create a struct which implements the above interface
type regPreCheck struct{}

func (r regPreCheck) FUT(email string) bool {
	fmt.Println("real FUT() called")
	return true
}

// Create a global variable and assign it to the struct
var regPreCondVar registrationPreCheckerInterface

func init() {
	regPreCondVar = regPreCheck{}
}

Now any code wishing to call FUT() will call regPreCondVar.FUT("")

A unit test can substitute its own implementaion of FUT() like so

// This will hold the mock of FUT we want to substitute
var FUTMock func(email string) bool

// create a new struct which implements the earlier interface
type regPreCheckMock struct{}

func (u regPreCheckMock) FUT(email string) bool {
	return FUTMock(email)
}

Everything is set up. Now a unit test can substitute in a mock like so:

func someUnitTest() {
    // This will call the actual implementaion of FUT()
	regPreCondVar.FUT("")

    // Now the test creates another struct to substitue. After this like all calls to FUT() will be diverted
	regPreCondVar = regPreCheckMock{}

    // substute another function to run instead of FUT()
	FUTMock = func(email string) bool {
		fmt.Println("FUT 1 called")
		return false
	}
    // This will call the function defined right above
	regPreCondVar.FUT("")

    // We can substitue another implementation
	FUTMock = func(email string) bool {
		fmt.Println("FUT 2 called")
		return false
	}
	regPreCondVar.FUT("")

    // put the old behavior back
	regPreCondVar = regPreCheck{}
    // this will call the actual implementation of FUT()
	regPreCondVar.FUT("")

}

A concrete example of this can be found in socket_writer.go and extension_test.go. Again, if anybody has a better way feel free to update this guide.

A simpler way to test a specific function is to write wrapper functions. Test code calls the inner function (ReadFileContentsImpl) and product code calls the wrapper function (ReadFileContents). The wrapper function provides any outside state which a unit test would want to control (like a function to read a file). This option makes product code more verbose, but probably easier to read too. Either way is acceptable.

func ReadFileContents(fullPathToFileName string) (string, error) {
	return ReadFileContentsImpl(fullPathToFileName, ioutil.ReadFile)
}