0

Following the example at https://golang.org/pkg/os/exec/#Cmd.StdoutPipe, suppose I have a function getPerson() defined like so:

package stdoutexample

import (
    "encoding/json"
    "os/exec"
)

// Person represents a person
type Person struct {
    Name string
    Age  int
}

func getPerson() (Person, error) {
    person := Person{}
    cmd := exec.Command("echo", "-n", `{"Name": "Bob", "Age": 32}`)
    stdout, err := cmd.StdoutPipe()
    if err != nil {
        return person, err
    }
    if err := cmd.Start(); err != nil {
        return person, err
    }
    if err := json.NewDecoder(stdout).Decode(&person); err != nil {
        return person, err
    }
    if err := cmd.Wait(); err != nil {
        return person, err
    }
    return person, nil
}

In my 'real' application, the command run can have different outputs, I'd like to write test cases for each of these scenarios. However, I'm not sure how to go about this.

So far all I have is a test case for one case:

package stdoutexample

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestGetPerson(t *testing.T) {
    person, err := getPerson()
    require.NoError(t, err)
    assert.Equal(t, person.Name, "Bob")
    assert.Equal(t, person.Age, 32)
}

Perhaps the way to go about this is to split this function into two parts, one which writes the output of the command to a string, and another which decodes the output of a string?

3 Answers 3

2

adding to https://stackoverflow.com/a/58107208/9353289,

Instead of writing separate Test functions for every test, I suggest you use a Table Driven Test approach instead. Here is an example,

func Test_getPerson(t *testing.T) {
    tests := []struct {
        name          string
        commandOutput []byte
        want          Person
    }{
        {
            name:          "Get Bob",
            commandOutput: []byte(`{"Name": "Bob", "Age": 32}`),
            want: Person{
                Name: "Bob",
                Age:  32,
            },
        },

        {
            name:          "Get Alice",
            commandOutput: []byte(`{"Name": "Alice", "Age": 25}`),
            want: Person{
                Name: "Alice",
                Age:  25,
            },
        },
    }
    for _, tt := range tests {
        t.Run(tt.name, func(t *testing.T) {
            got, err := getPerson(tt.commandOutput)
            require.NoError(t, err)
            assert.Equal(t, tt.want.Name, got.Name)
            assert.Equal(t, tt.want.Age, got.Age)

        })
    }
}

Simply adding test cases to the slice, will run all test cases.

Sign up to request clarification or add additional context in comments.

Comments

0

I added unit tests by splitting the function into two parts: one which reads the output to a slice of bytes, and one which parses that output to a Person:

package stdoutexample

import (
    "bytes"
    "encoding/json"
    "os/exec"
)

// Person represents a person
type Person struct {
    Name string
    Age  int
}

func getCommandOutput() ([]byte, error) {
    cmd := exec.Command("echo", "-n", `{"Name": "Bob", "Age": 32}`)
    return cmd.Output()
}

func getPerson(commandOutput []byte) (Person, error) {
    person := Person{}
    if err := json.NewDecoder(bytes.NewReader(commandOutput)).Decode(&person); err != nil {
        return person, err
    }
    return person, nil
}

The following test cases pass:

package stdoutexample

import (
    "testing"

    "github.com/stretchr/testify/assert"
    "github.com/stretchr/testify/require"
)

func TestGetPerson(t *testing.T) {
    commandOutput, err := getCommandOutput()
    require.NoError(t, err)
    person, err := getPerson(commandOutput)
    require.NoError(t, err)
    assert.Equal(t, person.Name, "Bob")
    assert.Equal(t, person.Age, 32)
}

func TestGetPersonBob(t *testing.T) {
    commandOutput := []byte(`{"Name": "Bob", "Age": 32}`)
    person, err := getPerson(commandOutput)
    require.NoError(t, err)
    assert.Equal(t, person.Name, "Bob")
    assert.Equal(t, person.Age, 32)
}

func TestGetPersonAlice(t *testing.T) {
    commandOutput := []byte(`{"Name": "Alice", "Age": 25}`)
    person, err := getPerson(commandOutput)
    require.NoError(t, err)
    assert.Equal(t, person.Name, "Alice")
    assert.Equal(t, person.Age, 25)
}

where the Bob and Alice test cases simulate different output which can be generated by the command.

Comments

0

your implementation design actively rejects fine grain testing because it does not allow any injection.

However, given the example, besides using a TestTable, there is not much to improve.

Now, on a real workload, you might encounter unacceptable slowdown dues to call to the external binary. This might justify another approach involving a design refactoring to mock and the setup of multiple tests stubs.

To mock your implementation you make use of interface capabilities. To stub your execution you create a mock that outputs stuff you want to check for.

package main

import (
    "encoding/json"
    "fmt"
    "os/exec"
)

type Person struct{}

type PersonProvider struct {
    Cmd outer
}

func (p PersonProvider) Get() (Person, error) {
    person := Person{}
    b, err := p.Cmd.Out()
    if err != nil {
        return person, err
    }
    err = json.Unmarshal(b, &person)
    return person, err
}

type outer interface{ Out() ([]byte, error) }

type echo struct {
    input string
}

func (e echo) Out() ([]byte, error) {
    cmd := exec.Command("echo", "-n", e.input)
    return cmd.Output()
}

type mockEcho struct {
    output []byte
    err    error
}

func (m mockEcho) Out() ([]byte, error) {
    return m.output, m.err
}

func main() {

    fmt.Println(PersonProvider{Cmd: echo{input: `{"Name": "Bob", "Age": 32}`}}.Get())
    fmt.Println(PersonProvider{Cmd: mockEcho{output: nil, err: fmt.Errorf("invalid json")}}.Get())

}

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.