在Golang测试中使用接口来模拟服务的例子

338 阅读2分钟

如果你想在Golang中编写单元测试时模拟服务,你可以简单地使用接口。如果你不希望你的测试调用 "真实 "的代码,那么模拟往往是必要的。我们的例子是对Twitter进行认证并向其发布消息。

结构

└── internal
    ├── app
    │   ├── app.go
    │   └── app_test.go
    └── twitter
        └── twitter.go

文件

app.go

package app

import (
	"github.com/inanzzz/internal/twitter"
)

type App struct {
	client twitter.Twitter
}

func New(c twitter.Twitter) App {
	return App{client: c}
}

func (a App) SendMessage(msg string) string {
	return a.client.Tweet(msg)
}

twitter.go

package twitter

import (
	"fmt"
)

type Twitter struct {
	BaseURL      string
	ClientID     string
	ClientSecret string
}

func New() Twitter {
	return Twitter{
		BaseURL:      "https://www.real-url.com",
		ClientID:     "real-id",
		ClientSecret: "real-secret",
	}
}

func (t Twitter) Tweet(msg string) string {
	return fmt.Sprintf(
		"Sending '%s' to '%s' using '%s:%s' credentials",
		msg,
		t.BaseURL,
		t.ClientID,
		t.ClientSecret,
	)
}

app_test.go

package app

import (
	"log"
	"testing"

	"github.com/inanzzz/internal/twitter"
)

func TestApp_SendMessage(t *testing.T) {
	twt := twitter.New()

	app := New(twt)

	res := app.SendMessage("hello")

	log.Println(res)
}

结果

正如你在下面看到的,当我们运行测试时,它实际上调用了真正的Twitter API和真正的配置参数。这是你绝对要避免的,让我们在下一阶段解决这个问题:

=== RUN   TestApp_SendMessage
2020/05/12 17:50:39 Sending 'hello' to 'https://www.real-url.com' using 'real-id:real-secret' credentials
--- PASS: TestApp_SendMessage (0.00s)
PASS

结构

└── internal
    ├── app
    │   ├── app.go
    │   └── app_test.go
    └── twitter
        ├── mock
        │   └── twitter.go // This is new
        └── twitter.go

文件

app.go

我们用twitter.SocialMedia 接口类型取代了具体的twitter.Twitter 类型:

package app

import (
	"github.com/inanzzz/internal/twitter"
)

type App struct {
	client twitter.SocialMedia
}

func New(c twitter.SocialMedia) App {
	return App{client: c}
}

func (a App) SendMessage(msg string) string {
	return a.client.Tweet(msg)
}

twitter.go

我们引入了SocialMedia 接口类型:

package twitter

import (
	"fmt"
)

type SocialMedia interface {
	Tweet(msg string) string
}

type Twitter struct {
	BaseURL      string
	ClientID     string
	ClientSecret string
}

func New() Twitter {
	return Twitter{
		BaseURL:      "https://www.real-url.com",
		ClientID:     "real-id",
		ClientSecret: "real-secret",
	}
}

func (t Twitter) Tweet(msg string) string {
	return fmt.Sprintf(
		"Sending '%s' to '%s' using '%s:%s' credentials",
		msg,
		t.BaseURL,
		t.ClientID,
		t.ClientSecret,
	)
}

mock/twitter.go

我们创建了这个模拟版本,它满足了twitter.SocialMedia 接口类型的签名:

package mock

import (
	"fmt"
)

type TwitterMock struct {
	BaseURL      string
	ClientID     string
	ClientSecret string
}

func (t TwitterMock) Tweet(msg string) string {
	return fmt.Sprintf(
		"Sending '%s' to '%s' using '%s:%s' credentials",
		msg,
		t.BaseURL,
		t.ClientID,
		t.ClientSecret,
	)
}

app_test.go

我们将mock.TwitterMock 作为依赖关系注入:

package app

import (
	"log"
	"testing"

	"github.com/inanzzz/internal/twitter/mock"
)

func TestApp_SendMessage(t *testing.T) {
	twt := mock.TwitterMock{
		BaseURL:      "https://www.mock-url.com",
		ClientID:     "mock-id",
		ClientSecret: "mock-secret",
	}

	app := New(twt)

	res := app.SendMessage("hello")

	log.Println(res)
}

结果

正如你在下面看到的,我们的测试与模拟服务进行了交互。如果有意义的话,你甚至可以在模拟文件中硬编码值:

=== RUN   TestApp_SendMessage
2020/05/12 18:01:51 Sending 'hello' to 'https://www.mock-url.com' using 'mock-id:mock-secret' credentials
--- PASS: TestApp_SendMessage (0.00s)
PASS