You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 20:35:05