· Environment-Specific Correctness (特定环境的正确性):除了确认 config, secret value 是有效的之外,我们还需要确保该值存在于正确的环境中。对于一个应用程序,应该有三种不同的环境:beta,rc(staging),和 real。每个 config or secret value 应该在这三个环境中都有其对应的值。再次以 MongoDB 连线字符串为例:在不同的环境中,它应该有不同的值来连接不同环境的数据库:
- Beta:mongodb://beta-username:beta-password@10.127.xxx.beta
- RC:mongodb://rc-username:rc-password@10.127.xxx.rc
- Real:mongodb://real-username:real-password@10.127.xxx.real
想象一下,假如我们不小心在 Real 环境中使用了 Beta MongoDB 连线字符串,那么尽管这个值是有效的(因为它可以连接到 beta MongoDB),但我们能说这个值是正确的吗?不行,因为它不在正确的环境中!因此这就是为什么除了验证一个 value 有效,我们还需要确保在环境层面上 config 及 secret 的正确性。
测试环境设置
首先,原本的 Kubernetes manifest repo 只有包含travel-apps 这个目录,其为典型的 Kustomization repository 架构,包含了 base and overlays。我们的方法是在同一层级新增一个采用 node.js + Jest + TypeScript 的目录。
它包含了各种测试,覆盖了需要检查的 overlays 中存在的不同类型的 config 及 secret value。至于 CI,我们使用 GitHub Actions 作为 pipeline。每当 manifest 内容发生变动时,就会触发 workflow 执行 config-secret-test
中的测试。
├── .github
│ └── workflows
│ └── pre-commit.yml
├── config-secret-test
│ ├── __tests__
│ │ ├── test1
│ │ ├── test2
│ │ ├── ...
│ ├── jest.config.js
│ └── package.json
└── travel-apps
├── base
└── overlays (containing configs and secrets)
测试方法
我们对 Travel 中现有的 Kubernetes manifest 内容进行分类。根据上述提到的 Two Key Testing Principle,不同类别需要通过不同的方法来验证。下面我们将解释划分的类别项目,以及每个类别如何验证有效性和环境正确性:
分布式数据库系统(MongoDB、Redis、ElasticSearch、S3)
有效性(Validity)
这类型的 secret value 会是连线字符串,因此验证有效性的方法很简单。针对不同的数据库系统,我们使用对应的 client library 尝试进行连接,并执行简单的操作(e.g., ping , check )以检查是否成功建立连线。
特定环境的正确性(Environment-specific correctness)
为了确认环境的正确性,我们需要找到一种方法来识别当给定一个连线字符串,我们要能够知道其所连接的数据库是处于哪一个环境。
A Naive Idea
起初,我们考虑,是否可以根据所连接数据库的资料量多寡来判断环境?例如:Beta 环境通常比 Real 环境拥有较少的资料,所以如果我们连线后发现资料量少于某个阀值(threshold),我们就认定该环境为 Beta。这个方法很简单,但问题在于它不够精确,因为资料量可能会变动,而这会影响我们的判断标准。
Golden Answer Approach
经过讨论,我们后来采用了 Golden Answer 方法,也就是先在每个环境中插入一个 Golden Answer 文件作为后续验证使用。这份文件储存了简单的信息:这个环境的标识(Beta、RC、Real)。如此,当我们想要验证一个连线字符串的环境时,我们就直接使用字符串进行连线并且检索该连线字符串对应数据库环境内的 Golden Answer 文件。当有了这项信息,我们只需要验证我们现在正在测试的连线字符串是否符合我们认为其所应该要连接到的环境。下图是 Golden Answer 方法的一个例子(假如我们想验证一个连线字符串是否是连到 Beta)及流程图:
下面也提供使用 jest 撰写的测试程序码供参:
用 Jest 测试 MongoDB 代码的两个关键原则
import { MongoClient } from "../utils/mongoClient";
import { currentTestEnv } from "../env.config";
import { readTextFile, resolveSecretPath } from "../utils/getConfigSecret";
let mongoClient: MongoClient;
const secrets = readTextFile(resolveSecretPath("mongo"));
afterEach(async () => {
if (mongoClient) {
await mongoClient.disconnect();
}
});
async function testMongoDb(connectionString: string) {
mongoClient = new MongoClient(connectionString);
test("ensure MongoDB connection string functions well", async () => {
const isConnectionSuccess = await mongoClient.tryConnect();
expect(isConnectionSuccess).toBe(true);
});
test("retrieve Env Doc from MongoDB", async () => {
await mongoClient.tryConnect();
const envDoc = await mongoClient.queryEnvCheckDoc();
expect(envDoc?.env).toBe(currentTestEnv);
});
}
// Run tests for each MongoDB connection string
testMongoDb(secrets["MONTHLY_REPORT_MONGODB_URL"]);
加密/解密密钥(Encryption / Decryption Keys)
有些 secret values 是用于数据的加密和解密。这种方法的有效性和环境正确性验证可以同时进行。检查方法类似于 Golden Answer 方法,首先插入一个 Golden value,然后在测试期间检索并验证这项数值。详细说明如下:
-
我们首先使用每个环境正确的 Encryption Key 对我们的 Golden Answer 文件进行加密,并将其插入到每个环境的数据库中。
-
在测试过程中,我们从当前的 secret values 中检索对应的 Decryption Key,并验证其是否能够解密。
-
如果能够解密,这意味着目前存在于 manifest 中的 Decryption Key 能够发挥作用并且存在于正确的环境中。
Tokens
以 JWT Tokens 为例,在某些情况下我们会为其他团队甚至外部服务发放 JWT Tokens 以存取我们的服务。JWT Tokens 需要验证的是当前 manifest 中用于生成 Token 的 secret key 其 value 的正确性,但我们又不能使用发给其他人的 Tokens 来验证 decode 的正确性,因为这相当于将我们发给其他人的 Tokens 推到我们的测试程序码 repository 上。
因此,我们可以使用jwt.io (https://jwt.io/)输入不同环境的 secret key 生成可用于测试每个环境的 tokens。如此,我们就可以安全地将这些测试 tokens 放入程序码中,以验证 secrert key decode 的正确性。
URLs
对于依赖各种 URL config 的服务,例如 CDN_URL, API_BASE_URL, 以及特定服务的 API host(LINETVL_xxx_service_API_HOST、xxx_BASE_URL),我们必须确保这些 configs 的格式正确性及可存取性。而针对 Urls, 我们可以使用下列方式涵盖 two key testing principles 的验证逻辑:
Validity
为了确定一个 URL 是否可用,我们其实能够透过dns.lookup 确认 URL 的 hostname 是否存在。如果存在,表示此 URL 可以存取,否则就不是一个有效的 URL。这种方法的优点是我们不需要实际连接到每个 URL,我们只需要解析(parse)其 hostname 并确认其存在,这样可以节省许多不必要的 end-to-end 测试成本。
特定环境的正确性(Environment-specific correctness)
由于 LINE 的 domain 及 hostname 在命名上有完整的规范,基本上每个环境的 URLs 都会带有一个后缀(suffix)或 substring,这些就可以用来辨识一个 URL 对应的环境。因此,我们可以透过正则表达式(Regex)简单地确定一个 URL 属于哪个环境。
URL 测试的范例程序码如下:
import dns from "dns";
import { readTextFile, resolveConfigPath } from "../../utils/getConfigSecret";
import { currentTestEnv } from "../../env.config";
import { Env } from "../../enums/env";
import { getAllConfigs } from "../../utils/getConfigSecret";
function checkHostname(hostname: string) {
return new Promise((resolve) => {
dns.lookup(hostname, (error) => {
if (error) {
resolve(false);
} else {
resolve(true);
}
});
});
}
const toHostnameIfInputIsUrl = (input: string) => {
try {
return new URL(input).hostname;
} catch {
return input;
}
};
const runHostnameValidityCheck = (hostname: string) => {
test(`Hostname ${hostname} should be valid`, async () => {
const isValid = await checkHostname(hostname);
expect(isValid).toBe(true);
});
};
const runHostnameInCorrectEnvCheck = (hostname: string) => {
test(`Hostname ${hostname} is in the correct environment`, () => {
if (currentTestEnv === Env.Real) {
expect(
hostname.endsWith("...your-own-naming-convention-here")
).toBe(true);
return;
}
expect(hostname.includes(currentTestEnv)).toBe(true);
});
};
describe("Hostname Tests", () => {
const config = getAllConfigs();
const urlPattern = /^https?:\/\//;
// Get keys that end with HOST or URL
const hostnameOrUrlKeys = Object.keys(config).filter(
(key) =>
key.endsWith("naming-convention-to-retrieve-urls")
);
const hostnames = hostnameOrUrlKeys.map((key) =>
toHostnameIfInputIsUrl(config[key]),
);
hostnames.forEach((hostname) => {
runHostnameValidityCheck(hostname);
runHostnameInCorrectEnvCheck(hostname);
});
});
通过设定以下 GitHub Actions workflow,我们可以将测试完善地整合进 CI Pipeline 中,并保护每次 commit 的变动,以确保 configs 和 secret values 其数值都是“正确”的:
CI Pipeline
name: pre-commit
on:
pull_request:
branches: [master]
env:
![img](https://img-blog.csdnimg.cn/img_convert/661dff99ad6621ab110f8687ba815e56.png)
![img](https://img-blog.csdnimg.cn/img_convert/e4e3388f2e37ac3a7593604dffc24282.png)
![img](https://img-blog.csdnimg.cn/img_convert/b4b8b476f6d9de28120d42315dee0762.png)
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上软件测试知识点,真正体系化!**
pull_request:
branches: [master]
env:
[外链图片转存中...(img-4rqxPQgM-1719264875048)]
[外链图片转存中...(img-gCRHDLnL-1719264875048)]
[外链图片转存中...(img-gIcpmJeq-1719264875049)]
**既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上软件测试知识点,真正体系化!**