Go中如何用Fake与接口模拟依赖HTTP请求的外部包结构体?
问题
我正在基于Poker Squares开发一款终端游戏,通过deckofcardsapi.com获取卡牌。Card结构体位于card包中,每个Card会被存储到外部board包的Board结构体中。由于Card需要向外部API发起请求,我计划通过Fake与接口创建Card的模拟对象,以此测试Board的isFull属性,以及用户尝试在已满或不存在的位置放置卡牌时触发的panic错误。
我有Python、Mock和单元测试经验,但这是我的第一个Go项目。我希望无需接口参数就能创建Card的Fake对象,但网上的示例大多带有参数,我阅读了《Go for DevOps》测试框架章节与《Go Workshop》接口章节,也做了大量调研,但复杂的示例让我困惑。我的问题是:如何使用Fake与接口模拟依赖HTTP请求的外部包结构体?
现有代码
board/board_test.go
package board import ( "errors" "poker_squares/card" "testing" ) type fakeCard struct { data card.Card err bool } func (c fakeCard) Card() (card.Card, error) { if c.err { return "", errors.New("Couldn't create card") } return c.data, nil } func TestBoard(t *testing.T) { tests := []struct { desc string board Board want string expectError bool }{ { desc: "Add a card, board isn't full and isFull property is false", expectError: false, }, // 省略其余测试用例 } }
card/card.go
package card type CardGetter interface { Card() (Card, error) } type Card struct { value string suit string } func(c Card)()(Card, error){ // 从deckofcards API获取卡牌 }
解决方案
核心思路是让Board依赖接口而非具体的Card结构体,这样就能用Fake实现替代真实的API调用逻辑,同时可以灵活创建Fake实例无需额外参数。
1. 修正Card包的接口与实现
首先修正card.go的语法错误,将API调用逻辑与Card数据结构分离:
package card // CardGetter 定义获取卡牌的接口 type CardGetter interface { GetCard() (Card, error) // 改名避免与结构体名混淆 } // Card 存储卡牌数据 type Card struct { Value string Suit string } // APICardGetter 真实的API实现,处理HTTP请求 type APICardGetter struct{} func (a APICardGetter) GetCard() (Card, error) { // 这里编写调用deckofcardsapi的HTTP请求逻辑 // 解析响应后返回Card实例或错误 return Card{}, nil }
2. 调整Board依赖接口
让Board结构体依赖CardGetter接口,通过依赖注入实现逻辑解耦:
package board import "poker_squares/card" // Board 代表Poker Squares的5x5棋盘 type Board struct { cards [25]card.Card // 固定25个位置 getter card.CardGetter } // NewBoard 创建Board实例,接受CardGetter作为依赖 func NewBoard(getter card.CardGetter) *Board { return &Board{getter: getter} } // AddCard 获取卡牌并放置到指定位置 func (b *Board) AddCard(pos int) error { if pos < 0 || pos >= 25 { panic("invalid position") } if b.cards[pos] != (card.Card{}) { panic("position already occupied") } c, err := b.getter.GetCard() if err != nil { return err } b.cards[pos] = c return nil } // IsFull 判断棋盘是否已满 func (b *Board) IsFull() bool { for _, c := range b.cards { if c == (card.Card{}) { return false } } return true }
3. 实现无参数的Fake CardGetter
创建Fake实现,通过结构体字段预设返回值,同时提供便捷构造函数简化实例化:
package board import ( "errors" "poker_squares/card" ) // FakeCardGetter 模拟CardGetter接口 type FakeCardGetter struct { ReturnCard card.Card ReturnErr error } // GetCard 实现CardGetter接口 func (f FakeCardGetter) GetCard() (card.Card, error) { return f.ReturnCard, f.ReturnErr } // NewFakeCard 快速创建返回指定卡牌的Fake func NewFakeCard(c card.Card) FakeCardGetter { return FakeCardGetter{ReturnCard: c} } // NewFakeErrCard 快速创建返回错误的Fake func NewFakeErrCard() FakeCardGetter { return FakeCardGetter{ReturnErr: errors.New("failed to get card")} }
4. 编写测试用例
现在可以轻松用Fake测试Board的核心逻辑,无需依赖真实API:
package board import ( "poker_squares/card" "testing" ) func TestBoard_AddCard(t *testing.T) { tests := []struct { name string setupBoard func() *Board pos int expectPanic bool expectFull bool }{ { name: "添加卡牌后棋盘未满", setupBoard: func() *Board { fake := NewFakeCard(card.Card{Value: "A", Suit: "Hearts"}) return NewBoard(fake) }, pos: 0, expectPanic: false, expectFull: false, }, { name: "向已占用位置添加卡牌触发panic", setupBoard: func() *Board { fake := NewFakeCard(card.Card{Value: "A", Suit: "Hearts"}) b := NewBoard(fake) _ = b.AddCard(0) // 预先占用位置 return b }, pos: 0, expectPanic: true, }, { name: "向无效位置添加卡牌触发panic", setupBoard: func() *Board { fake := NewFakeCard(card.Card{Value: "A", Suit: "Hearts"}) return NewBoard(fake) }, pos: 25, expectPanic: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { b := tt.setupBoard() if tt.expectPanic { defer func() { if r := recover(); r == nil { t.Errorf("expected panic but got none") } }() _ = b.AddCard(tt.pos) return } err := b.AddCard(tt.pos) if err != nil { t.Errorf("unexpected error: %v", err) } if b.IsFull() != tt.expectFull { t.Errorf("IsFull() = %v, want %v", b.IsFull(), tt.expectFull) } }) } }
关键说明
- 依赖倒置:Board只依赖
CardGetter接口契约,运行时传入真实API实现,测试时传入Fake,彻底解耦HTTP依赖。 - 灵活Fake:通过结构体字段预设返回值,或用便捷构造函数快速创建常用Fake实例,无需复杂参数。
- 隐式接口实现:Go的接口是隐式实现的,只要Fake的方法签名与接口匹配,就能直接替换真实实现,逻辑和Python Mock一致但更轻量。
内容的提问来源于stack exchange,提问作者wavesinaroom
相关产品推荐
相关产品推荐

