You can find all the code here
I am executing a command using os/exec.Command() which generated XML data. The command will be executed in a function called GetData().
In order to test GetData(), I have some testdata which I created.
In my _test.go I have a TestGetData which calls GetData() but that will use os.exec, instead I would like for it to use my testdata.
What is a good way to achieve this? When calling GetData should I have a "test" flag mode so it will read a file ie GetData(mode string)?
A few things
I have taken the liberty of guessing what the code might look like
type Payload struct {
Message string `xml:"message"`
}
func GetData() string {
cmd := exec.Command("cat", "msg.xml")
out, _ := cmd.StdoutPipe()
var payload Payload
decoder := xml.NewDecoder(out)
// these 3 can return errors but I'm ignoring for brevity
cmd.Start()
decoder.Decode(&payload)
cmd.Wait()
return strings.ToUpper(payload.Message)
}
exec.Command
which allows you to execute an external command to the processcmd.StdoutPipe
which returns us a io.ReadCloser
(this will become important)io.ReadCloser
and then we Start
the command and then wait for all the data to be read by calling Wait
. In between those two calls we decode the data into our Payload
struct.Here is what is contained inside msg.xml
<payload>
<message>Happy New Year!</message>
</payload>
I wrote a simple test to show it in action
func TestGetData(t *testing.T) {
got := GetData()
want := "HAPPY NEW YEAR!"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
Testable code is decoupled and single purpose. To me it feels like there are two main concerns for this code
strings.ToUpper
on the <message>
)The first part is just copying the example from the standard lib.
The second part is where we have our business logic and by looking at the code we can see where the "seam" in our logic starts; it's where we get our io.ReadCloser
. We can use this existing abstraction to separate concerns and make our code testable.
The problem with GetData is the business logic is coupled with the means of getting the XML. To make our design better we need to decouple them
Our TestGetData
can act as our integration test between our two concerns so we'll keep hold of that to make sure it keeps working.
Here is what the newly separated code looks like
type Payload struct {
Message string `xml:"message"`
}
func GetData(data io.Reader) string {
var payload Payload
xml.NewDecoder(data).Decode(&payload)
return strings.ToUpper(payload.Message)
}
func getXMLFromCommand() io.Reader {
cmd := exec.Command("cat", "msg.xml")
out, _ := cmd.StdoutPipe()
cmd.Start()
data, _ := ioutil.ReadAll(out)
cmd.Wait()
return bytes.NewReader(data)
}
func TestGetDataIntegration(t *testing.T) {
got := GetData(getXMLFromCommand())
want := "HAPPY NEW YEAR!"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
Now that GetData
takes its input from just an io.Reader
we have made it testable and it is no longer concerned how the data is retrieved; people can re-use the function with anything that returns an io.Reader
(which is extremely common). For example we could start fetching the XML from a URL instead of the command line.
func TestGetData(t *testing.T) {
input := strings.NewReader(`
<payload>
<message>Cats are the best animal</message>
</payload>`)
got := GetData(input)
want := "CATS ARE THE BEST ANIMAL"
if got != want {
t.Errorf("got %q, want %q", got, want)
}
}
Here is an example of a unit test for GetData
.
By separating the concerns and using existing abstractions within Go testing our important business logic is a breeze.
此处可能存在不合适展示的内容,页面不予展示。您可通过相关编辑功能自查并修改。
如您确认内容无涉及 不当用语 / 纯广告导流 / 暴力 / 低俗色情 / 侵权 / 盗版 / 虚假 / 无价值内容或违法国家有关法律法规的内容,可点击提交进行申诉,我们将尽快为您处理。