فهرست منبع

Add unit tests to cli/command/volume package

Signed-off-by: Vincent Demeester <vincent@sbr.pm>
Vincent Demeester 8 سال پیش
والد
کامیت
6da111b3bb
30فایلهای تغییر یافته به همراه815 افزوده شده و 39 حذف شده
  1. 1 0
      cli/command/cli.go
  2. 53 0
      cli/command/volume/client_test.go
  3. 1 2
      cli/command/volume/cmd.go
  4. 3 4
      cli/command/volume/create.go
  5. 142 0
      cli/command/volume/create_test.go
  6. 3 4
      cli/command/volume/inspect.go
  7. 150 0
      cli/command/volume/inspect_test.go
  8. 3 4
      cli/command/volume/list.go
  9. 124 0
      cli/command/volume/list_test.go
  10. 3 4
      cli/command/volume/prune.go
  11. 132 0
      cli/command/volume/prune_test.go
  12. 9 9
      cli/command/volume/remove.go
  13. 47 0
      cli/command/volume/remove_test.go
  14. 1 0
      cli/command/volume/testdata/volume-inspect-with-format.json-template.golden
  15. 1 0
      cli/command/volume/testdata/volume-inspect-with-format.simple-template.golden
  16. 22 0
      cli/command/volume/testdata/volume-inspect-without-format.multiple-volume-with-labels.golden
  17. 10 0
      cli/command/volume/testdata/volume-inspect-without-format.single-volume.golden
  18. 3 0
      cli/command/volume/testdata/volume-list-with-config-format.golden
  19. 3 0
      cli/command/volume/testdata/volume-list-with-format.golden
  20. 4 0
      cli/command/volume/testdata/volume-list-without-format.golden
  21. 2 0
      cli/command/volume/testdata/volume-prune-no.golden
  22. 7 0
      cli/command/volume/testdata/volume-prune-yes.golden
  23. 6 0
      cli/command/volume/testdata/volume-prune.deletedVolumes.golden
  24. 1 0
      cli/command/volume/testdata/volume-prune.empty.golden
  25. 3 0
      cli/internal/test/builders/doc.go
  26. 3 0
      cli/internal/test/builders/node.go
  27. 43 0
      cli/internal/test/builders/volume.go
  28. 30 7
      cli/internal/test/cli.go
  29. 5 0
      cli/internal/test/doc.go
  30. 0 5
      integration-cli/docker_cli_external_volume_driver_unix_test.go

+ 1 - 0
cli/command/cli.go

@@ -38,6 +38,7 @@ type Cli interface {
 	Out() *OutStream
 	Err() io.Writer
 	In() *InStream
+	ConfigFile() *configfile.ConfigFile
 }
 
 // DockerCli is an instance the docker command line client.

+ 53 - 0
cli/command/volume/client_test.go

@@ -0,0 +1,53 @@
+package volume
+
+import (
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	volumetypes "github.com/docker/docker/api/types/volume"
+	"github.com/docker/docker/client"
+	"golang.org/x/net/context"
+)
+
+type fakeClient struct {
+	client.Client
+	volumeCreateFunc  func(volumetypes.VolumesCreateBody) (types.Volume, error)
+	volumeInspectFunc func(volumeID string) (types.Volume, error)
+	volumeListFunc    func(filter filters.Args) (volumetypes.VolumesListOKBody, error)
+	volumeRemoveFunc  func(volumeID string, force bool) error
+	volumePruneFunc   func(filter filters.Args) (types.VolumesPruneReport, error)
+}
+
+func (c *fakeClient) VolumeCreate(ctx context.Context, options volumetypes.VolumesCreateBody) (types.Volume, error) {
+	if c.volumeCreateFunc != nil {
+		return c.volumeCreateFunc(options)
+	}
+	return types.Volume{}, nil
+}
+
+func (c *fakeClient) VolumeInspect(ctx context.Context, volumeID string) (types.Volume, error) {
+	if c.volumeInspectFunc != nil {
+		return c.volumeInspectFunc(volumeID)
+	}
+	return types.Volume{}, nil
+}
+
+func (c *fakeClient) VolumeList(ctx context.Context, filter filters.Args) (volumetypes.VolumesListOKBody, error) {
+	if c.volumeListFunc != nil {
+		return c.volumeListFunc(filter)
+	}
+	return volumetypes.VolumesListOKBody{}, nil
+}
+
+func (c *fakeClient) VolumesPrune(ctx context.Context, filter filters.Args) (types.VolumesPruneReport, error) {
+	if c.volumePruneFunc != nil {
+		return c.volumePruneFunc(filter)
+	}
+	return types.VolumesPruneReport{}, nil
+}
+
+func (c *fakeClient) VolumeRemove(ctx context.Context, volumeID string, force bool) error {
+	if c.volumeRemoveFunc != nil {
+		return c.volumeRemoveFunc(volumeID, force)
+	}
+	return nil
+}

+ 1 - 2
cli/command/volume/cmd.go

@@ -1,10 +1,9 @@
 package volume
 
 import (
-	"github.com/spf13/cobra"
-
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli/command"
+	"github.com/spf13/cobra"
 )
 
 // NewVolumeCommand returns a cobra command for `volume` subcommands

+ 3 - 4
cli/command/volume/create.go

@@ -19,7 +19,7 @@ type createOptions struct {
 	labels     opts.ListOpts
 }
 
-func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
+func newCreateCommand(dockerCli command.Cli) *cobra.Command {
 	opts := createOptions{
 		driverOpts: *opts.NewMapOpts(nil, nil),
 		labels:     opts.NewListOpts(opts.ValidateEnv),
@@ -32,8 +32,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
 		RunE: func(cmd *cobra.Command, args []string) error {
 			if len(args) == 1 {
 				if opts.name != "" {
-					fmt.Fprint(dockerCli.Err(), "Conflicting options: either specify --name or provide positional arg, not both\n")
-					return cli.StatusError{StatusCode: 1}
+					return fmt.Errorf("Conflicting options: either specify --name or provide positional arg, not both\n")
 				}
 				opts.name = args[0]
 			}
@@ -50,7 +49,7 @@ func newCreateCommand(dockerCli *command.DockerCli) *cobra.Command {
 	return cmd
 }
 
-func runCreate(dockerCli *command.DockerCli, opts createOptions) error {
+func runCreate(dockerCli command.Cli, opts createOptions) error {
 	client := dockerCli.Client()
 
 	volReq := volumetypes.VolumesCreateBody{

+ 142 - 0
cli/command/volume/create_test.go

@@ -0,0 +1,142 @@
+package volume
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	volumetypes "github.com/docker/docker/api/types/volume"
+	"github.com/docker/docker/cli/internal/test"
+	"github.com/docker/docker/pkg/testutil/assert"
+)
+
+func TestVolumeCreateErrors(t *testing.T) {
+	testCases := []struct {
+		args             []string
+		flags            map[string]string
+		volumeCreateFunc func(volumetypes.VolumesCreateBody) (types.Volume, error)
+		expectedError    string
+	}{
+		{
+			args: []string{"volumeName"},
+			flags: map[string]string{
+				"name": "volumeName",
+			},
+			expectedError: "Conflicting options: either specify --name or provide positional arg, not both",
+		},
+		{
+			args:          []string{"too", "many"},
+			expectedError: "requires at most 1 argument(s)",
+		},
+		{
+			volumeCreateFunc: func(createBody volumetypes.VolumesCreateBody) (types.Volume, error) {
+				return types.Volume{}, fmt.Errorf("error creating volume")
+			},
+			expectedError: "error creating volume",
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := newCreateCommand(
+			test.NewFakeCli(&fakeClient{
+				volumeCreateFunc: tc.volumeCreateFunc,
+			}, buf),
+		)
+		cmd.SetArgs(tc.args)
+		for key, value := range tc.flags {
+			cmd.Flags().Set(key, value)
+		}
+		cmd.SetOutput(ioutil.Discard)
+		assert.Error(t, cmd.Execute(), tc.expectedError)
+	}
+}
+
+func TestVolumeCreateWithName(t *testing.T) {
+	name := "foo"
+	buf := new(bytes.Buffer)
+	cli := test.NewFakeCli(&fakeClient{
+		volumeCreateFunc: func(body volumetypes.VolumesCreateBody) (types.Volume, error) {
+			if body.Name != name {
+				return types.Volume{}, fmt.Errorf("expected name %q, got %q", name, body.Name)
+			}
+			return types.Volume{
+				Name: body.Name,
+			}, nil
+		},
+	}, buf)
+
+	// Test by flags
+	cmd := newCreateCommand(cli)
+	cmd.Flags().Set("name", name)
+	assert.NilError(t, cmd.Execute())
+	assert.Equal(t, strings.TrimSpace(buf.String()), name)
+
+	// Then by args
+	buf.Reset()
+	cmd = newCreateCommand(cli)
+	cmd.SetArgs([]string{name})
+	assert.NilError(t, cmd.Execute())
+	assert.Equal(t, strings.TrimSpace(buf.String()), name)
+}
+
+func TestVolumeCreateWithFlags(t *testing.T) {
+	expectedDriver := "foo"
+	expectedOpts := map[string]string{
+		"bar": "1",
+		"baz": "baz",
+	}
+	expectedLabels := map[string]string{
+		"lbl1": "v1",
+		"lbl2": "v2",
+	}
+	name := "banana"
+
+	buf := new(bytes.Buffer)
+	cli := test.NewFakeCli(&fakeClient{
+		volumeCreateFunc: func(body volumetypes.VolumesCreateBody) (types.Volume, error) {
+			if body.Name != "" {
+				return types.Volume{}, fmt.Errorf("expected empty name, got %q", body.Name)
+			}
+			if body.Driver != expectedDriver {
+				return types.Volume{}, fmt.Errorf("expected driver %q, got %q", expectedDriver, body.Driver)
+			}
+			if !compareMap(body.DriverOpts, expectedOpts) {
+				return types.Volume{}, fmt.Errorf("expected drivers opts %v, got %v", expectedOpts, body.DriverOpts)
+			}
+			if !compareMap(body.Labels, expectedLabels) {
+				return types.Volume{}, fmt.Errorf("expected labels %v, got %v", expectedLabels, body.Labels)
+			}
+			return types.Volume{
+				Name: name,
+			}, nil
+		},
+	}, buf)
+
+	cmd := newCreateCommand(cli)
+	cmd.Flags().Set("driver", "foo")
+	cmd.Flags().Set("opt", "bar=1")
+	cmd.Flags().Set("opt", "baz=baz")
+	cmd.Flags().Set("label", "lbl1=v1")
+	cmd.Flags().Set("label", "lbl2=v2")
+	assert.NilError(t, cmd.Execute())
+	assert.Equal(t, strings.TrimSpace(buf.String()), name)
+}
+
+func compareMap(actual map[string]string, expected map[string]string) bool {
+	if len(actual) != len(expected) {
+		return false
+	}
+	for key, value := range actual {
+		if expectedValue, ok := expected[key]; ok {
+			if expectedValue != value {
+				return false
+			}
+		} else {
+			return false
+		}
+	}
+	return true
+}

+ 3 - 4
cli/command/volume/inspect.go

@@ -1,12 +1,11 @@
 package volume
 
 import (
-	"golang.org/x/net/context"
-
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli/command"
 	"github.com/docker/docker/cli/command/inspect"
 	"github.com/spf13/cobra"
+	"golang.org/x/net/context"
 )
 
 type inspectOptions struct {
@@ -14,7 +13,7 @@ type inspectOptions struct {
 	names  []string
 }
 
-func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
+func newInspectCommand(dockerCli command.Cli) *cobra.Command {
 	var opts inspectOptions
 
 	cmd := &cobra.Command{
@@ -32,7 +31,7 @@ func newInspectCommand(dockerCli *command.DockerCli) *cobra.Command {
 	return cmd
 }
 
-func runInspect(dockerCli *command.DockerCli, opts inspectOptions) error {
+func runInspect(dockerCli command.Cli, opts inspectOptions) error {
 	client := dockerCli.Client()
 
 	ctx := context.Background()

+ 150 - 0
cli/command/volume/inspect_test.go

@@ -0,0 +1,150 @@
+package volume
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/cli/internal/test"
+	// Import builders to get the builder function as package function
+	. "github.com/docker/docker/cli/internal/test/builders"
+	"github.com/docker/docker/pkg/testutil/assert"
+	"github.com/docker/docker/pkg/testutil/golden"
+)
+
+func TestVolumeInspectErrors(t *testing.T) {
+	testCases := []struct {
+		args              []string
+		flags             map[string]string
+		volumeInspectFunc func(volumeID string) (types.Volume, error)
+		expectedError     string
+	}{
+		{
+			expectedError: "requires at least 1 argument",
+		},
+		{
+			args: []string{"foo"},
+			volumeInspectFunc: func(volumeID string) (types.Volume, error) {
+				return types.Volume{}, fmt.Errorf("error while inspecting the volume")
+			},
+			expectedError: "error while inspecting the volume",
+		},
+		{
+			args: []string{"foo"},
+			flags: map[string]string{
+				"format": "{{invalid format}}",
+			},
+			expectedError: "Template parsing error",
+		},
+		{
+			args: []string{"foo", "bar"},
+			volumeInspectFunc: func(volumeID string) (types.Volume, error) {
+				if volumeID == "foo" {
+					return types.Volume{
+						Name: "foo",
+					}, nil
+				}
+				return types.Volume{}, fmt.Errorf("error while inspecting the volume")
+			},
+			expectedError: "error while inspecting the volume",
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := newInspectCommand(
+			test.NewFakeCli(&fakeClient{
+				volumeInspectFunc: tc.volumeInspectFunc,
+			}, buf),
+		)
+		cmd.SetArgs(tc.args)
+		for key, value := range tc.flags {
+			cmd.Flags().Set(key, value)
+		}
+		cmd.SetOutput(ioutil.Discard)
+		assert.Error(t, cmd.Execute(), tc.expectedError)
+	}
+}
+
+func TestVolumeInspectWithoutFormat(t *testing.T) {
+	testCases := []struct {
+		name              string
+		args              []string
+		volumeInspectFunc func(volumeID string) (types.Volume, error)
+	}{
+		{
+			name: "single-volume",
+			args: []string{"foo"},
+			volumeInspectFunc: func(volumeID string) (types.Volume, error) {
+				if volumeID != "foo" {
+					return types.Volume{}, fmt.Errorf("Invalid volumeID, expected %s, got %s", "foo", volumeID)
+				}
+				return *Volume(), nil
+			},
+		},
+		{
+			name: "multiple-volume-with-labels",
+			args: []string{"foo", "bar"},
+			volumeInspectFunc: func(volumeID string) (types.Volume, error) {
+				return *Volume(VolumeName(volumeID), VolumeLabels(map[string]string{
+					"foo": "bar",
+				})), nil
+			},
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := newInspectCommand(
+			test.NewFakeCli(&fakeClient{
+				volumeInspectFunc: tc.volumeInspectFunc,
+			}, buf),
+		)
+		cmd.SetArgs(tc.args)
+		assert.NilError(t, cmd.Execute())
+		actual := buf.String()
+		expected := golden.Get(t, []byte(actual), fmt.Sprintf("volume-inspect-without-format.%s.golden", tc.name))
+		assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected))
+	}
+}
+
+func TestVolumeInspectWithFormat(t *testing.T) {
+	volumeInspectFunc := func(volumeID string) (types.Volume, error) {
+		return *Volume(VolumeLabels(map[string]string{
+			"foo": "bar",
+		})), nil
+	}
+	testCases := []struct {
+		name              string
+		format            string
+		args              []string
+		volumeInspectFunc func(volumeID string) (types.Volume, error)
+	}{
+		{
+			name:              "simple-template",
+			format:            "{{.Name}}",
+			args:              []string{"foo"},
+			volumeInspectFunc: volumeInspectFunc,
+		},
+		{
+			name:              "json-template",
+			format:            "{{json .Labels}}",
+			args:              []string{"foo"},
+			volumeInspectFunc: volumeInspectFunc,
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := newInspectCommand(
+			test.NewFakeCli(&fakeClient{
+				volumeInspectFunc: tc.volumeInspectFunc,
+			}, buf),
+		)
+		cmd.SetArgs(tc.args)
+		cmd.Flags().Set("format", tc.format)
+		assert.NilError(t, cmd.Execute())
+		actual := buf.String()
+		expected := golden.Get(t, []byte(actual), fmt.Sprintf("volume-inspect-with-format.%s.golden", tc.name))
+		assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected))
+	}
+}

+ 3 - 4
cli/command/volume/list.go

@@ -3,14 +3,13 @@ package volume
 import (
 	"sort"
 
-	"golang.org/x/net/context"
-
 	"github.com/docker/docker/api/types"
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli/command"
 	"github.com/docker/docker/cli/command/formatter"
 	"github.com/docker/docker/opts"
 	"github.com/spf13/cobra"
+	"golang.org/x/net/context"
 )
 
 type byVolumeName []*types.Volume
@@ -27,7 +26,7 @@ type listOptions struct {
 	filter opts.FilterOpt
 }
 
-func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
+func newListCommand(dockerCli command.Cli) *cobra.Command {
 	opts := listOptions{filter: opts.NewFilterOpt()}
 
 	cmd := &cobra.Command{
@@ -48,7 +47,7 @@ func newListCommand(dockerCli *command.DockerCli) *cobra.Command {
 	return cmd
 }
 
-func runList(dockerCli *command.DockerCli, opts listOptions) error {
+func runList(dockerCli command.Cli, opts listOptions) error {
 	client := dockerCli.Client()
 	volumes, err := client.VolumeList(context.Background(), opts.filter.Value())
 	if err != nil {

+ 124 - 0
cli/command/volume/list_test.go

@@ -0,0 +1,124 @@
+package volume
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	volumetypes "github.com/docker/docker/api/types/volume"
+	"github.com/docker/docker/cli/config/configfile"
+	"github.com/docker/docker/cli/internal/test"
+	// Import builders to get the builder function as package function
+	. "github.com/docker/docker/cli/internal/test/builders"
+	"github.com/docker/docker/pkg/testutil/assert"
+	"github.com/docker/docker/pkg/testutil/golden"
+)
+
+func TestVolumeListErrors(t *testing.T) {
+	testCases := []struct {
+		args           []string
+		flags          map[string]string
+		volumeListFunc func(filter filters.Args) (volumetypes.VolumesListOKBody, error)
+		expectedError  string
+	}{
+		{
+			args:          []string{"foo"},
+			expectedError: "accepts no argument",
+		},
+		{
+			volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) {
+				return volumetypes.VolumesListOKBody{}, fmt.Errorf("error listing volumes")
+			},
+			expectedError: "error listing volumes",
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := newListCommand(
+			test.NewFakeCli(&fakeClient{
+				volumeListFunc: tc.volumeListFunc,
+			}, buf),
+		)
+		cmd.SetArgs(tc.args)
+		for key, value := range tc.flags {
+			cmd.Flags().Set(key, value)
+		}
+		cmd.SetOutput(ioutil.Discard)
+		assert.Error(t, cmd.Execute(), tc.expectedError)
+	}
+}
+
+func TestVolumeListWithoutFormat(t *testing.T) {
+	buf := new(bytes.Buffer)
+	cli := test.NewFakeCli(&fakeClient{
+		volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) {
+			return volumetypes.VolumesListOKBody{
+				Volumes: []*types.Volume{
+					Volume(),
+					Volume(VolumeName("foo"), VolumeDriver("bar")),
+					Volume(VolumeName("baz"), VolumeLabels(map[string]string{
+						"foo": "bar",
+					})),
+				},
+			}, nil
+		},
+	}, buf)
+	cli.SetConfigfile(&configfile.ConfigFile{})
+	cmd := newListCommand(cli)
+	assert.NilError(t, cmd.Execute())
+	actual := buf.String()
+	expected := golden.Get(t, []byte(actual), "volume-list-without-format.golden")
+	assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected))
+}
+
+func TestVolumeListWithConfigFormat(t *testing.T) {
+	buf := new(bytes.Buffer)
+	cli := test.NewFakeCli(&fakeClient{
+		volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) {
+			return volumetypes.VolumesListOKBody{
+				Volumes: []*types.Volume{
+					Volume(),
+					Volume(VolumeName("foo"), VolumeDriver("bar")),
+					Volume(VolumeName("baz"), VolumeLabels(map[string]string{
+						"foo": "bar",
+					})),
+				},
+			}, nil
+		},
+	}, buf)
+	cli.SetConfigfile(&configfile.ConfigFile{
+		VolumesFormat: "{{ .Name }} {{ .Driver }} {{ .Labels }}",
+	})
+	cmd := newListCommand(cli)
+	assert.NilError(t, cmd.Execute())
+	actual := buf.String()
+	expected := golden.Get(t, []byte(actual), "volume-list-with-config-format.golden")
+	assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected))
+}
+
+func TestVolumeListWithFormat(t *testing.T) {
+	buf := new(bytes.Buffer)
+	cli := test.NewFakeCli(&fakeClient{
+		volumeListFunc: func(filter filters.Args) (volumetypes.VolumesListOKBody, error) {
+			return volumetypes.VolumesListOKBody{
+				Volumes: []*types.Volume{
+					Volume(),
+					Volume(VolumeName("foo"), VolumeDriver("bar")),
+					Volume(VolumeName("baz"), VolumeLabels(map[string]string{
+						"foo": "bar",
+					})),
+				},
+			}, nil
+		},
+	}, buf)
+	cli.SetConfigfile(&configfile.ConfigFile{})
+	cmd := newListCommand(cli)
+	cmd.Flags().Set("format", "{{ .Name }} {{ .Driver }} {{ .Labels }}")
+	assert.NilError(t, cmd.Execute())
+	actual := buf.String()
+	expected := golden.Get(t, []byte(actual), "volume-list-with-format.golden")
+	assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected))
+}

+ 3 - 4
cli/command/volume/prune.go

@@ -3,13 +3,12 @@ package volume
 import (
 	"fmt"
 
-	"golang.org/x/net/context"
-
 	"github.com/docker/docker/api/types/filters"
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli/command"
 	units "github.com/docker/go-units"
 	"github.com/spf13/cobra"
+	"golang.org/x/net/context"
 )
 
 type pruneOptions struct {
@@ -17,7 +16,7 @@ type pruneOptions struct {
 }
 
 // NewPruneCommand returns a new cobra prune command for volumes
-func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
+func NewPruneCommand(dockerCli command.Cli) *cobra.Command {
 	var opts pruneOptions
 
 	cmd := &cobra.Command{
@@ -47,7 +46,7 @@ func NewPruneCommand(dockerCli *command.DockerCli) *cobra.Command {
 const warning = `WARNING! This will remove all volumes not used by at least one container.
 Are you sure you want to continue?`
 
-func runPrune(dockerCli *command.DockerCli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) {
+func runPrune(dockerCli command.Cli, opts pruneOptions) (spaceReclaimed uint64, output string, err error) {
 	if !opts.force && !command.PromptForConfirmation(dockerCli.In(), dockerCli.Out(), warning) {
 		return
 	}

+ 132 - 0
cli/command/volume/prune_test.go

@@ -0,0 +1,132 @@
+package volume
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"runtime"
+	"strings"
+	"testing"
+
+	"github.com/docker/docker/api/types"
+	"github.com/docker/docker/api/types/filters"
+	"github.com/docker/docker/cli/internal/test"
+	"github.com/docker/docker/pkg/testutil/assert"
+	"github.com/docker/docker/pkg/testutil/golden"
+)
+
+func TestVolumePruneErrors(t *testing.T) {
+	testCases := []struct {
+		args            []string
+		flags           map[string]string
+		volumePruneFunc func(args filters.Args) (types.VolumesPruneReport, error)
+		expectedError   string
+	}{
+		{
+			args:          []string{"foo"},
+			expectedError: "accepts no argument",
+		},
+		{
+			flags: map[string]string{
+				"force": "true",
+			},
+			volumePruneFunc: func(args filters.Args) (types.VolumesPruneReport, error) {
+				return types.VolumesPruneReport{}, fmt.Errorf("error pruning volumes")
+			},
+			expectedError: "error pruning volumes",
+		},
+	}
+	for _, tc := range testCases {
+		cmd := NewPruneCommand(
+			test.NewFakeCli(&fakeClient{
+				volumePruneFunc: tc.volumePruneFunc,
+			}, ioutil.Discard),
+		)
+		cmd.SetArgs(tc.args)
+		for key, value := range tc.flags {
+			cmd.Flags().Set(key, value)
+		}
+		cmd.SetOutput(ioutil.Discard)
+		assert.Error(t, cmd.Execute(), tc.expectedError)
+	}
+}
+
+func TestVolumePruneForce(t *testing.T) {
+	testCases := []struct {
+		name            string
+		volumePruneFunc func(args filters.Args) (types.VolumesPruneReport, error)
+	}{
+		{
+			name: "empty",
+		},
+		{
+			name:            "deletedVolumes",
+			volumePruneFunc: simplePruneFunc,
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := NewPruneCommand(
+			test.NewFakeCli(&fakeClient{
+				volumePruneFunc: tc.volumePruneFunc,
+			}, buf),
+		)
+		cmd.Flags().Set("force", "true")
+		assert.NilError(t, cmd.Execute())
+		actual := buf.String()
+		expected := golden.Get(t, []byte(actual), fmt.Sprintf("volume-prune.%s.golden", tc.name))
+		assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected))
+	}
+}
+func TestVolumePrunePromptYes(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		// FIXME(vdemeester) make it work..
+		t.Skip("skipping this test on Windows")
+	}
+	for _, input := range []string{"y", "Y"} {
+		buf := new(bytes.Buffer)
+		cli := test.NewFakeCli(&fakeClient{
+			volumePruneFunc: simplePruneFunc,
+		}, buf)
+
+		cli.SetIn(ioutil.NopCloser(strings.NewReader(input)))
+		cmd := NewPruneCommand(
+			cli,
+		)
+		assert.NilError(t, cmd.Execute())
+		actual := buf.String()
+		expected := golden.Get(t, []byte(actual), "volume-prune-yes.golden")
+		assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected))
+	}
+}
+
+func TestVolumePrunePromptNo(t *testing.T) {
+	if runtime.GOOS == "windows" {
+		// FIXME(vdemeester) make it work..
+		t.Skip("skipping this test on Windows")
+	}
+	for _, input := range []string{"n", "N", "no", "anything", "really"} {
+		buf := new(bytes.Buffer)
+		cli := test.NewFakeCli(&fakeClient{
+			volumePruneFunc: simplePruneFunc,
+		}, buf)
+
+		cli.SetIn(ioutil.NopCloser(strings.NewReader(input)))
+		cmd := NewPruneCommand(
+			cli,
+		)
+		assert.NilError(t, cmd.Execute())
+		actual := buf.String()
+		expected := golden.Get(t, []byte(actual), "volume-prune-no.golden")
+		assert.EqualNormalizedString(t, assert.RemoveSpace, actual, string(expected))
+	}
+}
+
+func simplePruneFunc(args filters.Args) (types.VolumesPruneReport, error) {
+	return types.VolumesPruneReport{
+		VolumesDeleted: []string{
+			"foo", "bar", "baz",
+		},
+		SpaceReclaimed: 2000,
+	}, nil
+}

+ 9 - 9
cli/command/volume/remove.go

@@ -2,12 +2,12 @@ package volume
 
 import (
 	"fmt"
-
-	"golang.org/x/net/context"
+	"strings"
 
 	"github.com/docker/docker/cli"
 	"github.com/docker/docker/cli/command"
 	"github.com/spf13/cobra"
+	"golang.org/x/net/context"
 )
 
 type removeOptions struct {
@@ -16,7 +16,7 @@ type removeOptions struct {
 	volumes []string
 }
 
-func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
+func newRemoveCommand(dockerCli command.Cli) *cobra.Command {
 	var opts removeOptions
 
 	cmd := &cobra.Command{
@@ -38,22 +38,22 @@ func newRemoveCommand(dockerCli *command.DockerCli) *cobra.Command {
 	return cmd
 }
 
-func runRemove(dockerCli *command.DockerCli, opts *removeOptions) error {
+func runRemove(dockerCli command.Cli, opts *removeOptions) error {
 	client := dockerCli.Client()
 	ctx := context.Background()
-	status := 0
+
+	var errs []string
 
 	for _, name := range opts.volumes {
 		if err := client.VolumeRemove(ctx, name, opts.force); err != nil {
-			fmt.Fprintf(dockerCli.Err(), "%s\n", err)
-			status = 1
+			errs = append(errs, err.Error())
 			continue
 		}
 		fmt.Fprintf(dockerCli.Out(), "%s\n", name)
 	}
 
-	if status != 0 {
-		return cli.StatusError{StatusCode: status}
+	if len(errs) > 0 {
+		return fmt.Errorf("%s", strings.Join(errs, "\n"))
 	}
 	return nil
 }

+ 47 - 0
cli/command/volume/remove_test.go

@@ -0,0 +1,47 @@
+package volume
+
+import (
+	"bytes"
+	"fmt"
+	"io/ioutil"
+	"testing"
+
+	"github.com/docker/docker/cli/internal/test"
+	"github.com/docker/docker/pkg/testutil/assert"
+)
+
+func TestVolumeRemoveErrors(t *testing.T) {
+	testCases := []struct {
+		args             []string
+		volumeRemoveFunc func(volumeID string, force bool) error
+		expectedError    string
+	}{
+		{
+			expectedError: "requires at least 1 argument",
+		},
+		{
+			args: []string{"nodeID"},
+			volumeRemoveFunc: func(volumeID string, force bool) error {
+				return fmt.Errorf("error removing the volume")
+			},
+			expectedError: "error removing the volume",
+		},
+	}
+	for _, tc := range testCases {
+		buf := new(bytes.Buffer)
+		cmd := newRemoveCommand(
+			test.NewFakeCli(&fakeClient{
+				volumeRemoveFunc: tc.volumeRemoveFunc,
+			}, buf))
+		cmd.SetArgs(tc.args)
+		cmd.SetOutput(ioutil.Discard)
+		assert.Error(t, cmd.Execute(), tc.expectedError)
+	}
+}
+
+func TestNodeRemoveMultiple(t *testing.T) {
+	buf := new(bytes.Buffer)
+	cmd := newRemoveCommand(test.NewFakeCli(&fakeClient{}, buf))
+	cmd.SetArgs([]string{"volume1", "volume2"})
+	assert.NilError(t, cmd.Execute())
+}

+ 1 - 0
cli/command/volume/testdata/volume-inspect-with-format.json-template.golden

@@ -0,0 +1 @@
+{"foo":"bar"}

+ 1 - 0
cli/command/volume/testdata/volume-inspect-with-format.simple-template.golden

@@ -0,0 +1 @@
+volume

+ 22 - 0
cli/command/volume/testdata/volume-inspect-without-format.multiple-volume-with-labels.golden

@@ -0,0 +1,22 @@
+[
+    {
+        "Driver": "local",
+        "Labels": {
+            "foo": "bar"
+        },
+        "Mountpoint": "/data/volume",
+        "Name": "foo",
+        "Options": null,
+        "Scope": "local"
+    },
+    {
+        "Driver": "local",
+        "Labels": {
+            "foo": "bar"
+        },
+        "Mountpoint": "/data/volume",
+        "Name": "bar",
+        "Options": null,
+        "Scope": "local"
+    }
+]

+ 10 - 0
cli/command/volume/testdata/volume-inspect-without-format.single-volume.golden

@@ -0,0 +1,10 @@
+[
+    {
+        "Driver": "local",
+        "Labels": null,
+        "Mountpoint": "/data/volume",
+        "Name": "volume",
+        "Options": null,
+        "Scope": "local"
+    }
+]

+ 3 - 0
cli/command/volume/testdata/volume-list-with-config-format.golden

@@ -0,0 +1,3 @@
+baz local foo=bar
+foo bar 
+volume local 

+ 3 - 0
cli/command/volume/testdata/volume-list-with-format.golden

@@ -0,0 +1,3 @@
+baz local foo=bar
+foo bar 
+volume local 

+ 4 - 0
cli/command/volume/testdata/volume-list-without-format.golden

@@ -0,0 +1,4 @@
+DRIVER              VOLUME NAME
+local               baz
+bar                 foo
+local               volume

+ 2 - 0
cli/command/volume/testdata/volume-prune-no.golden

@@ -0,0 +1,2 @@
+WARNING! This will remove all volumes not used by at least one container.
+Are you sure you want to continue? [y/N] Total reclaimed space: 0B

+ 7 - 0
cli/command/volume/testdata/volume-prune-yes.golden

@@ -0,0 +1,7 @@
+WARNING! This will remove all volumes not used by at least one container.
+Are you sure you want to continue? [y/N] Deleted Volumes:
+foo
+bar
+baz
+
+Total reclaimed space: 2kB

+ 6 - 0
cli/command/volume/testdata/volume-prune.deletedVolumes.golden

@@ -0,0 +1,6 @@
+Deleted Volumes:
+foo
+bar
+baz
+
+Total reclaimed space: 2kB

+ 1 - 0
cli/command/volume/testdata/volume-prune.empty.golden

@@ -0,0 +1 @@
+Total reclaimed space: 0B

+ 3 - 0
cli/internal/test/builders/doc.go

@@ -0,0 +1,3 @@
+// Package builders helps you create struct for your unit test while keeping them expressive.
+//
+package builders

+ 3 - 0
cli/internal/test/builders/node.go

@@ -8,6 +8,9 @@ import (
 
 // Node creates a node with default values.
 // Any number of node function builder can be pass to augment it.
+//
+//	n1 := Node() // Returns a default node
+//	n2 := Node(NodeID("foo"), NodeHostname("bar"), Leader())
 func Node(builders ...func(*swarm.Node)) *swarm.Node {
 	t1 := time.Date(2009, time.November, 10, 23, 0, 0, 0, time.UTC)
 	node := &swarm.Node{

+ 43 - 0
cli/internal/test/builders/volume.go

@@ -0,0 +1,43 @@
+package builders
+
+import (
+	"github.com/docker/docker/api/types"
+)
+
+// Volume creates a volume with default values.
+// Any number of volume function builder can be pass to augment it.
+func Volume(builders ...func(volume *types.Volume)) *types.Volume {
+	volume := &types.Volume{
+		Name:       "volume",
+		Driver:     "local",
+		Mountpoint: "/data/volume",
+		Scope:      "local",
+	}
+
+	for _, builder := range builders {
+		builder(volume)
+	}
+
+	return volume
+}
+
+// VolumeLabels sets the volume labels
+func VolumeLabels(labels map[string]string) func(volume *types.Volume) {
+	return func(volume *types.Volume) {
+		volume.Labels = labels
+	}
+}
+
+// VolumeName sets the volume labels
+func VolumeName(name string) func(volume *types.Volume) {
+	return func(volume *types.Volume) {
+		volume.Name = name
+	}
+}
+
+// VolumeDriver sets the volume driver
+func VolumeDriver(name string) func(volume *types.Volume) {
+	return func(volume *types.Volume) {
+		volume.Driver = name
+	}
+}

+ 30 - 7
cli/internal/test/cli.go

@@ -1,21 +1,23 @@
-// Package test is a test-only package that can be used by other cli package to write unit test
 package test
 
 import (
 	"io"
 	"io/ioutil"
+	"strings"
 
 	"github.com/docker/docker/cli/command"
+	"github.com/docker/docker/cli/config/configfile"
 	"github.com/docker/docker/client"
-	"strings"
 )
 
 // FakeCli emulates the default DockerCli
 type FakeCli struct {
 	command.DockerCli
-	client client.APIClient
-	out    io.Writer
-	in     io.ReadCloser
+	client     client.APIClient
+	configfile *configfile.ConfigFile
+	out        io.Writer
+	err        io.Writer
+	in         io.ReadCloser
 }
 
 // NewFakeCli returns a Cli backed by the fakeCli
@@ -23,6 +25,7 @@ func NewFakeCli(client client.APIClient, out io.Writer) *FakeCli {
 	return &FakeCli{
 		client: client,
 		out:    out,
+		err:    ioutil.Discard,
 		in:     ioutil.NopCloser(strings.NewReader("")),
 	}
 }
@@ -32,17 +35,37 @@ func (c *FakeCli) SetIn(in io.ReadCloser) {
 	c.in = in
 }
 
+// SetErr sets the standard error stream th cli should write on
+func (c *FakeCli) SetErr(err io.Writer) {
+	c.err = err
+}
+
+// SetConfigfile sets the "fake" config file
+func (c *FakeCli) SetConfigfile(configfile *configfile.ConfigFile) {
+	c.configfile = configfile
+}
+
 // Client returns a docker API client
 func (c *FakeCli) Client() client.APIClient {
 	return c.client
 }
 
-// Out returns the output stream the cli should write on
+// Out returns the output stream (stdout) the cli should write on
 func (c *FakeCli) Out() *command.OutStream {
 	return command.NewOutStream(c.out)
 }
 
-// In returns thi input stream the cli will use
+// Err returns the output stream (stderr) the cli should write on
+func (c *FakeCli) Err() io.Writer {
+	return c.err
+}
+
+// In returns the input stream the cli will use
 func (c *FakeCli) In() *command.InStream {
 	return command.NewInStream(c.in)
 }
+
+// ConfigFile returns the cli configfile object (to get client configuration)
+func (c *FakeCli) ConfigFile() *configfile.ConfigFile {
+	return c.configfile
+}

+ 5 - 0
cli/internal/test/doc.go

@@ -0,0 +1,5 @@
+// Package test is a test-only package that can be used by other cli package to write unit test.
+//
+// It as an internal package and cannot be used outside of github.com/docker/docker/cli package.
+//
+package test

+ 0 - 5
integration-cli/docker_cli_external_volume_driver_unix_test.go

@@ -292,11 +292,6 @@ func (s *DockerExternalVolumeSuite) TestVolumeCLICreateOptionConflict(c *check.C
 	out, _ = dockerCmd(c, "volume", "inspect", "--format={{ .Driver }}", "test")
 	_, _, err = dockerCmdWithError("volume", "create", "test", "--driver", strings.TrimSpace(out))
 	c.Assert(err, check.IsNil)
-
-	// make sure hidden --name option conflicts with positional arg name
-	out, _, err = dockerCmdWithError("volume", "create", "--name", "test2", "test2")
-	c.Assert(err, check.NotNil)
-	c.Assert(strings.TrimSpace(out), checker.Equals, "Conflicting options: either specify --name or provide positional arg, not both")
 }
 
 func (s *DockerExternalVolumeSuite) TestExternalVolumeDriverNamed(c *check.C) {