I'm trying to write unit test for struct constructor, which may return also nil if error happens during file.Open. I don't have idea how to test/mock file error with flags: os.O_RDWR|os.O_CREATE|os.O_APPEND
I tried to check nil value inside test, but it failed.
Constructor:
type App struct {
someField string
log *log.Logger
}
func New() *App {
app := &App{}
f, err := os.OpenFile("info.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
fmt.Printf("error opening file: %v", err)
return nil
}
mw := io.MultiWriter(os.Stdout, f)
l = log.New(mw, "APP", log.Ldate|log.LstdFlags|log.Lshortfile)
app.log = l
return app
}
And test for constructor:
func TestNew(t *testing.T) {
var a App
a = New()
// doesn't cover
if a == nil {
t.Fatal("Error opening file")
}
}
I expect to have covered error != nil, which in coverage is red:
f, err := os.OpenFile("info.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
fmt.Printf("error opening file: %v", err)
return nil
}
Mocking in Go means having interfaces, if that's something you really need you might consider using something like https://github.com/spf13/afero instead of using the os package directly. This also allows you to use in-memory filesystems and other things that make testing easier.
There are two things to consider.
The first is that O_RDWR|O_CREAT|O_APPEND
is almost nothing interesting when it comes to opening a file: it tells the OS that it should open the file for reading and writing, in append mode, and that if the file does not exist at the time of a call it should be created, and otherwise it's fine to append to it.
Now the only two reasons I may fathom this operation might fail are:
Now consider that in order to simulate one of such cases you need to manipulate some filesystem available to the process running your test. While it certainly possible to do within a unit-testing framework, it looks like belonging more to the domain of integration testing.
There are plenty of options to work towards this level of testing on Linux: "flakey" device-mapper target and friends, mounting a read-only image via loop device or FUSE, injecting faults into the running kernel etc. Still, these are mostly unsuitable for unit-testing.
If you want unit-test this stuff, there, again, are two approaches:
Abstract away the whole filesystem layer using something like https://github.com/spf13/afero as @Adrien suggested.
The upside is that you can easily test almost everything filesystem-related in your code.
Abstract away just a little bit of code using a variable.
Say, you might have
var whateverCreate = os.Create
use that whateverCreate
in your code and then override just that variable in the setup code of your test suite to assign it a function which returns whatever error you need in specific test(s).
You could make the the filename/file path configurable instead of using the hard coded info.log, then in your test you could use some non existing path for example.
There are multiple options for configuring it:
parameter in the constructor (maybe a separate constructor that could be called from New if you wanna keep the API as it is)
package level configuration (like a global variable defaultLogFileName), this is less flexible (for example if you wanna run tests parallel), but might suit in this case as well