go单测实战

1. 单元测试

  • 测试文件和目标文件一般放在一个包下,且测试文件命名有要求
    • hello.go:目标文件
    • hello_test.go:测试文件,需要以_test.go结尾
  • 测试函数必须以Test开头,后面加函数名
    • name:目标函数名
    • TestName:测试函数名
  • func TestAdd(t *testing.T){ ... }
    • 参数t用于报告测试失败和附加的日志信息。
    • testing.T的拥有的方法如下:
func (c *T) Cleanup(func())
func (c *T) Error(args ...interface{})
func (c *T) Errorf(format string, args ...interface{})
func (c *T) Fail()
func (c *T) FailNow()
func (c *T) Failed() bool
func (c *T) Fatal(args ...interface{})
func (c *T) Fatalf(format string, args ...interface{})
func (c *T) Helper()
func (c *T) Log(args ...interface{})
func (c *T) Logf(format string, args ...interface{})
func (c *T) Name() string
func (c *T) Skip(args ...interface{})
func (c *T) SkipNow()
func (c *T) Skipf(format string, args ...interface{})
func (c *T) Skipped() bool
func (c *T) TempDir() string

2. 一个简单的例子

目标文件

package simple_demo

import "strings"

func Split(s, sep string) (result []string) {
	i := strings.Index(s, sep)
	for i > -1 {
		result = append(result, s[:i])
		s = s[i+len(sep):]
		i = strings.Index(s, sep)
	}
	result = append(result, s)
	return
}

测试文件

package simple_demo

import (
	"reflect"
	"testing"
)

func TestSplit(t *testing.T) {
	got := Split("a:b:c", ":")         // 程序输出的结果
	want := []string{"a", "b", "c"}    // 期望的结果
	if !reflect.DeepEqual(want, got) { // 因为slice不能比较直接,借助反射包中的方法比较
		t.Errorf("expected:%v, got:%v", want, got) // 测试失败输出错误提示
	}
}

2.1 命令行下的一些命令

  • go test:将当前目录下的所有测试函数进行测试
  • go test -v:-v表示添加详细信息
  • go test -run=Split :只测试函数名包含Split的函数
  • go test -cover:查看测试覆盖率
  • go test -cover -coverprofile=c.out:将覆盖率输出到一个文件中
  • ``

2.2 子测试

func TestXXX(t *testing.T){
  t.Run("case1", func(t *testing.T){...})
  t.Run("case2", func(t *testing.T){...})
  t.Run("case3", func(t *testing.T){...})
}
  • 可以在一个函数中测试多个测试数据
  • 一般是通过切片 定义多个测试数据
package simple_demo

import (
	"reflect"
	"testing"
)

func TestSplitAll(t *testing.T) {
	tests := []struct {
		name  string
		input string
		sep   string
		want  []string
	}{
		{"base case", "a:b:c", ":", []string{"a", "b", "c"}},
		{"wrong sep", "a:b:c", ",", []string{"a:b:c"}},
		{"more sep", "abcd", "bc", []string{"a", "d"}},
		{"leading sep", "明日复明日,明日何其多", "明日", []string{"", "复", ",", "何其多"}},
	}
	// 遍历测试用例
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) { // 使用t.Run()执行子测试
			t.Parallel() // 将每个测试用例标记为能够彼此并行运行
			got := Split(tt.input, tt.sep)
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("expected:%#v, got:%#v", tt.want, got)
			}
		})
	}
}

t.Parallel():使得每次测试用例之间可以并行运行

3. 对网络的mock测试

3.1 使用httptest实现对内部API的mock

目标文件

package httptest_demo

import (
	"fmt"
	"net/http"

	"github.com/gin-gonic/gin"
)

// Param 请求参数
type Param struct {
	Name string `json:"name"`
}

// helloHandler /hello请求处理函数
func helloHandler(c *gin.Context) {
	var p Param
	if err := c.ShouldBindJSON(&p); err != nil {
		c.JSON(http.StatusOK, gin.H{
			"msg": "we need a name",
		})
		return
	}
	c.JSON(http.StatusOK, gin.H{
		"msg": fmt.Sprintf("hello %s", p.Name),
	})
}

// SetupRouter 路由
func SetupRouter() *gin.Engine {
	router := gin.Default()
	router.POST("/hello", helloHandler)
	return router
}

测试文件

package httptest_demo

import (
	"encoding/json"
	"net/http"
	"net/http/httptest"
	"strings"
	"testing"

	"github.com/stretchr/testify/assert"
)

func Test_helloHandler(t *testing.T) {
	// 定义两个测试用例
	tests := []struct {
		name   string
		param  string
		expect string
	}{
		{"base case", `{"name": "zhangsan"}`, "hello zhangsan"},
		{"bad case", "", "we need a name"},
	}

	r := SetupRouter()

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// mock一个HTTP请求
			req := httptest.NewRequest(
				"POST",                      // 请求方法
				"/hello",                    // 请求URL
				strings.NewReader(tt.param), // 请求参数
			)

			// mock一个响应记录器
			w := httptest.NewRecorder()

			// 让server端处理mock请求并记录返回的响应内容
			r.ServeHTTP(w, req)

			// 校验状态码是否符合预期
			assert.Equal(t, http.StatusOK, w.Code)

			// 解析并检验响应内容是否复合预期
			var resp map[string]string
			err := json.Unmarshal([]byte(w.Body.String()), &resp)
			assert.Nil(t, err)
			assert.Equal(t, tt.expect, resp["msg"])
		})
	}
}

3.2 使用gock对外部API的mock

目标文件

package gock_demo

import (
	"bytes"
	"encoding/json"
	"io/ioutil"
	"net/http"
)

type ReqParam struct {
	X int `json:"x"`
}

// Result API返回结果
type Result struct {
	Value int `json:"value"`
}

func GetResultByAPI(x, y int) int {
	p := &ReqParam{X: x}
	b, _ := json.Marshal(p)

	// 调用其他服务的API
	resp, err := http.Post(
		"http://your-api.com/post",
		"application/json",
		bytes.NewBuffer(b),
	)
	if err != nil {
		return -1
	}
	body, _ := ioutil.ReadAll(resp.Body)
	var ret Result
	if err := json.Unmarshal(body, &ret); err != nil {
		return -1
	}
	// 这里是对API返回的数据做一些逻辑处理
	return ret.Value + y
}

测试文件

package gock_demo

import (
	"testing"

	"github.com/stretchr/testify/assert"
	"gopkg.in/h2non/gock.v1"
)

func TestGetResultByAPI(t *testing.T) {
	defer gock.Off() // 测试执行后刷新挂起的mock

	// mock 请求外部api时传参x=1返回100
	gock.New("http://your-api.com").
		Post("/post").
		MatchType("json").
		JSON(map[string]int{"x": 1}).
		Reply(200).
		JSON(map[string]int{"value": 100})

	// 调用我们的业务函数
	res := GetResultByAPI(1, 1)
	// 校验返回结果是否符合预期
	assert.Equal(t, res, 101)

	// mock 请求外部api时传参x=2返回200
	gock.New("http://your-api.com").
		Post("/post").
		MatchType("json").
		JSON(map[string]int{"x": 2}).
		Reply(200).
		JSON(map[string]int{"value": 200})

	// 调用我们的业务函数
	res = GetResultByAPI(2, 2)
	// 校验返回结果是否符合预期
	assert.Equal(t, res, 202)

	assert.True(t, gock.IsDone()) // 断言mock被触发
}

4. 对数据库的mock测试

4.1 sqlmock实现对数据库访问的mock

目标文件

package main

import "database/sql"

// recordStats 记录用户浏览产品信息
func recordStats(db *sql.DB, userID, productID int64) (err error) {
	// 开启事务
	// 操作views和product_viewers两张表
	tx, err := db.Begin()
	if err != nil {
		return
	}

	defer func() {
		switch err {
		case nil:
			err = tx.Commit()
		default:
			tx.Rollback()
		}
	}()

	// 更新products表
	if _, err = tx.Exec("UPDATE products SET views = views + 1"); err != nil {
		return
	}
	// product_viewers表中插入一条数据
	if _, err = tx.Exec(
		"INSERT INTO product_viewers (user_id, product_id) VALUES (?, ?)",
		userID, productID); err != nil {
		return
	}
	return
}

func main() {
	// 注意:测试的过程中并不需要真正的连接
	db, err := sql.Open("mysql", "root@/blog")
	if err != nil {
		panic(err)
	}
	defer db.Close()
	// userID为1的用户浏览了productID为5的产品
	if err = recordStats(db, 1 /*some user id*/, 5 /*some product id*/); err != nil {
		panic(err)
	}
}

测试文件

package main

import (
	"fmt"
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
)

// TestShouldUpdateStats sql执行成功的测试用例
func TestShouldUpdateStats(t *testing.T) {
	// mock一个*sql.DB对象,不需要连接真实的数据库
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
	}
	defer db.Close()

	// mock执行指定SQL语句时的返回结果
	mock.ExpectBegin()
	mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectExec("INSERT INTO product_viewers").WithArgs(2, 3).WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectCommit()

	// 将mock的DB对象传入我们的函数中
	if err = recordStats(db, 2, 3); err != nil {
		t.Errorf("error was not expected while updating stats: %s", err)
	}

	// 确保期望的结果都满足
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("there were unfulfilled expectations: %s", err)
	}
}

// TestShouldRollbackStatUpdatesOnFailure sql执行失败回滚的测试用例
func TestShouldRollbackStatUpdatesOnFailure(t *testing.T) {
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("an error '%s' was not expected when opening a stub database connection", err)
	}
	defer db.Close()

	mock.ExpectBegin()
	mock.ExpectExec("UPDATE products").WillReturnResult(sqlmock.NewResult(1, 1))
	mock.ExpectExec("INSERT INTO product_viewers").
		WithArgs(2, 3).
		WillReturnError(fmt.Errorf("some error"))
	mock.ExpectRollback()

	// now we execute our method
	if err = recordStats(db, 2, 3); err == nil {
		t.Errorf("was expecting an error, but there was none")
	}

	// we make sure that all expectations were met
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("there were unfulfilled expectations: %s", err)
	}
}

4.2 miniredis实现对redis的mock

目标文件

package miniredis_demo

import (
	"context"
	"github.com/go-redis/redis/v8" // 注意导入版本
	"strings"
	"time"
)

const (
	KeyValidWebsite = "app:valid:website:list"
)

func DoSomethingWithRedis(rdb *redis.Client, key string) bool {
	// 这里可以是对redis操作的一些逻辑
	ctx := context.TODO()
	if !rdb.SIsMember(ctx, KeyValidWebsite, key).Val() {
		return false
	}
	val, err := rdb.Get(ctx, key).Result()
	if err != nil {
		return false
	}
	if !strings.HasPrefix(val, "https://") {
		val = "https://" + val
	}
	// 设置 blog key 五秒过期
	if err := rdb.Set(ctx, "blog", val, 5*time.Second).Err(); err != nil {
		return false
	}
	return true
}

测试文件


package miniredis_demo

import (
	"github.com/alicebob/miniredis/v2"
	"github.com/go-redis/redis/v8"
	"testing"
	"time"
)

func TestDoSomethingWithRedis(t *testing.T) {
	// mock一个redis server
	s, err := miniredis.Run()
	if err != nil {
		panic(err)
	}
	defer s.Close()

	// 准备数据
	s.Set("q1mi", "liwenzhou.com")
	s.SAdd(KeyValidWebsite, "q1mi")

	// 连接mock的redis server
	rdb := redis.NewClient(&redis.Options{
		Addr: s.Addr(), // mock redis server的地址
	})

	// 调用函数
	ok := DoSomethingWithRedis(rdb, "q1mi")
	if !ok {
		t.Fatal()
	}

	// 可以手动检查redis中的值是否复合预期
	if got, err := s.Get("blog"); err != nil || got != "https://liwenzhou.com" {
		t.Fatalf("'blog' has the wrong value")
	}
	// 也可以使用帮助工具检查
	s.CheckGet(t, "blog", "https://liwenzhou.com")

	// 过期检查
	s.FastForward(5 * time.Second) // 快进5秒
	if s.Exists("blog") {
		t.Fatal("'blog' should not have existed anymore")
	}
}

5. 常用的测试框架

5.1 Go Convey

  • GoConvey是一款针对Golang的测试框架,可以管理和运行测试用例
  • 同时提供了丰富的断言函数,并支持很多 Web 界面特性

目标文件

package convey_demo

func StringSliceEqual(a, b []string) bool {
	if len(a) != len(b) {
		return false
	}

	if (a == nil) != (b == nil) {
		return false
	}

	for i, v := range a {
		if v != b[i] {
			return false
		}
	}
	return true
}

测试文件

package convey_demo

import (
	"testing"

	. "github.com/smartystreets/goconvey/convey"
)

func TestStringSliceEqual(t *testing.T) {
	Convey("TestStringSliceEqual", t, func() {
		Convey("should return true when a != nil  && b != nil", func() {
			a := []string{"hello", "goconvey"}
			b := []string{"hello", "goconvey"}
			So(StringSliceEqual(a, b), ShouldBeTrue)
		})

		Convey("should return true when a == nil  && b == nil", func() {
			So(StringSliceEqual(nil, nil), ShouldBeTrue)
		})

		Convey("should return false when a == nil  && b != nil", func() {
			a := []string(nil)
			b := []string{}
			So(StringSliceEqual(a, b), ShouldBeFalse)
		})

		Convey("should return false when a != nil  && b != nil", func() {
			a := []string{"hello", "world"}
			b := []string{"hello", "goconvey"}
			So(StringSliceEqual(a, b), ShouldBeFalse)
		})
	})
}
  • Convey函数
    • 参数一:对测试函数的描述
    • 参数二:测试函数的入参
    • 参数三:func()类型的函数(无参无返回),习惯使用闭包
  • Convey函数可以嵌套
    • 嵌套内部的Convey函数不用再传入参数二
  • So函数
    • 作用:断言
    • 参数一:测试的函数执行结果
    • 参数二:预期的结果

5.2 Go Stub

  • 主要是用来对全局变量进行打桩
  • 也可以对函数与过程进行打桩
import (
   "testing"
   // 需要导入的包
   . "github.com/smartystreets/goconvey/convey"
)

// stubs对象的复用
func TestFuncDemo(t *testing.T) {
    Convey("TestFuncDemo", t, func() {
        Convey("for succ", func() {
        	// 对变量num打桩
            stubs := Stub(&num, 150)
            defer stubs.Reset()
            // 对函数Exec打桩
            stubs.StubFunc(&Exec,"xxx-vethName100-yyy", nil)
            var liLei = `{"name":"LiLei", "age":"21"}`
            stubs.StubFunc(&adapter.Marshal, []byte(liLei), nil)
            // 对过程DestroyResource进行打桩 
            stubs.StubFunc(&DestroyResource)
            //several So assert
        })
    })
}

5.3 Go Mockito

  • 主要用来对接口进行mock
package main

import (
   . "code.byted.org/luoshiqi/mockito"
   "fmt"
   "github.com/smartystreets/goconvey/convey"
   "testing"
)

func Fun(a string) string {
   return a
}

func TestFun(t *testing.T) {
   PatchConvey("test mock sample func", t, func() {
      funMocker := Mock(Fun).Return("c").Build()
      PatchConvey("test return", func() {
         r := Fun("a")
         convey.So(r, convey.ShouldEqual, "c")
         funMocker.Return("d")
         r = Fun("a")
         convey.So(r, convey.ShouldEqual, "d")
         t.Logf("mock函数调用次数: %+v", funMocker.Times())
         t.Log("开始解除mock func")
         funMocker.UnPatch()
         r = Fun("a")
         convey.So(r, convey.ShouldEqual, "a")
      })
   })
}

type Class struct {
}

func (*Class) FunA(a string) string {
   return a
}

func TestFun(t *testing.T) {
   PatchConvey("test mock sample func", t, func() {
      PatchConvey("test mock class func", func() {
         m := Mock((*Class).FunA).Return("c").Build()
         c := Class{}
         r := c.FunA("a")
         convey.So(r, convey.ShouldEqual, "c")
         t.Logf("mock函数调用次数: %+v", m.Times())
      })
   })
}

Mock(target interface{}) *MockBuilder:指明要mock的函数

func Fun(a string) string {
   fmt.Println(a)
   return a
}

type Class struct {
}

func (*Class) FunA(a string) string {
   fmt.Println(a)
   return a
}
func TestMock(t *testing.T) {
    Mock(Fun)                //对于普通函数使用这种
    Mock((*Class).FunA)      //对于class使用这种方式
}

When(when interface{}) *MockBuilder:什么情况下需要被mock

  • when指向一个函数,该函数的参数需要跟要被mock的参数一致
func TestMock(t *testing.T) {
    //对于普通函数使用这种
    Mock(Fun).When(func(p string) bool { return p == "a" })                
    //对于class使用这种方式
    Mock((*Class).FunA).When(func(self *Class, p string) bool { return p == "a" })     
}

Return(results ...interface{}) *MockBuilder:规定mock后的函数返回值

Mock(Fun).Return("c")

To(hook interface{}) *MockBuilder:将被mock的函数替换成函数hook

func Fun(a string) string {
   fmt.Println(a)
   return a
}

mock := func(p string) string {
   fmt.Println("b")
   return "b"
}
Mock(Fun).To(mock)

Origin(funcPtr interface{}) *MockBuilder:获取被mock的原函数

func Fun(a string) string {
   fmt.Println(a)
   return a
}

func TestMock(t *testing.T) {
    origin := Fun
    mock := func(p string) string {
       fmt.Println("b")
       origin(p)
       return "b"
    }
    mocker := Mock(Fun).To(mock).Origin(&origin).Build()
}

Build():执行mock操作

  • 返回值:Mocker对象,可以统计mock的次数
PatchConvey("test return", func() {
   mock3 := Mock(Fun).When(func(p string) bool { return p == "a" }).Return("c").Build()
   r := Fun("c")
   convey.So(r, convey.ShouldEqual, "a")
   convey.So(mock3.Times(), convey.ShouldEqual, 1)
   convey.So(mock3.MockTimes(), convey.ShouldEqual, 0)
})

UnPatch() *Mocker:取消mock代理

func TestResetPatch(t *testing.T)  {
   PatchConvey("test mock", t, func() {
      PatchConvey("test to", func() {
         origin := Fun
         mockFun := func(p string) string {
            fmt.Println("b")
            origin(p)
            return "b"
         }
         mocker := Mock(Fun).When(func(p string) bool { return p == "a" }).To(mockFun).Origin(&origin).Build()
         r := Fun("a")
         convey.So(r, convey.ShouldEqual, "b")
         convey.So(mocker.Times(), convey.ShouldEqual, 1)

         PatchConvey("test reset when", func() {
            mocker.When(func(p string) bool { return p == "b" })
            r := Fun("a")
            convey.So(r, convey.ShouldEqual, "a")
            convey.So(mocker.MockTimes(), convey.ShouldEqual, 0)
         })

         PatchConvey("test reset return", func() {
            mocker.Return("c")
            r := Fun("a")
            convey.So(r, convey.ShouldEqual, "c")
            convey.So(mocker.MockTimes(), convey.ShouldEqual, 1)
         })

         PatchConvey("test reset to and origin", func() {
            origin2 := Fun
            mockFun2 := func(p string) string {
               fmt.Println("d")
               return origin2("d") + p
            }
            mocker.To(mockFun2).Origin(&origin2)
            r := Fun("a")
            convey.So(r, convey.ShouldEqual, "da")
            convey.So(mocker.MockTimes(), convey.ShouldEqual, 1)
         })
      })
   })

}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Go语言实战是一本由CSDN出版社出版的Go语言教程书籍。这本书的主要目的是帮助读者快速入门和掌握Go语言的使用技巧,以便能够在实际项目中应用。 这本书的内容从Go语言的基础知识开始讲解,包括Go语言的语法、变量、常量、数据类型、控制流程等基本概念。随后,书中介绍了Go语言的并发编程,包括使用goroutine和channel来实现并发和并行编程。并发编程是Go语言的重要特性之一,可以大大提高程序的性能。 此外,书中还介绍了Go语言的网络编程,包括使用net包来实现TCP和UDP通信,以及使用http包来编写HTTP服务器和客户端程序。网络编程是现代软件开发中不可缺少的一部分,掌握这些知识可以帮助读者构建高效的网络应用。 书中还涵盖了Go语言的错误处理、试和性能优化等内容。错误处理是编写健壮程序的重要环节,试是保证程序质量的关键步骤,而性能优化则可以提升程序的执行效率。 总之,Go语言实战是一本较为全面的Go语言教程书籍,内容涵盖了Go语言的基础知识和实际应用技巧,适合读者从入门到进阶。通过学习这本书,读者可以快速掌握Go语言的核心概念和编程技巧,并能够使用Go语言开发高质量、高性能的软件应用。 ### 回答2: Go语言实战是一本由阮一峰编写的Go语言入门教程,通过该教程可以学习并掌握Go语言的基础知识和实战技巧。该教程在CSDN上有相应的推广和宣传,让更多的开发者了解和学习这门新兴的编程语言。 Go语言实战的特点之一是内容简洁明了,讲解方式通俗易懂,适合初学者入门。全书共分为14个章节,从Go语言的基础语法、数据类型、函数、控制结构等方面进行了详细讲解。每一章节都配有大量的示例代码和实战案例,读者可以通过实践来加深对概念和知识点的理解。 该教程同时也涵盖了一些高级的主题,例如并发编程、网络编程、数据库操作等,通过实际案例的演示,帮助读者深入理解和应用这些复杂的概念和技术。同时,书中还介绍了一些常用的开源库和工具,如Gin、Beego等,帮助读者提高开发效率。 此外,Go语言实战还针对实际项目的开发经验进行了分享,并提供了一些常见问题的解决方案。通过学习这些实战经验,读者可以更好地理解和应用Go语言开发实践中的一些技巧和规范。 总之,Go语言实战是一本很好的Go语言学习教程,适合想要入门这门编程语言的开发者。通过学习该教程,读者不仅可以掌握Go语言的基本概念和语法,还可以获得一些实际项目开发中的经验和技巧,为今后的Go语言开发之路打下坚实的基础。 ### 回答3: Go语言实战CSDN 是一本由CSDN社区出版的针对Go语言的实战教程。这本书主要围绕Go语言的实际应用展开,涵盖了Go语言的基础知识、语法和常用库的使用等内容。 首先,Go语言是一门开源的静态强类型编程语言,它具有简洁的语法、高效的并发模型以及出色的性能。因此,Go语言逐渐成为云计算、网络编程和分布式系统等领域的热门选择。通过该书的学习,读者可以全面掌握Go语言的各种特性和用法,为日后的开发工作打下坚实的基础。 该书按照实战的思路编排内容,结合大量的代码示例和案例分析,帮助读者逐步了解Go语言的核心概念和基本语法。同时,书中还讲解了Go语言的项目组织结构、依赖管理和试等方面的知识,帮助读者构建可维护、高效的Go语言项目。 此外,该书还涵盖了Go语言在Web开发、数据库操作、网络编程、并发编程和微服务等方面的实战应用。通过实际案例的讲解,读者可以更加深入地理解和掌握Go语言在不同场景下的灵活运用。 总而言之,Go语言实战CSDN 是一本全面系统的Go语言实战教程,适合希望学习和应用Go语言的开发者。通过阅读本书,读者可以获得丰富的实践经验和解决问题的能力,提升自己的Go语言开发水平。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值