gomonkey 1.0 正式发布!

gomonkey 介绍

gomonkey 是 golang 的一款打桩框架,目标是让用户在单元测试中低成本的完成打桩,从而将精力聚焦于业务功能的开发。gomonkey 接口友好,功能强大,目前已被很多项目使用,用户遍及世界多个国家。

gomonkey 1.0 特性列表

gomonkey 1.0 特性列表如下:

  • 支持为一个函数打一个桩
  • 支持为一个成员方法打一个桩
  • 支持为一个全局变量打一个桩
  • 支持为一个函数变量打一个桩
  • 支持为一个函数打一个特定的桩序列
  • 支持为一个成员方法打一个特定的桩序列
  • 支持为一个函数变量打一个特定的桩序列

下载地址:
https://github.com/agiledragon/gomonkey/releases/tag/v1.0

习惯用法

gomonkey 提供的 API 包括函数接口和成员方法接口,接口契约一致,扩展性很强。

示例代码中用到了 goconvey,它是一款针对 golang 的测试框架,可以管理和运行测试用例,同时提供了丰富的断言函数,并支持很多 Web 界面特性。

ApplyFunc

ApplyFunc 接口定义如下:

func ApplyFunc(target, double interface{}) *Patches
func (this *Patches) ApplyFunc(target, double interface{}) *Patches

ApplyFunc 第一个参数是函数名,第二个参数是桩函数。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。

函数打桩的习惯用法:

import (
    . "github.com/agiledragon/gomonkey"
    . "github.com/smartystreets/goconvey/convey"
    "testing"
    "github.com/agiledragon/gomonkey/test/fake"
    "encoding/json"
)

var (
    outputExpect = "xxx-vethName100-yyy"
)

func TestApplyFunc(t *testing.T) {
    Convey("TestApplyFunc", t, func() {

        Convey("one func for succ", func() {
            patches := ApplyFunc(fake.Exec, func(_ string, _ ...string) (string, error) {
                    return outputExpect, nil
                })
            defer patches.Reset()
            output, err := fake.Exec("", "")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, outputExpect)
        })

        Convey("one func for fail", func() {
            patches := ApplyFunc(fake.Exec, func(_ string, _ ...string) (string, error) {
                return "", fake.ErrActual
            })
            defer patches.Reset()
            output, err := fake.Exec("", "")
            So(err, ShouldEqual, fake.ErrActual)
            So(output, ShouldEqual, "")
        })

        Convey("two funcs", func() {
            patches := ApplyFunc(fake.Exec, func(_ string, _ ...string) (string, error) {
                return outputExpect, nil
            })
            defer patches.Reset()
            patches.ApplyFunc(fake.Belong, func(_ string, _ []string) bool {
                return true
            })
            output, err := fake.Exec("", "")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, outputExpect)
            flag := fake.Belong("", nil)
            So(flag, ShouldBeTrue)
        })

        Convey("input and output param", func() {
            patches := ApplyFunc(json.Unmarshal, func(_ []byte, v interface{}) error {
                p := v.(*map[int]int)
                *p = make(map[int]int)
                (*p)[1] = 2
                (*p)[2] = 4
                return nil
            })
            defer patches.Reset()
            var m map[int]int
            err := json.Unmarshal(nil, &m)
            So(err, ShouldEqual, nil)
            So(m[1], ShouldEqual, 2)
            So(m[2], ShouldEqual, 4)
        })
    })
}


ApplyMethod

ApplyMethod 接口定义如下:

func ApplyMethod(target reflect.Type, methodName string, double interface{}) *Patches
func (this *Patches) ApplyMethod(target reflect.Type, methodName string, double interface{}) *Patches

ApplyMethod 第一个参数是目标类的指针变量的反射类型,第二个参数是字符串形式的方法名,第三个参数是桩函数。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。

成员方法打桩的习惯用法:

import (
    . "github.com/agiledragon/gomonkey"
    . "github.com/smartystreets/goconvey/convey"
    "testing"
    "reflect"
    "github.com/agiledragon/gomonkey/test/fake"
)


func TestApplyMethod(t *testing.T) {
    slice := fake.NewSlice()
    var s *fake.Slice
    Convey("TestApplyMethod", t, func() {

        Convey("for succ", func() {
            err := slice.Add(1)
            So(err, ShouldEqual, nil)
            patches := ApplyMethod(reflect.TypeOf(s), "Add", func(_ *fake.Slice, _ int) error {
                return nil
            })
            defer patches.Reset()
            err = slice.Add(1)
            So(err, ShouldEqual, nil)
            err = slice.Remove(1)
            So(err, ShouldEqual, nil)
            So(len(slice), ShouldEqual, 0)
        })

        Convey("for already exist", func() {
            err := slice.Add(2)
            So(err, ShouldEqual, nil)
            patches := ApplyMethod(reflect.TypeOf(s), "Add", func(_ *fake.Slice, _ int) error {
                return fake.ERR_ELEM_EXIST
            })
            defer patches.Reset()
            err = slice.Add(1)
            So(err, ShouldEqual, fake.ERR_ELEM_EXIST)
            err = slice.Remove(2)
            So(err, ShouldEqual, nil)
            So(len(slice), ShouldEqual, 0)
        })

        Convey("two methods", func() {
            err := slice.Add(3)
            So(err, ShouldEqual, nil)
            defer slice.Remove(3)
            patches := ApplyMethod(reflect.TypeOf(s), "Add", func(_ *fake.Slice, _ int) error {
                return fake.ERR_ELEM_EXIST
            })
            defer patches.Reset()
            patches.ApplyMethod(reflect.TypeOf(s), "Remove", func(_ *fake.Slice, _ int) error {
                return fake.ERR_ELEM_NT_EXIST
            })
            err = slice.Add(2)
            So(err, ShouldEqual, fake.ERR_ELEM_EXIST)
            err = slice.Remove(1)
            So(err, ShouldEqual, fake.ERR_ELEM_NT_EXIST)
            So(len(slice), ShouldEqual, 1)
            So(slice[0], ShouldEqual, 3)
        })

        Convey("one func and one method", func() {
            err := slice.Add(4)
            So(err, ShouldEqual, nil)
            defer slice.Remove(4)
            patches := ApplyFunc(fake.Exec, func(_ string, _ ...string) (string, error) {
                return outputExpect, nil
            })
            defer patches.Reset()
            patches.ApplyMethod(reflect.TypeOf(s), "Remove", func(_ *fake.Slice, _ int) error {
                return fake.ERR_ELEM_NT_EXIST
            })
            output, err := fake.Exec("", "")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, outputExpect)
            err = slice.Remove(1)
            So(err, ShouldEqual, fake.ERR_ELEM_NT_EXIST)
            So(len(slice), ShouldEqual, 1)
            So(slice[0], ShouldEqual, 4)
        })
    })
}

ApplyGlobalVar

ApplyGlobalVar 接口定义如下:

func ApplyGlobalVar(target, double interface{}) *Patches
func (this *Patches) ApplyGlobalVar(target, double interface{}) *Patches

ApplyGlobalVar 第一个参数是全局变量的地址,第二个参数是全局变量的桩。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。

全局变量打桩的习惯用法:

import (
    . "github.com/agiledragon/gomonkey"
    . "github.com/smartystreets/goconvey/convey"
    "testing"
)

var num = 10

func TestApplyGlobalVar(t *testing.T) {
    Convey("TestApplyGlobalVar", t, func() {

        Convey("change", func() {
            patches := ApplyGlobalVar(&num, 150)
            defer patches.Reset()
            So(num, ShouldEqual, 150)
        })

        Convey("recover", func() {
            So(num, ShouldEqual, 10)
        })
    })
}

ApplyFuncVar

ApplyFuncVar 接口定义如下:

func ApplyFuncVar(target, double interface{}) *Patches
func (this *Patches) ApplyFuncVar(target, double interface{}) *Patches

ApplyFuncVar 第一个参数是函数变量的地址,第二个参数是桩函数。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。

函数变量打桩的习惯用法:

import (
    . "github.com/agiledragon/gomonkey"
    . "github.com/smartystreets/goconvey/convey"
    "testing"
    "github.com/agiledragon/gomonkey/test/fake"
)


func TestApplyFuncVar(t *testing.T) {
    Convey("TestApplyFuncVar", t, func() {

        Convey("for succ", func() {
            str := "hello"
            patches := ApplyFuncVar(&fake.Marshal, func (_ interface{}) ([]byte, error) {
                return []byte(str), nil
            })
            defer patches.Reset()
            bytes, err := fake.Marshal(nil)
            So(err, ShouldEqual, nil)
            So(string(bytes), ShouldEqual, str)
        })

        Convey("for fail", func() {
            patches := ApplyFuncVar(&fake.Marshal, func (_ interface{}) ([]byte, error) {
                return nil, fake.ErrActual
            })
            defer patches.Reset()
            _, err := fake.Marshal(nil)
            So(err, ShouldEqual, fake.ErrActual)
        })
    })
}

ApplyFuncSeq

ApplyFuncSeq 接口定义如下:

func ApplyFuncSeq(target interface{}, outputs []OutputCell) *Patches
func (this *Patches) ApplyFuncSeq(target interface{}, outputs []OutputCell) *Patches

其中,OutputCell 的定义为:

type Params []interface{}
type OutputCell struct {
    Values Params
    Times  int
}

ApplyFuncSeq 第一个参数是函数名,第二个参数是特定的桩序列参数。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。

函数打桩序列的习惯用法:

import (
    . "github.com/agiledragon/gomonkey"
    . "github.com/smartystreets/goconvey/convey"
    "testing"
    "github.com/agiledragon/gomonkey/test/fake"
)

func TestApplyFuncSeq(t *testing.T) {
    Convey("TestApplyFuncSeq", t, func() {

        Convey("default times is 1", func() {
            info1 := "hello cpp"
            info2 := "hello golang"
            info3 := "hello gomonkey"
            outputs := []OutputCell{
                {Values: Params{info1, nil}},
                {Values: Params{info2, nil}},
                {Values: Params{info3, nil}},
            }
            patches := ApplyFuncSeq(fake.ReadLeaf, outputs)
            defer patches.Reset()
            output, err := fake.ReadLeaf("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info1)
            output, err = fake.ReadLeaf("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info2)
            output, err = fake.ReadLeaf("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info3)
        })

        Convey("retry succ util the third times", func() {
            info1 := "hello cpp"
            outputs := []OutputCell{
                {Values: Params{"", fake.ErrActual}, Times: 2},
                {Values: Params{info1, nil}},
            }
            patches := ApplyFuncSeq(fake.ReadLeaf, outputs)
            defer patches.Reset()
            output, err := fake.ReadLeaf("")
            So(err, ShouldEqual, fake.ErrActual)
            output, err = fake.ReadLeaf("")
            So(err, ShouldEqual, fake.ErrActual)
            output, err = fake.ReadLeaf("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info1)
        })

        Convey("batch operations failed on the third time", func() {
            info1 := "hello gomonkey"
            outputs := []OutputCell{
                {Values: Params{info1, nil}, Times: 2},
                {Values: Params{"", fake.ErrActual}},
            }
            patches := ApplyFuncSeq(fake.ReadLeaf, outputs)
            defer patches.Reset()
            output, err := fake.ReadLeaf("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info1)
            output, err = fake.ReadLeaf("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info1)
            output, err = fake.ReadLeaf("")
            So(err, ShouldEqual, fake.ErrActual)
        })

    })
}


ApplyMethodSeq

ApplyMethodSeq 接口定义如下:

func ApplyMethodSeq(target reflect.Type, methodName string, outputs []OutputCell) *Patches
func (this *Patches) ApplyMethodSeq(target reflect.Type, methodName string, outputs []OutputCell) *Patches

ApplyMethodSeq 第一个参数是目标类的指针变量的反射类型,第二个参数是字符串形式的方法名,第三参数是特定的桩序列参数。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。

成员方法打桩序列的习惯用法:

import (
    . "github.com/agiledragon/gomonkey"
    . "github.com/smartystreets/goconvey/convey"
    "testing"
    "github.com/agiledragon/gomonkey/test/fake"
    "reflect"
)

func TestApplyMethodSeq(t *testing.T) {
    e := &fake.Etcd{}
    Convey("TestApplyMethodSeq", t, func() {

        Convey("default times is 1", func() {
            info1 := "hello cpp"
            info2 := "hello golang"
            info3 := "hello gomonkey"
            outputs := []OutputCell{
                {Values: Params{info1, nil}},
                {Values: Params{info2, nil}},
                {Values: Params{info3, nil}},
            }
            patches := ApplyMethodSeq(reflect.TypeOf(e), "Retrieve", outputs)
            defer patches.Reset()
            output, err := e.Retrieve("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info1)
            output, err = e.Retrieve("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info2)
            output, err = e.Retrieve("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info3)
        })

        Convey("retry succ util the third times", func() {
            info1 := "hello cpp"
            outputs := []OutputCell{
                {Values: Params{"", fake.ErrActual}, Times: 2},
                {Values: Params{info1, nil}},
            }
            patches := ApplyMethodSeq(reflect.TypeOf(e), "Retrieve", outputs)
            defer patches.Reset()
            output, err := e.Retrieve("")
            So(err, ShouldEqual, fake.ErrActual)
            output, err = e.Retrieve("")
            So(err, ShouldEqual, fake.ErrActual)
            output, err = e.Retrieve("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info1)
        })

        Convey("batch operations failed on the third time", func() {
            info1 := "hello gomonkey"
            outputs := []OutputCell{
                {Values: Params{info1, nil}, Times: 2},
                {Values: Params{"", fake.ErrActual}},
            }
            patches := ApplyMethodSeq(reflect.TypeOf(e), "Retrieve", outputs)
            defer patches.Reset()
            output, err := e.Retrieve("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info1)
            output, err = e.Retrieve("")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, info1)
            output, err = e.Retrieve("")
            So(err, ShouldEqual, fake.ErrActual)
        })

    })
}


ApplyFuncVarSeq

ApplyFuncVarSeq 接口定义如下:

func ApplyFuncVarSeq(target interface{}, outputs []OutputCell) *Patches
func (this *Patches) ApplyFuncVarSeq(target interface{}, outputs []OutputCell) *Patches

ApplyFuncVarSeq 第一个参数是函数变量地址,第二个参数是特定的桩序列参数。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。

函数变量打桩序列的习惯用法:

import (
    . "github.com/agiledragon/gomonkey"
    . "github.com/smartystreets/goconvey/convey"
    "testing"
    "github.com/agiledragon/gomonkey/test/fake"
)

func TestApplyFuncVarSeq(t *testing.T) {
    Convey("TestApplyFuncVarSeq", t, func() {

        Convey("default times is 1", func() {
            info1 := "hello cpp"
            info2 := "hello golang"
            info3 := "hello gomonkey"
            outputs := []OutputCell{
                {Values: Params{[]byte(info1), nil}},
                {Values: Params{[]byte(info2), nil}},
                {Values: Params{[]byte(info3), nil}},
            }
            patches := ApplyFuncVarSeq(&fake.Marshal, outputs)
            defer patches.Reset()
            bytes, err := fake.Marshal("")
            So(err, ShouldEqual, nil)
            So(string(bytes), ShouldEqual, info1)
            bytes, err = fake.Marshal("")
            So(err, ShouldEqual, nil)
            So(string(bytes), ShouldEqual, info2)
            bytes, err = fake.Marshal("")
            So(err, ShouldEqual, nil)
            So(string(bytes), ShouldEqual, info3)
        })

        Convey("retry succ util the third times", func() {
            info1 := "hello cpp"
            outputs := []OutputCell{
                {Values: Params{[]byte(""), fake.ErrActual}, Times: 2},
                {Values: Params{[]byte(info1), nil}},
            }
            patches := ApplyFuncVarSeq(&fake.Marshal, outputs)
            defer patches.Reset()
            bytes, err := fake.Marshal("")
            So(err, ShouldEqual, fake.ErrActual)
            bytes, err = fake.Marshal("")
            So(err, ShouldEqual, fake.ErrActual)
            bytes, err = fake.Marshal("")
            So(err, ShouldEqual, nil)
            So(string(bytes), ShouldEqual, info1)
        })

        Convey("batch operations failed on the third time", func() {
            info1 := "hello gomonkey"
            outputs := []OutputCell{
                {Values: Params{[]byte(info1), nil}, Times: 2},
                {Values: Params{[]byte(""), fake.ErrActual}},
            }
            patches := ApplyFuncVarSeq(&fake.Marshal, outputs)
            defer patches.Reset()
            bytes, err := fake.Marshal("")
            So(err, ShouldEqual, nil)
            So(string(bytes), ShouldEqual, info1)
            bytes, err = fake.Marshal("")
            So(err, ShouldEqual, nil)
            So(string(bytes), ShouldEqual, info1)
            bytes, err = fake.Marshal("")
            So(err, ShouldEqual, fake.ErrActual)
        })

    })
}

NewPatches

NewPatches 接口定义如下:

func NewPatches() *Patches

NewPatches 是 patches 对象的显式构造函数,一般用于目标和桩的表驱动场景。测试完成后,patches 对象通过 Reset 成员方法删除所有测试桩。

NewPatches 的习惯用法:

import (
    . "github.com/agiledragon/gomonkey"
    . "github.com/smartystreets/goconvey/convey"
    "testing"
    "github.com/agiledragon/gomonkey/test/fake"
    "encoding/json"
)

func TestPatchPair(t *testing.T) {
   
    Convey("TestPatchPair", t, func() {
        
        Convey("TestPatchPair", func() {
            patchPairs := [][2]interface{} {
                {
                    fake.Exec,
                    func(_ string, _ ...string) (string, error) {
                        return outputExpect, nil
                    },
                },
                {
                    json.Unmarshal,
                    func(_ []byte, v interface{}) error {
                        p := v.(*map[int]int)
                        *p = make(map[int]int)
                        (*p)[1] = 2
                        (*p)[2] = 4
                        return nil
                    },
                },

            }
            patches := NewPatches()
            defer patches.Reset()
            for _, pair := range patchPairs {
                patches.ApplyFunc(pair[0], pair[1])
            }

            output, err := fake.Exec("", "")
            So(err, ShouldEqual, nil)
            So(output, ShouldEqual, outputExpect)

            var m map[int]int
            err = json.Unmarshal(nil, &m)
            So(err, ShouldEqual, nil)
            So(m[1], ShouldEqual, 2)
            So(m[2], ShouldEqual, 4)
        })
    })
}

注意事项

如果 gomonkey 打桩失败,请确认:

  • 打桩目标是否为内联的函数或成员方法?如果是,请在测试时通过命令行参数 -gcflags=-l (go1.10 版本之前)或-gcflags=all=-l(go1.10 版本及之后)关闭内联优化;
  • gomonkey 是否在 amd64 架构的处理器上运行?如果是,则需要开发相关的功能代码;
  • gomonkey 是否被多协程并发用于同一个目标的打桩?如果是,则需要将之前的协程先优雅退出;
  • go1.6 版本的反射机制支持私有成员方法的查询,而 go1.7 及之后的版本却不支持。然而,所有版本的反射机制都支持私有函数的查询,所以当用户使用 go1.7 及之后的版本时,gomonkey 对于私有成员方法的打桩将触发一个异常。

后续计划

  • 支持为一个 interface 打一个桩
  • 支持为一个 interface 打一个特定的桩序列
  • 在必要的情况下,支持 amd64 架构的其它处理器
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容