Golang 测试替代
6 min read
什么是测试替代
测试替代(Test Doubles)一种用于在测试中替代真实实现的技术。
简单的实现
假设我们需要实现一个电话簿功能,可以通过姓名来查找号码。
一个简单的实现可能像这样:
type Person struct {
FirstName string
LastName string
Phone string
}
type Phonebook struct {
People []*Person
}
var (
ArgumentWrong = errors.New("argument error")
PersonNotExist = errors.New("person not exist")
)
func FindPhone(people *Phonebook, firstName, lastName string) (string, error) {
if firstName == "" || lastName == "" {
return "", ArgumentWrong
}
for _, p := range people.People {
if p.FirstName == firstName && p.LastName == lastName {
return p.Phone, nil
}
}
return "", PersonNotExist
}
对应的测试函数为:
var phonebook = &Phonebook{People: []*Person{
{
FirstName: "FirstName1",
LastName: "LastName1",
Phone: "Phone1",
},
{
FirstName: "FirstName2",
LastName: "LastName2",
Phone: "Phone2",
},
}}
func TestFindPhone(t *testing.T) {
tests := []struct {
inputFirstName string
inputLastName string
wantPhone string
wantErr error
}{
{"FirstName1", "LastName1", "Phone1", nil},
{"", "", "", ArgumentWrong},
{"FirstName3", "LastName3", "", PersonNotExist},
}
for _, tt := range tests {
gotPhone, gotErr := FindPhone(phonebook, tt.inputFirstName, tt.inputLastName)
if gotPhone != tt.wantPhone {
t.Errorf("got %v, want %v", gotPhone, tt.wantPhone)
}
if !errors.Is(gotErr, tt.wantErr) {
t.Errorf("gotErr %v, wantErr %v", gotErr, tt.wantErr)
}
}
}
但是,这样的实现不利于解耦,如果我们希望换一种搜索方式,就需要重新实现函数。
基于接口的实现
通过接口来实现,就能轻松的实现解耦。例如:
type Searcher interface {
Search(people []*Person, firstName string, lastName string) *Person
}
func (p *Phonebook) FindPhone(searcher Searcher, firstName, lastName string) (string, error) {
if firstName == "" || lastName == "" {
return "", ArgumentWrong
}
person := searcher.Search(p.People, firstName, lastName)
if person == nil {
return "", PersonNotExist
}
return person.Phone, nil
}
type NormalImpl struct{}
func (n NormalImpl) Search(people []*Person, firstName string, lastName string) *Person {
for _, p := range people {
if p.FirstName == firstName && p.LastName == lastName {
return p
}
}
return nil
}
对应的测试函数为:
func TestNormal(t *testing.T) {
tests := []struct {
inputFirstName string
inputLastName string
wantPhone string
wantErr error
}{
{"FirstName1", "LastName1", "Phone1", nil},
{"", "", "", ArgumentWrong},
{"FirstName3", "LastName3", "", PersonNotExist},
}
for _, tt := range tests {
gotPhone, gotErr := phonebook.FindPhone(&NormalImpl{}, tt.inputFirstName, tt.inputLastName)
if gotPhone != tt.wantPhone {
t.Errorf("got %v, want %v", gotPhone, tt.wantPhone)
}
if !errors.Is(gotErr, tt.wantErr) {
t.Errorf("gotErr %v, wantErr %v", gotErr, tt.wantErr)
}
}
}
Dummy
Dummy 是一个空的实例,不实现任何功能,只是用于防止编译器报错。例如:
type DummyImpl struct{}
func (d DummyImpl) Search(people []*Person, firstName string, lastName string) *Person {
return nil
}
在实践中最好不要通过 Dummy 来测试。
Stub
Stub 直接在调用时提供结果值。例如:
type StubImpl struct {
phone string
}
func (s StubImpl) Search(people []*Person, firstName string, lastName string) *Person {
return &Person{
FirstName: firstName,
LastName: lastName,
Phone: s.phone,
}
}
对应的测试函数为:
func TestStub(t *testing.T) {
tests := []struct {
inputFirstName string
inputLastName string
stubPhone string
wantErr error
}{
{"FirstName1", "LastName1", "Phone1", nil},
{"", "", "", ArgumentWrong},
}
for _, tt := range tests {
gotPhone, gotErr := phonebook.FindPhone(&StubImpl{tt.stubPhone}, tt.inputFirstName, tt.inputLastName)
if gotPhone != tt.stubPhone {
t.Errorf("got %v, want %v", gotPhone, tt.stubPhone)
}
if !errors.Is(gotErr, tt.wantErr) {
t.Errorf("gotErr %v, wantErr %v", gotErr, tt.wantErr)
}
}
}
这时可以通过 Stub 的值来测试代码的某些路径,如:返回 ArgumentWrong 错误的路径。
Spy
Spy 在 Stub 的基础上,还可以记住是否进行了调用。例如:
type SpyImpl struct {
phone string
called bool
}
func (s *SpyImpl) Search(people []*Person, firstName string, lastName string) *Person {
s.called = true
return &Person{
FirstName: firstName,
LastName: lastName,
Phone: s.phone,
}
}
对应的测试函数为:
func TestSpy(t *testing.T) {
tests := []struct {
inputFirstName string
inputLastName string
spyPhone string
called bool
}{
{"FirstName1", "LastName1", "Phone1", true},
}
for _, tt := range tests {
spyImpl := &SpyImpl{tt.spyPhone, tt.called}
gotPhone, _ := phonebook.FindPhone(spyImpl, tt.inputFirstName, tt.inputLastName)
if gotPhone != tt.spyPhone {
t.Errorf("got %v, want %v", gotPhone, tt.spyPhone)
}
if spyImpl.called != tt.called {
t.Errorf("got call %v, want call %v", spyImpl.called, tt.called)
}
}
}
Mock
Mock 在 Stub 和 Spy 的基础上,还提供了验证是否调用了某个函数的功能。例如:
type MockImpl struct {
phone string
methodsToCall map[string]bool
}
func (m *MockImpl) Search(people []*Person, firstName string, lastName string) *Person {
m.methodsToCall["Search"] = true
return &Person{
FirstName: firstName,
LastName: lastName,
Phone: m.phone,
}
}
func (m *MockImpl) ExpectToCall(methodName string) {
if m.methodsToCall == nil {
m.methodsToCall = make(map[string]bool)
}
m.methodsToCall[methodName] = false
}
func (m *MockImpl) Verify() bool {
for _, called := range m.methodsToCall {
if !called {
return false
}
}
return true
}
对应的测试函数为:
func TestMock(t *testing.T) {
tests := []struct {
inputFirstName string
inputLastName string
mockPhone string
expectToCall string
wantVerifyResult bool
}{
{"FirstName1", "LastName1", "Phone1", "Search", true},
{"FirstName1", "LastName1", "Phone1", "UnCalled", false},
}
for _, tt := range tests {
mock := &MockImpl{phone: tt.mockPhone}
mock.ExpectToCall(tt.expectToCall)
gotPhone, _ := phonebook.FindPhone(mock, tt.inputFirstName, tt.inputLastName)
if gotPhone != tt.mockPhone {
t.Errorf("got %v, want %v", gotPhone, tt.mockPhone)
}
gotVerifyResult := mock.Verify()
if gotVerifyResult != tt.wantVerifyResult {
t.Errorf("got %v, want %v", gotVerifyResult, tt.wantVerifyResult)
}
}
}
Fake
Fake 的仿真度更接近于真实实现,只不过是通过一种简单的方式来实现。例如:
type FakeImpl struct {
people []*Person
}
func (f *FakeImpl) Search(people []*Person, firstName string, lastName string) *Person {
if len(f.people) == 0 {
return nil
}
return f.people[0]
}
对应的测试函数为:
func TestFake(t *testing.T) {
tests := []struct {
inputFirstName string
inputLastName string
fakePhone string
inputPeople []*Person
}{
{"FirstName1", "LastName1", "Phone1", []*Person{{
FirstName: "",
LastName: "",
Phone: "Phone1",
}}},
{"FirstName1", "LastName1", "", nil},
}
for _, tt := range tests {
gotPhone, _ := phonebook.FindPhone(&FakeImpl{tt.inputPeople}, tt.inputFirstName, tt.inputLastName)
if gotPhone != tt.fakePhone {
t.Errorf("got %v, want %v", gotPhone, tt.fakePhone)
}
}
}