黑盒测试(实践)

三角形问题

有一个程序,接收三个整数输入a、b和c,分别表示三角形的三条边。a、b和c的取值范围都为[1,100]。程序根据输入的三条边判断三角形的类型:等边三角形、等腰三角形、普通三角形和不构成三角形。如果输入的a、b和c不在有效范围,程序会输出一条消息来说明问题,如:边a不在有效范围内。

解题:不同的变量进行边界值分析(对一个变量进行分析时另外两个变量为稳定且安全变量)

输入输入输入输出
abc
05050边a不在有效范围内
15050等腰三角形
25050等腰三角形
995050等腰三角形
1005050不构成三角形
1015050边a不在有效范围内
50050边b不在有效范围内
50150等腰三角形
50250等腰三角形
509950等腰三角形
5010050不构成三角形
5010150边b不在有效范围内
50500边c不在有效范围内
50501等腰三角形
50502等腰三角形
505099等腰三角形
5050100不构成三角形
5050101边c不在有效范围内
505050等边三角形

【计算题】

佣金问题

:某公司生产机器人及部件,机器人包含3大部件:主控模块、通信模块及执行模块。该公司的代理商负责销售机器人整机和部件;公司要求每个代理商每月最少销售一整套机器人(即三类部件至少各销售一个);受限于公司产能,公司每个月最多给每个代理商提供80个主控模块、90个通信模块以及100个执行模块。每个主控模块售价90元、每个通信模块售价60元、每个执行模块售价50元。到6月末的时候,公司会根据代理商的销售情况计算佣金。

佣金计算方法如下:

没有销售额在1000元以下(含)的部分,佣金为10%;

超过1000元但不超过2400元(含)的部分,佣金为15%;

超过2400的部分,佣金为20%。

佣金计算函数:

import java.util.Scanner;

public class test {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while(scanner.hasNext()){
            int x = scanner.nextInt();
            double m =0;
            if(x<=1000){
                m = x*0.1;
            }else if(x<=2400){
                m = (x-1000)*0.15+100;
            }else{
                m = (x-2400)*0.2+310;
            }
            System.out.println(m);
        }
    }
}
主控模块通信模块执行模块
最多8090100
价格906050
  1. 边界分析

按照边界取值方法

模块正常取值边界值选取
主控模块[1,80]{0,1,2,40,79,80,81}
通信模块[1,90]{-1,0,1,45,89,90,91}
执行模块[1,100]{-1,0,1,50,99,100,101}

输入进行考虑

测试用例主控模块通信模块执行模块总销售额预期输出
test01045505200主控模块输入非法
test02145505290佣金:888
test03245505380佣金:906
test0479455012310佣金:2292
test0580455012400佣金:2310
test0681455012490主控模块输入非法
test07400506100通信模块输入非法
test08401506160佣金:1062
test09402506220佣金:1074
test1040895011440佣金:2118
test1140905011500佣金:2130
test1240915011560通信模块输入非法
test13404506300执行模块输入非法
test14404516350佣金:1100
test15404526400佣金:1110
test1640459911250佣金:2080
test17404510011300佣金:2090
test18404510111350执行模块输入非法
test194045508800佣金:1590
  1. 次边界分析

从输出角度对改程序进行测试,因为代理商每月最低售出主控模块1块、通信模块1块、执行模块1块.其销售额为200,佣金为20元.

最多售出主控模块80块、通信模块90块、执行模块100块,其销售额为17600元,佣金为3350元.

销售额等价类划分为[200,1000],(1000,2400],(2400,17600]

按照此等价类分别取边界值为

{

略小于200,200,略大于200,

略小于1000,1000,略大于1000,

略小于2400,2400,略大于2400,

略小于17600,17600,略大约17600

}

测试用例主控模块通信模块执行模块总销售额预期输出
test01110150执行模块输入非法
test02111200佣金:20
test03112250佣金:25
test04554950佣金:95
test055551000佣金:100
test065561050佣金:107.5
test071212112350佣金:302.5
test081212122400佣金:310
test091212132450佣金:320
test1080909917550佣金:3340
test11809010017600佣金:3350
test12809010117650执行模块输入非法

酒水佣金问题

某酒水销售公司指派销售员销售各种酒水,其中白酒卖168元/瓶,红酒卖120元/瓶,啤酒卖5元/瓶。对于每个销售员,白酒每月的最高供应量为5000瓶,红酒为3000瓶,啤酒为30000瓶,各销售员每月至少需售出白酒50瓶,红酒30瓶,啤酒300瓶。奖金计算方法如下:

销售额2万元以下(含)的为4%;

销售额2万元(不含)到4.5万元(含)的为1%;

销售额4.5万元以上(不含)的为0.5%。

(请运用边界值法设计测试用例)

边界值分析

  1. 确定边界
白酒红酒啤酒
最少5030300
最多5000300030000
售价1681205

2)确定边界值

白酒{49,50,51,2525,4999,5000,5001}

红酒{29,30,31,1515,2999,3000,3001}

啤酒{299,300,301,15150,29999,30000,30001}

测试用例白酒红酒啤酒销售额预期输出
test0149151515150265782白酒值输入非法
test0250151515150265950佣金4404.75
test0351151515150266118佣金4405.59
test0449991515151501097382佣金8561.91
test0550001515151501097550佣金8562.75
test0650011515151501097718白酒值输入非法
test0725252915150503430红酒值输入非法
test0825253015150503550佣金5592.75
test0925253115150503670佣金5593.35
test102525299915150859830佣金7374.15
test112525300015150859950佣金7374.75
test122525300115150860070红酒值输入非法
test1325251515299607495啤酒值输入非法
test1425251515300607500佣金6112.5
test1525251515301607505佣金6112.525
test162525151529999755995佣金6854.975
test172525151530000756000佣金6855
test182525151530001756005啤酒值输入非法
test192525151515150681750佣金6483.75

考虑次边界

通过输出考虑

销售额可选取边界{1.35,2,4.5,135}

因此按照等价类可选取边界

{

略小于1.35,1.35,略大于1.35,1.675

略小于2,2,略大于2,3.25

略小于4.5,4.5,略大于4.5,69.75

略小于135,135,略大于135,

}

测试用例白酒红酒啤酒销售额预期输出
test01503029913495啤酒输入非法
test02503030013500佣金:540
test03503030113505佣金:540.2
test045030333416750佣金:3553.75
test055030159919995佣金:799.8
test065030160020000佣金:800
test075030160120005佣金:800.05
test085030648432500佣金:1044.2
test095030659944995佣金:1049.95
test105030660045000佣金:1050
test115030660145005佣金:3300.025
test121115300029813697500佣金:6557.765
test1350003000299991349995佣金:9824.975
test1450003000300001350000佣金:9825
test1550003000300011350005啤酒输入非法

佣金计算代码(快速得到实验数据):

public class test {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        while (scanner.hasNext()) {
            int x = scanner.nextInt();
            double m = 0;
            if (x <= 20000) {
                m = x * 0.04;
            } else if (x <= 45000) {
                m = (x - 20000) * 0.01 + 20000 * 0.04;
            } else { m = (x-45000)*0.005+25000*0.01+20000*0.04; }
            System.out.println(m);
        }
    }
}

日期检查模块

设有一个档案管理系统,要求用户输入以年月表示的日期。假设日期限定在1990年1月~2049年12月,并规定日期由6位数字字符组成,前4位表示年,后2位表示月。现用等价类划分法设计测试用例,来测试程序的"日期检查功能"。(请运用等价类法设计测试用例)

通过等价类划分标准,进行划分等价类

pp

为每个等价类设计测试用例

测试用例实验数据覆盖情况
1200012覆盖有效等价类①⑤⑧
2sasd12覆盖无效等价类②
312312433覆盖无效等价类③
421343覆盖无效等价类④
5122210覆盖无效等价类⑥
6299910覆盖无效等价类⑦
7200000覆盖无效等价类⑨
8200018覆盖无效等价类⑩

下一天日期问题

NextDate是一个接受年(year)、月(month)和日(day)三个输入变量的函数,程序输出所输入日期后面一天的日期。其中,年、月和日的取值满足如下条件:

(1)1896<=year<=2096

(2)1<=month<=12

(3)1<=day<=31

将输入不合法情况统一给出消息提示“输入不在有效范围”

1.列出所有的条件桩和动作桩

  • 条件桩

    Y1={年份:平年}
    Y2={年份:闰年}
    M1={月份:30天/月}
    M2={月份:31天/月}
    M3={月份:12月}
    M4={月份:2月}
    D1={日期:1~27日}
    D2={日期:28日}
    D3={日期:29日}
    D4={日期:30日}
    D5={日期:31日}
    条件桩C1取{Y1,Y2}中其一,C2取{M1,M2,M3,M4}中其一,C3取{ D1, D2, D3, D4, D5}中其一

  • 动作桩:

    A1:不可能
    A2:日期加一
    A3:日期置一
    A4:月份加一
    A5:月份置一
    A6:年份加一

2.确定规则的个数:

​ 除了28,29两种,条件为该项的个数:

​ 所以总的规则项数为4X5+2

3.填入条件项和动作项,形成初始决策表并简化,得到决策表
在这里插入图片描述

4.由决策表设计测试用例:
在这里插入图片描述

实验代码

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.Arrays;
import java.util.List;

public class test {
    public static void main(String[] args) {

        Day day = new Day();
        // System.out.println(testx.getNextDay(2001, 4, 12));
        File file = new File("test.txt");
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(file));
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);

                // 获取日期信息
                String[] times = line.split(" ");
                int yearx = Integer.parseInt(times[0]);
                int monthx = Integer.parseInt(times[1]);
                int dayx = Integer.parseInt(times[2]);
                String nextDay = day.getNextDay(yearx, monthx, dayx);
                System.out.println(nextDay);
            }

        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (Exception e) {
                // TODO: handle exception
            }

        }

    }
}
class Day{
    public int today_Y;
    public int today_M;
    public int today_D;
    public int MaxMonth;
    public List<Integer> Day31Monther;
    public List<Integer> Day30Monther;

    public void init() {
        Integer[] arr31 = { 1, 3, 5, 7, 8, 10, 12 };
        Integer[] arr30 = { 4, 6, 9, 11 };
        Day31Monther = Arrays.asList(arr31);
        Day30Monther = Arrays.asList(arr30);
    }

    public void setTest(int day, int month, int year) {
        init();
        this.today_D = day;
        this.today_M = month;
        this.today_Y = year;
        this.MaxMonth = setMaxMonthDay();

    }

    // 对于输入的数据进行判断,本年,本月,的天数
    public int setMaxMonthDay() {
        Integer month = new Integer(today_M);
        if (Day31Monther.contains(month)) {
            // 如果是三十一天的
            return 31;
        } else if (Day30Monther.contains(month)) {
            return 30;
        } else if (today_M == 2) {

            if (today_Y % 400 == 0 || (today_Y % 100 != 0 && today_Y % 4 == 0)) {
                return 29;
            } else
                return 28;
        }
        return -1;
    }

    public boolean dataTrue() {
        boolean b = true;
        if (today_D > MaxMonth) {
            b = false;
        }
        return b;
    }

    public String getNextDay(int in_year, int in_month, int in_day) {
        setTest(in_day, in_month, in_year);
        // System.out.println(today_D+":"+today_M+":"+today_Y+":"+MaxMonth);
        String info;

        if (!dataTrue()) {
            info = "输入不在有效范围";
            return info;
        }
        int day = today_D;
        int month = today_M;
        int year = today_Y;
        if (day == MaxMonth) {
            if (month == 12) {
                day = 1;
                month = 1;
                year += 1;
            } else {
                day = 1;
                month += 1;
            }
        } else {
            day += 1;
        }
        // day = (day + 1) % (MaxMonth+1);
        // month = (month + (day + 1) / MaxMonth) % 12;
        // year = year + ((month + (day + 1) / MaxMonth) / 12);
        info = year + "年"+  month+ "月"+  day +"日";
        return info;
    }

}

售票问题:

某游乐场售票系统提供三种游戏:“过山车”、“摩天轮”和“海盗船”,门票均为10元。现设计一个自动售票系统,只接受10元、20元的纸币。

(1) 若投入的是10元纸币,并按下“过山车”、“摩天轮”或“海盗船”按钮,就会送出相应游戏的门票。

(2) 若投入的是20元纸币,并在送出相应门票的同时会找还10元纸币(假设不存在没有零钱找的情况)。

  1. 条件桩:

    A1={投入金额10},

    A2={投入金额20},

    C1={选择1(过山车)}

    C2={选择2(摩天轮)}

    C3={选择3(海盗船)}

  2. 动作桩:

    D1={是否找钱},

    P1={出过山车票},

    p2={出摩天轮票},

    p3={出海盗船票}

  3. 确定规格个数: 2*3

  4. 填入条件项和动作项,形成初始决策表并简化,得到决策表

test1test2test3test4test5test6
投币A1A1A1A2A2A2
按键C1C2C3C1C2C3
D1:找10元
P1:出过山车票
P2:出摩天轮票
P3:出海盗船票
  1. 由决策表设计测试用例:
用例编号投币按键预期输出
1101出过山车票
2102出摩天轮票
3103出海盗船票
4201找10元 出过山车票
5202找10元 出摩天轮票
6203找10元 出海盗船票

实验代码

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.Arrays;
import java.util.List;

public class test {
    public static void main(String[] args) {

        Day day = new Day();
        // System.out.println(testx.getNextDay(2001, 4, 12));
        File file = new File("test.txt");
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(file));
            String line;
            while ((line = br.readLine()) != null) {
                System.out.println(line);

                // 获取日期信息
                String[] times = line.split(" ");
                int yearx = Integer.parseInt(times[0]);
                int monthx = Integer.parseInt(times[1]);
                int dayx = Integer.parseInt(times[2]);
                String nextDay = day.getNextDay(yearx, monthx, dayx);
                System.out.println(nextDay);
            }

        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (Exception e) {
                // TODO: handle exception
            }

        }

    }
}
class Day{
    public int today_Y;
    public int today_M;
    public int today_D;
    public int MaxMonth;
    public List<Integer> Day31Monther;
    public List<Integer> Day30Monther;

    public void init() {
        Integer[] arr31 = { 1, 3, 5, 7, 8, 10, 12 };
        Integer[] arr30 = { 4, 6, 9, 11 };
        Day31Monther = Arrays.asList(arr31);
        Day30Monther = Arrays.asList(arr30);
    }

    public void setTest(int day, int month, int year) {
        init();
        this.today_D = day;
        this.today_M = month;
        this.today_Y = year;
        this.MaxMonth = setMaxMonthDay();

    }

    // 对于输入的数据进行判断,本年,本月,的天数
    public int setMaxMonthDay() {
        Integer month = new Integer(today_M);
        if (Day31Monther.contains(month)) {
            // 如果是三十一天的
            return 31;
        } else if (Day30Monther.contains(month)) {
            return 30;
        } else if (today_M == 2) {

            if (today_Y % 400 == 0 || (today_Y % 100 != 0 && today_Y % 4 == 0)) {
                return 29;
            } else
                return 28;
        }
        return -1;
    }

    public boolean dataTrue() {
        boolean b = true;
        if (today_D > MaxMonth) {
            b = false;
        }
        return b;
    }

    public String getNextDay(int in_year, int in_month, int in_day) {
        setTest(in_day, in_month, in_year);
        // System.out.println(today_D+":"+today_M+":"+today_Y+":"+MaxMonth);
        String info;

        if (!dataTrue()) {
            info = "输入不在有效范围";
            return info;
        }
        int day = today_D;
        int month = today_M;
        int year = today_Y;
        if (day == MaxMonth) {
            if (month == 12) {
                day = 1;
                month = 1;
                year += 1;
            } else {
                day = 1;
                month += 1;
            }
        } else {
            day += 1;
        }
        // day = (day + 1) % (MaxMonth+1);
        // month = (month + (day + 1) / MaxMonth) % 12;
        // year = year + ((month + (day + 1) / MaxMonth) / 12);
        info = year + "年"+  month+ "月"+  day +"日";
        return info;
    }

}

隔一日问题:

程序有三个输入变量month、day、year分别作为输入日期的月份、日、年份,通过程序可以输出该输入日期在日历上隔一天的日期。例如:输入为2020年3月21日,则该程序的输出为2020年3月23日。

  1. 列出所有的条件桩和动作桩
  • 条件桩

    Y1={年份:平年}

    Y2={年份:闰年}

    M1={月份:30天/月}

    M2={月份:31天/月}

    M3={月份:12月}

    M4={月份:2月}

    D1={日期:1~26日}

    D2={日期:27日}

    D3={日期:28日}

    D4={日期:29日}

    D5={日期:30日}

    D6={日期:31日}

    条件桩C1取{Y1,Y2}中其一,C2取{M1,M2,M3,M4}中其一,C3取{ D1, D2, D3, D4, D5,D6}中其一

  • 动作桩:

    A1:不可能

    A2:日期加二

    A3:日期置一

    A4:日期置二

    A5:月份加一

    A6:月份置一

    A7:年份加一

  1. 确定规则个数

3*6+1*6*2

  1. 填入条件项和动作项,形成初始决策表并简化,得到决策表

在这里插入图片描述

  1. 由决策表设计测试用例:

在这里插入图片描述

实验代码:

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.util.Arrays;
import java.util.List;

public class test03 {
    public static void main(String[] args) {

        // System.out.println(testx.getNextDay(2001, 4, 12));
        // 测试数据在下面,自行建立文档,只是为了更好的得到实验数据
        File file = new File("test.txt");
        BufferedReader br = null;
        try {
            br = new BufferedReader(new FileReader(file));
            String line;
            GetNextDay kit = new GetNextDay();
            while ((line = br.readLine()) != null) {
                System.out.println(line);

                // 获得时间
                String[] times = line.split(" ");
                int yearx = Integer.parseInt(times[0]);
                int monthx = Integer.parseInt(times[1]);
                int dayx = Integer.parseInt(times[2]);
                String nextDay = kit.getNextDayS(yearx, monthx, dayx);
                System.out.println(nextDay);
            }

        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (Exception e) {
                // TODO: handle exception
            }

        }

    }
}

class GetNextDay {
    public int today_Y;
    public int today_M;
    public int today_D;
    public int MaxMonth;
    public List<Integer> Day31Monther;
    public List<Integer> Day30Monther;

    public void init() {
        Integer[] arr31 = { 1, 3, 5, 7, 8, 10, 12 };
        Integer[] arr30 = { 4, 6, 9, 11 };
        Day31Monther = Arrays.asList(arr31);
        Day30Monther = Arrays.asList(arr30);
    }

    private void setTest(int day, int month, int year) {
        init();
        this.today_D = day;
        this.today_M = month;
        this.today_Y = year;
        this.MaxMonth = setMaxMonthDay();

    }
    // 找到本月最大天数
    public int setMaxMonthDay() {
        Integer month = new Integer(today_M);
        if (Day31Monther.contains(month)) {
            return 31;
        } else if (Day30Monther.contains(month)) {
            return 30;
        } else if (today_M == 2) {

            if (today_Y % 400 == 0 || (today_Y % 100 != 0 && today_Y % 4 == 0)) {
                return 29;
            } else
                return 28;
        }
        return -1;

    }

    public boolean dataTrue() {
        boolean b = true;
        if (today_D > MaxMonth) {
            b = false;
        }
        return b;
    }

    public String getNextDayS(int in_year, int in_month, int in_day) {
        setTest(in_day, in_month, in_year);
        
        String info;

        if (!dataTrue()) {
            info = "输入错误";
            return info;
        }
        int day = today_D;
        int month = today_M;
        int year = today_Y;
        
        day +=2;
        if(day>MaxMonth){
            day++;
            day = day%(MaxMonth+1);
            month++;
            if(month>12){
                month=1;
                year++;
            }
        }

        info = year + "年" + month + "月" + day + "日";
        return info;
    }

}

测试用例:

2001 4 12
2001 4 27
2001 4 28
2001 4 29
2001 4 30
2001 4 31
2001 5 12
2001 5 27
2001 5 28
2001 5 29
2001 5 30
2001 5 31
2001 12 12
2001 12 27
2001 12 28
2001 12 29
2001 12 30
2001 12 31
2001 2 12
2001 2 27
2001 2 28
2001 2 29
2004 2 27
2004 2 28
2004 2 29
2001 2 30
2001 2 31

自动售票机问题:

有一个处理单价为5角钱的饮料的自动售货机软件测试用例的设计。

其规格说明如下:

(1)若投入5角钱的硬币,押下【橙汁】或【啤酒】的按钮,则相应的饮料就送出来。

(2)若投入1元钱的硬币,此时售货机没有零钱找,则一个显示【零钱找完】的红灯亮,这时在押下【橙汁】或【啤酒】按钮后,饮料不送出来而且1元硬币也退出来。

(3)若投入1元钱的硬币,此时售货机有零钱找,则显示【零钱找完】的红灯灭,这时在押下【橙汁】或【啤酒】按钮后,会送出饮料的同时退还5角硬币。

  1. 确定条件桩与条件项

    • 条件桩:

      T1={投5角}

      T2={投1元}

      B1={按【橙汁】按钮}

      B2={按【啤酒】按钮}

      H1={有零钱}

      H2={无零钱}

      T取{T1,T2}其中一项

      B取{B1,B2}其中一项

      H取{H1,H2}其中一项

    • 条件项

      A1={出橙汁}

      A2={出啤酒}

      A3={红灯亮}

      A4={找零钱}

      A5={无响应}

  2. 确定组合可能

    其中H条件只会出现在T2条件下,故总可能为2+2*2 六种可能

  3. 填入条件项和动作项,形成初始决策表并简化,得到决策表

在这里插入图片描述

​ 其中test5和test6中B为无关项,所以可以合并
在这里插入图片描述

  1. 由决策表设计测试用例:

在这里插入图片描述

设置键

T1=1

T2=2

B1=3

B2=4

H1=5

H2=6

实验代码:

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;

public class test04 {
    public static void main(String[] args) {
        // 只为了得到数据,可以自行修改输入流
        File f = new File("text04.txt");
        BufferedReader br = null;
        DoAction doAction = new DoAction();
        try {
            br = new BufferedReader(new FileReader(f));
            String line;
            String result;
            while ((line = br.readLine()) != null) {
                String[] choose = line.split(" ");
                //System.out.println(line);
                result = doAction.getResult(Integer.parseInt(
                    choose[0]), Integer.parseInt(choose[1]), Integer.parseInt(choose[2]));
                System.out.println(result);
            }
        } catch (Exception e) {
            // TODO: handle exception
            e.printStackTrace();
        } finally {
            try {
                if (br != null) {
                    br.close();
                }
            } catch (Exception e) {
                // TODO: handle exception
                e.printStackTrace();
            }
        }

    }
}

class DoAction {
    private final int T1 = 1; // 投5
    private final int T2 = 2; // 投1元
    private final int B1 = 3; // 按橙汁
    private final int B2 = 4; // 按啤酒
    private final int H1 = 5; // 有零钱
    private final int H2 = 6; // 无零钱
    private final String[] responseS = { "出橙汁","出啤酒","找零钱","无响应"} ; 

    public String getResult(int T, int B, int H) {
        StringBuffer info = new StringBuffer("");
        if (H == H2 && T == T2) {
            return responseS[3];
        }
        if (B == B1) {
            info.append(responseS[0]);
        } else {
            info.append(responseS[1]);
        }
        if (T == T2) {
            info.append(" " + responseS[2]);
        }
        return info.toString();
    }
}


实验数据:

1 3 0
1 4 0
2 3 5
2 4 5
2 3 6
Logo

开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!

更多推荐