登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go 随机数 验证码如何固定测试随机性

来源:17golang原创

时间:2026-10-31 20:39:12 150浏览 收藏

Go 验证码测试不应该靠“碰巧生成了某个字符串”。更稳妥的做法是把“从随机源取一个范围内整数”抽成接口:生产环境注入 crypto/rand,单元测试注入固定序列。这样验证码的拼接、长度和字符表逻辑可以稳定复现,同时不会把可预测的测试随机数带入线上。

官方资料:https://pkg.go.dev/crypto/rand 测试包:https://pkg.go.dev/testing

固定的是测试替身的输出,不是生产随机源。验证码如果承担人机校验或访问控制职责,生产路径仍应保留安全随机源和错误处理。

先把验证码逻辑和随机来源分开

验证码生成通常包含三件事:校验字符表和长度、重复取得随机索引、把索引映射为字符。真正让测试不稳定的只是第二件事,所以不要在业务函数内部直接调用包级随机函数,而是只依赖一个很小的接口。

部分生产实现测试实现
随机索引crypto/rand.Int固定整数序列
验证码拼接与测试共用与生产共用
错误处理向上返回,不静默降级注入错误并断言
Go 验证码生成器、随机索引接口、字符表和测试替身的静态关系说明图
图1:随机来源边界说明图,展示验证码生成逻辑如何同时连接生产随机源与测试替身;这是结构说明图,不是运行截图。

生产路径使用 crypto/rand 生成索引

crypto/rand.Int 返回半开区间 [0,max) 的均匀随机值;封装后,验证码生成器无需知道随机数来自哪里。这里把字符表作为参数,便于排除容易混淆的字符。

package captcha

import (
	"crypto/rand"
	"fmt"
	"math/big"
)

// IntSource 只暴露验证码需要的随机索引能力,方便测试替换。
type IntSource interface {
	Intn(n int) (int, error)
}

// SecureIntSource 使用密码学安全随机源生成 [0, n) 的索引。
type SecureIntSource struct{}

func (SecureIntSource) Intn(n int) (int, error) {
	// 字符表为空时不能构造合法的随机上界。
	if n = len(alphabet) {
			return "", fmt.Errorf("random index %d is out of range", index)
		}
		code[i] = alphabet[index]
	}
	return string(code), nil
}

调用生产代码时可以显式传入 SecureIntSource{}。如果验证码只是测试夹具,不要把固定种子或固定序列写进生产构造函数;测试性来自依赖注入,而不是来自降低随机性。

测试路径注入固定序列并复现结果

固定替身只需要实现 Intn。除了返回预先安排的索引,还应检查调用是否超出序列长度,这能发现验证码长度变化却没有同步更新测试数据的问题。

package captcha_test

import (
	"fmt"
	"testing"
)

import "example.com/captcha"

type fixedSource struct {
	values []int
	pos    int
}

func (s *fixedSource) Intn(n int) (int, error) {
	// 测试替身拒绝非法上界,模拟真实实现的边界约束。
	if n = len(s.values) {
		return 0, fmt.Errorf("fixed source exhausted or invalid bound")
	}
	value := s.values[s.pos]
	s.pos++
	return value, nil
}

func TestGenerateFixedCode(t *testing.T) {
	// 六个固定索引让期望验证码与执行顺序完全确定。
	source := &fixedSource{values: []int{0, 1, 2, 3, 0, 1}}
	got, err := captcha.Generate(source, "ABCDEFGHJKLMNPQRSTUVWXYZ23456789", 6)
	if err != nil {
		t.Fatal(err)
	}
	if got != "ABCDAB" {
		t.Fatalf("unexpected code %q", got)
	}
	if source.pos != 6 {
		t.Fatalf("random source called %d times", source.pos)
	}
}

func TestGenerateCases(t *testing.T) {
	tests := []struct {
		name    string
		values  []int
		length  int
		want    string
		wantErr bool
	}{
		{name: "empty alphabet", values: []int{}, length: 4, wantErr: true},
		{name: "fixed boundary", values: []int{0, 3}, length: 2, want: "AD"},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			// 子测试保持每组输入独立,避免共享随机状态造成顺序依赖。
			source := &fixedSource{values: tt.values}
			got, err := captcha.Generate(source, "ABCD", tt.length)
			if (err != nil) != tt.wantErr || (!tt.wantErr && got != tt.want) {
				t.Fatalf("got %q, err %v", got, err)
			}
		})
	}
}

固定序列测试的重点不是验证“看起来够随机”,而是验证“给定索引后,验证码如何映射和失败”。每个子测试独立创建替身,避免共享可变位置计数造成顺序依赖。

Go 固定序列测试替身、字符索引、期望验证码和错误分支的静态关系说明图
图2:固定测试替身的关系说明图,展示索引序列如何映射到期望验证码,并保留长度与错误分支;这是结构说明图,不是运行截图。

用边界指标判断测试是否够稳定

可以把测试质量收敛到三个可观察指标:输入是否固定、随机源调用次数是否等于验证码长度、错误是否被原样传回。这样比断言某次“看起来够随机”更有意义,也不会把统计波动误判成业务回归。

  • 长度:长度为零或负数直接失败,不能生成空验证码掩盖配置错误。
  • 字符表:字符表为空直接失败;索引必须落在 [0,len(alphabet))。
  • 错误:随机源返回错误时,生成函数应停止并返回错误,不自动改用 math/rand。
  • 并发:生产源可被安全地共享,但测试替身若带有可变位置计数,就应每个测试独立创建,或自行加同步。

常见问题

固定测试序列会不会让验证码失去随机性?

不会,只要固定序列只存在于测试替身。生产构造仍然使用 crypto/rand,测试只替换接口依赖。

能不能直接给 math/rand 设置固定种子?

可以用于非安全模拟,但不建议把它当作验证码生产路径。验证码的安全属性应由生产随机源决定,测试复现应由注入替身决定。

总结起来,验证码测试的稳定性来自“随机来源可替换、生成逻辑共用、错误边界可断言”。把这三个边界写进接口后,既能稳定复现测试,也不会为了测试方便而改变线上随机策略。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>