I have a go function called setupConfig() and I have a Test_setupconfig that tests it and tests it just fine. But when I do a cover test on it and look at the HTML report, it shows my handling of some error returns by the Viper package is not covered. Why is this not covered and how do I handle this?
The coverage report tells you how much of your code is executed during tests. What you're seeing is the return
statements within those if
blocks are not tested, which means you don't have any unit tests that are designed to fail and return errors. While testing that you code works when given correct input, it's also important to ensure that tests fail correctly and safely when given bad input.
However, this is a weird situation because those errors aren't within your own package, those errors are coming from the viper
package. at this point it's important to ask yourself the question "what am I really testing?". If you're using the viper
package then the assumption is that package is thoroughly tested, and by creating your own tests for the viper errors you're just doubling up on those tests with no real improvements. For this reason, sometimes we choose to omit testing these branches because realistically, if the viper package errors - and assuming all your inputs are static and hard coded like shown - then that's not a problem with your code that's a problem with the viper
library.
If you really want to get 100% coverage and test all decision trees, the only way to do so is to put the viper
package behind some kind of abstraction. Most likely an interface that's passed into the function allowing for multiple implementations whether you're running in production or running in tests.
With that said, hard coding all your values within the function like that isn't recommended practice. Ideally you'd want the values of your config struct coming from a local config file, environment variables, commandline flags, or a combination of them. By doing this, you could have this setup function accept an interface that it uses to retrieve the configuration, making this function easily testable as all you'd need to do is mock out the implementation of that interface in your tests. So it would be something like the following:
config.go
:
type ConfigController interface {
GetInput() Config
}
func setupConfig(controller ConfigController) error {
config := controller.GetInput()
// your code here
}
config_test.go
:
type mockConfigController struct {}
func (m *mockConfigController) GetConfig() Config {
return Config{
// your config here
}
}
func Test_setupConfig(t *testing.T) {
configController := &mockConfigController{}
err := setupConfig(configController)
// rest of test here
}
By doing this and giving your setupConfig()
function accepting an interface, it means you can give it a function implementation when it's running in production, but also mock it out with hard-coded test data when running tests. This is quite often used when interacting with other services too, like databases. Instead of having to spin up a database and connect to it when running tests, you can have your code accept an interface that tells it how to interact with the database and mock it out in tests. This allows you to isolate parts of your code and only test exactly what you're wanting to.