ToB企服应用市场:ToB评测及商务社交产业平台
标题:
Go 单位测试基本介绍
[打印本页]
作者:
九天猎人
时间:
2024-5-17 12:39
标题:
Go 单位测试基本介绍
目录
一、单位测试基本介绍
1.1 什么是单位测试?
1.2 怎样写好单位测试
1.3 单位测试的优点
1.4 单位测试的设计原则
二、Go语言测试
2.1 Go单位测试概要
2.2 Go单位测试基本规范
2.3 一个简单例子
2.3.1 使用Goland 生成测试文件
2.3.2 运行单位测试
2.3.3 完善测试用例
2.3.5 回归测试
2.4 Goland 直接运行单位测试
2.5 Go Test 命令参数
2.6 运行一个文件中的单个测试
2.7 测试覆盖率
2.8 公共的资助函数(helpers)
三、testing.T的拥有的方法
四、表格驱动测试
4.1 介绍
4.2 举个例子
五、testify/assert 断言工具包
5.1 介绍
5.2 安装
5.3 使用
六、单位测试代码模板
七、参考文档
一、单位测试基本介绍
1.1 什么是单位测试?
单位测试(Unit Tests, UT) 是一个良好项目不可或缺的一部分,是对软件中的最小可测试部分进行查抄和验证。在面向对象编程中,最小测试单位通常是一个方法或函数。单位测试通常由开发者编写,用于验证代码的一个很小的、很具体的功能是否精确。单位测试是自动化测试的一部分,可以频繁地运行以检测代码的更改是否引入了新的错误。
特殊是在一些频繁变动和多人合作开发的项目中尤为重要。你或多或少都会有因为自己的提交,导致应用挂掉或服务宕机的经历。假如这个时间你的修改导致测试用例失败,你再重新审视自己的修改,发现之前的修改还有一些特殊场景没有包含,恭喜你镌汰了一次上库失误。也会有这样的情况,项目很大,启动环境很复杂,你优化了一个函数的性能,或是添加了某个新的特性,假如部署在正式环境上之后再进行测试,本钱太高。对于这种场景,几个小小的测试用例大概就能够覆盖大部分的测试场景。而且在开发过程中,服从最高的莫过于所见即所得了,单位测试也能够资助你做到这一点,试想一下,
假如你一口气写完一千行代码,debug 的过程也不会轻松,假如在这个过程中,对于一些逻辑较为复杂的函数,同时添加一些测试用例,即时确保精确性,最后集成的时间,会是另外一番体验。
1.2 怎样写好单位测试
首先,学会写测试用例。好比怎样测试单个函数/方法;好比怎样做基准测试;好比怎样写出简洁精炼的测试代码;再好比遇到数据库访问等的方法调用时,怎样 mock。
然后,写可测试的代码。
高内聚,低耦合
是软件工程的原则,同样,对测试而言,函数/方法写法不同,测试难度也是不一样的。职责单一,参数类型简单,与其他函数耦合度低的函数往往更容易测试。我们常常会说,“这种代码没法测试”,这种时间,就得思考函数的写法可不可以改得更好一些。为了代码可测试而重构是值得的。
1.3 单位测试的优点
单位测试讲求的是快速测试、快速修复。
测试该环节中的业务问题,好比说在写测试的时间,发现业务流程设计得不合理。
测试该环节中的技能问题,好比说nil之类的问题。
单位测试,从理论上来说,你不能依赖任何第三方组件。也就是说,你不能使用MySQL或者Redis。
如图,要快速启动测试,快速发现BUG,快速修复,快速重测。
1.4 单位测试的设计原则
每个测试单位必须完全独立、能单独运行。
一个测试单位应只关注一个功能函数,证实它是精确的;
测试代码要能够快速执行。
不能为了单位测试而修改已完成的代码在编写代码后执行针对本次的单位测试,并执行之前的单位测试用例。
以包管你后来编写的代码不会粉碎任何事情;
单位测试函数使用长的而且具有描述性的名字,例如都以test_开头,然后加上具体的函数名字或者功能描述;例如:func_test.go。
测试代码必须具有可读性。
二、Go语言测试
2.1 Go单位测试概要
Go 语言的单位测试默认采用官方自带的测试框架,通过引入 testing 包以及 执行 go test 命令来实现单位测试功能。
在源代码包目录内,所有以 _test.go 为后缀名的源文件会被 go test 认定为单位测试的文件,这些单位测试的文件不会包含在 go build 的源代码构建中,而是单独通过 go test 来编译并执行。
2.2 Go单位测试基本规范
Go 单位测试的基本规范如下:
每个测试函数都必须导入 testing 包。测试函数的命名类似func TestName(t *testing.T),入参必须是 *testing.T
测试函数的函数名必须以大写的 Test 开头,后面紧跟的函数名,要么是大写开关,要么就是下划线,好比 func TestName(t *testing.T) 或者 func Test_name(t *testing.T) 都是 ok 的, 但是 func Testname(t *testing.T)不会被检测到
通常情况下,需要将测试文件和源代码放在同一个包内。一般测试文件的命名,都是 {source_filename}_test.go,好比我们的源代码文件是allen.go ,那么就会在 allen.go 的相同目录下,再创建一个 allen_test.go 的单位测试文件去测试 allen.go 文件里的相干方法。
当运行 go test 命令时,go test 会遍历所有的 *_test.go 中符合上述命名规则的函数,然后生成一个临时的 main 包用于调用相应的测试函数,然后构建并运行、报告测试结果,最后清理测试中生成的临时文件。
2.3 一个简单例子
2.3.1 使用Goland 生成测试文件
我们来创建一个示例,创建名为 add.go的文件
package main
func Add(a int, b int) int {
return a + b
}
func Mul(a int, b int) int {
return a * b
}
复制代码
这里借助Goland给 ADD 函数生成并且编写测试用例,只需要右键点击函数,转到Generate -> Test for file function(生成函数测试)。
Goland 为我们生成了add_test.go单测文件
package main
import "testing"
func TestAdd(t *testing.T) {
type args struct {
a int
b int
}
tests := []struct {
name string
args args
want int
}{
// TODO: Add test cases.
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("Add() = %v, want %v", got, tt.want)
}
})
}
}
复制代码
2.3.2 运行单位测试
运行 go test,该 package 下所有的测试用例都会被执行。
go test .
ok gotest 1.060s
复制代码
或 go test -v,-v 参数会显示每个用例的测试结果,另外 -cover 参数可以查看覆盖率。
go test -v
=== RUN TestAdd
--- PASS: TestAdd (0.00s)
PASS
ok gotest 1.208s
复制代码
2.3.3 完善测试用例
接着我们来完善上面的测试用例,代码如下:
package main
import "testing"
func TestAdd(t *testing.T) {
type args struct {
a int
b int
}
tests := []struct {
name string
args args
want int
}{
{
name: "Adding positive numbers",
args: args{a: 2, b: 3},
want: 5,
},
{
name: "Adding negative numbers",
args: args{a: -2, b: -3},
want: -5,
},
{
name: "Adding positive and negative numbers",
args: args{a: 2, b: -3},
want: -1,
},
{
name: "Adding zero",
args: args{a: 2, b: 0},
want: 2,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Add(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("Add() = %v, want %v", got, tt.want)
}
})
}
}
复制代码
2.3.5 回归测试
我们修改了代码之后仅仅执行那些失败的测试用例或新引入的测试用例是错误且危险的,精确的做法应该是完整运行所有的测试用例,包管不会因为修改代码而引入新的问题。
go test -v
=== RUN TestAdd
=== RUN TestAdd/Adding_positive_numbers
=== RUN TestAdd/Adding_negative_numbers
=== RUN TestAdd/Adding_positive_and_negative_numbers
=== RUN TestAdd/Adding_zero
--- PASS: TestAdd (0.00s)
--- PASS: TestAdd/Adding_positive_numbers (0.00s)
--- PASS: TestAdd/Adding_negative_numbers (0.00s)
--- PASS: TestAdd/Adding_positive_and_negative_numbers (0.00s)
--- PASS: TestAdd/Adding_zero (0.00s)
=== RUN TestMul
=== RUN TestMul/结果为0
=== RUN TestMul/结果为-1
=== RUN TestMul/结果为1
--- PASS: TestMul (0.00s)
--- PASS: TestMul/结果为0 (0.00s)
--- PASS: TestMul/结果为-1 (0.00s)
--- PASS: TestMul/结果为1 (0.00s)
PASS
ok gotest 0.912s
复制代码
测试结果表明我们的单位测试全部通过。
2.4 Goland 直接运行单位测试
假如你的测试方法署名没错的话,就能看到这个绿色图标,点击就能看到许多选项。
最重要的是:
Run:运行模式,直接运行整个测试。
Debug:Debug模式,你可以打断点。
Run xxx with Coverage:运行并且输出测试覆盖率。
其它Profile都是性能分析,很少用。
除非你要看测试覆盖率,不然都用Debug。
2.5 Go Test 命令参数
go test 是 Go 语言的测试工具,你可以使用它来运行 Go 程序的测试函数。
你可以在命令行中使用以下参数来调用 go test 命令:
-run:指定要运行的测试函数的名称的正则表达式。例如,使用 go test -run TestAdd 可以运行名称为 TestSum 的测试函数。
-bench:指定要运行的基准测试的名称的正则表达式。例如,使用 go test -bench . 可以运行所有基准测试。
-count:指定要运行测试函数或基准测试的次数。例如,使用 go test -count 2 可以运行测试函数或基准测试两次。
-v:输出测试函数或基准测试的详细输出。
-timeout:设置测试函数或基准测试的超时时间。例如,使用 go test -timeout 1s 可以将超时时间设置为 1 秒。
以下是一个go Test命令表格:
参数说明-bench regexp仅运行与正则表达式匹配的基准测试。默认不运行任何基准测试。使用 -bench . 或 -bench= 来运行所有基准测试。-benchtime t运行每个基准测试足够多的迭代,以达到指定的时间 t(例如 -benchtime 1h30s)。默认为1秒(1s)。特殊语法 Nx 表示运行基准测试 N 次(例如 -benchtime 100x)。-count n运行每个测试、基准测试和模糊测试 n 次(默认为1次)。假如设置了 -cpu,则为每个 GOMAXPROCS 值运行 n 次。示例总是运行一次。-count 不实用于通过 -fuzz 匹配的模糊测试。-cover启用覆盖率分析。-covermode set,count,atomic设置覆盖率分析的 mode。默认为 "set",假如启用了 -race,则为 "atomic"。-coverpkg pattern1,pattern2,pattern3对匹配模式的包应用覆盖率分析。默认情况下,每个测试仅分析正在测试的包。-cpu 1,2,4指定一系列的 GOMAXPROCS 值,在这些值上执行测试、基准测试或模糊测试。默认为当前的 GOMAXPROCS 值。-cpu 不实用于通过 -fuzz 匹配的模糊测试。-failfast在第一个测试失败后不启动新的测试。-fullpath在错误消息中显示完整的文件名。-fuzz regexp运行与正则表达式匹配的模糊测试。当指定时,命令行参数必须精确匹配主模块中的一个包,并且正则表达式必须精确匹配该包中的一个模糊测试。-fuzztime t在模糊测试期间运行足够多的模糊目的迭代,以达到指定的时间 t(例如 -fuzztime 1h30s)。默认为永远运行。特殊语法 Nx 表示运行模糊目的 N 次(例如 -fuzztime 1000x)。-fuzzminimizetime t在每次最小化尝试期间运行足够多的模糊目的迭代,以达到指定的时间 t(例如 -fuzzminimizetime 30s)。默认为60秒。特殊语法 Nx 表示运行模糊目的 N 次(例如 -fuzzminimizetime 100x)。-json以 JSON 格式记录详细输出和测试结果。这以机器可读的格式出现 -v 标志的相同信息。-list regexp列出与正则表达式匹配的测试、基准测试、模糊测试或示例。不会运行任何测试、基准测试、模糊测试或示例。-parallel n允许并行执行调用 t.Parallel 的测试函数,以及运行种子语料库时的模糊目的。此标志的值是同时运行的最大测试数。-run regexp仅运行与正则表达式匹配的测试、示例和模糊测试。-short告诉长时间运行的测试缩短其运行时间。默认情况下是关闭的,但在 all.bash 中设置,以便在安装 Go 树时可以运行健全性查抄,但不耗费时间运行详尽的测试。-shuffle off,on,N随机化测试和基准测试的执行顺序。默认情况下是关闭的。假如 -shuffle 设置为 on,则使用系统时钟种子随机化器。假如 -shuffle 设置为整数 N,则 N 将用作种子值。在这两种情况下,种子将报告以便复现。-skip regexp仅运行与正则表达式不匹配的测试、示例、模糊测试和基准测试。-timeout d假如测试二进制文件运行时间超过一连时间 d,则发生 panic。假如 d 为0,则禁用超时。默认为10分钟(10m)。-v详细输出:记录所有运行的测试。即使测试乐成,也打印所有来自 Log 和 Logf 调用的文本。-vet list设置在 "go test" 期间对 "go vet" 的调用,以使用由逗号分隔的 vet 查抄列表。假如列表为空,"go test" 使用被认为总是值得办理的精选查抄列表运行 "go vet"。假如列表为更多可以参考 Go 语言的
官方文档
或使用 go help test 命令查看资助信息
2.6 运行一个文件中的单个测试
假如只想运行其中的一个用例,例如 TestAdd,可以用 -run 参数指定,该参数支持通配符 *,和部分正则表达式,例如 ^、$。
go test -run TestAdd -v
=== RUN TestAdd
=== RUN TestAdd/Adding_positive_numbers
=== RUN TestAdd/Adding_negative_numbers
=== RUN TestAdd/Adding_positive_and_negative_numbers
=== RUN TestAdd/Adding_zero
--- PASS: TestAdd (0.00s)
--- PASS: TestAdd/Adding_positive_numbers (0.00s)
--- PASS: TestAdd/Adding_negative_numbers (0.00s)
--- PASS: TestAdd/Adding_positive_and_negative_numbers (0.00s)
--- PASS: TestAdd/Adding_zero (0.00s)
PASS
ok gotest 1.008s
复制代码
2.7 测试覆盖率
测试覆盖率是指代码被测试套件覆盖的百分比。通常我们使用的都是语句的覆盖率,也就是在测试中至少被运行一次的代码占总代码的比例。在公司内部一般会要求测试覆盖率达到80%左右。
Go提供内置功能来查抄你的代码覆盖率,即使用go test -cover来查看测试覆盖率。
go test -cover
PASS
coverage: 100.0% of statements
ok gotest 1.381s
复制代码
还可以使用 -coverprofile 标志将覆盖率数据输出到一个文件中,然后使用 go tool cover 命令来查看更详细的覆盖率报告。
2.8 公共的资助函数(helpers)
对一些重复的逻辑,抽取出来作为公共的资助函数(helpers),可以增加测试代码的可读性和可维护性。 借助资助函数,可以让测试用例的主逻辑看起来更清晰。
例如,我们可以将创建多次使用的逻辑抽取出来:
type addCase struct{ A, B, want int }
func createAddTestCase(t *testing.T, c *addCase) {
// t.Helper()
if ans := Add(c.A, c.B); ans != c.want {
t.Fatalf("%d * %d expected %d, but %d got",
c.A, c.B, c.want, ans)
}
}
func TestAdd2(t *testing.T) {
createAddTestCase(t, &addCase{1, 1, 2})
createAddTestCase(t, &addCase{2, -3, -1})
createAddTestCase(t, &addCase{0, -1, 0}) // wrong case
}
复制代码
在这里,我们故意创建了一个错误的测试用例,运行 go test,用例失败,会报告错误发生的文件和行号信息:
go test
--- FAIL: TestAdd2 (0.00s)
add_test.go:109: 0 * -1 expected 0, but -1 got
FAIL
exit status 1
FAIL gotest 1.090s
复制代码
可以看到,错误发生在第11行,也就是资助函数 createAddTestCase 内部。116, 117, 118行都调用了该方法,我们第一时间并不能够确定是哪一行发生了错误。有些资助函数还大概在不同的函数中被调用,报错信息都在同一处,不方便问题定位。因此,Go 语言在 1.9 版本中引入了 t.Helper(),用于标注该函数是资助函数,报错时将输出资助函数调用者的信息,而不是资助函数的内部信息。
修改 createAddTestCaseV1,调用 t.Helper()
type addCaseV1 struct {
name string
A, B int
want int
}
func createAddTestCaseV1(c *addCaseV1, t *testing.T) {
t.Helper()
t.Run(c.name, func(t *testing.T) {
if ans := Add(c.A, c.B); ans != c.want {
t.Fatalf("%s: %d + %d expected %d, but %d got",
c.name, c.A, c.B, c.want, ans)
}
})
}
func TestAddV1(t *testing.T) {
createAddTestCaseV1(&addCaseV1{"case 1", 1, 1, 2}, t)
createAddTestCaseV1(&addCaseV1{"case 2", 2, -3, -1}, t)
createAddTestCaseV1(&addCaseV1{"case 3", 0, -1, 0}, t)
}
复制代码
运行 go test,报错信息如下,可以非常清晰地知道,错误发生在第 131 行。
go test
--- FAIL: TestAddV1 (0.00s)
--- FAIL: TestAddV1/case_3 (0.00s)
add_test.go:131: case 3: 0 + -1 expected 0, but -1 got
FAIL
exit status 1
FAIL gotest 0.434s
复制代码
关于 helper 函数的 2 个发起:
不要返回错误, 资助函数内部直接使用 t.Error 或 t.Fatal 即可,在用例主逻辑中不会因为太多的错误处理代码,影响可读性。
调用 t.Helper() 让报错信息更准确,有助于定位。
固然,假如你是用Goland 编辑器的话,可以不使用t.Helper(),自动会帮你打印出错误详细信息
三、testing.T的拥有的方法
以下是提供的 *testing.T 类型的方法及其用途的注释:
// T 是 Go 语言测试框架中的一个结构体类型,它提供了用于编写测试的方法。
// 它通常通过测试函数的参数传递给测试函数。
// Cleanup 注册一个函数,该函数将在测试结束时执行,用于清理测试过程中创建的资源。
func (c *T) Cleanup(func())
// Error 记录一个错误信息,但不会立即停止测试的执行。
func (c *T) Error(args ...interface{})
// Errorf 根据 format 和 args 记录一个格式化的错误信息,但不会立即停止测试的执行。
func (c *T) Errorf(format string, args ...interface{})
// Fail 标记测试函数为失败,但不会停止当前测试的执行。
func (c *T) Fail()
// FailNow 标记测试函数为失败,并立即停止当前测试的执行。
func (c *T) FailNow()
// Failed 检查测试是否失败。
func (c *T) Failed() bool
// Fatal 记录一个错误信息,并立即停止测试的执行。
func (c *T) Fatal(args ...interface{})
// Fatalf 记录一个格式化的错误信息,并立即停止测试的执行。
func (c *T) Fatalf(format string, args ...interface{})
// Helper 标记当前函数为辅助函数,当测试失败时,辅助函数的文件名和行号将不会显示在错误消息中。
func (c *T) Helper()
// Log 记录一些信息,这些信息只有在启用详细日志(-v标志)时才会显示。
func (c *T) Log(args ...interface{})
// Logf 记录一些格式化的信息,这些信息只有在启用详细日志(-v标志)时才会显示。
func (c *T) Logf(format string, args ...interface{})
// Name 返回当前测试或基准测试的名称。
func (c *T) Name() string
// Skip 标记测试为跳过,并记录一个错误信息。
func (c *T) Skip(args ...interface{})
// SkipNow 标记测试为跳过,并立即停止当前测试的执行。
func (c *T) SkipNow()
// Skipf 标记测试为跳过,并记录一个格式化的错误信息。
func (c *T) Skipf(format string, args ...interface{})
// Skipped 检查测试是否被跳过。
func (c *T) Skipped() bool
// TempDir 返回一个临时目录的路径,该目录在测试结束时会被自动删除。
func (c *T) TempDir() string
复制代码
四、表格驱动测试
4.1 介绍
表格驱动测试不是工具、包或其他任何东西,它只是编写更清晰测试的一种方式和视角。
编写好的测试并非易事,但在许多情况下,表格驱动测试可以涵盖许多方面:表格里的每一个条目都是一个完整的测试用例,包含输入和预期结果,有时还包含测试名称等附加信息,以使测试输出易于阅读。
使用表格驱动测试能够很方便的维护多个测试用例,避免在编写单位测试时频繁的复制粘贴。
表格驱动测试的步骤通常是界说一个测试用例表格,然后遍历表格,并使用t.Run对每个条目执行必要的测试。
4.2 举个例子
func TestMul(t *testing.T) {
type args struct {
a int
b int
}
tests := []struct {
name string
args args
want int
}{
{
name: "结果为0",
args: args{a: 2, b: 0},
want: 0,
},
{
name: "结果为-1",
args: args{a: -1, b: 1},
want: -1,
},
{
name: "结果为1",
args: args{a: -1, b: -1},
want: 1,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := Mul(tt.args.a, tt.args.b); got != tt.want {
t.Errorf("Mul() = %v, want %v", got, tt.want)
}
})
}
}
复制代码
五、testify/assert 断言工具包
5.1 介绍
testify/assert 是一个流行的Go语言断言库,它提供了一组丰富的断言函数,用于简化测试代码的编写。这个库提供了一种更声明式的方式来编写测试,使得测试意图更加明确,代码更加简洁。
使用 testify/assert 时,您不再需要编写大量的 if 语句和 Error 方法调用来查抄条件和记录错误。相反,您可以使用像 assert.Equal、assert.Nil、assert.True 这样的断言函数来验证测试的期望结果。
5.2 安装
go get github.com/stretchr/testify
复制代码
5.3 使用
断言包提供了一些有用的方法,可以资助您在Go语言中编写更好的测试代码。
打印友好、易于阅读的失败描述
允许编写可读性强的代码
可以为每个断言添加可选的注释信息
看看它的实际应用:
package yours
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSomething(t *testing.T) {
// 断言相等
assert.Equal(t, 123, 123, "它们应该相等")
// 断言不等
assert.NotEqual(t, 123, 456, "它们不应该相等")
// 断言为nil(适用于错误处理)
assert.Nil(t, object)
// 断言不为nil(当你期望得到某个结果时使用)
if assert.NotNil(t, object) {
// 现在我们知道object不是nil,我们可以安全地进行
// 进一步的断言而不会引起任何错误
assert.Equal(t, "Something", object.Value)
}
}
复制代码
每个断言函数都担当 testing.T 对象作为第一个参数。这就是它怎样通过正常的Go测试能力输出错误信息的方式。
每个断言函数都返回一个布尔值,指示断言是否乐成。这对于在特定条件下继承进行进一步的断言非常有用。
当我们有多个断言语句时,还可以使用assert := assert.New(t)创建一个assert对象,它拥有前面所有的断言方法,只是不需要再传入Testing.T参数了。
package yours
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestSomething(t *testing.T) {
assert := assert.New(t)
// 断言相等
assert.Equal(123, 123, "它们应该相等")
// 断言不等
assert.NotEqual(123, 456, "它们不应该相等")
// 断言为nil(适用于错误处理)
assert.Nil(object)
// 断言不为nil(当你期望得到某个结果时使用)
if assert.NotNil(object) {
// 现在我们知道object不是nil,我们可以安全地进行
// 进一步的断言而不会引起任何错误
assert.Equal("Something", object.Value)
}
}
复制代码
在上面的示例中,assert.New(t) 创建了一个新的 assert 实例,然后您可以使用这个实例的方法来进行断言。假如断言失败,testify/assert 会自动标志测试为失败,并记录一个详细的错误消息。
六、单位测试代码模板
func Test_Function(t *testing.T) {
testCases := []struct {
name string //测试用例的名称
args any //测试用例的输入参数
want string //期望的返回值
}{
// 测试用例,测试用例表格
{},
{},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
//具体的测试代码
})
}
}
复制代码
七、参考文档
Go Test 单位测试简明教程
Go单位测试入门
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/)
Powered by Discuz! X3.4