Unit testing in golang - unit-testing

I'm currently looking into creating some unit tests for my service in Go, as well as other functions that build up on top of that functionality, and I'm wondering what is the best way to unit test that in Go? My code looks like:
type BBPeripheral struct {
client *http.Client
endpoint string
}
type BBQuery struct {
Name string `json:"name"`
}
type BBResponse struct {
Brand string `json:"brand"`
Model string `json:"model"`
...
}
type Peripheral struct {
Brand string
Model string
...
}
type Service interface {
Get(name string) (*Peripheral, error)
}
func NewBBPeripheral(config *peripheralConfig) (*BBPeripheral, error) {
transport, err := setTransport(config)
if err != nil {
return nil, err
}
BB := &BBPeripheral{
client: &http.Client{Transport: transport},
endpoint: config.Endpoint[0],
}
return BB, nil
}
func (this *BBPeripheral) Get(name string) (*Peripheral, error) {
data, err := json.Marshal(BBQuery{Name: name})
if err != nil {
return nil, fmt.Errorf("BBPeripheral.Get Marshal: %s", err)
}
resp, err := this.client.Post(this.endpoint, "application/json", bytes.NewBuffer(data))
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf(resp.StatusCode)
}
var BBResponse BBResponse
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
err = json.Unmarshal(body, &BBResponse)
if err != nil {
return nil, err
}
peripheral := &Peripheral{}
peripheral.Model = BBResponse.Model
if peripheral.Model == "" {
peripheral.Model = NA
}
peripheral.Brand = BBResponse.Brand
if peripheral.Brand == "" {
peripheral.Brand = NA
}
return peripheral, nil
}
Is the most efficient way of testing this code and the code that uses these functions to spin up a separate goroutine to act like the server, use http.httptest package, or something else? that's the first time that i try to write a test then i don't realy know how.

It really completely depends. Go provides pretty much all the tools you need to test your application at every single level.
Unit Tests
Design is important because there aren't many tricks to dynamically provide mock/stub objects. You can override variables for tests, but it unlocks all sorts of problems with cleanup. I would focus on IO free unit tests to check that your specific logic works.
For example, you could test BBPeripheral.Get method by making client an interface, requiring it during instantiation, and providing a stub one for the test.
func Test_BBPeripheral_Get_Success(*testing.T) {
bb := BBPeripheral{client: &StubSuccessClient, ...}
p, err := bb.Get(...)
if err != nil {
t.Fail()
}
}
Then you could create a stub error client that exercises error handling in the Get method:
func Test_BBPeripheral_Get_Success(*testing.T) {
bb := BBPeripheral{client: &StubErrClient, ...}
_, err := bb.Get(...)
if err == nil {
t.Fail()
}
}
Component/Integration Tests
These tests can help exercise that each individual unit in your package can work together in unison. Since your code talks over http, Go provides the httptest package that could be used.
To do this the test could create an httptest server with a handler registered to provide the response that this.endpoint expects. You could then exercise your code using its public interface by requesting a NewBBPeripheral, passing in this.endpoint corresponding to the Server.URL property.
This allows you to simulate your code talking to a real server.
Go Routine Tests
Go makes it so easy to write concurrent code, and makes it just as easy to test it. Testing the top level code that spawns a go routine that exercises NewBBPeripheral could look very much like the test above. In addition to starting up a test server your test will have to wait your your asynchronous code to complete. If you don't have a service wide way to cancel/shutdown/signal complete then one may be required to test it using go routines.
RaceCondition/Load Testing
Using go's built in bechmark test combined with -race flag, you can easily exercise your code, and profile it for race conditions, leveraging the tests you wrote above.
One thing to keep in mind, if the implementation of your application is still in flux, writing unit tests may cost a large amount of time. Creating a couple tests, which exercise the public interface of your code, should allow you to easily verify that your application is working, while allowing the implementation to change.

Related

Unit test function that is opening and reading a file

I am working on learning go with a simple program that is doing some file reading and am working on adding unit testing to my program. I have ran into an issue/question while doing this. I want to unit test the function below and my question is that the function takes a name of the file which is then opened and processed. During testing I do not want to actually pass it a real file. I am wondering is this something I can somehow mock so that I can just pass it a "fake" file and have it process that instead? Thanks!
func openAndReadFile(fileName string) [][]string {
file, err := os.Open(fileName)
if err != nil {
fmt.Printf("Failed to read file: %s", fileName)
}
r := csv.NewReader(file)
lines, err := r.ReadAll()
if err != nil {
log.Fatal(err)
}
return lines
}
You need to refactor your code and make more suitable for testing.
Here is how I would do it:
func openAndReadFile(fileName string) [][]string {
file, err := os.Open(fileName)
if err != nil {
fmt.Printf("Failed to open file: %s", fileName)
}
lines, err := readFile(file)
if err != nil {
fmt.Printf("Failed to read file: %s", fileName)
}
return lines
}
func readFile(reader io.Reader) ([][]string, error) {
r := csv.NewReader(reader)
lines, err := r.ReadAll()
if err != nil {
log.Fatal(err)
}
return lines, err
}
Then for testing you can simply use any data structure that implements the io.reader interface. For example, I use a bytes buffer, but you can choose a network connection:
func TestReadFile(t *testing.T) {
var buffer bytes.Buffer
buffer.WriteString("fake, csv, data")
content, err := readFile(&buffer)
if err != nil {
t.Error("Failed to read csv data")
}
fmt.Print(content)
}
The function you have shown is dominated by interactions: Interactions with the file system and interactions with the csv reader. To be sure that these interactions work nicely you will later anyway have to do some integration-testing against the file system and the csv reader. Think about which bugs you are hoping to find, and you will see that bugs are more likely on the interaction level: Is the order of file,err correct, or should it be the other way around? Is nil really the value indicating no error? Do you have to give more arguments to Open? etc.
Therefore, I would not concentrate on unit-testing this function. However, this function is a good candidate to be mocked to make unit-testing the surrounding code easier. Thus, mock openAndReadFile to unit-test the surrounding code, and test openAndReadFile using integration-testing.
I'd strongly suggest using an interface instead of the filename string like the other answers here are recommending, but if you really must do this the only way is likely with a temp file. The decision to use a string file name has locked the code into assuming something to be present on the file system and has pushed in the responsibility of file management.

Unit testing strategy for database connection get

There's a method in the code under test, that simply tries to get database connection, or returns error if unable to.
It, and the structs involved are defined as follows:
type DatabaseContext struct {
Context
Database DatabaseSt
}
// //GetInfo Returns the context.
// func (c *DatabaseContext) GetInfo() *Context {
// return &c.Context
// }
//GetDB Gets the database connection from the connection string.
func (c *DatabaseContext) GetDB() (*sql.DB, *errors.ErrorSt) {
var errSt *errors.ErrorSt
if c.Database.dbConnection == nil {
c.Database.dbConnection, errSt = c.openDB()
if errSt != nil {
return nil, errSt
}
c.Database.dbConnection.SetMaxOpenConns(50)
}
return c.Database.dbConnection, nil
}
The other methods, in the same file, which it may hit, are as follows:
//openDB opens the database with the connection string.
func (c *DatabaseContext) openDB() (*sql.DB, *errors.ErrorSt) {
if c.Database.DBConnectionStr == "" {
c.GetDatabase()
}
return db.OpenConnection(c.Database.DBConnectionStr, c.Database.InterpolateParams)
}
//CloseDB Closes the database.
func (c *DatabaseContext) CloseDB() {
if c.Database.dbConnection != nil {
c.Database.dbConnection.Close()
}
}
//SetDatabaseString Sets the database string into the session.
func (c *DatabaseContext) SetDatabaseString(str string) {
c.Database.DBConnectionStr = str
i := strings.Index(str, ")/") + 2
c.Database.DBName = str[i:]
c.SetDatabase()
}
//GetDatabaseString Gets the database string from the session.
func (c *DatabaseContext) GetDatabase() {
if dbIntf := c.GetFromSession("Database"); dbIntf != nil {
c.Database = dbIntf.(DatabaseSt)
}
}
//SetDatabaseString Sets the database string into the session.
func (c *DatabaseContext) SetDatabase() {
c.SetToSession("Database", c.Database)
}
Fortunately, DatabaseContext implements DatabaseContextIntf, which I want to use for testing. My instinct is to straight up mock DatabaseContext, but that won't work because it's not an interface (in Golang, you can only mock interfaces).
How would I go about testing this, without hitting a real database, which can fail beyond my control (thus creating false fails in the test)?
UPDATE My question differs from the suspected duplicate as their question is about database entries, and not connections. The flagged duplicate refers to this library as the answer, however, there is no method in it to return a "connection" that is nil, for the sake of the test. The best it has is New which creates a test double connection, and there's no way to control the state of the returned value (I need it to be nil in one test ("No Connection") and non-nil in another ("Sanity Test"))
I ended up making the package of the test the same as that of the code under test (this allows the test generator in Visual Studio Code to place the generated test right in the test file, and not get confused, as well as give me access to unexported fields, which I used), and just straight up made a fake DatabaseContext
My test case looks like this:
t.Run("SanityTest", func(t *testing.T) {
c := new(DatabaseContext)
assert.Nil(t, c.Database.dbConnection)
database, err := c.GetDB()
defer database.Close()
assert.NotNil(t, database)
if !assert.Nil(t, err) {
t.Error(err.ToString(false))
}
})

mock Stdin and Stdout in Golang [duplicate]

How do I fill os.Stdin in my test for a function that reads from it using a scanner?
I request a user command line input via a scanner using following function:
func userInput() error {
scanner := bufio.NewScanner(os.Stdin)
println("What is your name?")
scanner.Scan()
username = scanner.Text()
/* ... */
}
Now how do I test this case and simulate a user input?
Following example does not work. Stdin is still empty.
func TestUserInput(t *testing.T) {
var file *os.File
file.Write([]byte("Tom"))
os.Stdin = file
err := userInput()
/* ... */
}
Mocking os.Stdin
You're on the right track that os.Stdin is a variable (of type *os.File) which you can modify, you can assign a new value to it in tests.
Simplest is to create a temporary file with the content you want to simulate as the input on os.Stdin. To create a temp file, use ioutil.TempFile(). Then write the content into it, and seek back to the beginning of the file. Now you can set it as os.Stdin and perform your tests. Don't forget to cleanup the temp file.
I modified your userInput() to this:
func userInput() error {
scanner := bufio.NewScanner(os.Stdin)
fmt.Println("What is your name?")
var username string
if scanner.Scan() {
username = scanner.Text()
}
if err := scanner.Err(); err != nil {
return err
}
fmt.Println("Entered:", username)
return nil
}
And this is how you can test it:
func TestUserInput(t *testing.T) {
content := []byte("Tom")
tmpfile, err := ioutil.TempFile("", "example")
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
if _, err := tmpfile.Write(content); err != nil {
log.Fatal(err)
}
if _, err := tmpfile.Seek(0, 0); err != nil {
log.Fatal(err)
}
oldStdin := os.Stdin
defer func() { os.Stdin = oldStdin }() // Restore original Stdin
os.Stdin = tmpfile
if err := userInput(); err != nil {
t.Errorf("userInput failed: %v", err)
}
if err := tmpfile.Close(); err != nil {
log.Fatal(err)
}
}
Running the test, we see an output:
What is your name?
Entered: Tom
PASS
Also see related question about mocking the file system: Example code for testing the filesystem in Golang
The easy, preferred way
Also note that you can refactor userInput() to not read from os.Stdin, but instead it could receive an io.Reader to read from. This would make it more robust and a lot easier to test.
In your app you can simply pass os.Stdin to it, and in tests you can pass any io.Reader to it created / prepared in the tests, e.g. using strings.NewReader(), bytes.NewBuffer() or bytes.NewBufferString().
os.Pipe()
Instead of messing with the actual file system and doing writes and reads to and from real files on a storage device, the simplest solution is using os.Pipe().
Example
The code of your userInput() does have to be adjusted, and #icza's solution would indeed do for that purpose. But the test itself should be something more like this:
func Test_userInput(t *testing.T) {
input := []byte("Alice")
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
_, err = w.Write(input)
if err != nil {
t.Error(err)
}
w.Close()
// Restore stdin right after the test.
defer func(v *os.File) { os.Stdin = v }(os.Stdin)
os.Stdin = r
if err = userInput(); err != nil {
t.Fatalf("userInput: %v", err)
}
}
Details
There are several important points about this code:
Always close your w stream when you're done writing. Many utilities rely on an io.EOF returned by a Read() call to know that no more data is coming, and the bufio.Scanner is no exception. If you don't close the stream, your scanner.Scan() call will never return, but keep looping internally and waiting for more input until the program is terminated forcefully (as when the test times out).
The pipe buffer capacity varies from system to system, as discussed at length in a post in the Unix & Linux Stack Exchange, so if the size of your simulated input could exceed that, you should wrap your write(s) in a goroutine like so:
//...
go func() {
_, err = w.Write(input)
if err != nil {
t.Error(err)
}
w.Close()
}()
//...
This prevents a deadlock when the pipe is full and writes have to wait for it to start emptying, but the code that's supposed to be reading from and emptying the pipe (userInput() in this case) is not starting, because of writing not being over yet.
A test should also verify that errors are handled properly, in this case, returned by userInput(). This means you'd have to figure out a way to make the scanner.Err() call return an error in a test. One approach could be closing the r stream it was supposed to be reading, before it has had the chance.
Such a test would look almost identical to the nominal case, only you don't write anything at the w end of the pipe, just close the r end, and you actually expect and want userInput() to return an error. And when you have two or more tests of the same function that are almost identical, it is often a good time to implement them as a single table driven test. See Go playground for an example.
io.Reader
The example of userInput() is trivial enough that you could (and should) refactor it and similar cases to read from an io.Reader, just like #icza suggests (see the playground).
You should always strive to rely on some form of dependency injection instead of global state (os.Stdin, in this case, is a global variable in the os package), as that gives more control to the calling code to determine how a called piece of code behaves, which is essential to unit testing, and facilitates better code reuse in general.
Return of os.Pipe()
There may also be cases when you can't really alter a function to take injected dependencies, as when you have to test the main() function of a Go executable. Altering the global state in the test (and hoping that you can properly restore it by the end not to affect subsequent tests) is your only option then. This is where we come back to os.Pipe()
When testing main(), do use os.Pipe() to simulate input to stdin (unless you already hava a file prepared for the purpose) and to capture the output of stdout and stderr (see the playground for an example of the latter).
Implementation of #icza's easy, preferred way:
Also note that you can refactor userInput() to not read from os.Stdin,
but instead it could receive an io.Reader to read from. This would
make it more robust and a lot easier to test.
In your app you can simply pass os.Stdin to it, and in tests you can
pass any io.Reader to it created / prepared in the tests, e.g. using
strings.NewReader(), bytes.NewBuffer() or bytes.NewBufferString().
hello.go
package main
import (
"bufio"
"fmt"
"os"
"io"
)
func userInput(reader io.Reader) error {
scanner := bufio.NewScanner(reader)
var username string
fmt.Println("What is your name?")
if scanner.Scan() {
username = scanner.Text()
}
if scanner.Err() != nil {
return scanner.Err()
}
fmt.Println("Hello", username)
return nil
}
func main() {
userInput(os.Stdin)
}
hello_test.go
package main
import (
"bytes"
"io"
"strings"
"testing"
)
func TestUserInputWithStringsNewReader(t *testing.T) {
input := "Tom"
var reader io.Reader = strings.NewReader(input)
err := userInput(reader)
if err != nil {
t.Errorf("Failed to read from strings.NewReader: %w", err)
}
}
func TestUserInputWithBytesNewBuffer(t *testing.T) {
input := "Tom"
var reader io.Reader = bytes.NewBuffer([]byte(input))
err := userInput(reader)
if err != nil {
t.Errorf("Failed to read from bytes.NewBuffer: %w", err)
}
}
func TestUserInputWithBytesNewBufferString(t *testing.T) {
input := "Tom"
var reader io.Reader = bytes.NewBufferString(input)
err := userInput(reader)
if err != nil {
t.Errorf("Failed to read from bytes.NewBufferString: %w", err)
}
}
Running the program:
go run hello.go
What is your name?
Tom
Hello Tom
Running the test:
go test hello_test.go hello.go -v
=== RUN TestUserInputWithStringsNewReader
What is your name?
Hello Tom
--- PASS: TestUserInputWithStringsNewReader (0.00s)
=== RUN TestUserInputWithBytesNewBuffer
What is your name?
Hello Tom
--- PASS: TestUserInputWithBytesNewBuffer (0.00s)
=== RUN TestUserInputWithBytesNewBufferString
What is your name?
Hello Tom
--- PASS: TestUserInputWithBytesNewBufferString (0.00s)
PASS
ok command-line-arguments 0.141s
You can use *bufio.Scanner to abstract io.Stdin and io.Writer to abstract io.Stdout while passing them as dependencies to your struct, see
Gist: https://gist.github.com/antonzhukov/2a6749f780b24f38b08c9916caa96663 and
Playground: https://play.golang.org/p/BZMqpACupSc

How to write unit tests for function that reads and writes to STDIO in GO? [duplicate]

How do I fill os.Stdin in my test for a function that reads from it using a scanner?
I request a user command line input via a scanner using following function:
func userInput() error {
scanner := bufio.NewScanner(os.Stdin)
println("What is your name?")
scanner.Scan()
username = scanner.Text()
/* ... */
}
Now how do I test this case and simulate a user input?
Following example does not work. Stdin is still empty.
func TestUserInput(t *testing.T) {
var file *os.File
file.Write([]byte("Tom"))
os.Stdin = file
err := userInput()
/* ... */
}
Mocking os.Stdin
You're on the right track that os.Stdin is a variable (of type *os.File) which you can modify, you can assign a new value to it in tests.
Simplest is to create a temporary file with the content you want to simulate as the input on os.Stdin. To create a temp file, use ioutil.TempFile(). Then write the content into it, and seek back to the beginning of the file. Now you can set it as os.Stdin and perform your tests. Don't forget to cleanup the temp file.
I modified your userInput() to this:
func userInput() error {
scanner := bufio.NewScanner(os.Stdin)
fmt.Println("What is your name?")
var username string
if scanner.Scan() {
username = scanner.Text()
}
if err := scanner.Err(); err != nil {
return err
}
fmt.Println("Entered:", username)
return nil
}
And this is how you can test it:
func TestUserInput(t *testing.T) {
content := []byte("Tom")
tmpfile, err := ioutil.TempFile("", "example")
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
if _, err := tmpfile.Write(content); err != nil {
log.Fatal(err)
}
if _, err := tmpfile.Seek(0, 0); err != nil {
log.Fatal(err)
}
oldStdin := os.Stdin
defer func() { os.Stdin = oldStdin }() // Restore original Stdin
os.Stdin = tmpfile
if err := userInput(); err != nil {
t.Errorf("userInput failed: %v", err)
}
if err := tmpfile.Close(); err != nil {
log.Fatal(err)
}
}
Running the test, we see an output:
What is your name?
Entered: Tom
PASS
Also see related question about mocking the file system: Example code for testing the filesystem in Golang
The easy, preferred way
Also note that you can refactor userInput() to not read from os.Stdin, but instead it could receive an io.Reader to read from. This would make it more robust and a lot easier to test.
In your app you can simply pass os.Stdin to it, and in tests you can pass any io.Reader to it created / prepared in the tests, e.g. using strings.NewReader(), bytes.NewBuffer() or bytes.NewBufferString().
os.Pipe()
Instead of messing with the actual file system and doing writes and reads to and from real files on a storage device, the simplest solution is using os.Pipe().
Example
The code of your userInput() does have to be adjusted, and #icza's solution would indeed do for that purpose. But the test itself should be something more like this:
func Test_userInput(t *testing.T) {
input := []byte("Alice")
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
_, err = w.Write(input)
if err != nil {
t.Error(err)
}
w.Close()
// Restore stdin right after the test.
defer func(v *os.File) { os.Stdin = v }(os.Stdin)
os.Stdin = r
if err = userInput(); err != nil {
t.Fatalf("userInput: %v", err)
}
}
Details
There are several important points about this code:
Always close your w stream when you're done writing. Many utilities rely on an io.EOF returned by a Read() call to know that no more data is coming, and the bufio.Scanner is no exception. If you don't close the stream, your scanner.Scan() call will never return, but keep looping internally and waiting for more input until the program is terminated forcefully (as when the test times out).
The pipe buffer capacity varies from system to system, as discussed at length in a post in the Unix & Linux Stack Exchange, so if the size of your simulated input could exceed that, you should wrap your write(s) in a goroutine like so:
//...
go func() {
_, err = w.Write(input)
if err != nil {
t.Error(err)
}
w.Close()
}()
//...
This prevents a deadlock when the pipe is full and writes have to wait for it to start emptying, but the code that's supposed to be reading from and emptying the pipe (userInput() in this case) is not starting, because of writing not being over yet.
A test should also verify that errors are handled properly, in this case, returned by userInput(). This means you'd have to figure out a way to make the scanner.Err() call return an error in a test. One approach could be closing the r stream it was supposed to be reading, before it has had the chance.
Such a test would look almost identical to the nominal case, only you don't write anything at the w end of the pipe, just close the r end, and you actually expect and want userInput() to return an error. And when you have two or more tests of the same function that are almost identical, it is often a good time to implement them as a single table driven test. See Go playground for an example.
io.Reader
The example of userInput() is trivial enough that you could (and should) refactor it and similar cases to read from an io.Reader, just like #icza suggests (see the playground).
You should always strive to rely on some form of dependency injection instead of global state (os.Stdin, in this case, is a global variable in the os package), as that gives more control to the calling code to determine how a called piece of code behaves, which is essential to unit testing, and facilitates better code reuse in general.
Return of os.Pipe()
There may also be cases when you can't really alter a function to take injected dependencies, as when you have to test the main() function of a Go executable. Altering the global state in the test (and hoping that you can properly restore it by the end not to affect subsequent tests) is your only option then. This is where we come back to os.Pipe()
When testing main(), do use os.Pipe() to simulate input to stdin (unless you already hava a file prepared for the purpose) and to capture the output of stdout and stderr (see the playground for an example of the latter).
Implementation of #icza's easy, preferred way:
Also note that you can refactor userInput() to not read from os.Stdin,
but instead it could receive an io.Reader to read from. This would
make it more robust and a lot easier to test.
In your app you can simply pass os.Stdin to it, and in tests you can
pass any io.Reader to it created / prepared in the tests, e.g. using
strings.NewReader(), bytes.NewBuffer() or bytes.NewBufferString().
hello.go
package main
import (
"bufio"
"fmt"
"os"
"io"
)
func userInput(reader io.Reader) error {
scanner := bufio.NewScanner(reader)
var username string
fmt.Println("What is your name?")
if scanner.Scan() {
username = scanner.Text()
}
if scanner.Err() != nil {
return scanner.Err()
}
fmt.Println("Hello", username)
return nil
}
func main() {
userInput(os.Stdin)
}
hello_test.go
package main
import (
"bytes"
"io"
"strings"
"testing"
)
func TestUserInputWithStringsNewReader(t *testing.T) {
input := "Tom"
var reader io.Reader = strings.NewReader(input)
err := userInput(reader)
if err != nil {
t.Errorf("Failed to read from strings.NewReader: %w", err)
}
}
func TestUserInputWithBytesNewBuffer(t *testing.T) {
input := "Tom"
var reader io.Reader = bytes.NewBuffer([]byte(input))
err := userInput(reader)
if err != nil {
t.Errorf("Failed to read from bytes.NewBuffer: %w", err)
}
}
func TestUserInputWithBytesNewBufferString(t *testing.T) {
input := "Tom"
var reader io.Reader = bytes.NewBufferString(input)
err := userInput(reader)
if err != nil {
t.Errorf("Failed to read from bytes.NewBufferString: %w", err)
}
}
Running the program:
go run hello.go
What is your name?
Tom
Hello Tom
Running the test:
go test hello_test.go hello.go -v
=== RUN TestUserInputWithStringsNewReader
What is your name?
Hello Tom
--- PASS: TestUserInputWithStringsNewReader (0.00s)
=== RUN TestUserInputWithBytesNewBuffer
What is your name?
Hello Tom
--- PASS: TestUserInputWithBytesNewBuffer (0.00s)
=== RUN TestUserInputWithBytesNewBufferString
What is your name?
Hello Tom
--- PASS: TestUserInputWithBytesNewBufferString (0.00s)
PASS
ok command-line-arguments 0.141s
You can use *bufio.Scanner to abstract io.Stdin and io.Writer to abstract io.Stdout while passing them as dependencies to your struct, see
Gist: https://gist.github.com/antonzhukov/2a6749f780b24f38b08c9916caa96663 and
Playground: https://play.golang.org/p/BZMqpACupSc

Fill os.Stdin for function that reads from it

How do I fill os.Stdin in my test for a function that reads from it using a scanner?
I request a user command line input via a scanner using following function:
func userInput() error {
scanner := bufio.NewScanner(os.Stdin)
println("What is your name?")
scanner.Scan()
username = scanner.Text()
/* ... */
}
Now how do I test this case and simulate a user input?
Following example does not work. Stdin is still empty.
func TestUserInput(t *testing.T) {
var file *os.File
file.Write([]byte("Tom"))
os.Stdin = file
err := userInput()
/* ... */
}
Mocking os.Stdin
You're on the right track that os.Stdin is a variable (of type *os.File) which you can modify, you can assign a new value to it in tests.
Simplest is to create a temporary file with the content you want to simulate as the input on os.Stdin. To create a temp file, use ioutil.TempFile(). Then write the content into it, and seek back to the beginning of the file. Now you can set it as os.Stdin and perform your tests. Don't forget to cleanup the temp file.
I modified your userInput() to this:
func userInput() error {
scanner := bufio.NewScanner(os.Stdin)
fmt.Println("What is your name?")
var username string
if scanner.Scan() {
username = scanner.Text()
}
if err := scanner.Err(); err != nil {
return err
}
fmt.Println("Entered:", username)
return nil
}
And this is how you can test it:
func TestUserInput(t *testing.T) {
content := []byte("Tom")
tmpfile, err := ioutil.TempFile("", "example")
if err != nil {
log.Fatal(err)
}
defer os.Remove(tmpfile.Name()) // clean up
if _, err := tmpfile.Write(content); err != nil {
log.Fatal(err)
}
if _, err := tmpfile.Seek(0, 0); err != nil {
log.Fatal(err)
}
oldStdin := os.Stdin
defer func() { os.Stdin = oldStdin }() // Restore original Stdin
os.Stdin = tmpfile
if err := userInput(); err != nil {
t.Errorf("userInput failed: %v", err)
}
if err := tmpfile.Close(); err != nil {
log.Fatal(err)
}
}
Running the test, we see an output:
What is your name?
Entered: Tom
PASS
Also see related question about mocking the file system: Example code for testing the filesystem in Golang
The easy, preferred way
Also note that you can refactor userInput() to not read from os.Stdin, but instead it could receive an io.Reader to read from. This would make it more robust and a lot easier to test.
In your app you can simply pass os.Stdin to it, and in tests you can pass any io.Reader to it created / prepared in the tests, e.g. using strings.NewReader(), bytes.NewBuffer() or bytes.NewBufferString().
os.Pipe()
Instead of messing with the actual file system and doing writes and reads to and from real files on a storage device, the simplest solution is using os.Pipe().
Example
The code of your userInput() does have to be adjusted, and #icza's solution would indeed do for that purpose. But the test itself should be something more like this:
func Test_userInput(t *testing.T) {
input := []byte("Alice")
r, w, err := os.Pipe()
if err != nil {
t.Fatal(err)
}
_, err = w.Write(input)
if err != nil {
t.Error(err)
}
w.Close()
// Restore stdin right after the test.
defer func(v *os.File) { os.Stdin = v }(os.Stdin)
os.Stdin = r
if err = userInput(); err != nil {
t.Fatalf("userInput: %v", err)
}
}
Details
There are several important points about this code:
Always close your w stream when you're done writing. Many utilities rely on an io.EOF returned by a Read() call to know that no more data is coming, and the bufio.Scanner is no exception. If you don't close the stream, your scanner.Scan() call will never return, but keep looping internally and waiting for more input until the program is terminated forcefully (as when the test times out).
The pipe buffer capacity varies from system to system, as discussed at length in a post in the Unix & Linux Stack Exchange, so if the size of your simulated input could exceed that, you should wrap your write(s) in a goroutine like so:
//...
go func() {
_, err = w.Write(input)
if err != nil {
t.Error(err)
}
w.Close()
}()
//...
This prevents a deadlock when the pipe is full and writes have to wait for it to start emptying, but the code that's supposed to be reading from and emptying the pipe (userInput() in this case) is not starting, because of writing not being over yet.
A test should also verify that errors are handled properly, in this case, returned by userInput(). This means you'd have to figure out a way to make the scanner.Err() call return an error in a test. One approach could be closing the r stream it was supposed to be reading, before it has had the chance.
Such a test would look almost identical to the nominal case, only you don't write anything at the w end of the pipe, just close the r end, and you actually expect and want userInput() to return an error. And when you have two or more tests of the same function that are almost identical, it is often a good time to implement them as a single table driven test. See Go playground for an example.
io.Reader
The example of userInput() is trivial enough that you could (and should) refactor it and similar cases to read from an io.Reader, just like #icza suggests (see the playground).
You should always strive to rely on some form of dependency injection instead of global state (os.Stdin, in this case, is a global variable in the os package), as that gives more control to the calling code to determine how a called piece of code behaves, which is essential to unit testing, and facilitates better code reuse in general.
Return of os.Pipe()
There may also be cases when you can't really alter a function to take injected dependencies, as when you have to test the main() function of a Go executable. Altering the global state in the test (and hoping that you can properly restore it by the end not to affect subsequent tests) is your only option then. This is where we come back to os.Pipe()
When testing main(), do use os.Pipe() to simulate input to stdin (unless you already hava a file prepared for the purpose) and to capture the output of stdout and stderr (see the playground for an example of the latter).
Implementation of #icza's easy, preferred way:
Also note that you can refactor userInput() to not read from os.Stdin,
but instead it could receive an io.Reader to read from. This would
make it more robust and a lot easier to test.
In your app you can simply pass os.Stdin to it, and in tests you can
pass any io.Reader to it created / prepared in the tests, e.g. using
strings.NewReader(), bytes.NewBuffer() or bytes.NewBufferString().
hello.go
package main
import (
"bufio"
"fmt"
"os"
"io"
)
func userInput(reader io.Reader) error {
scanner := bufio.NewScanner(reader)
var username string
fmt.Println("What is your name?")
if scanner.Scan() {
username = scanner.Text()
}
if scanner.Err() != nil {
return scanner.Err()
}
fmt.Println("Hello", username)
return nil
}
func main() {
userInput(os.Stdin)
}
hello_test.go
package main
import (
"bytes"
"io"
"strings"
"testing"
)
func TestUserInputWithStringsNewReader(t *testing.T) {
input := "Tom"
var reader io.Reader = strings.NewReader(input)
err := userInput(reader)
if err != nil {
t.Errorf("Failed to read from strings.NewReader: %w", err)
}
}
func TestUserInputWithBytesNewBuffer(t *testing.T) {
input := "Tom"
var reader io.Reader = bytes.NewBuffer([]byte(input))
err := userInput(reader)
if err != nil {
t.Errorf("Failed to read from bytes.NewBuffer: %w", err)
}
}
func TestUserInputWithBytesNewBufferString(t *testing.T) {
input := "Tom"
var reader io.Reader = bytes.NewBufferString(input)
err := userInput(reader)
if err != nil {
t.Errorf("Failed to read from bytes.NewBufferString: %w", err)
}
}
Running the program:
go run hello.go
What is your name?
Tom
Hello Tom
Running the test:
go test hello_test.go hello.go -v
=== RUN TestUserInputWithStringsNewReader
What is your name?
Hello Tom
--- PASS: TestUserInputWithStringsNewReader (0.00s)
=== RUN TestUserInputWithBytesNewBuffer
What is your name?
Hello Tom
--- PASS: TestUserInputWithBytesNewBuffer (0.00s)
=== RUN TestUserInputWithBytesNewBufferString
What is your name?
Hello Tom
--- PASS: TestUserInputWithBytesNewBufferString (0.00s)
PASS
ok command-line-arguments 0.141s
You can use *bufio.Scanner to abstract io.Stdin and io.Writer to abstract io.Stdout while passing them as dependencies to your struct, see
Gist: https://gist.github.com/antonzhukov/2a6749f780b24f38b08c9916caa96663 and
Playground: https://play.golang.org/p/BZMqpACupSc