一个测试工程师走进一家酒吧……,阿里软件测试开发手册

先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7

深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年最新软件测试全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
img
img
img
img
img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上软件测试知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新

如果你需要这些资料,可以添加V获取:vip1024b (备注软件测试)
img

正文

一个测试工程师走进一家酒吧,要了-1 杯啤酒;

一个测试工程师走进一家酒吧,要了 2^32 杯啤酒;

一个测试工程师走进一家酒吧,要了一杯洗脚水;

一个测试工程师走进一家酒吧,要了一杯蜥蜴;

一个测试工程师走进一家酒吧,要了一份 asdfQwer@24dg!&*(@;

一个测试工程师走进一家酒吧,什么也没要;

一个测试工程师走进一家酒吧,又走出去又从窗户进来又从后门出去从下水道钻进来;

一个测试工程师走进一家酒吧,又走出去又进来又出去又进来又出去,最后在外面把老板打了一顿;

一个测试工程师走进一家酒吧,要了一杯烫烫烫的锟斤拷;

一个测试工程师走进一家酒吧,要了 NaN 杯 Null;

一个测试工程师冲进一家酒吧,要了 500T 啤酒咖啡洗脚水野猫狼牙棒奶茶;

一个测试工程师把酒吧拆了;

一个测试工程师化装成老板走进一家酒吧,要了 500 杯啤酒并且不付钱;

一万个测试工程师在酒吧门外呼啸而过;

一个测试工程师走进一家酒吧,要了一杯啤酒’;DROP TABLE 酒吧;

测试工程师们满意地离开了酒吧。

然后一名顾客点了一份炒饭,酒吧炸了。

上面是网上流行的一个关于测试的笑话,其主要核心思想是——你永远无法把所有问题都充分测试。

在软件工程中,测试是极其重要的一环,比重通常可以与编码相同,甚至大大超过。那么在 Golang 里,怎么样把测试写好,写正确?本文将对这个问题做一些简单的介绍。 当前文章将主要分两个部分:

  • Golang 测试的一些基本写法和工具

  • 如何写“正确”的测试,这个部分虽然代码是用 golang 编写,但是其核心思想不限语言

由于篇幅问题,本文将不涉及性能测试,之后会另起一篇来谈。

为什么要写测试


我们举个不太恰当的例子,测试也是代码,我们假定写代码时出现 bug 的概率是 p(0<p<1),那么我们同时写测试的话,两边同时出现 bug 的概率就是(我们认为两个事件相互独立)

P(代码出现 bug) * P(测试出现 Bug) = p^2 < p

例如 p 是 1%的话,那么同时写出现 bug 的概率就只有 0.01%了。

测试同样也是代码,有可能也写出 bug,那么怎么保证测试的正确性呢?给测试也写测试?给测试的测试继续写测试?

我们定义 t(0)为原始的代码,任意的 i,i > 0,t(i+1)为对于 t(i)的测试,t(i+1)正确为 t(i)正确的必要条件,那么对所有的 i,i>0,t(i)正确都是 t(0)正确的必要条件。。。

测试的种类


测试的种类有非常多,我们这里只挑几个对一般开发者来说比较重要的测试,做简略的说明。

白盒测试、黑盒测试

首先是从测试方法上可以分为白盒测试和黑盒测试(当然还存在所谓的灰盒测试,这里不讨论)

  • 白盒测试 (White-box testing):白盒测试又称透明盒测试、结构测试等,软件测试的主要方法之一,也称结构测试、逻辑驱动测试或基于程序本身的测试。测试应用程序的内部结构或运作,而不是测试应用程序的功能。在白盒测试时,以编程语言的角度来设计测试案例。测试者输入数据验证数据流在程序中的流动路径,并确定适当的输出,类似测试电路中的节点。

  • 黑盒测试 (Black-box testing):黑盒测试,软件测试的主要方法之一,也可以称为功能测试、数据驱动测试或基于规格说明的测试。测试者不了解程序的内部情况,不需具备应用程序的代码、内部结构和编程语言的专门知识。只知道程序的输入、输出和系统的功能,这是从用户的角度针对软件界面、功能及外部结构进行测试,而不考虑程序内部逻辑结构。

我们写的单元测试一般属于白盒测试,因为我们对测试对象的内部逻辑有着充分了解。

单元测试、集成测试

从测试的维度上,又可以分为单元测试和集成测试:

  • 在计算机编程中,单元测试又称为模块测试,是针对程序模块来进行正确性检验的测试工作。程序单元是应用的最小可测试部件。在过程化编程中,一个单元就是单个程序、函数、过程等;对于面向对象编程,最小单元就是方法,包括基类、抽象类、或者派生类中的方法。

  • 整合测试又称组装测试,即对程序模块采用一次性或增值方式组装起来,对系统的接口进行正确性检验的测试工作。整合测试一般在单元测试之后、系统测试之前进行。实践表明,有时模块虽然可以单独工作,但是并不能保证组装起来也可以同时工作。

单元测试可以是黑盒测试,集成测试亦可以是白盒测试

回归测试

  • 回归测试是软件测试的一种,旨在检验软件原有功能在修改后是否保持完整。

回归测试主要是希望维持软件的不变性,我们举一个例子来说明。例如我们发现软件在运行的过程中出现了问题,在 gitlab 上开启了一个 issue。之后我们并且定位到了问题,我们可以先写一个测试(测试的名称可以带上 issue 的 ID)来复现问题(该版本代码运行此测试结果失败)。之后我们修复问题后,再次运行测试,测试的结果应当成功。那么我们之后每次运行测试的时候,通过运行这个测试,可以保证同样的问题不会复现。

一个基本的测试


我们先来看一个 Golang 的代码:

// add.go

package add

func Add(a, b int) int {

return a + b

}

一个测试用例可以写成:

// add_test.go

package add

import (

“testing”

)

func TestAdd(t *testing.T) {

res := Add(1, 2)

if res != 3 {

t.Errorf(“the result is %d instead of 3”, res)

}

}

在命令行我们使用 go test

go test

这个时候 go 会执行该目录下所有的以_test.go 为后缀中的测试,测试成功的话会有如下输出:

% go test

PASS

ok      code.byted.org/ek/demo_test/t01_basic/correct       0.015s

假设这个时候我们把 Add 函数修改成错误的实现

// add.go

package add

func Add(a, b int) int {

return a - b

}

再次执行测试命令

% go test

— FAIL: TestAddWrong (0.00s)

add_test.go:11: the result is -1 instead of 3

FAIL

exit status 1

FAIL    code.byted.org/ek/demo_test/t01_basic/wrong 0.006s

会发现测试失败。

只执行一个测试文件

那么如果我们想只测试这一个文件,输入

go test add_test.go

会发现命令行输出

% go test add_test.go

# command-line-arguments [command-line-arguments.test]

./add_test.go:9:9: undefined: Add

FAIL    command-line-arguments [build failed]

FAIL

这是因为我们没有附带测试对象的代码,修改测试后可以获得正确的输出:

% go test add_test.go add.go

ok      command-line-arguments  0.007s

测试的几种书写方式


子测试

通常来说我们测试某个函数和方法,可能需要测试很多不同的 case 或者边际条件,例如我们为上面的 Add 函数写两个测试,可以写成:

// add_test.go

package add

import (

“testing”

)

func TestAdd(t *testing.T) {

res := Add(1, 0)

if res != 1 {

t.Errorf(“the result is %d instead of 1”, res)

}

}

func TestAdd2(t *testing.T) {

res := Add(0, 1)

if res != 1 {

t.Errorf(“the result is %d instead of 1”, res)

}

}

测试的结果:(使用-v 可以获得更多输出)

% go test -v

=== RUN   TestAdd

— PASS: TestAdd (0.00s)

=== RUN   TestAdd2

— PASS: TestAdd2 (0.00s)

PASS

ok      code.byted.org/ek/demo_test/t02_subtest/non_subtest     0.007s

另一种写法是写成子测试的形式

// add_test.go

package add

import (

“testing”

)

func TestAdd(t *testing.T) {

t.Run(“test1”, func(t *testing.T) {

res := Add(1, 0)

if res != 1 {

t.Errorf(“the result is %d instead of 1”, res)

}

})

t.Run(“”, func(t *testing.T) {

res := Add(0, 1)

if res != 1 {

t.Errorf(“the result is %d instead of 1”, res)

}

})

}

执行结果:

% go test -v

=== RUN   TestAdd

=== RUN   TestAdd/test1

=== RUN   TestAdd/#00

— PASS: TestAdd (0.00s)

— PASS: TestAdd/test1 (0.00s)

— PASS: TestAdd/#00 (0.00s)

PASS

ok      code.byted.org/ek/demo_test/t02_subtest/subtest 0.007s

可以看到输出中会将测试按照嵌套的结构分类,子测试的嵌套没有层数限制,如果不写测试名的话,会自动按照顺序给予序号作为其测试名(例如上面的#00)

对 IDE(Goland)友好的子测试

有一种测试的写法是:

tcList := map[string][]int{

“t1”: {1, 2, 3},

“t2”: {4, 5, 9},

}

for name, tc := range tcList {

t.Run(name, func(t *testing.T) {

require.Equal(t, tc[2], Add(tc[0], tc[1]))

})

}

看上去没什么问题,然而有一个缺点是,这个测试对 IDE 并不友好:

我们无法在出错的时候对单个测试重新执行 所以推荐尽可能对每个 t.Run 都要独立书写,例如:

f := func(a, b, exp int) func(t *testing.T) {

return func(t *testing.T) {

require.Equal(t, exp, Add(a, b))

}

}

t.Run(“t1”, f(1, 2, 3))

t.Run(“t2”, f(4, 5, 9))

测试分包

我们上面的 add.go 和 add_test.go 文件都处于同一个目录下,顶部的 package 名称都是 add,那么在写测试的过程中,也可以为测试启用与非测试文件不同的包名,例如我们现在将测试文件的包名改为 add_test:

// add_test.go

package add_test

import (

“testing”

)

func TestAdd(t *testing.T) {

res := Add(1, 2)

if res != 3 {

t.Errorf(“the result is %d instead of 3”, res)

}

}

这个时候执行 go test 会发现

% go test

# code.byted.org/ek/demo_test/t03_diffpkg_test [code.byted.org/ek/demo_test/t03_diffpkg.test]

./add_test.go:9:9: undefined: Add

FAIL    code.byted.org/ek/demo_test/t03_diffpkg [build failed]

由于包名变化了,我们无法再访问到 Add 函数,这个时候我们增加 import 即可:

// add_test.go

package add_test

import (

“testing”

. “code.byted.org/ek/demo_test/t03_diffpkg”

)

func TestAdd(t *testing.T) {

res := Add(1, 2)

if res != 3 {

t.Errorf(“the result is %d instead of 3”, res)

}

}

我们使用上面的方式来导入包内的函数即可。 但使用了这种方式后,将无法访问包内未导出的函数(以小写开头的)。

测试的工具库


github.com/stretchr/testify

我们可以使用强大的 testify 来方便我们写测试 例如上面的测试我们可以用这个库写成:

// add_test.go

package correct

import (

“testing”

“github.com/stretchr/testify/require”

)

func TestAdd(t *testing.T) {

res := Add(1, 2)

require.Equal(t, 3, res)

/*

must := require.New(t)

res := Add(1, 2)

must.Equal(3, res)

*/

}

如果执行失败,则会在命令行看到如下输出:

% go test

ok      code.byted.org/ek/demo_test/t04_libraries/testify/correct       0.008s

— FAIL: TestAdd (0.00s)

add_test.go:12:

Error Trace:    add_test.go:12

Error:          Not equal:

expected: 3

actual  : -1

Test:           TestAdd

FAIL

FAIL    code.byted.org/ek/demo_test/t04_libraries/testify/wrong 0.009s

FAIL

库提供了格式化的错误详情(堆栈、错误值、期望值等)来方便我们调试。

github.com/DATA-DOG/go-sqlmock

对于需要测试 sql 的地方可以使用 go-sqlmock 来测试

  • 优点:不需要依赖数据库

  • 缺点:脱离了数据库的具体实现,所以需要写比较复杂的测试代码

github.com/golang/mock

强大的对 interface 的 mock 库,例如我们要测试函数 ioutil.ReadAll

func ReadAll(r io.Reader) ([]byte, error)

我们 mock 一个 io.Reader

// package: 输出包名

// destination: 输出文件

// io: mock对象的包

// Reader: mock对象的interface名

mockgen -package gomock -destination mock_test.go io Reader

可以在目录下看到 mock_test.go 文件里,包含了一个 io.Reader 的 mock 实现 我们可以使用这个实现去测试 ioutil.Reader,例如

ctrl := gomock.NewController(t)

defer ctrl.Finish()

m := NewMockReader(ctrl)

m.EXPECT().Read(gomock.Any()).Return(0, errors.New(“error”))

_, err := ioutil.ReadAll(m)

require.Error(t, err)

net/http/httptest

通常我们测试服务端代码的时候,会先启动服务,再启动测试。官方的 httptest 包给我们提供了一种方便地启动一个服务实例来测试的方法。

其他

其他一些测试工具可以前往 awesome-go#testing 查找

  • https://github.com/avelino/awesome-go#testing

如何写好测试


上面介绍了测试的基本工具和写法,我们已经完成了“必先利其器”,下面我们将介绍如何“善其事”。

并发测试

在平时,大家写服务的时候,基本都必须考虑并发,我们使用 IDE 测试的时候,IDE 默认情况下并不会主动测试并发状态,那么如何保证我们写出来的代码是并发安全的? 我们来举个例子,比如我们有个计数器,作用就是计数。

type Counter int32

func (c *Counter) Incr() {

*c++

}

很显然这个计数器在并发情况下是不安全的,那么我们如何写一个测试来做这个计数器的并发测试呢?

import (

“sync”

“testing”

“github.com/stretchr/testify/require”

)

func TestA_Incr(t *testing.T) {

var a Counter

eg := sync.WaitGroup{}

count := 10

eg.Add(count)

for i := 0; i < count; i++ {

go func() {

defer eg.Done()

a.Incr()

}()

}

eg.Wait()

require.Equal(t, count, int(a))

}

通过多次执行上面的测试,我们发现有些时候,测试的结果返回 OK,有些时候测试的结果返回 FAIL。也就是说,即便写了测试,有可能在某次测试中被标记为通过测试。那么有没有什么办法直接发现问题呢?答案就是在测试的时候增加-race 的 flag

-race 标志不适合 benchmark 测试

go test -race

这时候终端会输出:

WARNING: DATA RACE

Read at 0x00c00001ca50 by goroutine 9:

code.byted.org/ek/demo_test/t05_race/race.(*A).Incr()

/Users/bytedance/go/src/code.byted.org/ek/demo_test/t05_race/race/race.go:6 +0x6f

code.byted.org/ek/demo_test/t05_race/race.TestA_Incr.func1()

/Users/bytedance/go/src/code.byted.org/ek/demo_test/t05_race/race/race_test.go:18 +0x66

Previous write at 0x00c00001ca50 by goroutine 8:

code.byted.org/ek/demo_test/t05_race/race.(*A).Incr()

/Users/bytedance/go/src/code.byted.org/ek/demo_test/t05_race/race/race.go:6 +0x85

code.byted.org/ek/demo_test/t05_race/race.TestA_Incr.func1()

/Users/bytedance/go/src/code.byted.org/ek/demo_test/t05_race/race/race_test.go:18 +0x66

Goroutine 9 (running) created at:

code.byted.org/ek/demo_test/t05_race/race.TestA_Incr()

/Users/bytedance/go/src/code.byted.org/ek/demo_test/t05_race/race/race_test.go:16 +0xe4

testing.tRunner()

/usr/local/Cellar/go/1.15/libexec/src/testing/testing.go:1108 +0x202

Goroutine 8 (finished) created at:

code.byted.org/ek/demo_test/t05_race/race.TestA_Incr()

/Users/bytedance/go/src/code.byted.org/ek/demo_test/t05_race/race/race_test.go:16 +0xe4

testing.tRunner()

/usr/local/Cellar/go/1.15/libexec/src/testing/testing.go:1108 +0x202

go 主动提示,我们的代码中发现了竞争(race)态,这个时候我们就要去修复代码

type Counter int32

func (c *Counter) Incr() {

atomic.AddInt32((*int32)©, 1)

}

修复完成后再次伴随-race 进行测试,我们的测试成功通过!

Golang 原生的并发测试

golang 的测试类 testing.T 有一个方法 Parallel(),所有在测试中调用了该方法的都会被标记为并发,但是注意,如果需要使用并发测试的结果的话,必须在外层用一个额外的测试函数将其包住:

func TestA_Incr(t *testing.T) {

var a Counter

t.Run(“outer”, func(t *testing.T) {

for i := 0; i < 100; i++ {

t.Run(“inner”, func(t *testing.T) {

t.Parallel()

a.Incr()

})

}

})

t.Log(a)

}

如果没有第三行的 t.Run,那么 11 行的打印结果将不正确

Golang 的 testing.T 还有很多别的实用方法,大家可以自己去查看一下,这里不详细讨论

正确测试返回值

作为一个 gopher 平时要写大量的 if err != nil,那么在测试一个函数返回的 error 的时候,我们比如有下面的例子

type I interface {

Foo() error

}

func Bar(i1, i2 I) error {

i1.Foo()

return i2.Foo()

}

Bar 函数希望依次处理 i1 和 i2 两个输入,当遇到第一个错误就返回,于是我们写了一个看起来“正确”的测试

import (

“errors”

“testing”

“github.com/stretchr/testify/require”

)

type impl string

func (i impl) Foo() error {

return errors.New(string(i))

}

func TestBar(t *testing.T) {

i1 := impl(“i1”)

i2 := impl(“i2”)

err := Bar(i1, i2)

require.Error(t, err) // assert err != nil

}

这个测试结果“看起来”很完美,函数正确返回了一个错误。但是实际上我们知道这个函数的返回值是错误的,所以我们应当把测试稍作修改,将 error 当作一个返回值来校验起内容,而不是简单的判 nil 处理

func TestBarFixed(t *testing.T) {

i1 := impl(“i1”)

i2 := impl(“i2”)

err := Bar(i1, i2)

// 两种写法都可

require.Equal(t, errors.New(“i1”), err)

require.Equal(t, “i1”, err.Error())

}

这个时候我们就能发现到,代码中出现了错误,需要修复了。 同理可以应用到别的返回值,我们不应当仅仅做一些简单的判断,而应当尽可能做“精确值”的判断。

测试输入参数

上面我们讨论过了测试返回值,输入值同样需要测试,这一点我们主要结合 gomock 来说,举个例子我们的代码如下:

type I interface {

Foo(ctx context.Context, i int) (int, error)

}

type bar struct {

i I

}

func (b bar) Bar(ctx context.Context, i int) (int, error) {

i, err := b.i.Foo(context.Background(), i)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注软件测试)
img

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
和 i2 两个输入,当遇到第一个错误就返回,于是我们写了一个看起来“正确”的测试

import (

“errors”

“testing”

“github.com/stretchr/testify/require”

)

type impl string

func (i impl) Foo() error {

return errors.New(string(i))

}

func TestBar(t *testing.T) {

i1 := impl(“i1”)

i2 := impl(“i2”)

err := Bar(i1, i2)

require.Error(t, err) // assert err != nil

}

这个测试结果“看起来”很完美,函数正确返回了一个错误。但是实际上我们知道这个函数的返回值是错误的,所以我们应当把测试稍作修改,将 error 当作一个返回值来校验起内容,而不是简单的判 nil 处理

func TestBarFixed(t *testing.T) {

i1 := impl(“i1”)

i2 := impl(“i2”)

err := Bar(i1, i2)

// 两种写法都可

require.Equal(t, errors.New(“i1”), err)

require.Equal(t, “i1”, err.Error())

}

这个时候我们就能发现到,代码中出现了错误,需要修复了。 同理可以应用到别的返回值,我们不应当仅仅做一些简单的判断,而应当尽可能做“精确值”的判断。

测试输入参数

上面我们讨论过了测试返回值,输入值同样需要测试,这一点我们主要结合 gomock 来说,举个例子我们的代码如下:

type I interface {

Foo(ctx context.Context, i int) (int, error)

}

type bar struct {

i I

}

func (b bar) Bar(ctx context.Context, i int) (int, error) {

i, err := b.i.Foo(context.Background(), i)

网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。

需要这份系统化的资料的朋友,可以添加V获取:vip1024b (备注软件测试)
[外链图片转存中…(img-teSqBq38-1713446978702)]

一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!

  • 28
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 作为一名软件测试工程师,你的主要职责是确保软件产品在开发过程中按照预期的方式运行,并确保软件质量达到预期的水平。这通常包括规划和执行测试用例,分析测试结果,并根据测试结果编写测试报告。软件测试工程师还负责检测和报告软件中的错误(也称为缺陷或故障),并协助解决这些问题。在某些情况下,测试工程师还可能被要求协助开发人员调试代码。 ### 回答2: 我是一个软件测试工程师,负责确保软件的质量和稳定性。我的主要工作是通过测试软件的功能、性能和安全性来发现和修复缺陷。我负责编写测试计划、设计和执行测试用例,并记录和报告测试结果。 作为一个软件测试工程师,我具备扎实的软件开发测试知识。我熟悉测试工具和技术,能够使用自动化测试工具提高测试效率和准确性。我有良好的问题解决能力和分析能力,能够快速定位和解决软件中的问题。 我还注重与开发团队的合作和沟通。我与开发人员密切合作,及早发现和解决问题。我与产品经理和项目经理密切合作,明确需求和测试目标,并及时汇报测试进展和风险。 随着软件开发的快速发展,我也不断学习和更新自己的技能。我紧跟最新的测试趋势和技术,不断提升自己的能力。我参加行业培训和学习新的测试方法,以确保我的测试工作能够跟上软件发展的步伐。 作为一个软件测试工程师,我深知软件质量对用户体验和企业形象的重要性。因此,我将始终尽力保证软件质量,为用户提供高质量的软件产品。我对我的工作充满热情和责任心,愿意不断学习和提高自己的技术水平,为软件行业的发展做出贡献。 ### 回答3: 我是一名软件测试工程师,主要负责确保软件产品质量的高标准。我的工作职责包括制定测试计划、编写测试用例、执行测试、记录缺陷并与开发人员进行沟通和解决问题。 首先,我会与产品经理和开发团队进行需求分析,以了解软件的功能和目标。然后,在制定测试计划时,我会确定测试目标、测试范围和测试时间表,以便对软件进行全面且有序的测试。在编写测试用例时,我会根据需求和设计文档来设计各种测试场景,以确保软件在各种情况下的正确运行。 接下来,我会执行各种测试方法,包括功能测试、性能测试、兼容性测试等,以验证软件的各个方面是否符合预期。在执行测试过程中,我会记录并报告软件中发现的任何缺陷,同时与开发人员密切合作,确保缺陷得到及时修复。 此外,我还会对测试环境进行配置和维护,确保测试过程的稳定性和可重复性。我熟悉各种测试工具和技术,例如自动化测试工具和性能测试工具,以提高测试效率和准确性。 作为一个软件测试工程师,我需要具备耐心、细心和有逻辑思维的能力。同时,我也需要不断学习和跟进最新的测试技术和行业动态,以保持自己的专业水平。 总之,作为一名软件测试工程师,我致力于为用户提供高质量、稳定和可靠的软件产品,确保用户的良好体验并满足他们的需求。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值