软件工程实践第二次作业——个人实战

这个作业属于哪个课程https://bbs.csdn.net/forums/2401_CS_SE_FZU
这个作业要求在哪里https://bbs.csdn.net/topics/619294904
这个作业的目标git的应用,网络爬虫与数据收集,文件读写,类和接口封装,单元测试
其他参考文献

CodeArts项目地址

PSP表格

PSPPersonal Software Process Stages预估耗时(分钟)实际耗时(分钟)
Planning计划6070
• Estimate• 估计这个任务需要多少时间6070
Development开发12201280
• Analysis• 需求分析 (包括学习新技术)120150
• Design Spec• 生成设计文档6070
• Design Review• 设计复审3040
• Coding Standard• 代码规范 (为目前的开发制定合适的规范)2030
• Design• 具体设计100120
• Coding• 具体编码700740
• Code Review• 代码复审5060
• Test• 测试(自我测试,修改代码,提交修改)140160
Reporting报告160150
• Test Repor• 测试报告6050
• Size Measurement• 计算工作量2020
• Postmortem & Process Improvement Plan• 事后总结, 并提出过程改进计划8080
合计14401500

解题思路描述

问题1:数据爬取

首先需求是实现一个项目,能够输出2024巴黎奥运会的奖牌总榜以及每日赛程。

那么我们就先点进去链接看看这个数据大致长什么样,先有个印象。

在这里插入图片描述

在这里插入图片描述

然后我们分析:数据的获取方式就两种,要么http请求联网获取,要么本地持久化。可以发现2024巴黎奥运会已经结束了,所以就不需要时刻获取最新的数据,因此我们就可以选用本地持久化的方法,并且获得远优于http请求的数据获取性能。

接着就是如何爬取数据。有两种方式:

  • 第一种就是用requests、beautiful soup这些常规爬虫库爬取。我初步采取了这种方法,结果发现爬下来为空。

在这里插入图片描述

才发现他的数据是用js渲染的,因此我又写了一版用selenium的方法,成功爬取了结果,但是收集到的数据格式已经写死了,灵活性很差。

在这里插入图片描述

  • 第二种方法就是抓包,抓网络请求。我们在这个网站上抓到两个网络请求,他们分别返回一个jsonp格式的数据,分别就是奖牌榜数据和每日赛程和比赛结果的详细数据。而jsonp可以轻易转换成json,并且json具有非常大的好处,首先这是抓的接口,所以数据是最全的,其次json格式规范统一,数据灵活性强,再者json数据可以完美转换成数据类,对于后面的文件操作可以节省很大的工作量,避免很多麻烦。

在这里插入图片描述

然后这两个都用python做处理,把每天的赛程数据分别存储成一个文件(如20240724.json),以及奖牌榜的数据存到medal.json。弄完之后就可以为后面java项目编写做准备。

img

这是爬取并且预处理过后的每日赛程的json数据情况:

img

这是爬取并且预处理过后的奖牌榜的json数据情况:

在这里插入图片描述

这是爬取奖牌榜以及相应数据预处理的python代码

import requests
import re
import json

# 请求的 URL
url = 'https://api.cntv.cn/Olympic/getOlyMedals?serviceId=2024aoyun&olyseason=2024S&itemcode=GEN-------------------------------&t=jsonp&cb=banomedals'

# 发送请求
response = requests.get(url)

# 检查请求是否成功
if response.status_code == 200:
    # 提取 JSONP 包裹的 JSON 数据
    jsonp_text = response.text
    json_data = re.search(r'banomedals\((.*)\)', jsonp_text).group(1)  # 提取括号内的 JSON 数据

    # 解析 JSON 数据
    data = json.loads(json_data)

    # 构建最终的 JSON 结构
    result = {
        'total': data['data']['total'],
        'data': data['data']['medalsList']
    }

    # 将 JSON 数据写入文件
    with open('medal.json', 'w', encoding='utf-8') as f:
        json.dump(result, f, ensure_ascii=False, indent=4)

    print("数据已保存到 medal.json 文件中。")
else:
    print(f"请求失败,状态码: {response.status_code}")

这是爬取赛程详情以及对应数据预处理的python代码:

import requests
import json
import re
from datetime import datetime, timedelta

# 基础URL模板
base_url = 'https://api.cntv.cn/olympic/getOlyMatchList?startdate={}&t=jsonp&cb=OM&serviceId=2024aoyun&olyseason=2024S'

# 日期范围设置
start_date = datetime.strptime('20240724', '%Y%m%d')
end_date = datetime.strptime('20240811', '%Y%m%d')

# 日期范围遍历
current_date = start_date
while current_date <= end_date:
    # 格式化日期
    date_str = current_date.strftime('%Y%m%d')

    # 构造当天的URL
    url = base_url.format(date_str)

    try:
        # 发送请求获取JSONP数据
        response = requests.get(url)

        if response.status_code == 200:
            # 提取JSONP中的JSON部分
            jsonp_text = response.text
            json_data = re.search(r'OM\((.*)\)', jsonp_text).group(1)  # 提取括号内的 JSON 数据

            # 将JSON解析为Python对象
            data = json.loads(json_data)

            # 提取并简化matchList中的所需字段
            match_list = data['data']['matchList']
            simplified_data = []
            for match in match_list:
                # 提取时间部分 (HH:MM) 格式
                start_time = match.get('startdatecn', '')
                if start_time:
                    start_time = start_time.split(' ')[1][:5]  # 提取时间部分

                simplified_match = {
                    'startdatecn': start_time,
                    'itemcodename': match.get('itemcodename', ''),
                    'homename': match.get('homename', ''),
                    'awayname': match.get('awayname', ''),
                    'venuename': match.get('venuename', ''),
                    'homecode': match.get('homecode', ''),
                    'awaycode': match.get('awaycode', ''),
                    'title': match.get('title', '')
                }
                simplified_data.append(simplified_match)

            # 构造最终的数据结构
            final_data = {
                'total': data['data']['total'],
                'data': simplified_data
            }

            # 保存为JSON文件,文件名为日期
            file_name = f"{date_str}.json"
            with open(file_name, 'w', encoding='utf-8') as json_file:
                json.dump(final_data, json_file, ensure_ascii=False, indent=4)

            print(f"已保存简化数据到: {file_name}")
        else:
            print(f"请求失败: {url}, 状态码: {response.status_code}")

    except Exception as e:
        print(f"处理 {date_str} 数据时出错: {e}")

    # 日期加一天
    current_date += timedelta(days=1)

问题2:功能实现

有了这些数据后,那就看看要怎么来规划功能块,以及类的封装。我们按照要求配置好我们的java项目结构后,就能开始一步步实现了。

  • **文件工具类:**这部分封装读文件、写文件、文件清空、文件判空等方法
  • **Json工具类:**这部分封装Json文件的转换成数据类的方法
  • **常量类:**这部分封装一些常量,比如文件路径,正则表达式等等
  • **日期工具类:**这部分用来做赛程日期的合法性判断的相关方法封装
  • **数据类:**实现从Json中提取数据。
  • **处理类:**封装不同输入命令的不同处理方法逻辑,以及对于异常输入的处理。
  • **处理类接口:**对处理类做一层抽象,实现接口封装。

然后按这样去划分就好了。Json的转换就用Gson来做,大体思路如此。

接口设计和实现过程

数据处理器接口

由于核心模块主要负责数据的处理、计算、和业务逻辑。这个模块不依赖任何特定的用户界面,它只提供计算和数据处理的功能。因此我设计一个专门进行数据处理的模块,也就是数据处理器。

/**
 * 奥运数据处理器接口
 *
 * @author robin
 */
public interface OlympicDataProcessor {

    void process() throws IOException;

    void processTotal(String cmd) throws IOException;

    void processSchedule(String cmd, String date) throws IOException;

    void processError();
}

然后,如果要使用它的话,就编写对应的实现类逻辑并接入它。

/**
 * 奥运数据处理器实现类
 *
 * @auther robin
 */
public class OlympicDataProcessorImpl implements OlympicDataProcessor {

    /**
     * 赛程缓存
     */
    private static final Map<String, ScheduleResponse> scheduleCache = new HashMap<>();

    /**
     * 奖牌缓存
     */
    private static MedalResponse medalCache;

    /**
     * 处理输入数据
     *
     * @throws IOException IO异常
     */
    @Override
    public void process() throws IOException {
        String inputFilePath = Constant.inputFilePath;
        List<String> lines = FileUtils.readLines(inputFilePath);
        for (String line : lines) {
            String[] parts = line.split(" ");
            switch (parts.length) {
                case 1:
                    processTotal(parts[0]);
                    break;
                case 2:
                    processSchedule(parts[0], parts[1]);
                    break;
                default:
                    processError();
            }

        }
    }

    /**
     * 处理 total 命令
     *
     * @param cmd 命令
     * @throws IOException IO异常
     */
    public void processTotal(String cmd) throws IOException {
        if (cmd.equals("total")) {
            if (medalCache == null) {
                String medalFilePath = Constant.MEDAL_FILE_PATH;
                if (FileUtils.isFileExist(medalFilePath)) {
                    medalCache = JsonUtils.fromJsonFile(medalFilePath, MedalResponse.class);
                } else {
                    throw new IOException("错误: 未找到奖牌数据文件");
                }
            }
            if (medalCache != null) {
                String output = medalCache.getData().stream()
                        .map(MedalData::toString)
                        .collect(Collectors.joining("\n"));
                FileUtils.appendLine(Constant.outputFilePath, output);
            } else {
                throw new IOException("错误: 读取奖牌数据文件出错");
            }
        } else {
            processError();
        }
    }

    /**
     * 处理 schedule 命令
     *
     * @param cmd  命令
     * @param date 日期
     * @throws IOException IO异常
     */
    public void processSchedule(String cmd, String date) throws IOException {
        // 如果是schedule命令,且日期格式正确
        if (cmd.equals("schedule") && DateUtils.validate(date)) {
            // 检查缓存是否已存在该日期的赛程数据
            if (scheduleCache.containsKey(date)) {
                ScheduleResponse cachedResponse = scheduleCache.get(date);
                writeScheduleToFile(cachedResponse);
                return;
            }

            String scheduleFilePath = Constant.getScheduleFilePath(date);
            // 如果文件存在,读取文件内容并写入输出文件
            if (FileUtils.isFileExist(scheduleFilePath)) {
                ScheduleResponse scheduleResponse = JsonUtils.fromJsonFile(scheduleFilePath, ScheduleResponse.class);
                if (scheduleResponse != null) {
                    // 将数据写入缓存
                    scheduleCache.put(date, scheduleResponse);
                    // 写入文件
                    writeScheduleToFile(scheduleResponse);
                } else {
                    throw new IOException("错误: 读取赛程数据文件出错");
                }
            } else {
                throw new IOException("错误: 未找到对应日期的赛程数据文件");
            }
        }
        // 如果是schedule命令,但日期格式不正确
        else if (cmd.equals("schedule") && !DateUtils.isValidFormat(date)) {
            processError();
        }
        // 如果是schedule命令,但日期不在范围内
        else if (cmd.equals("schedule") && !DateUtils.isInRange(date)) {
            FileUtils.appendLine(Constant.outputFilePath, "N/A");
        }
        // 其他情况
        else {
            processError();
        }
    }

    /**
     * 将赛程数据写入文件
     *
     * @param scheduleResponse 赛程数据
     */
    private void writeScheduleToFile(ScheduleResponse scheduleResponse) {
        String output = scheduleResponse.getData().stream()
                .map(ScheduleData::toString)
                .collect(Collectors.joining("\n"));
        FileUtils.appendLine(Constant.outputFilePath, output);
    }

    /**
     * 处理错误
     */
    public void processError() {
        FileUtils.appendLine(Constant.outputFilePath, "Error");
    }
}

那么在任何需要用到这个核心模块的地方(单元测试、GUI、命令行等等),那就只需要实例化OlympicDataProcessorImpl就好了,然后就可以调用对应的方法,看是要处理total的数据还是schedule的数据,或者是调用完整的数据处理都可以。

关键代码展示

Json数据处理部分 (JsonUtils)

json到数据类的转换可以极大简化我们后续的逻辑处理,就不需要麻烦且容易出错的文本处理了,直接对数据类做实例化,json的字段就是数据类的私有成员变量,然后对字段的操作就转变成对私有成员的操作,就非常方便。所以这个步骤是至关重要的。我们采用google官方开发的Gson进行实现。并采用泛型函数的技巧简化代码书写。

/**
 * JSON 工具类
 *
 * @author robin
 */
public class JsonUtils {

    private static final Gson gson = new GsonBuilder().setPrettyPrinting().create();

    /**
     * 从 JSON 文件中读取数据并转换为指定类型的对象
     *
     * @param filePath JSON 文件路径
     * @param clazz    目标类类型
     * @param <T>      泛型类型,表示返回的对象类型
     * @return 解析后的对象
     * @throws JsonSyntaxException JSON 语法错误
     * @throws Exception           解析 JSON 文件时出错
     */
    public static <T> T fromJsonFile(String filePath, Class<T> clazz) {
        try (FileReader reader = new FileReader(filePath)) {
            return gson.fromJson(reader, clazz);
        }
        catch (JsonSyntaxException e) {
            System.out.println("错误:JSON 语法错误: " + e.getMessage());
            return null;
        }
        catch (Exception e) {
            System.out.println("错误:解析 JSON 文件时出错: " + e.getMessage());
            return null;
        }
    }

}

日期处理部分 (DateUtils)

因为查询赛程这个功能需要用到时间,然后需要对这个时间做各种处理,比如时间是否符合格式时间是否有在范围内等,因此对这个的封装也是很有必要的,可以让代码更清晰。而最终就可以简洁地暴露一个validate的方法供上层函数调用。

/**
 * 日期工具类
 *
 * @author robin
 */
public class DateUtils {

    /**
     * 日期正则表达式
     */
    public static final Pattern DATE_PATTERN = Pattern.compile("\\d{4}");

    /**
     * 判断日期格式是否正确
     *
     * @param dateStr 日期字符串
     * @return 是否正确
     */
    public static boolean isValidFormat(String dateStr) {
        return DATE_PATTERN.matcher(dateStr).matches();
    }

    /**
     * 判断日期是否在范围内
     *
     * @param dateStr 日期字符串
     * @return 是否在范围内
     */
    public static boolean isInRange(String dateStr) {
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd");
        LocalDate date = LocalDate.parse("2024" + dateStr, formatter);
        LocalDate startDate = LocalDate.of(2024, 7, 24);
        LocalDate endDate = LocalDate.of(2024, 8, 11);
        return !date.isBefore(startDate) && !date.isAfter(endDate);
    }

    /**
     * 验证日期是否正确且在范围内
     *
     * @param dateStr 日期字符串
     * @return 是否正确且在范围内
     */
    public static boolean validate(String dateStr) {
        if (!isValidFormat(dateStr)) {
            return false;
        }
        return isInRange(dateStr);
    }

常数类 (Constant)

常数类可以做一个全局状态管理,也可以集中对一些很重要的常量进行封装,方便后续维护。本项目就是对应的各种文件路径的维护

/**
 * 常量类
 *
 * @author robin
 */
public class Constant {

    public static String inputFilePath = "input.txt";

    public static String outputFilePath = "output.txt";

    public static final String EXPECTED_TOTAL_FILE_PATH = "expectedTotal.txt";

    public static final String EXPECTED_SCHEDULE_FILE_PATH = "expectedSchedule.txt";

    public static final String MEDAL_FILE_PATH = "./src/data/medal.json";

    private static final String SCHEDULE_FILE_PATH_BASE = "./src/data/2024";

    /**
     *
     * @param inputPath 输入文件路径
     * @param outputPath 输出文件路径
     */
    public static void initialize(String inputPath, String outputPath) {
            inputFilePath = inputPath;
            outputFilePath = outputPath;
    }

    /**
     * 获取赛程文件路径
     *
     * @param date 日期
     * @return 赛程文件路径
     */
    public static String getScheduleFilePath(String date) {
        return SCHEDULE_FILE_PATH_BASE + date + ".json";
    }


文件工具类 (FileUtils)

这一部分主要就是把文件的读文件的写判断文件是否为空清空文件这些关于文件操作的方法,进行集中管理和统一封装,这样子外层就只需要关注它本身想实现的逻辑本身,而不用操心文件的处理该怎么办,也就是我们只需要在这个文件工具类写好就行了。实现较好的低耦合,便于后续维护。

/**
 * 文件工具类
 *
 * @author robin
 */
public class FileUtils {

    /**
     * 检查文件是否存在
     *
     * @param filePath 文件路径
     * @return 是否存在
     */
    public static boolean isFileExist(String filePath) {
        File file = new File(filePath);
        return file.exists();
    }

    /**
     * 读取文件的所有行
     *
     * @param filePath 文件路径
     * @return 所有行
     */
    public static List<String> readLines(String filePath) {
        List<String> lines = new ArrayList<>();
        try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
            String line;
            while ((line = reader.readLine()) != null) {
                lines.add(line);
            }
        } catch (IOException e) {
            System.out.println("错误:读取文件所有行时文件不存在 " + e.getMessage());
        }
        return lines;
    }

    /**
     * 追加写入单行数据
     *
     * @param filePath 文件路径
     * @param line    单行数据
     */
    public static void appendLine(String filePath, String line) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath, true))) {
            writer.write(line);
            writer.newLine();
        } catch (IOException e) {
            System.out.println("追加单行数据时出错: " + e.getMessage());
        }
    }

    /**
     * 清空文件内容
     *
     * @param filePath 文件路径
     */
    public static void clearFile(String filePath) {
        try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {
            writer.write("");
        } catch (IOException e) {
            System.out.println("清空文件时出错: " + e.getMessage());
        }
    }

性能改进

改进分析

我们可以发现,这个项目最核心,也是开销时间最大的部分其实就是那个数据处理器,然后其中最主要的时间开销,其实都是集中在文件读写部分。尤其是因为数据有限,所以很多数据被反复重复读,这样就有很多不必要的时间开销,所以我们可以利用静态变量实现缓存的效果

首先就是这两个的定义:

/**
 * 赛程缓存
 */
private static final Map<String, ScheduleResponse> scheduleCache = new HashMap<>();

/**
 * 奖牌缓存
 */
private static MedalResponse medalCache;
total的缓存优化逻辑
  1. 首先如果当时缓存没东西,那我就把json转换的结果存缓存
  2. 如果medal文件正常,那从此以后这个medalCache都是有东西的
  3. 所以如果medal文件为空,说明就是json转换出错了,我们就抛错
  4. 如果medalCache有数据,那就是直接用了
        if (medalCache == null) {
            String medalFilePath = Constant.MEDAL_FILE_PATH;
            if (FileUtils.isFileExist(medalFilePath)) {
                medalCache = JsonUtils.fromJsonFile(medalFilePath, MedalResponse.class);
            } else {
                throw new IOException("错误: 未找到奖牌数据文件");
            }
        }
        if (medalCache != null) {
            String output = medalCache.getData().stream()
                    .map(MedalData::toString)
                    .collect(Collectors.joining("\n"));
            FileUtils.appendLine(Constant.outputFilePath, output);
        } else {
            throw new IOException("错误: 读取奖牌数据文件出错");
        }
    } else {
        processError();
    }
}
schedule的缓存逻辑
  1. 每次接收到对某一天赛程的搜索,先在缓存里找找有没有
  2. 如果有就直接写入文件,然后就结束了
  3. 不然就继续,看看赛程数据文件存不存在
  4. 如果存在就把当前这天的赛程数据文件存进缓存
  5. 并同时写入到文件
// 如果是schedule命令,且日期格式正确
if (cmd.equals("schedule") && DateUtils.validate(date)) {
    // 检查缓存是否已存在该日期的赛程数据
    if (scheduleCache.containsKey(date)) {
        ScheduleResponse cachedResponse = scheduleCache.get(date);
        writeScheduleToFile(cachedResponse);
        return;
    }
    String scheduleFilePath = Constant.getScheduleFilePath(date);
    // 如果文件存在,读取文件内容并写入输出文件
    if (FileUtils.isFileExist(scheduleFilePath)) {
        ScheduleResponse scheduleResponse = 	
          JsonUtils.fromJsonFile(scheduleFilePath,ScheduleResponse.class);
        if (scheduleResponse != null) {
            // 将数据写入缓存
            scheduleCache.put(date, scheduleResponse);
            // 写入文件
            writeScheduleToFile(scheduleResponse);
        } else {
            throw new IOException("错误: 读取赛程数据文件出错");
        }
    } else {
        throw new IOException("错误: 未找到对应日期的赛程数据文件");
    }
}
// 如果是schedule命令,但日期格式不正确
else if (cmd.equals("schedule") && !DateUtils.isValidFormat(date)) {
    processError();
}
// 如果是schedule命令,但日期不在范围内
else if (cmd.equals("schedule") && !DateUtils.isInRange(date)) {
    FileUtils.appendLine(Constant.outputFilePath, "N/A");
}
// 其他情况
else {
    processError();
}

改进效果

单元测试的混合命令测试中,这个测试用例是基本上在一次输入中测试各种输入情况,包括所有日期的赛程,奖牌榜,各种错误边界情况,所以整个的运算量还是比较庞大的,因此我们以这个做评估分析。

单独运行这个测试用例,得到的时间开销是286ms左右。 完整单元测试下来,Test18的时间开销是52ms左右。速度提升了450.0%

img

img

单元测试

测试覆盖率

类覆盖率为100%,函数覆盖率100%,就算是代码行覆盖率也达到了极高的94%。

img

单元测试样例设计

在我的单元测试设计中,我针对不同的输入场景和错误情况进行了全面覆盖,确保程序在各种情况下都能正确运行。测试分为以下几类:

  1. 参数不足或过多的情况
  2. 输入文件不存在
  3. 错误的 JSON 文件格式
  4. 总计(total)的正确性测试
  5. 赛程(schedule)的正确性测试
  6. 混合命令的测试
1. 参数不足或过多的情况

这些测试主要是为了确保当程序接受到不同数量的参数时,能够正确处理并给出合适的提示。比如,当参数不足或过多时,程序应该打印相应的错误信息。

@Test
public void test01() {
    // 参数不足情况:0个参数
    System.out.println("-----Test1: 参数不足情况:0个参数-----");
    OlympicSearch.main(new String[]{""});
}

@Test
public void test02() {
    // 参数不足情况:1个参数
    System.out.println("-----Test2: 参数不足情况:1个参数-----");
    OlympicSearch.main(new String[]{"xxxxx.txt"});
}

@Test
public void test03() {
    // 参数过多情况:3个参数
    System.out.println("-----Test3: 参数过多情况:3个参数-----");
    OlympicSearch.main(new String[]{"xxxxx.txt", "xxxxx.txt", "xxxxx.txt"});
}

@Test
public void test04() {
    // 参数过多情况:4个参数
    System.out.println("-----Test4: 参数过多情况:4个参数-----");
    OlympicSearch.main(new String[]{"xxxxx.txt", "xxxxx.txt", "xxxxx.txt", "xxxxx.txt"});
}
2. 输入文件不存在

在这种情况下,测试了当输入的文件路径不存在时,程序能否正确处理并给出提示。

@Test
public void test05() {
    // 输入文件不存在情况
    System.out.println("-----Test5: 输入文件不存在情况-----");
    OlympicSearch.main(new String[]{"xxxxx.txt", "xxxxx.txt"});
}
3. 错误的 JSON 文件格式

这个测试用于检查当 JSON 文件格式有问题时,程序是否能够捕获错误并给出提示。具体是模拟将 20240724.json 文件的第一个左大括号删掉的情况。

@Test
public void test06() {
    // json文件错误情况(把20240724.json文件的第一个左大括号删掉了)
    System.out.println("-----Test6: json文件错误情况(把20240724.json文件的第一个左大括号删掉了)-----");
    OlympicSearch.main(new String[]{"input1.txt", "output1.txt"});
}
4. 总计(total)的测试

此测试确保当输入 total 命令时,程序能够正确输出奖牌总数信息,并与预期的结果进行比较。

@Test
public void test07() {
    // 正常情况:测试单行total
    System.out.println("-----Test7: 正常情况:测试单行total-----");
    OlympicSearch.main(new String[]{"input2.txt", "output2.txt"});
    List<String> samples = FileUtils.readLines("expectedOutput2.txt");
    List<String> outputs = FileUtils.readLines("output2.txt");
    assertEquals(samples.size(), outputs.size(), "文件行数不一致");
    for (int i = 0; i < samples.size(); i++) {
        assertEquals(samples.get(i), outputs.get(i), "文件内容不一致,行号:" + (i + 1));
    }
    System.out.println("文件内容完全一致, 测试通过");
}
5. 赛程(schedule)的测试

此部分的测试主要针对 schedule 命令,测试包括日期格式不正确、日期超出范围等异常情况。

@Test
public void test08() {
    // 错误情况:测试schedule之缺少日期参数 (schedule)
    System.out.println("-----Test8: 错误情况:测试schedule之缺少日期参数 (schedule)-----");
    OlympicSearch.main(new String[]{"input3.txt", "output3.txt"});
    List<String> samples = FileUtils.readLines("expectedOutput3.txt");
    List<String> outputs = FileUtils.readLines("output3.txt");
    assertEquals(samples.size(), outputs.size(), "文件行数不一致");
    for (int i = 0; i < samples.size(); i++) {
        assertEquals(samples.get(i), outputs.get(i), "文件内容不一致,行号:" + (i + 1));
    }
    System.out.println("文件内容完全一致, 测试通过");
}

@Test
public void test09() {
    // 错误情况:测试schedule之日期格式错误 (schedule 114514)
    System.out.println("-----Test9: 错误情况:测试schedule之日期格式错误 (schedule 114514)-----");
    OlympicSearch.main(new String[]{"input4.txt", "output4.txt"});
    List<String> samples = FileUtils.readLines("expectedOutput4.txt");
    List<String> outputs = FileUtils.readLines("output4.txt");
    assertEquals(samples.size(), outputs.size(), "文件行数不一致");
    for (int i = 0; i < samples.size(); i++) {
        assertEquals(samples.get(i), outputs.get(i), "文件内容不一致,行号:" + (i + 1));
    }
    System.out.println("文件内容完全一致, 测试通过");
}

@Test
public void test10() {
    // 错误情况:测试schedule之日期格式错误 (schedule mewww)
    System.out.println("-----Test10: 错误情况:测试schedule之日期格式错误 (schedule mewww)-----");
    OlympicSearch.main(new String[]{"input5.txt", "output5.txt"});
    List<String> samples = FileUtils.readLines("expectedOutput5.txt");
    List<String> outputs = FileUtils.readLines("output5.txt");
    assertEquals(samples.size(), outputs.size(), "文件行数不一致");
    for (int i = 0; i < samples.size(); i++) {
        assertEquals(samples.get(i), outputs.get(i), "文件内容不一致,行号:" + (i + 1));
    }
    System.out.println("文件内容完全一致, 测试通过");
}

@Test
public void test11() {
    // 错误情况:测试schedule之日期不在范围 (schedule 0723)
    System.out.println("-----Test11: 错误情况:测试schedule之日期不在范围 (schedule 0723)-----");
    OlympicSearch.main(new String[]{"input6.txt", "output6.txt"});
    List<String> samples = FileUtils.readLines("expectedOutput6.txt");
    List<String> outputs = FileUtils.readLines("output6.txt");
    assertEquals(samples.size(), outputs.size(), "文件行数不一致");
    for (int i = 0; i < samples.size(); i++) {
        assertEquals(samples.get(i), outputs.get(i), "文件内容不一致,行号:" + (i + 1));
    }
    System.out.println("文件内容完全一致, 测试通过");
}

@Test
public void test12() {
    // 错误情况:测试schedule之日期不在范围 (schedule 0812)
    System.out.println("-----Test12: 错误情况:测试schedule之日期不在范围 (schedule 0812)-----");
    OlympicSearch.main(new String[]{"input7.txt", "output7.txt"});
    List<String> samples = FileUtils.readLines("expectedOutput7.txt");
    List<String> outputs = FileUtils.readLines("output7.txt");
    assertEquals(samples.size(), outputs.size(), "文件行数不一致");
    for (int i = 0; i < samples.size(); i++) {
        assertEquals(samples.get(i), outputs.get(i), "文件内容不一致,行号:" + (i + 1));
    }
    System.out.println("文件内容完全一致, 测试通过");
}
6. 正确的 schedule 命令测试

此测试用于检查当 schedule 命令输入正确时,程序能否正确处理,并输出相应的赛程信息。

@Test
public void test13() {
    // 正常情况:测试schedule之日期在范围内 (schedule 0724)
    System.out.println("-----Test13: 正常情况:测试schedule之日期在范围内 (schedule 0724)-----");
    OlympicSearch.main(new String[]{"input8.txt", "output8.txt"});
    List<String> samples = FileUtils.readLines("expectedOutput8.txt");
    List<String> outputs = FileUtils.readLines("output8.txt");
    assertEquals(samples.size(), outputs.size(), "文件行数不一致");
    for (int i = 0; i < samples.size(); i++) {
        assertEquals(samples.get(i), outputs.get(i), "文件内容不一致,行号:" + (i + 1));
    }
    System.out.println("文件内容完全一致, 测试通过");
}
7. 错误命令测试

这些测试确保当输入的命令不正确时,程序能捕获错误并处理。例如输入了一个不存在的命令或者错误的参数。

@Test
public void test14() {
    // 错误情况:出现错误的命令 (mewww)
    System.out.println("-----Test14: 错误情况:出现错误的命令 (mewww)-----");
    OlympicSearch.main(new String[]{"input9.txt", "output9.txt"});
    List<String> samples = FileUtils.readLines("expectedOutput9.txt");
    List<String> outputs = FileUtils.readLines("output9.txt");
    assertEquals(samples.size(), outputs.size(), "文件行数不一致");
    for (int i = 0; i < samples.size(); i++) {
        assertEquals(samples.get(i), outputs.get(i), "文件内容不一致,行号:" + (i + 1));
    }
    System.out.println("文件内容完全一致, 测试通过");
}

@Test
public void test15() {
    // 错误情况:出现错误的命令 (schedule 114514 mewww)
    System.out.println("-----Test15: 错误情况:出现错误的命令 (schedule 114514 mewww)-----");
    OlympicSearch.main(new String[]{"input10.txt", "output10.txt"});
    List<String> samples = FileUtils.readLines("expectedOutput10.txt");
    List<String> outputs = FileUtils.readLines("output10.txt");
    assertEquals(samples.size(), outputs.size(), "文件行数不一致");
    for (int i = 0; i < samples.size(); i++) {
        assertEquals(samples.get(i), outputs.get(i), "文件内容不一致,行号:" + (i + 1));
    }
    System.out.println("文件内容完全一致, 测试通过");
}

@Test
public void test16() {
    // 错误情况:出现错误的命令 (mewww 114514)
    System.out.println("-----Test16: 错误情况:出现错误的命令 (mewww 114514)-----");
    OlympicSearch.main(new String[]{"input11.txt", "output11.txt"});
    List<String> samples = FileUtils.readLines("expectedOutput11.txt");
    List<String> outputs = FileUtils.readLines("output11.txt");
    assertEquals(samples.size(), outputs.size(), "文件行数不一致");
    for (int i = 0; i < samples.size(); i++) {
        assertEquals(samples.get(i), outputs.get(i), "文件内容不一致,行号:" + (i + 1));
    }
    System.out.println("文件内容完全一致, 测试通过");
}

@Test
public void test17() {
    // 错误情况:出现错误的命令 (total mewww)
    System.out.println("-----Test17: 错误情况:出现错误的命令 (total mewww)-----");
    OlympicSearch.main(new String[]{"input12.txt", "output12.txt"});
    List<String> samples = FileUtils.readLines("expectedOutput12.txt");
    List<String> outputs = FileUtils.readLines("output12.txt");
    assertEquals(samples.size(), outputs.size(), "文件行数不一致");
    for (int i = 0; i < samples.size(); i++) {
        assertEquals(samples.get(i), outputs.get(i), "文件内容不一致,行号:" + (i + 1));
    }
    System.out.println("文件内容完全一致, 测试通过");
}

8. 正确的混合命令测试

此测试用于检查当 scheduletotal命令都是正确的情况下,把两者命令进行混合输入,同时还加上了上述的各种错误情况的输入。

混合测试用例内容如下:

mewww
total mewww
schedule
schedule 114514
schedule mewww
schedule 0801 mewww
schedule 1001
total
schedule 0725
schedule 0726
schedule 0727
schedule 0728
schedule 0729
schedule 0730
schedule 0731
schedule 0801
schedule 0802
schedule 0803
schedule 0804
schedule 0805
schedule 0806
schedule 0807
schedule 0808
schedule 0809
schedule 0810
schedule 0811
@Test
public void test18() {
    // 正常情况:schedule与total混合情况
    System.out.println("-----Test18: 混合情况:以上出现的所有schedule和total的混合情况-----");
    OlympicSearch.main(new String[]{"input13.txt", "output13.txt"});
    List<String> samples = FileUtils.readLines("expectedOutput13.txt");
    List<String> outputs = FileUtils.readLines("output13.txt");
    assertEquals(samples.size(), outputs.size(), "文件行数不一致");
    for (int i = 0; i < samples.size(); i++) {
        assertEquals(samples.get(i), outputs.get(i), "文件内容不一致,行号:" + (i + 1));
    }
    System.out.println("文件内容完全一致, 测试通过");
}
9. total模块单元测试

我们准备了一个奖牌榜的正确输出结果,然后将其与直接调用total模块的结果做比较,最终结果是两者文件内容完全一致,说明total模块的正确性可以笃定

@Test
public void testTotal() {
    System.out.println("-----TestTotal: Total的单元测试-----");
    Constant.initialize( "input.txt", "output.txt");
    FileUtils.clearFile(Constant.outputFilePath);
    List<String> samples = FileUtils.readLines(Constant.EXPECTED_TOTAL_FILE_PATH);
    OlympicDataProcessorImpl processor = new OlympicDataProcessorImpl();
    try {
        processor.processTotal("total");
        List<String> outputs = FileUtils.readLines(Constant.outputFilePath);
        assertEquals(samples.size(), outputs.size(), "文件行数不一致");
        for (int i = 0; i < samples.size(); i++) {
            assertEquals(samples.get(i), outputs.get(i), "文件内容不一致,行号:" + (i + 1));
        }
        System.out.println("文件内容完全一致, Total的单元测试通过");
    } catch (Exception e) {
        System.out.println("测试抛错: " + e.getMessage());
    }
}
10. schedule模块单元测试

同理,我们准备了一个7月30日的赛程的正确数据结果,然后与其和我们跑出来的output相比对,结果完全一致,说明schedule的正确性也有所保证。

@Test
public void testSchedule() {
    // 针对Schedule的单元测试
    System.out.println("-----TestSchedule: Schedule的单元测试-----");
    Constant.initialize( "input.txt", "output.txt");
    FileUtils.clearFile(Constant.outputFilePath);
    List<String> samples = FileUtils.readLines(Constant.EXPECTED_SCHEDULE_FILE_PATH);
    OlympicDataProcessorImpl processor = new OlympicDataProcessorImpl();
    try {
        processor.processSchedule("schedule", "0730");
        List<String> outputs = FileUtils.readLines(Constant.outputFilePath);
        assertEquals(samples.size(), outputs.size(), "文件行数不一致");
        for (int i = 0; i < samples.size(); i++) {
            assertEquals(samples.get(i), outputs.get(i), "文件内容不一致,行号:" + (i + 1));
        }
        System.out.println("文件内容完全一致, Schedule的单元测试通过");
    } catch (Exception e) {
        System.out.println("测试抛错: " + e.getMessage());
    }
}

异常处理

命令行参数

这个部分对命令行的各种情况做详尽的异常分析,确保只有参数数量正确才能往下走,并对各种例外情况做细致的说明。

if (args.length == 0) {
    System.out.println("错误:读取到0个文件目录,请提供输入文件和输出文件的路径。例如:input.txt output.txt");
    return;
} else if (args.length == 1) {
    if (args[0].isEmpty()) {
        System.out.println("错误:读取到0个文件目录,请提供输入文件和输出文件的路径。例如:input.txt output.txt");
    } else {
        System.out.println("错误:只读取到1个文件目录,请提供输出文件的路径。例如:input.txt output.txt");
    }
    return;
} else if (args.length > 2) {
    System.out.println("错误:读取到多个文件目录,请提供输入文件和输出文件的路径。例如:input.txt output.txt");
    return;
}

if (!FileUtils.isFileExist(args[0])) {
    System.out.println("错误:输入文件不存在");
    return;
}

Json提取

这一部分就包括异常处理了。我特地把20240724的json的格式破坏掉,即删掉开头的第一个左大括号,来触发这个情况。对于这个情况就需要精确的异常捕获,这样才不会崩溃。然后对应的这个json转换函数在遇到无效json后,return的是null,那么我们在上级方法中也要对这个null做一下检查,防止触发意料外的异常抛出。

public static <T> T fromJsonFile(String filePath, Class<T> clazz) {
    try (FileReader reader = new FileReader(filePath)) {
        return gson.fromJson(reader, clazz);
    }
    catch (JsonSyntaxException e) {
        System.out.println("错误:JSON 语法错误: " + e.getMessage());
        return null;
    }
    catch (Exception e) {
        System.out.println("错误:解析 JSON 文件时出错: " + e.getMessage());
        return null;
    }
}

我们手动定制一个IOException实例然后抛出,增强我们项目的鲁棒性。

if (medalCache != null) {
    String output = medalCache.getData().stream()
            .map(MedalData::toString)
            .collect(Collectors.joining("\n"));
    FileUtils.appendLine(Constant.outputFilePath, output);
} else {
    throw new IOException("错误: 读取奖牌数据文件出错");
}

文件操作

由于涉及到IO,所以比如说文件不存在、文件损坏等各种情况,也会触发异常抛出。因此我们需要在这些地方及时catch,并将异常提示信息改成自己的内容和格式,统一风格。

/**
 * 读取文件的所有行
 *
 * @param filePath 文件路径
 * @return 所有行
 */
public static List<String> readLines(String filePath) {
    List<String> lines = new ArrayList<>();
    try (BufferedReader reader = new BufferedReader(new FileReader(filePath))) {
        String line;
        while ((line = reader.readLine()) != null) {
            lines.add(line);
        }
    } catch (IOException e) {
        System.out.println("错误:读取文件所有行时文件不存在 " + e.getMessage());
    }
    return lines;
}

/**
 * 追加写入单行数据
 *
 * @param filePath 文件路径
 * @param line    单行数据
 */
public static void appendLine(String filePath, String line) {
    try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath, true))) {
        writer.write(line);
        writer.newLine();
    } catch (IOException e) {
        System.out.println("追加单行数据时出错: " + e.getMessage());
    }
}

/**
 * 清空文件内容
 *
 * @param filePath 文件路径
 */
public static void clearFile(String filePath) {
    try (BufferedWriter writer = new BufferedWriter(new FileWriter(filePath))) {
        writer.write("");
    } catch (IOException e) {
        System.out.println("清空文件时出错: " + e.getMessage());
    }
}

心得体会

通过此次实践作业,我锻炼了对程序进行单元测试的技能,在过去的开发中我没有这样的习惯,因为当时总是觉得浪费时间+没有用,尤其是在并不强制要求测试的情况下,我对测试这块就没有很重视。

经过这次实践,我通过单元测试发现了不少疏漏和错误,也确实方便了我的调试。这让我明白了单元测试在规范、标准的生产开发环境下还是非常有用的,尤其是在比较大型的项目中,单元测试所带来的便利会是巨大的。

git情况

commit记录

img

MR请求

img

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值