如果你想在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