4、单元测试方法:针对解析器的单元测试框架(JUnit/Pytest)、Mock对象的使用、边界值测试

单元测试,说白了就是给代码的每个最小单元做体检。我做了这么多年金融协议解析,最深的体会就是:解析器这玩意儿,不出事则已,一出事就是大问题。你想想看,一个字段解析错了,可能整个交易流水就乱了,钱算错了,那可不是闹着玩的。

所以今天咱们聊聊,怎么给解析器写靠谱的单元测试。

4.1 为什么解析器需要专门的单元测试?

金融协议解析器有个特点:输入格式严格,输出要求精确。不像普通业务逻辑,解析器面对的是二进制流、定长字段、校验码这些东西。我见过太多项目,解析器看起来跑通了,结果上线第一天就崩了——因为遇到了一个没考虑到的边界情况。

我个人习惯,写解析器之前,先把测试框架搭好。这不是浪费时间,这是给自己上保险。

4.2 单元测试框架:JUnit vs Pytest

Java项目用JUnit,Python项目用Pytest,这基本是行业共识。但具体怎么用,还是有讲究的。

4.2.1 JUnit 实战(Java)

假设我们有个FIX协议的解析器,要测试解析一条订单消息。我会这么写:

public class FixParserTest {
    
    @Test
    public void testParseOrderMessage() {
        // 准备:构造一条标准的FIX消息
        String fixMessage = "8=FIX.4.2|9=78|35=D|49=INVESTOR|56=BROKER|55=AAPL|54=1|38=100|40=2|44=150.00|10=123|";
        
        // 执行
        FixMessage result = FixParser.parse(fixMessage);
        
        // 验证
        assertEquals("FIX.4.2", result.getBeginString());
        assertEquals("D", result.getMsgType());
        assertEquals("AAPL", result.getSymbol());
        assertEquals(100, result.getOrderQty());
        assertEquals(150.00, result.getPrice(), 0.001);
    }
}

这里有个小技巧:测试方法名要能说明意图。我见过有人写test1、test2,过两周自己都看不懂测的是什么。

4.2.2 Pytest 实战(Python)

Python这边更灵活一些。我习惯用fixture来管理测试数据:

import pytest
from fix_parser import FixParser

@pytest.fixture
def sample_fix_message():
    return "8=FIX.4.2|9=78|35=D|49=INVESTOR|56=BROKER|55=AAPL|54=1|38=100|40=2|44=150.00|10=123|"

def test_parse_order_message(sample_fix_message):
    result = FixParser.parse(sample_fix_message)
    
    assert result.begin_string == "FIX.4.2"
    assert result.msg_type == "D"
    assert result.symbol == "AAPL"
    assert result.order_qty == 100
    assert result.price == 150.00
我的经验:测试数据尽量用真实交易中的消息,但要去掉敏感信息。我曾经用假数据测试,结果上线后才发现真实数据里有些字段是可选填的,解析器直接崩了。

4.3 Mock对象的使用

解析器往往不是孤立的。它可能要调用数据库查字典表,或者通过网络获取最新汇率。这时候就需要Mock了。

为什么要Mock? 说白了,就是让测试只关注解析器本身,不依赖外部环境。你想想看,如果测试一个解析器还要连数据库,那这测试跑起来得多慢?而且数据库一挂,测试全红,你都不知道是解析器的问题还是数据库的问题。

4.3.1 Mock 场景举例

假设我们的解析器在解析SWIFT消息时,需要查询一个货币代码表:

public class SwiftParser {
    private CurrencyService currencyService;
    
    public SwiftMessage parse(String rawMessage) {
        // 解析逻辑...
        String currencyCode = extractCurrencyCode(rawMessage);
        String currencyName = currencyService.getCurrencyName(currencyCode);
        // 继续解析...
    }
}

测试时,我们不想真的去查数据库。用Mockito可以这样:

@Test
public void testParseWithMockCurrencyService() {
    // 创建Mock对象
    CurrencyService mockService = mock(CurrencyService.class);
    when(mockService.getCurrencyName("USD")).thenReturn("美元");
    
    // 注入Mock
    SwiftParser parser = new SwiftParser(mockService);
    
    // 执行测试
    SwiftMessage result = parser.parse("{1:F01BANKBEBBAXXX}{2:O103...}");
    
    // 验证
    assertEquals("美元", result.getCurrencyName());
    verify(mockService).getCurrencyName("USD");
}
核心原则:Mock外部依赖,但不要Mock解析器自身的逻辑。我见过有人把解析器内部的方法也Mock了,那测试还有什么意义?

4.4 边界值测试

这是解析器测试里最容易翻车的地方。我总结了几类常见的边界情况:

边界类型 示例 测试要点
空值/空字符串 消息体为空 应抛出明确异常,而非空指针
字段长度边界 字段最大长度、最小长度 定长字段截断或填充的处理
数值边界 金额为0、负数、极大值 精度丢失、溢出问题
特殊字符 分隔符出现在字段值中 转义处理是否正确
可选字段缺失 非必填字段未出现 默认值或空值处理

举个例子,测试一个金额字段的解析:

@Test
public void testParseAmountBoundary() {
    // 测试0值
    assertEquals(new BigDecimal("0.00"), parser.parseAmount("0.00"));
    
    // 测试负数
    assertEquals(new BigDecimal("-100.50"), parser.parseAmount("-100.50"));
    
    // 测试极大值
    assertEquals(new BigDecimal("999999999999.99"), 
                 parser.parseAmount("999999999999.99"));
    
    // 测试精度
    assertEquals(new BigDecimal("100.123"), parser.parseAmount("100.123"));
}
我曾经踩过的坑:测试一个ISO8583的金额字段,长度是12位,最后2位是小数。我测试了正常值,但没测全0的情况。结果上线后,有一笔交易金额是0,解析器直接抛了NumberFormatException。从那以后,0值、空值、负值成了我测试用例里的标配。

4.5 知识体系总览

下面这张图,是我对解析器单元测试方法的一个总结。你可以把它当作一个检查清单:

解析器单元测试方法体系 解析器单元测试 测试框架 JUnit (Java) Pytest (Python) Mock对象 Mockito / unittest.mock 隔离外部依赖 边界值测试 空值/空字符串 字段长度边界 数值边界 目标:覆盖所有解析路径,确保解析正确性 测试驱动开发(TDD) + 持续集成(CI) 每次修改解析器,先跑一遍所有测试用例

4.6 测试覆盖率与持续集成

写测试不是目的,保证质量才是。我建议把单元测试集成到CI流水线里,每次提交代码自动跑一遍。覆盖率目标可以定在80%以上,但别盲目追求100%。

你想想看,有些异常路径的代码,比如网络超时后的重试逻辑,测试起来成本很高。这时候就要权衡了——核心解析逻辑必须全覆盖,边缘场景可以适当取舍

一个小建议:每次发现线上bug,先写一个能复现这个bug的测试用例,然后再修代码。这样既能确保bug被修复,又能防止以后回归。这叫「测试驱动修复」,我用了好多年,效果很好。

嗯,关于解析器的单元测试,今天就聊这么多。记住一句话:测试不是负担,是你晚上能睡个好觉的保证