为了账号安全,请及时绑定邮箱和手机立即绑定

应对非确定性软件的挑战:超越传统的测试方法

非确定性的软件开发变得越来越普遍。从有不可信输入的分布式系统到由AI驱动的解决方案,在这些难以完全预测的环境中确保系统的可靠性和一致性正面临着日益增长的挑战。大型语言模型(LLMs)和其他AI技术的集成实际上每次计算时都可能引入不同的数据。

非确定性程序因其特性,可以在看似相同的条件下对相同的输入可能会产生不同的输出。这种不可预测性给测试带来了很大的挑战。

本文探讨了非确定性软件的一些基本特性,讨论了此类系统测试中公认的最好实践,考察了该领域的最近创新,重点关注以AI为基础的技术,并提供了带有Python代码示例的实际例子。还将探讨大型语言模型(LLMs)在软件测试中带来的独特挑战,并提供相关建议。提供针对这些复杂系统的全面测试策略实施的指导。

所有代码都可在this仓库中找到。但同样的理念也可以用于任何编程语言。最后,我要回顾一些可用于其他编程语言的测试框架,包括C#、Java、JavaScript和Rust等。

非确定性软件的特点和面临的挑战

非确定性的软件可以被视为我们所处的复杂且常常难以预测的世界的反映。与确定性软件系统不同,后者总是对相同的输入产生相同的输出,非确定性的软件引入了变化的元素。

软件中的非确定性可能来自各种来源,例如所使用的算法中的固有随机性或无法从外部观察到的内部状态。也可能是由于数值计算中的错误。例如,在处理浮点算术时,微小的舍入误差会累积并导致不同的计算结果。

一个新的非确定性因素来源是集成生成式人工智能组件,比如大型语言模型(LLMs),这主要是因为每次调用时,即使输入相同,它们的输出也可能有很大差异。

为了展示非确定性的行为,我们来看一个简单的 Python 示例吧。

    import random
    from typing import Optional

    def non_deterministic_function(x: int) -> Optional[int]:
        if random.random() < 0.1:  # 10% 的失败概率
            return None
        return x * 2

    # 多次运行此函数,使用相同的输入
    from collections import Counter

    results = Counter()
    for _ in range(20):
        result = non_deterministic_function(5)
        results[result] += 1

    for result, count in results.items():
        print(f"结果 {result} 出现了 {count} 次")

全屏模式 退出全屏

如果你运行这段代码,大多数时候它会返回 10,因为函数的输入是 5,但大约 10% 的时候,它会返回 None。这个例子说明了测试不确定性的软件的挑战:如何为一个行为不总是相同的函数编写测试?

为了应对这些挑战,我们可以调整传统的测试方法,并采用新的方法。从基于属性的测试到利用AI生成测试,软件测试领域正在发展以适应日益不确定的数字世界的需求。

非确定性软件的有效测试策略:

测试非确定性软件需要我们改变对待软件质量保证的方式。一个有趣的方法是使用属性驱动测试来测试非确定性软件。

使用基于属性的测试时,您不是为特定的输入输出对编写测试,而是定义对于所有可能输入都应成立的属性条件。测试框架随后会生成大量随机输入,并检查这些输入是否符合定义的属性。

让我们来看一看一个使用Python中的Hypothesis库进行基于属性的测试的例子:

    import random
    from typing import List
    from hypothesis import given, strategies as st

    def non_deterministic_sort(lst: List[int]) -> List[int]:
        """一个非确定性的排序函数,偶尔会出错。"""
        if random.random() < 0.1:  # 10% 的概率出错
            return lst  # 返回未排序的列表
        return sorted(lst)

    @given(st.lists(st.integers()))
    def test_non_deterministic_sort(lst: List[int]) -> None:
        result = non_deterministic_sort(lst)

        # 性质 1:结果的长度应该和输入一样
        assert len(result) == len(lst), "结果的长度应该和输入一样"

        # 性质 2:结果应该包含所有的输入元素
        assert set(result) == set(lst), "结果应该包含所有的输入元素"

        # 性质 3:在大多数情况下,结果应该是排序的
        attempts = [non_deterministic_sort(lst) for _ in range(100)]

        # 考虑到非确定性,允许一些失败
        # 将 'any' 替换为 'all',这样测试会在任何尝试中不正确排序时失败
        assert any(attempt == sorted(lst) for attempt in attempts), "函数应该在多次尝试中生成正确的排序结果"

    # 运行这个测试
    if __name__ == "__main__":
        test_non_deterministic_sort()

全屏模式,退出全屏

在这个例子中,我们正在测试一个非确定性的排序函数,它有时候会出错。而不是检查特定的输出结果,我们可以验证一些无论函数如何都应该成立的性质。例如,我们可以检查输出的长度与输入相同,包含相同的元素,以及在多次尝试中至少有一次正确排序。

虽然基于属性的测试非常强大,但在测试用例中涉及大型语言模型(LLM)时,它可能会变得缓慢且昂贵。这是因为每个测试运行可能需要多次调用大型语言模型,这在计算上可能非常耗费资源和时间。因此,在与LLM一起工作时,设计基于属性的测试以平衡全面性和效率是非常重要的。

另一个测试非确定性软件的关键策略是看看是否可以创建一个可重复的测试环境。这包括尽可能控制所有可能的变量以减少测试期间非确定性的来源。例如,你可以使用固定的随机数种子、模拟外部依赖项,并通过容器化来确保环境的一致性。

在处理AI时,尤其是大规模语言模型(LLM)时,你可以使用语义相似度来评估输出,而不是期望完全匹配。例如,在测试基于LLM的聊天机器人时,你可能需要检查模型的回复是否与一组可接受的答案语义相似,而不是寻找特定的表达。

比如,这是使用语义相似度测试LLM生成结果的方法:

    import json
    import boto3
    from typing import List, Callable

    from scipy.spatial.distance import cosine

    AWS_REGION = "us-east-1"
    EMBEDDING_MODEL_ID = "amazon.titan-embed-text-v2:0"

    bedrock_runtime = boto3.client('bedrock-runtime', region_name=AWS_REGION)

    def get_embedding(text: str) -> List[float]:
        body = json.dumps({"inputText": text})
        response = bedrock_runtime.invoke_model(
            modelId=EMBEDDING_MODEL_ID,
            contentType="application/json",
            accept="application/json",
            body=body
        )
        response_body = json.loads(response['body'].read())
        return response_body['embedding']

    def semantic_similarity(text1: str, text2: str) -> float:
        embedding1 = get_embedding(text1)
        embedding2 = get_embedding(text2)
        return 1 - cosine(embedding1, embedding2)

    def test_llm_response(llm_function: Callable[[str], str], input_text: str, acceptable_responses: List[str], similarity_threshold: float = 0.8) -> bool:
        llm_response = llm_function(input_text)
        print("llm_response:", llm_response)

        for acceptable_response in acceptable_responses:
            similarity = semantic_similarity(llm_response, acceptable_response)
            print("acceptable_response:", acceptable_response)
            if similarity >= similarity_threshold:
                print("similarity:", similarity)
                return True

        return False

    # Example usage
    def mock_llm(input_text: str) -> str:
        # 这是一个演示用的模拟LLM函数
        return "巴黎是法国的首都,以其标志性的埃菲尔铁塔著称。"

    input_text = "法国的首都是哪个城市?"
    acceptable_responses = [
        "法国的首都是巴黎。",
        "巴黎是法国的首都。",
        "法国的首都是巴黎,以丰富的历史和文化而闻名。"
    ]

    result = test_llm_response(mock_llm, input_text, acceptable_responses)
    print(f"LLM响应测试是否通过了: {result}")

全屏 退出全屏

在这个例子中,我们使用Amazon Bedrock来计算模拟的大型语言模型(LLM)的响应和一系列可接受的响应的语义嵌入向量。然后,我们使用余弦相似度值来确定LLM的输出是否在语义上接近任何一个可接受的响应。

另外一方面,一个与非确定性的软件测试不严格相关但有趣的发展是使用LLM来生成测试数据并检查测试输出。这种做法利用了LLM理解上下文并生成多样的、现实的测试案例(test cases)的能力。

比如,这里有一个用JSON格式生成结构化测试数据的例子

    import json
    from typing import Union, List, Dict, Any
    import boto3

    AWS_REGION = "us-east-1"

    MODEL_ID = "us.anthropic.claude-3-5-sonnet-20240620-v1:0"   

    bedrock_runtime = boto3.client('bedrock-runtime', region_name=AWS_REGION)

    def generate_structured_test_data(prompt: str, num_samples: int = 5) -> Union[List[Dict[str, Any]], None]:
        response = bedrock_runtime.converse(
            modelId=MODEL_ID,
            messages=[{
                'role': 'user',
                'content': [{ 'text': prompt }]
            }]
        )
        generated_data = response['output']['message']['content'][0]['text']
        try:
            json_data = json.loads(generated_data)
        except json.JSONDecodeError:
            print("生成的数据不是有效的JSON格式")
            return None  # 或者可以抛出异常

        return json_data

    # 对话
    def generate_structured_test_data(prompt: str, num_samples: int = 5) -> Union[List[Dict[str, Any]], None]:
        response = bedrock_runtime.converse(
            modelId=MODEL_ID,
            messages=[{
                'role': 'user',
                'content': [{ 'text': prompt }]
            }]
        )
        generated_data = response['output']['message']['content'][0]['text']
        try:
            json_data = json.loads(generated_data)
        except json.JSONDecodeError:
            print("生成的数据不是有效的JSON格式")
            return None  # 或者可以抛出异常

        return json_data

    # 示例用法
    prompt = """生成5个JSON对象,代表天气预报应用的潜在用户输入。每个对象应包含'location' 和 'query' 字段。输出结果应为一个有效的JSON数组。仅输出JSON,不输出其他内容。这里是一个格式示例:
    [
      {
        "location": "New York",
        "query": "明天新 York 的温度是多少?"
      }
    ]"""

    test_inputs = generate_structured_test_data(prompt)

    print(json.dumps(test_inputs, indent=2))

进入全屏 退出全屏

在这个例子中,我们使用了Amazon Bedrock和Anthropic Claude 3.5 Sonnet模型来为一个天气预报应用的测试生成结构化的JSON测试数据。通过这种方法,你可以创建一系列测试用例,包括一些起初可能难以想到的极端情况。这些测试用例可以被保存并重复使用。

同样,LLMs 可以用于检查测试输出,尤其是在正确答案可能带有主观性或受上下文影响的任务中。这种方法虽然比单纯依赖语义相似性更准确,但速度较慢且成本更高。这两种方法可以结合使用,相辅相成。例如,如果语义相似性测试通过了,我们就使用 LLM 进行进一步检查。

    import boto3
    from typing import Any, Dict, List

    AWS_REGION = "us-east-1"

    MODEL_ID = "us.anthropic.claude-3-5-sonnet-20240620-v1:0"   

    bedrock_runtime = boto3.client('bedrock-runtime', region_name=AWS_REGION)

    def check_output_with_llm(input_text: str, test_output: str, prompt_template: str) -> bool:
        prompt = prompt_template.format(input=input_text, output=test_output)

        response: Dict[str, Any] = bedrock_runtime.converse(
            modelId=MODEL_ID,
            messages=[{
                'role': 'user',
                'content': [{ 'text': prompt }]
            }]
        )

        response_content: str = response['output']['message']['content'][0]['text'].strip().lower()
        if response_content not in ["yes", "no"]:
            raise ValueError(f"LLM返回了意外的响应: {response_content}")
        return response_content == "yes"

    # 示例用法如下
    input_text = "今天的天气怎么样?"
    test_output = "天气晴朗,最高气温75°F (24°C),最低气温60°F (16°C)。"
    prompt_template = "对于输入问题 '{input}',这是一个合理的回答吗:'{output}'?请只回答是或否,不要回答其他内容。"

    is_valid = check_output_with_llm(input_text, test_output, prompt_template)

    print('input_text:', input_text)
    print('test_output:', test_output)
    print(f"测试输出是否是一个合理的响应?{is_valid},是的话输出True,否则输出False。")

进入全屏 退出全屏

在这个例子中,我们再次使用 Anthropic Claude 模型来评估系统响应是否合理,以给定的输入问题为基础。根据测试难度,我们可以根据需要选择更强大的模型或较弱的模型,来优化速度和节省成本。

这种方法可用于测试聊天机器人、内容生成系统,或其他输出难以用简单规则定义的应用程序。

这些策略——例如基于属性的测试、可重复的环境、语义相似性检查以及LLM(Large Language Model)辅助的测试生成和验证——构成了有效测试非确定性软件的基础。即使无法预测确切的输出,这些策略也使得我们可以对系统行为作出有意义的判断。

测试复杂非确定性系统测试的高级测试技术

使用AI生成测试用例可以超越生成式AI和大语言模型的范畴。例如,机器学习模型可以分析历史测试数据和系统行为,来识别模式并生成测试用例,这些测试用例最有可能发现bug或边缘情况,是人类测试人员可能忽略的。

让我们来看一个简单的机器学习模型为非确定性函数生成测试用例的例子。

    import numpy as np
    from sklearn.ensemble import RandomForestClassifier
    from sklearn.model_selection import train_test_split
    from typing import List, Tuple

    # 模拟的历史测试数据如下
    # 特征包括:input_a,input_b,system_load
    # 目标:0(通过)或1(失败),其中0表示通过,1表示失败,
    X = np.array([
        [1, 2, 0.5], [2, 3, 0.7], [3, 4, 0.3], [4, 5, 0.8], [5, 6, 0.4],
        [2, 2, 0.6], [3, 3, 0.5], [4, 4, 0.7], [5, 5, 0.2], [6, 6, 0.9]
    ])
    y = np.array([0, 0, 0, 1, 0, 0, 0, 1, 0, 1])

    # 将数据划分
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

    # 训练一个随机森林分类器
    clf = RandomForestClassifier(n_estimators=100, random_state=42)
    clf.fit(X_train, y_train)

    # 用于生成新测试用例的函数
    def generate_test_cases_from_historical_test_data(n_cases: int) -> np.ndarray:
        # 生成新的随机输入
        new_cases = np.random.rand(n_cases, 3)
        new_cases[:, 0] *= 10  # 将input_a的值调整到0到10之间
        new_cases[:, 1] *= 10  # 将input_b的值调整到0到10之间

        # 预测每个测试用例的失败概率
        failure_prob = clf.predict_proba(new_cases)[:, 1]

        # 按失败概率对用例进行排序
        sorted_indices = np.argsort(failure_prob)[::-1]

        return new_cases[sorted_indices]

    # 生成并打印最有可能失败的前5个测试用例如下
    top_test_cases = generate_test_cases_from_historical_test_data(100)[:5]
    print("最有可能失败的前5个测试用例:")
    for i, case in enumerate(top_test_cases, 1):
        print(f"用例 {i}: 测试用例 input_a = {case[0]:.2f}, input_b = {case[1]:.2f}, system_load = {case[2]:.2f}")

进入全屏;退出全屏

这个例子说明了如何利用随机森林分类器生成更有可能揭示系统问题的测试用例。模型可以比人类更好地从历史数据中学习,以预测哪些输入组合和系统条件更可能引发故障。

另一种相关技术是使用混沌工程学来测试非确定性系统或不可预测系统。例如,你可以故意向系统中引入故障和干扰来测试其恢复能力,并在实际生产环境中出现问题之前识别潜在问题。

例如,你可以随机停止分布式系统中的实例,模拟网络延迟,或向数据流中插入错误。通过在一个受控的环境下系统地引入混沌,你可以发现这些系统在常规测试条件下可能不容易被发现的弱点。

谈到测试人工智能系统,特别是涉及大型语言模型(LLM)的系统时,一种类似的方法是使用对抗测试,其中设计输入提示来挑战LLM的理解并生成边缘案例。

这里是一个简单的例子,说明如何为LLM实现对抗性测试框架。

    import random
    import string

    def generate_adversarial_prompt(base_prompt, num_perturbations=3):
        perturbations = [
            lambda s: s.upper(),
            lambda s: s.lower(),
            lambda s: ''.join(random.choice([c.upper(), c.lower()]) for c in s),
            lambda s: s.replace(' ', '_'),
            lambda s: s + ' ' + ''.join(random.choices(string.ascii_letters, k=5)),
        ]

        adversarial_prompt = base_prompt
        for _ in range(num_perturbations):
            perturbation = random.choice(perturbations)
            adversarial_prompt = perturbation(adversarial_prompt)

        return adversarial_prompt

    def test_llm_robustness(llm_function, base_prompt, expected_topic, num_tests=10):
        for _ in range(num_tests):
            adversarial_prompt = generate_adversarial_prompt(base_prompt)
            response = llm_function(adversarial_prompt)

            # 我使用我的语义相似度函数来检查响应是否仍然相关,尽管存在对抗性输入。
            is_on_topic = semantic_similarity(response, expected_topic) > 0.7

            print(f"提示: {adversarial_prompt}")
            print(f"响应是否相关: {is_on_topic}")
            print("---")

    # 示例用法(假设我已经有了 LLM 函数和语义相似度函数)
    base_prompt = "法国的首都是什么?"
    expected_topic = "巴黎是法国的首都"

    test_llm_robustness(mock_llm, base_prompt, expected_topic)

全屏 / 退出全屏

这个例子通过对基础提示语进行随机修改来生成对抗性提示,然后测试LLM在这种情况下是否仍能生成相关主题的回复。生成对抗性提示的其他方式包括使用不同的自然语言、要求以诗歌形式或其他特定格式输出,以及要求给出内部信息,如工具使用方法。

因为没有一种技术是万能的,最有效的测试策略通常包括多种方法,根据被测系统的具体特性和需求进行定制。

在接下来的部分中,我们来探讨如何制定一种综合测试策略,结合高级技术和传统方法,以确保即使是复杂且非确定性系统也能得到稳健的测试。

测试非确定性软件的策略

为了有效测试复杂系统,我们需要结合多种技术和适应每个系统具体需求和挑战的全面策略。

让我们来看看如何实现这样的策略,以一个假设的基于人工智能的推荐系统为例。该系统使用机器学习模型来预测用户偏好,结合实时数据,并结合大型语言模型来生成个性化的描述内容。我们可以用它作为一个含有多种不确定因素的不确定系统的例子。

第一步是识别系统的关键组件,并评估潜在的故障影响。在这个示例推荐系统里,我们能够发现几个高风险区域:

  • 核心推荐算法系统
  • 实时数据处理管道系统
  • 基于LLM的内容描述生成系统

对于这些组件中的每一个,让我们考虑故障对用户体验、数据完整性和系统稳定性的可能影响。这个评估可以用来指导我们的测试工作,确保资源被集中使用在最需要的地方。

那么,有了之前的风险评估,我们可以规划一个多层次的测试方法,结合多种技术。

单元测试中的属性测试

对于各个组件,我们可以使用基于特性的测试来确保它们在各种输入情况下正常工作。这里有一个测试推荐算法的例子。

    从 hypothesis 导入 given, strategies as st
    导入 numpy as np
    从 typing 导入 List

    def recommendation_algorithm(user_preferences: List[float], item_features: List[float]) -> float:
        # 注释:简化的推荐算法
        return np.dot(user_preferences, item_features)

    @given(
        st.lists(st.floats(min_value=-1, max_value=1), min_size=5, max_size=5),
        st.lists(st.lists(st.floats(min_value=-1, max_value=1), min_size=5, max_size=5), min_size=1, max_size=10)
    )
    def test_recommendation_algorithm(user_preferences: List[float], item_features_list: List[List[float]]) -> None:
        recommendations = [recommendation_algorithm(user_preferences, item) for item in item_features_list]

        # 属性 1:推荐应该在[-5, 5]范围内,鉴于我们的输入范围
        assert all(-5 <= r <= 5 for r in recommendations), "推荐超出预期范围"

        # 属性 2:更高的点积应该产生更高的推荐
        sorted_recommendations = sorted(zip(recommendations, item_features_list), reverse=True)
        for i in range(len(sorted_recommendations) - 1):
            assert np.dot(user_preferences, sorted_recommendations[i][1]) >= np.dot(user_preferences, sorted_recommendations[i+1][1]), "推荐未正确排序"

    # Run the test
    test_recommendation_algorithm()

全屏显示。退出全屏。

混沌工程中的集成测试

为了测试我们的组件在各种条件下如何协同工作,我们可以使用混沌工程技术。例如,我们可以随机降级实时数据管道的性能表现,模拟网络故障,或者在API响应中引入延时。这有助于确保系统在不理想条件下仍能保持稳定。

用AI生成的测试用例做系统测试

为了全流程测试,我们可以使用AI生成多样且具有挑战性的测试场景。这可能涉及到创建复杂的用户画像,模拟各种使用场景,并为我们的大型语言模型生成边缘情况的数据输入。

持续监控与调整

一个好的测试策略并不会随着我们部署到生产环境而结束。我们需要一些强大的监控和可见性工具来找出在测试阶段可能被忽略的问题。

这其中有:

  • 实时性能监控
  • 异常检测算法以识别不寻常的行为
  • A/B 测试以逐步推出变更
  • 收集并分析用户反馈

可观测性工具常常包含内置的异常检测功能,以帮助在大量的遥测数据中找到重要信息。例如,像Amazon CloudWatch这样的工具支持对指标日志的异常检测。

下面是一个使用简单的统计方法进行异常检测的简单示例:

    import numpy as np
    from scipy import stats
    from typing import List, Union

    class AnomalyDetector:
        def __init__(self, window_size: int = 100) -> None:
            self.window_size: int = window_size
            self.values: List[float] = []

        def add_value(self, value: float) -> None:
            self.values.append(value)
            if len(self.values) > self.window_size:
                self.values.pop(0)

        def is_anomaly(self, new_value: float, z_threshold: float = 3.0) -> bool:
            if len(self.values) < self.window_size:
                return False  # 目前数据不足,无法检测异常

            mean = np.mean(self.values)
            std = np.std(self.values)

            if std == 0:
                return False # 为了避免所有值相同导致的错误。

            z_score = (new_value - mean) / std

            return abs(z_score) > z_threshold

    # 使用方法
    detector = AnomalyDetector()

    # 模拟一些正常数据点
    for _ in range(100):
        detector.add_value(np.random.normal(0, 1))

    # 测试正常值
    print(detector.is_anomaly(1.5))  # 应该是 False

    # 测试异常值
    print(detector.is_anomaly(10))  # 应该是 True

全屏 退出全屏

其他编程语言中的另一种属性驱动测试工具

在下面的示例中,我使用了 Hypothesis Python 模块库。这里有一些其他编程语言中的有趣替代方案。

语言 推荐库 原因
C# FsCheck 在 .NET 生态系统中被广泛使用,支持 C# 和 F#。
Clojure test.check 是 Clojure 的 core.spec 的一部分,与语言集成得很好。
Haskell QuickCheck 基于属性的测试库的原始版本,在 Haskell 中仍然是标准。
Java jqwik 现代设计,文档完备,与 JUnit 5 无缝地集成。
JavaScript fast-check 积极维护,文档完备,与流行的 JS 测试框架集成良好。
Python Hypothesis 在 Python 生态系统中,它是最成熟、功能最丰富的工具,而且被广泛采用。
Scala ScalaCheck Scala 中基于属性的测试的事实上的标准。
Ruby Rantly 相比于其他备选项,维护更为积极,与 RSpec 集成良好。
Rust proptest 相比于 Rust 的 quickcheck,开发更为活跃,具备如失败示例持久化等有用功能。
持续改进

一个好的测试计划应该包含持续改进的措施。例如,可以定期回顾测试结果,调整测试方法等。要不断改进测试,经常看看哪些地方可以改进,然后做出调整。

  • 定期审查测试结果和生产事件
  • 根据新见解更新测试用例
  • 了解新的测试技术及工具
  • 根据系统的变化调整测试策略

实施全面的测试策略绝非易事。这需要结合技术技能、创造力以及对被测系统深入的理解。这需要结合技术技能、创造力以及对被测系统深入的理解。然而,通过结合多种测试方法,利用人工智能和机器学习,并保持持续改进的承诺,我们可以在不确定性面前创建出既稳健又可靠的系统。

当我们展望未来时,唯一可以确定的是这一点:软件测试领域将持续演变。随着系统越来越强大和复杂,新的挑战就会随之而来。基于本文探讨的内容——从属性驱动的测试到人工智能生成的测试,从混沌工程到语义相似度检测——我们有了一个坚实的基础来继续发展。

这里讨论的战略和技术并不是一成不变的。它们只是一个起点,一个基础,在此基础上你可以根据自己的特定需求和挑战添加自己的方法。我们也必须随着软件世界的改变而改变测试方法。我鼓励大家拥抱不确定性,保持好奇心,不断学习。软件测试的未来肯定很有趣,而学习并尽可能塑造它,都掌握在我们自己手中。

要继续学习,请查看一下这个包含这篇文章中所有代码的代码库

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消