首页> 标签> MaxCompute
"MaxCompute"
共 5597 条结果
全部 问答 文章 公开课 课程 电子书 技术圈 体验
请问怎么区分DataWorks工作空间和MaxCompute项目啊?
请问怎么区分DataWorks工作空间和MaxCompute项目啊?
问答
分布式计算  ·  DataWorks  ·  MaxCompute
2023-01-29
开通了dataphin试用版本,导入模型提示需要绑定对应的maxcompute 项目这个可以规避嘛?
目前客户开通了dataphin试用版本,在导入模型的时候提示需要绑定对应的maxcompute 项目,但是客户目前未创建ODPS项目,这个可以规避嘛还是必须创建ODPS项目?
问答
分布式计算  ·  MaxCompute
2023-02-07
sls数据传输到maxcompute ,创建数据源是否可以用内网互通呢?
sls数据传输到maxcompute ,创建数据源是否可以用内网互通呢?
问答
分布式计算  ·  MaxCompute
2023-02-06
MaxCompute项目访问外部网络申请表单审批不通过的原因一般是什么呀?我这边接受短信没有说明原因
MaxCompute项目访问外部网络申请表单审批不通过的原因一般是什么呀?我这边接受短信没有说明原因
问答
分布式计算  ·  MaxCompute
2023-02-07
maxcompute访问外部服务,外部服务需要添加maxcompute出口IP白名单。都打开是什么?
maxcompute里面访问外部服务,外部服务需要添加maxcompute出口IP的白名单。都打开是什么意思?
问答
分布式计算  ·  MaxCompute
2023-02-07
Java单元测试典型案例集锦
前言近期,阿里巴巴CTO线卓越工程小组举办了阿里巴巴第一届单元测试比赛《这!就是单测》并取得了圆满成功。本人有幸作为评委,在仔细地阅读了各个小组的单元测试用例后,发现了两大单元测试问题:无效验证问题:不进行有效地验证数据对象、抛出异常和调用方法。测试方法问题:不知道如何测试某些典型案例,要么错误地测试、要么不进行测试、要么利用集成测试来保证覆盖率。比如:①错误地测试:利用测试返回节点占比来测试随机负载均衡策略;②不进行测试:没有人针对虚基类进行单独地测试;③利用集成测试:很多案例中,直接注入真实依赖对象,然后一起进行集成测试。针对无效验证问题,在我的ATA文章《那些年,我们写过的无效单元测试》中,介绍了如何识别和解决单元测试无效验证问题,这里就不再累述了。在本文中,作者收集了一些的Java单元测试典型案例,主要是为了解决这个测试方法问题。1. 如何测试不可达代码在程序代码中,由于无法满足进入条件,永远都不会执行到的代码,我们称之为"不可达代码"。不可达代码的危害主要有:复杂了代码逻辑,增加了代码运行和维护成本。不可达代码是可以由单元测试检测出来的——不管如何构造单元测试用例,都无法覆盖到不可达代码。1.1. 案例代码在下面的案例代码中,就存在一段不可达代码。/** * 交易订单服务类 */ @Service public class TradeOrderService { /** 注入依赖对象 */ /** 交易订单DAO */ @Autowired private TradeOrderDAO tradeOrderDAO; /** * 查询交易订单 * * @param orderQuery 订单查询 * @return 交易订单分页 */ public PageDataVO<TradeOrderVO> queryTradeOrder(TradeOrderQueryVO orderQuery) { // 查询交易订单 // 查询交易订单: 总共数量 Long totalSize = tradeOrderDAO.countByCondition(orderQuery); // 查询交易订单: 数据列表 List<TradeOrderVO> dataList = null; if (NumberHelper.isPositive(totalSize)) { List<TradeOrderDO> tradeOrderList = tradeOrderDAO.queryByCondition(orderQuery); if (CollectionUtils.isNotEmpty(tradeOrderList)) { dataList = convertTradeOrders(tradeOrderList); } } // 返回分页数据 return new PageDataVO<>(totalSize, dataList); } /** * 转化交易订单列表 * * @param tradeOrderList 交易订单DO列表 * @return 交易订单VO列表 */ private static List<TradeOrderVO> convertTradeOrders(List<TradeOrderDO> tradeOrderList) { // 检查订单列表 if (CollectionUtils.isEmpty(tradeOrderList)) { return Collections.emptyList(); } // 转化订单列表 return tradeOrderList.stream().map(TradeOrderService::convertTradeOrder) .collect(Collectors.toList()); } /** * 转化交易订单 * * @param tradeOrder 交易订单DO * @return 交易订单VO */ private static TradeOrderVO convertTradeOrder(TradeOrderDO tradeOrder) { TradeOrderVO tradeOrderVO = new TradeOrderVO(); tradeOrderVO.setId(tradeOrder.getId()); // ... return tradeOrderVO; } }由于方法convertTradeOrders(转化交易订单列表)传入的参数tradeOrderList(交易订单列表)不可能为空,所以“检查订单列表”这段代码是不可达代码。 // 检查订单列表 if (CollectionUtils.isEmpty(tradeOrderList)) { return Collections.emptyList(); }1.2. 方案1:删除不可达代码(推荐)最简单的方法,就是删除方法convertTradeOrders(转化交易订单列表)中的不可达代码。/** * 转化交易订单列表 * * @param tradeOrderList 交易订单DO列表 * @return 交易订单VO列表 */ private static List<TradeOrderVO> convertTradeOrders(List<TradeOrderDO> tradeOrderList) { return tradeOrderList.stream().map(TradeOrderService2::convertTradeOrder) .collect(Collectors.toList()); }1.3. 方案2:利用不可达代码(推荐)还有一种方法,把不可达代码利用起来,可以降低方法queryTradeOrder(查询交易订单)的代码复杂度。/** * 查询交易订单 * * @param orderQuery 订单查询 * @return 交易订单分页 */ public PageDataVO<TradeOrderVO> queryTradeOrder(TradeOrderQueryVO orderQuery) { // 查询交易订单 // 查询交易订单: 总共数量 Long totalSize = tradeOrderDAO.countByCondition(orderQuery); // 查询交易订单: 数据列表 List<TradeOrderVO> dataList = null; if (NumberHelper.isPositive(totalSize)) { List<TradeOrderDO> tradeOrderList = tradeOrderDAO.queryByCondition(orderQuery); dataList = convertTradeOrders(tradeOrderList); } // 返回分页数据 return new PageDataVO<>(totalSize, dataList); }1.4. 方案3:测试不可达代码(不推荐)对于一些祖传代码,有些小伙伴不敢删除代码。在某些情况下,可以针对不可达代码进行单独测试。/** * 测试: 转化交易订单列表-交易订单列表为空 * * @throws Exception 异常信息 */ @Test public void testConvertTradeOrdersWithTradeOrderListEmpty() throws Exception { List<TradeOrderDO> tradeOrderList = null; Assert.assertSame("交易订单列表不为空", Collections.emptyList(), Whitebox.invokeMethod(TradeOrderService1.class, "convertTradeOrders", tradeOrderList)); }2. 如何测试内部的构造方法在这次单元测试总决赛中,有一个随机负载均衡策略,需要针对Random(随机数)进行单元测试。2.1. 代码案例按照题目要求,编写了一个简单的随机负载均衡策略。/** * 随机负载均衡策略类 */ public class RandomLoadBalanceStrategy implements LoadBalanceStrategy { /** * 选择服务节点 * * @param serverNodeList 服务节点列表 * @param clientRequest 客户请求 * @return 服务节点 */ @Override public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) { // 检查节点列表 if (CollectionUtils.isEmpty(serverNodeList)) { return null; } // 计算随机序号 int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum(); int randomIndex = new Random().nextInt(totalWeight); // 查找对应节点 for (ServerNode serverNode : serverNodeList) { int currentWeight = serverNode.getWeight(); if (currentWeight > randomIndex) { return serverNode; } randomIndex -= currentWeight; } return null; } }2.2. 方法1:直接测试法(不推荐)有些参赛选手,不知道如何测试随机数(主要原因是因为不知道如何Mock构造方法),所以直接利用测试返回节点占比来测试随机负载均衡策略。/** * 随机负载均衡策略测试类 */ @RunWith(MockitoJUnitRunner.class) public class RandomLoadBalanceStrategyTest { /** 定义测试对象 */ /** 随机负载均衡策略 */ @InjectMocks private RandomLoadBalanceStrategy randomLoadBalanceStrategy; /** * 测试: 选择服务节点-随机 * * @throws Exception 异常信息 */ @Test public void testSelectNodeWithRandom() throws Exception { int nodeCount1 = 0; int nodeCount2 = 0; int nodeCount3 = 0; ServerNode serverNode1 = new ServerNode(1L, 10); ServerNode serverNode2 = new ServerNode(2L, 20); ServerNode serverNode3 = new ServerNode(3L, 30); List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3); ClientRequest clientRequest = new ClientRequest(); for (int i = 0; i < 1000; i++) { ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest); if (serviceNode == serverNode1) { nodeCount1++; } else if (serviceNode == serverNode2) { nodeCount2++; } else if (serviceNode == serverNode3) { nodeCount3++; } } Assert.assertEquals("节点1占比不一致", serverNode1.getWeight() / 60.0D, nodeCount1 / 1000.0D, 1E-3D); Assert.assertEquals("节点2占比不一致", serverNode2.getWeight() / 60.0D, nodeCount2 / 1000.0D, 1E-3D); Assert.assertEquals("节点3占比不一致", serverNode3.getWeight() / 60.0D, nodeCount3 / 1000.0D, 1E-3D); } }这个测试用例主要存在3个问题:执行时间长:被测方法需要被执行1000遍;不一定通过:由于随机数是随机,并不一定保证比例,所以导致测试用例并不一定通过;测试目标变更:单测测试的测试目标应该是负载均衡逻辑,现在感觉测试目标变成了Random方法。2.3. 方法2:直接mock法(不推荐)用过PowerMockito高级功能的,知道如何去Mock构造方法。/** * 随机负载均衡策略测试类 */ @RunWith(PowerMockRunner.class) @PrepareForTest(RandomLoadBalanceStrategy.class) public class RandomLoadBalanceStrategyTest { /** 定义测试对象 */ /** 随机负载均衡策略 */ @InjectMocks private RandomLoadBalanceStrategy randomLoadBalanceStrategy; /** * 测试: 选择服务节点-第一个节点 * * @throws Exception 异常信息 */ @Test public void testSelectNodeWithFirstNode() throws Exception { // 模拟依赖方法 Random random = Mockito.mock(Random.class); Mockito.doReturn(9).when(random).nextInt(Mockito.anyInt()); PowerMockito.whenNew(Random.class).withNoArguments().thenReturn(random); // 调用测试方法 ServerNode serverNode1 = new ServerNode(1L, 10); ServerNode serverNode2 = new ServerNode(2L, 20); ServerNode serverNode3 = new ServerNode(3L, 30); List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3); ClientRequest clientRequest = new ClientRequest(); ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest); Assert.assertEquals("服务节点不一致", serverNode1, serviceNode); // 验证依赖方法 int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum(); Mockito.verify(random).nextInt(totalWeight); } }但是,这个测试用例也存在问题:需要把RandomLoadBalanceStrategy加到@PrepareForTest注解中,导致Jacoco无法统计单元测试的覆盖率。2.4. 方法3:工具方法法(推荐)其实,随机数生成,还有很多工具方法,我们可以利用工具方法RandomUtils.nextInt代替构造方法。2.4.1. 重构代码/** * 随机负载均衡策略类 */ public class RandomLoadBalanceStrategy implements LoadBalanceStrategy { /** * 选择服务节点 * * @param serverNodeList 服务节点列表 * @param clientRequest 客户请求 * @return 服务节点 */ @Override public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) { // 检查节点列表 if (CollectionUtils.isEmpty(serverNodeList)) { return null; } // 计算随机序号 int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum(); int randomIndex = RandomUtils.nextInt(0, totalWeight); // 查找对应节点 for (ServerNode serverNode : serverNodeList) { int currentWeight = serverNode.getWeight(); if (currentWeight > randomIndex) { return serverNode; } randomIndex -= currentWeight; } return null; } }2.4.2. 测试用例/** * 随机负载均衡策略测试类 */ @RunWith(PowerMockRunner.class) @PrepareForTest(RandomUtils.class) public class RandomLoadBalanceStrategyTest { /** 定义测试对象 */ /** 随机负载均衡策略 */ @InjectMocks private RandomLoadBalanceStrategy randomLoadBalanceStrategy; /** * 测试: 选择服务节点-第一个节点 */ @Test public void testSelectNodeWithFirstNode() { // 模拟依赖方法 PowerMockito.mockStatic(RandomUtils.class); PowerMockito.when(RandomUtils.nextInt(Mockito.eq(0), Mockito.anyInt())).thenReturn(9); // 调用测试方法 ServerNode serverNode1 = new ServerNode(1L, 10); ServerNode serverNode2 = new ServerNode(2L, 20); ServerNode serverNode3 = new ServerNode(3L, 30); List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3); ClientRequest clientRequest = new ClientRequest(); ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest); Assert.assertEquals("服务节点不一致", serverNode1, serviceNode); // 验证依赖方法 PowerMockito.verifyStatic(RandomUtils.class); int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum(); RandomUtils.nextInt(0, totalWeight); } }2.5. 方法4:注入对象法(推荐)如果不愿意使用工具方法,也可以注入依赖对象,我们可以利用RandomProvider(随机数提供者)来代替构造方法。2.5.1. 重构代码/** * 随机负载均衡策略类 */ public class RandomLoadBalanceStrategy implements LoadBalanceStrategy { /** 注入依赖对象 */ /** 随机数提供者 */ @Autowired private RandomProvider randomProvider; /** * 选择服务节点 * * @param serverNodeList 服务节点列表 * @param clientRequest 客户请求 * @return 服务节点 */ @Override public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) { // 检查节点列表 if (CollectionUtils.isEmpty(serverNodeList)) { return null; } // 计算随机序号 int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum(); int randomIndex = randomProvider.nextInt(totalWeight); // 查找对应节点 for (ServerNode serverNode : serverNodeList) { int currentWeight = serverNode.getWeight(); if (currentWeight > randomIndex) { return serverNode; } randomIndex -= currentWeight; } return null; } }2.5.2. 测试用例/** * 随机负载均衡策略测试类 */ @RunWith(MockitoJUnitRunner.class) public class RandomLoadBalanceStrategyTest { /** 模拟依赖方法 */ /** 随机数提供者 */ @Mock private RandomProvider randomProvider; /** 定义测试对象 */ /** 随机负载均衡策略 */ @InjectMocks private RandomLoadBalanceStrategy randomLoadBalanceStrategy; /** * 测试: 选择服务节点-第一个节点 */ @Test public void testSelectNodeWithFirstNode() { // 模拟依赖方法 Mockito.doReturn(9).when(randomProvider).nextInt(Mockito.anyInt()); // 调用测试方法 ServerNode serverNode1 = new ServerNode(1L, 10); ServerNode serverNode2 = new ServerNode(2L, 20); ServerNode serverNode3 = new ServerNode(3L, 30); List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3); ClientRequest clientRequest = new ClientRequest(); ServerNode serviceNode = randomLoadBalanceStrategy.selectNode(serverNodeList, clientRequest); Assert.assertEquals("服务节点不一致", serverNode1, serviceNode); // 验证依赖方法 int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum(); Mockito.verify(randomProvider).nextInt(totalWeight); } }3. 如何测试虚基类和子类在这次单元测试比赛中,很多选手都编写了虚基类,但是没有看到任何一个选手针对虚基类进行了单独的测试。3.1. 案例代码这里,以Diamond属性配置加载为例说明。3.1.1. 虚基类定义首先,定义一个通用的虚基类,定义了需要子类实现的虚方法,实现了通用的配置解析方法。/** * 虚属性回调类 * * @param <T> 配置类型 */ @Slf4j public abstract class AbstractPropertiesCallback<T> implements DiamondDataCallback { /** 注入依赖对象 */ /** 环境 */ @Autowired private Environment environment; /** 转化服务 */ @Autowired private ConversionService conversionService; /** * 接收到数据 * * @param data 配置数据 */ @Override public void received(String data) { // 获取配置参数 String configName = getConfigName(); Assert.notNull(configName, "配置名称不能为空"); T configInstance = getConfigInstance(); Assert.notNull(configInstance, "配置实例不能为空"); // 解析配置数据 try { log.info("绑定属性配置文件开始: configName={}", configName); Properties properties = new Properties(); byte[] bytes = Optional.ofNullable(data.getBytes()).orElseGet(() -> new byte[0]); InputStream inputStream = new ByteArrayInputStream(bytes); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); properties.load(bufferedReader); Bindable<T> bindable = Bindable.ofInstance(configInstance); Binder binder = new Binder(ConfigurationPropertySources.from( new PropertiesPropertySource(configName, properties)), new PropertySourcesPlaceholdersResolver(environment), conversionService); BindResult<T> result = binder.bind(configName, bindable); if (!result.isBound()) { log.error("绑定属性配置文件失败: configName={}", configName); return; } log.info("绑定属性配置文件成功: configName={}, configInstance={}", configName, JSON.toJSONString(configInstance)); } catch (IOException | RuntimeException e) { log.error("绑定属性配置文件异常: configName={}", configName, e); } } /** * 获取配置名称 * * @return 配置名称 */ @NonNull protected abstract String getConfigName(); /** * 获取配置实例 * * @return 配置实例 */ @NonNull protected abstract T getConfigInstance(); }3.1.2. 子类实现其次,定义了具体配置的子类,简单地实现了基类定义的虚方法。/** * 例子配置回调类 */ @DiamondListener(groupId = "unittest-example", dataId = "example.properties", executeAfterInit = true) public class ExampleConfigCallback extends AbstractPropertiesCallback<ExampleConfig> { /** 注入依赖对象 */ /** 例子配置 */ @Resource private ExampleConfig exampleConfig; /** * 获取配置名称 * * @return 配置名称 */ @Override protected String getConfigName() { return "example"; } /** * 获取配置实例 * * @return 配置实例 */ @Override protected ExampleConfig getConfigInstance() { return exampleConfig; } }3.2. 方法1:联合测试法(不推荐)最简单的测试方法,就是通过子类对虚基类进行联合测试,这样同时把子类和虚基类都测试了。/** * 例子配置回调测试类 */ @RunWith(MockitoJUnitRunner.class) public class ExampleConfigCallbackTest { /** 定义静态常量 */ /** 资源路径 */ private static final String RESOURCE_PATH = "testExampleConfigCallback/"; /** 模拟依赖对象 */ /** 配置环境 */ @Mock private ConfigurableEnvironment environment; /** 转化服务 */ @Mock private ConversionService conversionService; /** 定义测试对象 */ /** BOSS取消费配置回调 */ @InjectMocks private ExampleConfigCallback exampleConfigCallback; /** * 测试: 接收-正常 */ @Test public void testReceivedWithNormal() { // 模拟依赖对象 ExampleConfig exampleConfig = new ExampleConfig(); Whitebox.setInternalState(exampleConfigCallback, "exampleConfig", exampleConfig); // 调用测试方法 String text = ResourceHelper.getResourceAsString(getClass(), RESOURCE_PATH + "exampleConfig.properties"); exampleConfigCallback.received(text); // 验证依赖对象 text = ResourceHelper.getResourceAsString(getClass(), RESOURCE_PATH + "exampleConfig.json"); Assert.assertEquals("取消费用配置不一致", text, JSON.toJSONString(exampleConfig, SerializerFeature.MapSortField)); } }3.3. 方法2:独立测试法(推荐)其实,更好的方法是对虚基类和子类独立单元测试。3.3.1. 基类测试虚基类的单元测试,专注于虚基类的通用配置解析。/** * 虚属性回调测试类 */ @RunWith(MockitoJUnitRunner.class) public class AbstractPropertiesCallbackTest { /** 静态常量相关 */ /** 资源目录 */ private static final String RESOURCE_PATH = "testAbstractPropertiesCallback/"; /** 模拟依赖对象 */ /** 环境 */ @Mock private ConfigurableEnvironment environment; /** 转化服务 */ @Mock private ConversionService conversionService; /** 定义测试对象 */ /** 虚属性回调 */ @InjectMocks private AbstractPropertiesCallback<ExampleConfig> propertiesCallback = CastUtils.cast(Mockito.spy(AbstractPropertiesCallback.class)); /** * 测试: 接收到-正常 */ @Test public void testReceivedWithNormal() { // 模拟依赖方法 // 模拟依赖方法: propertiesCallback.getConfigName String configName = "example"; Mockito.doReturn(configName).when(propertiesCallback).getConfigName(); // 模拟依赖方法: propertiesCallback.getConfigInstance ExampleConfig configInstance = new ExampleConfig(); Mockito.doReturn(configInstance).when(propertiesCallback).getConfigInstance(); // 调用测试方法 String text1 = ResourceHelper.getResourceAsString(getClass(), RESOURCE_PATH + "exampleConfig.properties"); propertiesCallback.received(text1); String text2 = ResourceHelper.getResourceAsString(getClass(), RESOURCE_PATH + "exampleConfig.json"); Assert.assertEquals("任务配置不一致", text2, JSON.toJSONString(configInstance)); // 验证依赖方法 // 验证依赖方法: propertiesCallback.received Mockito.verify(propertiesCallback).received(text1); // 验证依赖方法: propertiesCallback.getConfigName Mockito.verify(propertiesCallback).getConfigName(); // 验证依赖方法: propertiesCallback.getConfigInstance Mockito.verify(propertiesCallback).getConfigInstance(); } }3.3.2. 子类测试子类的单元测试,专注于对虚基类定义虚方法的实现,避免了每个子类都要针对虚基类的通用配置解析进行测试。/** * 例子配置回调测试类 */ @RunWith(MockitoJUnitRunner.class) public class ExampleConfigCallbackTest { /** 定义测试对象 */ /** BOSS取消费配置回调 */ @InjectMocks private ExampleConfigCallback exampleConfigCallback; /** * 测试: 获取配置实例 */ @Test public void testGetConfigInstance() { Assert.assertEquals("配置实例不一致", exampleConfig, exampleConfigCallback.getConfigInstance()); } /** * 测试: 获取配置名称 */ @Test public void testGetConfigName() { Assert.assertEquals("配置名称不一致", "example", exampleConfigCallback.getConfigName()); } }4. 如何测试策略模式的策略服务4.1. 案例代码在这次单元测试比赛中,很多选手都编写了策略服务类,但是没有看到任何一个选手针对策略服务类进行了单独的测试。这里,还是以负载均衡的策略服务为例说明。4.1.1. 策略接口首先,定义一个负载均衡策略接口。/** * 负载均衡策略接口 */ public interface LoadBalanceStrategy { /** * 支持策略类型 * * @return 策略类型 */ LoadBalanceStrategyType supportType(); /** * 选择服务节点 * * @param serverNodeList 服务节点列表 * @param clientRequest 客户请求 * @return 服务节点 */ ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest); } 4.1.2. 策略服务其次,实现一个负载均衡策略服务,根据负载均衡策略类型选择对应的负载均衡策略来执行。/** * 负载均衡服务类 */ public class LoadBalanceService { /** 负载均衡策略映射 */ private final Map<LoadBalanceStrategyType, LoadBalanceStrategy> strategyMap; /** * 构造方法 * * @param strategyList 负载均衡策略列表 */ public LoadBalanceService(List<LoadBalanceStrategy> strategyList) { strategyMap = new EnumMap<>(LoadBalanceStrategyType.class); for (LoadBalanceStrategy strategy : strategyList) { strategyMap.put(strategy.supportType(), strategy); } } /** * 选择服务节点 * * @param strategyType 策略类型 * @param serverNodeList 服务节点列表 * @param clientRequest 客户请求 * @return 服务节点 */ public ServerNode selectNode(LoadBalanceStrategyType strategyType, List<ServerNode> serverNodeList, ClientRequest clientRequest) { // 获取负载均衡策略 LoadBalanceStrategy strategy = strategyMap.get(strategyType); if (Objects.isNull(strategy)) { throw new BusinessException("负载均衡策略不存在"); } // 执行负载均衡策略 return strategy.selectNode(serverNodeList, clientRequest); } }4.1.3. 策略实现最后,实现一个随机负载均衡策略实现类。/** * 随机负载均衡策略类 */ public class RandomLoadBalanceStrategy implements LoadBalanceStrategy { /** * 支持策略类型 * * @return 策略类型 */ @Override public LoadBalanceStrategyType supportType() { return LoadBalanceStrategyType.RANDOM; } /** * 选择服务节点 * * @param serverNodeList 服务节点列表 * @param clientRequest 客户请求 * @return 服务节点 */ @Override public ServerNode selectNode(List<ServerNode> serverNodeList, ClientRequest clientRequest) { // 检查节点列表 if (CollectionUtils.isEmpty(serverNodeList)) { return null; } // 计算随机序号 int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum(); int randomIndex = RandomUtils.nextInt(0, totalWeight); // 查找对应节点 for (ServerNode serverNode : serverNodeList) { int currentWeight = serverNode.getWeight(); if (currentWeight > randomIndex) { return serverNode; } randomIndex -= currentWeight; } return null; } }4.2. 方法1:联合测试法(不推荐)很多时候,策略模式是用来优化if-else代码的。所以,采用联合测试法(策略服务和策略实现同时测试),能够最大限度地利用原有的单元测试代码。/** * 负载均衡服务测试类 */ @RunWith(PowerMockRunner.class) @PrepareForTest(RandomUtils.class) public class LoadBalanceServiceTest { /** * 测试: 选择服务节点-正常 */ @Test public void testSelectNodeWithNormal() { // 模拟依赖方法 PowerMockito.mockStatic(RandomUtils.class); PowerMockito.when(RandomUtils.nextInt(Mockito.eq(0), Mockito.anyInt())).thenReturn(9); // 调用测试方法 ServerNode serverNode1 = new ServerNode(1L, 10); ServerNode serverNode2 = new ServerNode(2L, 20); ServerNode serverNode3 = new ServerNode(3L, 30); List<ServerNode> serverNodeList = Arrays.asList(serverNode1, serverNode2, serverNode3); ClientRequest clientRequest = new ClientRequest(); RandomLoadBalanceStrategy randomLoadBalanceStrategy = new RandomLoadBalanceStrategy(); LoadBalanceService loadBalanceService = new LoadBalanceService(Arrays.asList(randomLoadBalanceStrategy)); ServerNode serviceNode = loadBalanceService.selectNode(LoadBalanceStrategyType.RANDOM, serverNodeList, clientRequest); Assert.assertEquals("服务节点不一致", serverNode1, serviceNode); // 验证依赖方法 PowerMockito.verifyStatic(RandomUtils.class); int totalWeight = serverNodeList.stream().mapToInt(ServerNode::getWeight).sum(); RandomUtils.nextInt(0, totalWeight); } }策略模式的联合测试法主要有以下问题:策略服务依赖于策略实现,需要了解策略实现的具体逻辑,才能写出策略服务的单元测试;对于策略服务来说,该单元测试并不关心策略服务的实现,这是黑盒测试而不是白盒测试。如果我们对策略服务进行以下破坏,该单元测试并不能发现问题:strategyMap没有根据strategyList生成;strategyMap.get(strategyType)为空时,初始化一个RandomLoadBalanceStrategy。/** * 负载均衡服务类 */ public class LoadBalanceService { /** 负载均衡策略映射 */ private final Map<LoadBalanceStrategyType, LoadBalanceStrategy> strategyMap; /** * 构造方法 * * @param strategyList 负载均衡策略列表 */ public LoadBalanceService(List<LoadBalanceStrategy> strategyList) { strategyMap = new EnumMap<>(LoadBalanceStrategyType.class); } /** * 选择服务节点 * * @param strategyType 策略类型 * @param serverNodeList 服务节点列表 * @param clientRequest 客户请求 * @return 服务节点 */ public ServerNode selectNode(LoadBalanceStrategyType strategyType, List<ServerNode> serverNodeList, ClientRequest clientRequest) { // 获取负载均衡策略 LoadBalanceStrategy strategy = strategyMap.get(strategyType); if (Objects.isNull(strategy)) { strategy = new RandomLoadBalanceStrategy(); } // 执行负载均衡策略 return strategy.selectNode(serverNodeList, clientRequest); } }4.3. 方法2:独立测试法(推荐)现在,先假设策略实现RandomLoadBalanceStrategy(随机负载均衡策略)不存在,直接对策略服务LoadBalanceService(负载均衡服务)独立测试,而且是分别对构造方法和selectNode(选择服务节点)方法进行独立测试。其中,测试构造方法是为了保证strategyMap构造逻辑没有问题,测试selectNode(选择服务节点)方法是为了保证选择策略逻辑没有问题。/** * 负载均衡服务测试类 */ public class LoadBalanceServiceTest { /** * 测试: 构造方法 */ @Test public void testConstructor() { // 模拟依赖方法 LoadBalanceStrategy loadBalanceStrategy = Mockito.mock(LoadBalanceStrategy.class); Mockito.doReturn(LoadBalanceStrategyType.RANDOM).when(loadBalanceStrategy).supportType(); // 调用测试方法 LoadBalanceService loadBalanceService = new LoadBalanceService(Arrays.asList(loadBalanceStrategy)); Map<LoadBalanceStrategyType, LoadBalanceStrategy> strategyMap = Whitebox.getInternalState(loadBalanceService, "strategyMap"); Assert.assertEquals("策略映射大小不一致", 1, strategyMap.size()); Assert.assertEquals("策略映射对象不一致", loadBalanceStrategy, strategyMap.get(LoadBalanceStrategyType.RANDOM)); // 验证依赖方法 Mockito.verify(loadBalanceStrategy).supportType(); } /** * 测试: 选择服务节点-正常 */ @Test public void testSelectNodeWithNormal() { // 模拟依赖方法 LoadBalanceStrategy loadBalanceStrategy = Mockito.mock(LoadBalanceStrategy.class); // 模拟依赖方法: loadBalanceStrategy.supportType Mockito.doReturn(LoadBalanceStrategyType.RANDOM).when(loadBalanceStrategy).supportType(); // 模拟依赖方法: loadBalanceStrategy.selectNode ServerNode serverNode = Mockito.mock(ServerNode.class); Mockito.doReturn(serverNode).when(loadBalanceStrategy) .selectNode(Mockito.anyList(), Mockito.any(ClientRequest.class)); // 调用测试方法 List<ServerNode> serverNodeList = CastUtils.cast(Mockito.mock(List.class)); ClientRequest clientRequest = Mockito.mock(ClientRequest.class); LoadBalanceService loadBalanceService = new LoadBalanceService(Arrays.asList(loadBalanceStrategy)); Assert.assertEquals("服务节点不一致", serverNode, loadBalanceService.selectNode(LoadBalanceStrategyType.RANDOM, serverNodeList, clientRequest)); // 验证依赖方法 // 验证依赖方法: loadBalanceStrategy.supportType Mockito.verify(loadBalanceStrategy).supportType(); // 验证依赖方法: loadBalanceStrategy.selectNode Mockito.verify(loadBalanceStrategy).selectNode(serverNodeList, clientRequest); } }其实,不只是策略模式,很多模式下都不建议联合测试,而是推荐采用独立的单元测试。因为单元测试是白盒测试——一种专注于自身代码逻辑的测试。5. 如何测试Lambda表达式在有些单元测试中,Lambda表达式并不一定被执行,所以导致Lambda表达式没有被测试。5.1. 案例代码这里,以从ODPS中查询用户交易订单为例说明。5.1.1. 被测代码交易订单查询服务,其中有一段转化订单的Lambda表达式。/** * 交易订单服务 */ @Service public class TradeOrderService { /** 注入依赖对象 */ /** 交易ODPS服务 */ @Autowired private TradeOdpsService tradeOdpsService; /** * 查询交易订单 * * @param userId 用户标识 * @param maxCount 最大数量 * @return 交易订单列表 */ public List<TradeOrderVO> queryTradeOrder(Long userId, Integer maxCount) { String format = ResourceHelper.getResourceAsString(getClass(), "query_trade_order.sql"); String sql = String.format(format, userId, maxCount); return tradeOdpsService.executeQuery(sql, record -> { TradeOrderVO tradeOrder = new TradeOrderVO(); tradeOrder.setId(record.getBigint("id")); // ... return tradeOrder; }); } }5.1.2. 依赖代码封装了通用的ODPS查询方法。/** * 交易ODPS服务类 */ @Slf4j @Service public class TradeOdpsService { /** 注入依赖对象 */ /** 交易ODPS */ @Resource(name = "tradeOdps") private Odps tradeOdps; /** * 执行查询 * * @param <T> 模板类型 * @param sql SQL语句 * @param dataParser 数据解析器 * @return 查询结果列表 */ public <T> List<T> executeQuery(String sql, Function<Record, T> dataParser) { try { // 打印提示信息 log.info("开始执行ODPS数据查询..."); // 执行ODPS查询 Instance instance = SQLTask.run(tradeOdps, sql); instance.waitForSuccess(); // 获取查询结果 List<Record> recordList = SQLTask.getResult(instance); if (CollectionUtils.isEmpty(recordList)) { log.info("完成执行ODPS数据查询: totalSize=0"); return Collections.emptyList(); } // 依次读取数据 List<T> dataList = new ArrayList<>(); for (Record record : recordList) { T data = dataParser.apply(record); if (Objects.nonNull(data)) { dataList.add(data); } } // 打印提示信息 log.info("完成执行ODPS数据查询: totalSize={}", dataList.size()); // 返回查询结果 return dataList; } catch (OdpsException e) { log.warn("执行ODPS数据查询异常: sql={}", sql, e); throw new BusinessException("执行ODPS数据查询异常", e); } } }5.2. 方法1:直接测试法(不推荐)按照通用的单元测试方法进行测试,发现Lambda表达式没有被测试到。/** * 交易订单服务测试类 */ @RunWith(MockitoJUnitRunner.class) public class TradeOrderServiceTest { /** 定义静态常量 */ /** 资源路径 */ private static final String RESOURCE_PATH = "testTradeOrderService/"; /** 模拟依赖对象 */ /** 交易ODPS服务 */ @Mock private TradeOdpsService tradeOdpsService; /** 定义测试对象 */ /** 交易订单服务 */ @InjectMocks private TradeOrderService tradeOrderService; /** * 测试: 查询交易订单-正常 */ @Test public void testQueryTradeOrderWithNormal() { // 模拟依赖方法 // 模拟依赖方法: tradeOdpsService.executeQuery List<TradeOrderVO> tradeOrderList = CastUtils.cast(Mockito.mock(List.class)); Mockito.doReturn(tradeOrderList).when(tradeOdpsService).executeQuery(Mockito.anyString(), Mockito.any()); // 调用测试方法 Long userId = 12345L; Integer maxCount = 100; Assert.assertSame("交易订单列表不一致", tradeOrderList, tradeOrderService.queryTradeOrder(userId, maxCount)); // 验证依赖方法 // 验证依赖方法: tradeOdpsService.executeQuery String path = RESOURCE_PATH + "testQueryTradeOrderWithNormal/"; String text = ResourceHelper.getResourceAsString(getClass(), path + "queryTradeOrder.sql"); Mockito.verify(tradeOdpsService).executeQuery(Mockito.eq(text), Mockito.any()); } }5.3. 方法2:联合测试法(不推荐)有人建议,可以把TradeOrderService(交易订单服务)和TradeOdpsService(交易ODPS服务)联合测试,这样就可以保证Lambda表达式被测试到。/** * 交易订单服务测试类 */ @RunWith(PowerMockRunner.class) @PrepareForTest({SQLTask.class}) public class TradeOrderServiceTest { /** 定义静态常量 */ /** 资源路径 */ private static final String RESOURCE_PATH = "testTradeOrderService/"; /** 模拟依赖对象 */ /** 交易ODPS */ @Mock(name = "tradeOdps") private Odps tradeOdps; /** 定义测试对象 */ /** 交易ODPS服务 */ @InjectMocks private TradeOdpsService tradeOdpsService = Mockito.spy(TradeOdpsService.class); /** 交易订单服务 */ @InjectMocks private TradeOrderService tradeOrderService; /** * 测试: 查询交易订单-正常 * * @throws OdpsException ODPS异常 */ @Test public void testQueryTradeOrderWithNormal() throws OdpsException { // 模拟依赖方法 PowerMockito.mockStatic(SQLTask.class); // 模拟依赖方法: SQLTask.run Instance instance = Mockito.mock(Instance.class); PowerMockito.when(SQLTask.run(Mockito.eq(tradeOdps), Mockito.anyString())).thenReturn(instance); // 模拟依赖方法: SQLTask.getResult Record record1 = PowerMockito.mock(Record.class); Record record2 = PowerMockito.mock(Record.class); List<Record> recordList = Arrays.asList(record1, record2); PowerMockito.when(SQLTask.getResult(instance)).thenReturn(recordList); // 模拟依赖方法: record.getString Mockito.doReturn(1L).when(record1).getBigint("id"); Mockito.doReturn(2L).when(record2).getBigint("id"); // 调用测试方法 Long userId = 12345L; Integer maxCount = 100; List<TradeOrderVO> tradeOrderList = tradeOrderService.queryTradeOrder(userId, maxCount); String path = RESOURCE_PATH + "testQueryTradeOrderWithNormal/"; String text = ResourceHelper.getResourceAsString(getClass(), path + "tradeOrderList.json"); Assert.assertEquals("交易订单列表不一致", text, JSON.toJSONString(tradeOrderList)); // 验证依赖方法 PowerMockito.verifyStatic(SQLTask.class); // 验证依赖方法: SQLTask.run text = ResourceHelper.getResourceAsString(getClass(), path + "queryTradeOrder.sql"); SQLTask.run(tradeOdps, text); // 验证依赖方法: SQLTask.getResult SQLTask.getResult(instance); // 验证依赖方法: instance.waitForSuccess Mockito.verify(instance).waitForSuccess(); // 验证依赖方法: record.getString Mockito.verify(record1).getBigint("id"); Mockito.verify(record2).getBigint("id"); } }主要问题:需要了解TradeOdpsService.executeQuery(执行查询)方法的逻辑并构建单元测试用例,导致TradeOrderService.queryTradeOrder(查询交易订单)方法的单测测试用例非常复杂。5.3. 方法3:重构测试法(推荐)其实,只需要把这段Lambda表达式提取成一个convertTradeOrder(转化交易订单)方法,即可让代码变得清晰明了,又可以让代码更容易单元测试。5.3.1. 重构代码提取Lambda表达式为convertTradeOrder(转化交易订单)方法。/** * 交易订单服务类 */ @Service public class TradeOrderService { /** 注入依赖对象 */ /** 交易ODPS服务 */ @Autowired private TradeOdpsService tradeOdpsService; /** * 查询交易订单 * * @param userId 用户标识 * @param maxCount 最大数量 * @return 交易订单列表 */ public List<TradeOrderVO> queryTradeOrder(Long userId, Integer maxCount) { String format = ResourceHelper.getResourceAsString(getClass(), "query_trade_order.sql"); String sql = String.format(format, userId, maxCount); return tradeOdpsService.executeQuery(sql, TradeOrderService2::convertTradeOrder); } /** * 转化交易订单 * * @param record ODPS记录 * @return 交易订单 */ private static TradeOrderVO convertTradeOrder(Record record) { TradeOrderVO tradeOrder = new TradeOrderVO(); tradeOrder.setId(record.getBigint("id")); // ... return tradeOrder; } }5.3.2. 测试用例针对queryTradeOrder(查询交易订单)方法和convertTradeOrder(转化交易订单)方法分别进行单元测试。/** * 交易订单服务测试类 */ @RunWith(MockitoJUnitRunner.class) public class TradeOrderServiceTest { /** 定义静态常量 */ /** 资源路径 */ private static final String RESOURCE_PATH = "testTradeOrderService/"; /** 模拟依赖对象 */ /** 交易ODPS服务 */ @Mock private TradeOdpsService tradeOdpsService; /** 定义测试对象 */ /** 交易订单服务 */ @InjectMocks private TradeOrderService tradeOrderService; /** * 测试: 查询交易订单-正常 */ @Test public void testQueryTradeOrderWithNormal() { // 模拟依赖方法 // 模拟依赖方法: tradeOdpsService.executeQuery List<TradeOrderVO> tradeOrderList = CastUtils.cast(Mockito.mock(List.class)); Mockito.doReturn(tradeOrderList).when(tradeOdpsService).executeQuery(Mockito.anyString(), Mockito.any()); // 调用测试方法 Long userId = 12345L; Integer maxCount = 100; Assert.assertSame("交易订单列表不一致", tradeOrderList, tradeOrderService.queryTradeOrder(userId, maxCount)); // 验证依赖方法 // 验证依赖方法: tradeOdpsService.executeQuery String path = RESOURCE_PATH + "testQueryTradeOrderWithNormal/"; String text = ResourceHelper.getResourceAsString(getClass(), path + "queryTradeOrder.sql"); Mockito.verify(tradeOdpsService).executeQuery(Mockito.eq(text), Mockito.any()); } /** * 测试: 转化交易订单 * * @throws Exception 异常信息 */ @Test public void testConvertTradeOrder() throws Exception { // 模拟依赖方法 Long id = 12345L; Record record = Mockito.mock(Record.class); Mockito.doReturn(id).when(record).getBigint("id"); // 调用测试方法 TradeOrderVO tradeOrder = Whitebox.invokeMethod(TradeOrderService2.class, "convertTradeOrder", record); Assert.assertEquals("订单标识不一致", id, tradeOrder.getId()); // 验证依赖方法 Mockito.verify(record).getBigint("id"); } }6. 如何测试链式调用在日常编码过程中,很多人都喜欢使用链式调用,这样可以让代码变得更简洁。6.1. 案例代码这里,通过修改后的添加跨域支持代码来举例说明(原方法没有返回值)。/** * 跨域辅助类 */ public class CorsHelper { /** 定义静态常量 */ /** 最大生命周期 */ private static final long MAX_AGE = 3600L; /** * 添加跨域支持 * * @param registry 跨域注册器 * @return 跨域注册 */ public static CorsRegistration addCorsMapping(CorsRegistry registry) { return registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(MAX_AGE) .allowedHeaders("*"); } }6.2. 方法1:普通测试法(不推荐)正常情况下,每一个依赖对象及其调用方法都要mock,编写的代码如下:/** * 跨域辅助测试类 */ public class CorsHelperTest { /** * 测试: 添加跨域支持 */ @Test public void testAddCorsMapping() { // 模拟依赖方法 CorsRegistry registry = Mockito.mock(CorsRegistry.class); CorsRegistration registration = Mockito.mock(CorsRegistration.class); Mockito.doReturn(registration).when(registry).addMapping(Mockito.anyString()); Mockito.doReturn(registration).when(registration).allowedOrigins(Mockito.any()); Mockito.doReturn(registration).when(registration).allowedMethods(Mockito.any()); Mockito.doReturn(registration).when(registration).allowCredentials(Mockito.anyBoolean()); Mockito.doReturn(registration).when(registration).maxAge(Mockito.anyLong()); Mockito.doReturn(registration).when(registration).allowedHeaders(Mockito.any()); // 调用测试方法 Assert.assertEquals("跨域注册不一致", registration, CorsHelper.addCorsMapping(registry)); // 验证依赖方法 Mockito.verify(registry).addMapping("/**"); Mockito.verify(registration).allowedOrigins("*"); Mockito.verify(registration).allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"); Mockito.verify(registration).allowCredentials(true); Mockito.verify(registration).maxAge(3600L); Mockito.verify(registration).allowedHeaders("*"); } }6.3. 方法2:利用RETURNS_DEEP_STUBS参数法(推荐)对于链式调用,Mockito提供了更加简便的单元测试方法——提供Mockito.RETURNS_DEEP_STUBS参数,实现链式调用返回对象的自动mock。利用Mockito.RETURNS_DEEP_STUBS参数编写的测试用例如下:/** * 跨域辅助测试类 */ public class CorsHelperTest { /** * 测试: 添加跨域支持 */ @Test public void testAddCorsMapping() { // 模拟依赖方法 CorsRegistry registry = Mockito.mock(CorsRegistry.class, Answers.RETURNS_DEEP_STUBS); CorsRegistration registration = Mockito.mock(CorsRegistration.class); Mockito.when(registry.addMapping(Mockito.anyString()) .allowedOrigins(Mockito.any()) .allowedMethods(Mockito.any()) .allowCredentials(Mockito.anyBoolean()) .maxAge(Mockito.anyLong()) .allowedHeaders(Mockito.any())) .thenReturn(registration); // 调用测试方法 Assert.assertEquals("跨域注册不一致", registration, CorsHelper.addCorsMapping(registry)); // 验证依赖方法 Mockito.verify(registry.addMapping("/**") .allowedOrigins("*") .allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS") .allowCredentials(true) .maxAge(3600L)) .allowedHeaders("*"); } }代码说明:在mock对象时,需要指定Mockito.RETURNS_DEEP_STUBS参数;在mock方法时,采用when-then模式,when内容是链式调用,then内容是返回的值;在verify方法时,只需要验证最后1次方法调用,verify内容是前n次链式调用;如果验证时某个方法调用的某个参数指定错误时,最后一个方法调用验证将因为这个mock对象没有方法调用而抛出异常。6.4. 方法3:利用RETURNS_SELF参数法(推荐)对于相同返回值的链式调用,Mockito提供了更加简便的单元测试方法——提供Mockito.RETURNS_SELF参数,实现链式调用返回对象的自动mock,而且还能返回同一mock对象。利用Mockito.RETURNS_SELF参数编写的测试用例如下:/** * 跨域辅助测试类 */ public class CorsHelperTest { /** * 测试: 添加跨域支持 */ @Test public void testAddCorsMapping() { // 模拟依赖方法 CorsRegistry registry = Mockito.mock(CorsRegistry.class); CorsRegistration registration = Mockito.mock(CorsRegistration.class, Answers.RETURNS_SELF); Mockito.doReturn(registration).when(registry).addMapping(Mockito.anyString()); // 调用测试方法 Assert.assertEquals("跨域注册不一致", registration, CorsHelper.addCorsMapping(registry)); // 验证依赖方法 Mockito.verify(registry).addMapping("/**"); Mockito.verify(registration).allowedOrigins("*"); Mockito.verify(registration).allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS"); Mockito.verify(registration).allowCredentials(true); Mockito.verify(registration).maxAge(3600L); Mockito.verify(registration).allowedHeaders("*"); } }代码说明:在mock对象时,对于自返回对象,需要指定Mockito.RETURNS_SELF参数;在mock方法时,无需对自返回对象进行mock方法,因为框架已经mock方法返回了自身;在verify方法时,可以像普通测试法一样优美地验证所有方法调用。方法对比:普通测试法:mock调用方法语句较多;利用RETURNS_DEEP_STUBS参数法:mock调用方法语句较少,适合于链式调用返回不同值;利用RETURNS_SELF参数法:mock调用方法语句最少,适合于链式调用返回相同值。7. 如何测试相同参数返回不同值在有些场景下,存在相同参数多次调用返回不同值的情况,比如:读取文本文件的readLine方法。7.1. 案例代码这里,以ODPS的RecordReader为例,读取每一行数据记录。/** * 读取数据 * * @param <T> 模板类型 * @param recordReader 记录读取器 * @param dataParser 数据解析器 * @return 数据列表 * @throws IOException IO异常 */ public static <T> List<T> readData(RecordReader recordReader, Function<Record, T> dataParser) throws IOException { Record record; List<T> dataList = new ArrayList<>(); while (Objects.nonNull(record = recordReader.read())) { T data = dataParser.apply(record); if (Objects.nonNull(data)) { dataList.add(data); } } return dataList; }7.2. 测试用例为了mock相同参数返回不同值,需要使用到Mockito.doReturn的可变数组功能。/** * 测试: 读取数据-正常 * * @throws IOException IO异常 */ @Test public void testReadDataWithNormal() throws IOException { // 模拟依赖方法 // 模拟依赖方法: recordReader.read Record record1 = Mockito.mock(Record.class); Record record2 = Mockito.mock(Record.class); TunnelRecordReader recordReader = Mockito.mock(TunnelRecordReader.class); Mockito.doReturn(record1, record2, null).when(recordReader).read(); // 模拟依赖方法: dataParser.apply Function<Record, Object> dataParser = CastUtils.cast(Mockito.mock(Function.class)); Object object1 = new Object(); Object object2 = new Object(); Mockito.doReturn(object1).when(dataParser).apply(record1); Mockito.doReturn(object2).when(dataParser).apply(record2); // 调用测试方法 List<Object> dataList = OdpsHelper.readData(recordReader, dataParser); Assert.assertEquals("数据列表不一致", Arrays.asList(object1, object2), dataList); // 验证依赖方法 // 验证依赖方法: recordReader.read Mockito.verify(recordReader, Mockito.times(3)).read(); // 验证依赖方法: dataParser.apply Mockito.verify(dataParser).apply(record1); Mockito.verify(dataParser).apply(record2); }8. 如何测试已变更的方法参数值在单元测试中,我们通常通过ArgumentCaptor进行方法参数捕获并验证。但是,在有些情况下,我们捕获的可能是已经变更的方法参数,所以无法对这些方法参数值进行验证。8.1. 案例代码这里,以分批读取并保存ODPS数据为例说明。其中,dataList在每次存储后,都进行了一次清除操作。/** * 读取数据 * * @param <T> 模板类型 * @param recordReader 记录读取器 * @param batchSize 批量大小 * @param dataParser 数据解析器 * @param dataStorage 数据存储器 * @throws IOException IO异常 */ public static <T> void readData(RecordReader recordReader, int batchSize, Function<Record, T> dataParser, Consumer<List<T>> dataStorage) throws IOException { // 依次读取数据 Record record; List<T> dataList = new ArrayList<>(batchSize); while (Objects.nonNull(record = recordReader.read())) { // 解析添加数据 T data = dataParser.apply(record); if (Objects.nonNull(data)) { dataList.add(data); } // 批量存储数据 if (dataList.size() == batchSize) { dataStorage.accept(dataList); dataList.clear(); } } // 存储剩余数据 if (CollectionUtils.isNotEmpty(dataList)) { dataStorage.accept(dataList); dataList.clear(); } }8.2. 问题测试通常情况下,我们利用ArgumentCaptor编写的测试用例如下:/** * 测试: 读取数据-正常 * * @throws IOException IO异常 */ @Test public void testReadDataWithNormal() throws IOException { // 模拟依赖方法 // 模拟依赖方法: recordReader.read Record record1 = Mockito.mock(Record.class); Record record2 = Mockito.mock(Record.class); TunnelRecordReader recordReader = Mockito.mock(TunnelRecordReader.class); Mockito.doReturn(record1, record2, null).when(recordReader).read(); // 模拟依赖方法: dataParser.apply Function<Record, Object> dataParser = CastUtils.cast(Mockito.mock(Function.class)); Object object1 = new Object(); Object object2 = new Object(); Mockito.doReturn(object1).when(dataParser).apply(record1); Mockito.doReturn(object2).when(dataParser).apply(record2); // 调用测试方法 int batchSize = 2; Consumer<List<Object>> dataStorage = CastUtils.cast(Mockito.mock(Consumer.class)); OdpsHelper.readData(recordReader, batchSize, dataParser, dataStorage); // 验证依赖方法 // 验证依赖方法: recordReader.read Mockito.verify(recordReader, Mockito.times(3)).read(); // 验证依赖方法: dataParser.apply Mockito.verify(dataParser).apply(record1); Mockito.verify(dataParser).apply(record2); // 验证依赖方法: dataStorage.test ArgumentCaptor<List<Object>> dataListCaptor = CastUtils.cast(ArgumentCaptor.forClass(List.class)); Mockito.verify(dataStorage).accept(dataListCaptor.capture()); Assert.assertEquals("数据列表不一致", Arrays.asList(object1, object2), dataListCaptor.getValue()); }执行该单元测试后,会出现以下错误:java.lang.AssertionError: 数据列表不一致 expected:<[java.lang.Object@7eaa2bc6, java.lang.Object@6dae70f9]> but was:<[]>因为,我们捕获的方法参数dataList只是一个对象引用,其数据内容早已被clear方法清除干净了。8.3. 正确测试对于这种情况,我们可以利用Mockito.doAnswer来保存这些临时值,最后再进行统一的数据验证。/** * 测试: 读取数据-正常 * * @throws IOException IO异常 */ @Test public void testReadDataWithNormal() throws IOException { // 模拟依赖方法 // 模拟依赖方法: recordReader.read Record record1 = Mockito.mock(Record.class); Record record2 = Mockito.mock(Record.class); TunnelRecordReader recordReader = Mockito.mock(TunnelRecordReader.class); Mockito.doReturn(record1, record2, null).when(recordReader).read(); // 模拟依赖方法: dataParser.apply Function<Record, Object> dataParser = CastUtils.cast(Mockito.mock(Function.class)); Object object1 = new Object(); Object object2 = new Object(); Mockito.doReturn(object1).when(dataParser).apply(record1); Mockito.doReturn(object2).when(dataParser).apply(record2); // 模拟依赖方法: dataStorage.test List<Object> dataList = new ArrayList<>(); Consumer<List<Object>> dataStorage = CastUtils.cast(Mockito.mock(Consumer.class)); Mockito.doAnswer(invocation -> dataList.addAll(invocation.getArgument(0))) .when(dataStorage).accept(Mockito.anyList()); // 调用测试方法 int batchSize = 2; OdpsHelper.readData(recordReader, batchSize, dataParser, dataStorage); Assert.assertEquals("数据列表不一致", Arrays.asList(object1, object2), dataList); // 验证依赖方法 // 验证依赖方法: recordReader.read Mockito.verify(recordReader, Mockito.times(3)).read(); // 验证依赖方法: dataParser.apply Mockito.verify(dataParser).apply(record1); Mockito.verify(dataParser).apply(record2); // 验证依赖方法: dataStorage.test Mockito.verify(dataStorage).accept(Mockito.anyList()); }9. 如何测试相同返回值的代码分支在业务代码中,经常会出现不同的代码分支返回相同值的情况。这个时候,仅通过验证返回值是没法判断是否命中了对应的代码分支的。那么,这种情况如何进行单元测试呢?9.1. 案例代码这里,以灰度发布服务判定方法为例说明。/** * 灰度发布服务类 */ @Slf4j @Service public class GrayReleaseService { /** 定义静态常量 */ /** 灰度发布分子 */ private static final long GRAY_NUMERATOR = 0L; /** 灰度发布分母 */ private static final long GRAY_DENOMINATOR = 10000L; /** 注入依赖对象 */ /** 灰度发布配置 */ @Autowired private GrayReleaseConfig grayReleaseConfig; /** * 是否灰度发布 * * @param key 主键 * @param channel 渠道 * @param userId 用户标识 * @param value 取值 * @return 判断结果 */ public boolean isGrayRelease(String key, String channel, String userId, Object value) { // 判断灰度发布取值 if (Objects.isNull(value)) { log.info("命中灰度取值为空"); return false; } // 获取灰度发布映射 Map<String, GrayReleaseItem> grayReleaseMap = grayReleaseConfig.getGrayReleaseMap(); if (MapUtils.isEmpty(grayReleaseMap)) { log.info("命中灰度发布映射为空"); return false; } // 获取灰度发布项 GrayReleaseItem grayReleaseItem = grayReleaseMap.get(key); if (Objects.isNull(grayReleaseItem)) { log.info("命中灰度发布映项为空: key={}", key); return false; } // 判断渠道白名单 Set<String> channelWhiteSet = grayReleaseItem.getChannelWhiteSet(); if (CollectionUtils.isNotEmpty(channelWhiteSet) && channelWhiteSet.contains(channel)) { log.info("命中渠道白名单灰度: key={}, channel={}", key, channel); return true; } // 判断用户白名单 Set<String> userIdWhiteSet = grayReleaseItem.getUserIdWhiteSet(); if (CollectionUtils.isNotEmpty(userIdWhiteSet) && userIdWhiteSet.contains(userId)) { log.info("命中用户白名单灰度: key={}, userId={}", key, userId); return true; } // 判断灰度发布比例 long grayNumerator = Optional.ofNullable(grayReleaseItem.getGrayNumerator()).orElse(GRAY_NUMERATOR); long grayDenominator = Optional.ofNullable(grayReleaseItem.getGrayDenominator()).orElse(GRAY_DENOMINATOR); boolean isGray = Math.abs(Objects.hashCode(value)) % grayDenominator <= grayNumerator; log.info("命中灰度发布比例: key={}, value={}, isGray={}", key, value, isGray); return isGray; } }9.2. 普通测试法(不推荐)这里,只测试了命中渠道白名单的情况。/** * 灰度发布服务测试类 */ @RunWith(MockitoJUnitRunner.class) public class GrayReleaseServiceTest { /** 模拟依赖方法 */ /** 灰度发布配置 */ @Mock private GrayReleaseConfig grayReleaseConfig; /** 定义测试对象 */ /** 灰度发布服务 */ @InjectMocks private GrayReleaseService grayReleaseService; /** * 测试: 是否灰度发布-命中渠道白名单 */ @Test public void testIsGrayReleaseWithChannelWhiteSet() { // 模拟依赖方法 GrayReleaseItem grayReleaseItem = new GrayReleaseItem(); grayReleaseItem.setChannelWhiteSet(Sets.newHashSet("alipay")); grayReleaseItem.setUserIdWhiteSet(Sets.newHashSet("123456")); Map<String, GrayReleaseItem> grayReleaseMap = Maps.newHashMap(); grayReleaseMap.put("test", grayReleaseItem); Mockito.doReturn(grayReleaseMap).when(grayReleaseConfig).getGrayReleaseMap(); // 调用测试方法 String key = "test"; String channel = "alipay"; String userId = "123456"; Object value = 1234567890L; Assert.assertTrue("判断结果不为真", grayReleaseService.isGrayRelease(key, channel, userId, value)); // 验证依赖方法 Mockito.verify(grayReleaseConfig).getGrayReleaseMap(); } }在一次代码重构中,把"判断用户白名单"放在"判断渠道白名单"之前,这个单元测试是无法检测出来的。9.3. 验证测试法(推荐)通过对mock方法的验证,可以相对准确地确定命中的代码分支。/** * 灰度发布服务测试类 */ @RunWith(MockitoJUnitRunner.class) public class GrayReleaseServiceTest { /** 模拟依赖方法 */ /** 灰度发布配置 */ @Mock private GrayReleaseConfig grayReleaseConfig; /** 定义测试对象 */ /** 灰度发布服务 */ @InjectMocks private GrayReleaseService grayReleaseService; /** * 测试: 是否灰度发布-命中渠道白名单 */ @Test public void testIsGrayReleaseWithChannelWhiteSet() { // 模拟依赖方法 // 模拟依赖方法: grayReleaseItem.getChannelWhiteSet GrayReleaseItem grayReleaseItem = Mockito.mock(GrayReleaseItem.class); Mockito.doReturn(Sets.newHashSet("alipay")).when(grayReleaseItem).getChannelWhiteSet(); // 模拟依赖方法: grayReleaseItem.getUserIdWhiteSet Mockito.doReturn(Sets.newHashSet("123456")).when(grayReleaseItem).getUserIdWhiteSet(); // 模拟依赖方法: grayReleaseConfig.getGrayReleaseMap Map<String, GrayReleaseItem> grayReleaseMap = Maps.newHashMap(); grayReleaseMap.put("test", grayReleaseItem); Mockito.doReturn(grayReleaseMap).when(grayReleaseConfig).getGrayReleaseMap(); // 调用测试方法 String key = "test"; String channel = "alipay"; String userId = "123456"; Object value = 1234567890L; Assert.assertTrue("判断结果不为真", grayReleaseService.isGrayRelease(key, channel, userId, value)); // 验证依赖方法 // 验证依赖方法: grayReleaseConfig.getGrayReleaseMap Mockito.verify(grayReleaseConfig).getGrayReleaseMap(); // 验证依赖方法: grayReleaseItem.getChannelWhiteSet Mockito.verify(grayReleaseItem).getChannelWhiteSet(); // 验证依赖对象 Mockito.verifyNoMoreInteractions(grayReleaseConfig, grayReleaseItem); } }如果把"判断用户白名单"放在"判断渠道白名单"之前,这个单元测试会报出以下错误日志:Wanted but not invoked: grayReleaseItem.getChannelWhiteSet();错误日志告诉我们,grayReleaseItem.getChannelWhiteSet方法并没有被调用,所以不可能命中渠道白名单代码分支。9.4. 日志测试法(推荐)对于有日志打印的代码,可以通过验证日志方法来确定命中的代码分支,而且这种验证方法是非常简单直白的。如果没有日志打印,我们也可以添加日志打印(可能会涉及日志存储成本的增加)。/** * 灰度发布服务测试类 */ @RunWith(MockitoJUnitRunner.class) public class GrayReleaseServiceTest { /** 模拟依赖方法 */ /** 日志器 */ @Mock private Logger log; /** 灰度发布配置 */ @Mock private GrayReleaseConfig grayReleaseConfig; /** 定义测试对象 */ /** 灰度发布服务 */ @InjectMocks private GrayReleaseService grayReleaseService; /** * 在测试前 */ @Before public void beforeTest() { FieldHelper.writeStaticFinalField(GrayReleaseService.class, "log", log); } /** * 测试: 是否灰度发布-命中渠道白名单 */ @Test public void testIsGrayReleaseWithChannelWhiteSet() { // 模拟依赖方法 GrayReleaseItem grayReleaseItem = new GrayReleaseItem(); grayReleaseItem.setChannelWhiteSet(Sets.newHashSet("alipay")); grayReleaseItem.setUserIdWhiteSet(Sets.newHashSet("123456")); Map<String, GrayReleaseItem> grayReleaseMap = Maps.newHashMap(); grayReleaseMap.put("test", grayReleaseItem); Mockito.doReturn(grayReleaseMap).when(grayReleaseConfig).getGrayReleaseMap(); // 调用测试方法 String key = "test"; String channel = "alipay"; String userId = "123456"; Object value = 1234567890L; Assert.assertTrue("判断结果不为真", grayReleaseService.isGrayRelease(key, channel, userId, value)); // 验证依赖方法 // 验证依赖方法: grayReleaseConfig.getGrayReleaseMap Mockito.verify(grayReleaseConfig).getGrayReleaseMap(); // 验证依赖方法: log.info Mockito.verify(log).info("命中渠道白名单灰度: key={}, channel={}", key, channel); // 验证依赖对象 Mockito.verifyNoInteractions(log, grayReleaseConfig); } }如果把"判断用户白名单"放在"判断渠道白名单"之前,这个单元测试会报出以下错误日志:Argument(s) are different! Wanted: log.info( "命中渠道白名单灰度: key={}, channel={}", "test", "alipay" ); -> at ... Actual invocations have different arguments: log.info( "命中用户白名单灰度: key={}, userId={}", "test", "123456" );错误日志告诉我们,我们期望命中渠道白名单灰度代码分支,实际却命中的是用户白名单灰度代码分支。10. 如何测试多线程并发编程Java多线程并发编程,就是通过多个线程同时执行多个任务来缩短执行时间、提高执行效率的方法。在JDK1.8中,新增了CompletableFuture类,实现了对任务编排的能力——可以轻松地组织不同任务的运行顺序、规则及方式。10.1. 案例代码这里,以并行获取批量交易订单为例说明。/** * 交易订单服务类 */ @Slf4j @Service public class TradeOrderService { /** 定义静态常量 */ /** 等待时间(毫秒) */ private static final long WAIT_TIME = 1000L; /** 注入依赖对象 */ /** 交易订单DAO */ @Autowired private TradeOrderDAO tradeOrderDAO; /** 执行器服务 */ @Autowired private ExecutorService executorService; /** * 获取交易订单列表 * * @param orderIdList 订单标识列表 * @return 交易订单列表 */ public List<TradeOrderVO> getTradeOrders(List<Long> orderIdList) { // 检查订单标识列表 if (CollectionUtils.isEmpty(orderIdList)) { return Collections.emptyList(); } // 获取交易订单期望 List<CompletableFuture<TradeOrderVO>> futureList = orderIdList.stream() .map(this::getTradeOrder).collect(Collectors.toList()); // 聚合交易订单期望 CompletableFuture<List<TradeOrderVO>> joinFuture = CompletableFuture.allOf(futureList.toArray(new CompletableFuture[0])) .thenApply(v -> futureList.stream().map(CompletableFuture::join).collect(Collectors.toList())); // 返回交易订单列表 try { return joinFuture.get(WAIT_TIME, TimeUnit.MILLISECONDS); } catch (InterruptedException e) { Thread.currentThread().interrupt(); log.warn("获取订单中断异常", e); throw new BusinessException("获取订单中断异常", e); } catch (ExecutionException | TimeoutException | RuntimeException e) { log.warn("获取订单其它异常", e); throw new BusinessException("获取订单其它异常", e); } } /** * 获取交易订单 * * @param orderId 订单标识 * @return 交易订单期望 */ private CompletableFuture<TradeOrderVO> getTradeOrder(Long orderId) { return CompletableFuture.supplyAsync(() -> tradeOrderDAO.get(orderId), executorService) .thenApply(TradeOrderService::convertTradeOrder); } /** * 转化交易订单 * * @param tradeOrder 交易订单DO * @return 交易订单VO */ private static TradeOrderVO convertTradeOrder(TradeOrderDO tradeOrder) { TradeOrderVO tradeOrderVO = new TradeOrderVO(); tradeOrderVO.setId(tradeOrder.getId()); // ... return tradeOrderVO; } }10.2. 测试用例对于多线程并发编程,如果采集mock静态方法的方式进行单元测试,将会使单元测试用例变得非常复杂。通过实践总结,采用注入线程池的方式,将会使单元测试用例变得非常简单。/** * 交易订单服务测试类 */ @RunWith(MockitoJUnitRunner.class) public class TradeOrderServiceTest { /** 定义静态常量 */ /** 资源路径 */ private static final String RESOURCE_PATH = "testTradeOrderService/"; /** 模拟依赖对象 */ /** 交易订单DAO */ @Mock private TradeOrderDAO tradeOrderDAO; /** 执行器服务 */ @Spy private ExecutorService executorService = Executors.newFixedThreadPool(10); /** 定义测试对象 */ /** 交易订单服务 */ @InjectMocks private TradeOrderService tradeOrderService; /** * 测试: 获取交易订单列表-正常 */ @Test public void testGetTradeOrdersWithNormal() { // 模拟依赖方法 // 模拟依赖方法: tradeOrderDAO.get String path = RESOURCE_PATH + "testGetTradeOrdersWithNormal/"; String text = ResourceHelper.getResourceAsString(getClass(), path + "tradeOrderMap.json"); Map<Long, TradeOrderDO> tradeOrderMap = JSON.parseObject(text, new TypeReference<Map<Long, TradeOrderDO>>() {}); Mockito.doAnswer(invocation -> tradeOrderMap.get(invocation.getArgument(0))) .when(tradeOrderDAO).get(Mockito.anyLong()); // 调用测试方法 text = ResourceHelper.getResourceAsString(getClass(), path + "orderIdList.json"); List<Long> orderIdList = JSON.parseArray(text, Long.class); List<TradeOrderVO> tradeOrderList = tradeOrderService.getTradeOrders(orderIdList); text = ResourceHelper.getResourceAsString(getClass(), path + "tradeOrderList.json"); Assert.assertEquals("交易订单列表不一致", text, JSON.toJSONString(tradeOrderList)); // 验证依赖方法 // 验证依赖方法: tradeOrderDAO.get ArgumentCaptor<Long> orderIdCaptor = ArgumentCaptor.forClass(Long.class); Mockito.verify(tradeOrderDAO, Mockito.atLeastOnce()).get(orderIdCaptor.capture()); Assert.assertEquals("订单标识列表不一致", orderIdList, orderIdCaptor.getAllValues()); } }11. 附录11.1. 引入Maven单测包<dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.1</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>3.3.3</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-module-junit4</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency> <dependency> <groupId>org.powermock</groupId> <artifactId>powermock-api-mockito2</artifactId> <version>2.0.9</version> <scope>test</scope> </dependency>11.2. 使用到的工具方法11.2.1. 以字符串方式获取资源ResourceHelper.getResourceAsString(以字符串方式获取资源)通过Apache的IOUtils.toString方法实现,提供以字符串方式获取资源的功能。/** * 资源辅助类 */ public final class ResourceHelper { /** * 以字符串方式获取资源 * * @param clazz 类 * @param name 资源名称 * @return 字符串 */ public static <T> String getResourceAsString(Class<T> clazz, String name) { try (InputStream is = clazz.getResourceAsStream(name)) { return IOUtils.toString(is, StandardCharsets.UTF_8); } catch (IOException e) { throw new IllegalArgumentException(String.format("以字符串方式获取资源(%s)异常", name), e); } } }11.2.2. 写入静态常量字段FieldHelper.writeStaticFinalField(写入静态常量字段)通过Apache的FieldUtils相关方法实现,提供写入静态常量字段的功能。/** * 字段辅助类 */ public final class FieldHelper { /** * 写入静态常量字段 * * @param clazz 类 * @param fieldName 字段名称 * @param fieldValue 字段取值 */ public static void writeStaticFinalField(Class<?> clazz, String fieldName, Object fieldValue) { try { Field field = clazz.getDeclaredField(fieldName); FieldUtils.removeFinalModifier(field); FieldUtils.writeStaticField(field, fieldValue, true); } catch (NoSuchFieldException | IllegalAccessException e) { throw new UnsupportedOperationException("写入静态常量字段异常", e); } } }后记其实在很久之前,有人就希望我整理一个单元测试案例库。我迟迟没有行动,主要原因如下:单元测试案例是无穷无尽的,如何系统化地呈现给读者是个大工程;单元测试案例必须典型、合理、有意义,如何构建这些案例也很消耗精力。现在,终于鼓起勇气整理这篇《Java单元测试典型案例集锦》,主要是因为单元测试案例是单元测试重要的一环,也是为了给我的Java单元测试系列文章划上一个完美的句号。最后,根据本文主题吟诗一首:《单测案例》单元测试百家说,案例总结方法多。芳草满园花满目,绿肥红瘦自斟酌。
文章
存储  ·  分布式计算  ·  负载均衡  ·  Java  ·  测试技术  ·  Apache  ·  MaxCompute  ·  Maven
2023-02-07
MaxCompute项目访问外部网络申请表单审批不通过的原因一般是什么呀?我这边接受短信没有说明原因
MaxCompute项目访问外部网络申请表单审批不通过的原因一般是什么呀?我这边接受短信没有说明原因
问答
分布式计算  ·  MaxCompute
2023-02-07
DataWorks如何修改odps表的生命周期?
DataWorks如何修改odps表的生命周期?
问答
分布式计算  ·  DataWorks  ·  MaxCompute
2023-02-07
有没有什么其他方法能够每日增量拉取到TableStore到MaxCompute上的?
想把TableStore的表拉取到MaxCompute上,每日拉取增量数据,但是我在TableStore的这张表的主键是id和订单id,我看文档它好像只能根据TableStore的主键范围来拉取数据,这样我就无法按照时间字段来拉取了,而且TableStore的时间字段只支持时间戳格式,有没有什么其他方法能够每日增量拉取到TableStore到MaxCompute上的?
问答
分布式计算  ·  NoSQL  ·  MaxCompute
2023-02-07
DataWorks odps上自定义函数一直报noclassdeffounderror,为什么?
请问下 DataWorks odps上自定义函数一直报noclassdeffounderror,这个类是jdk8的,本地测试可以的,请问啥原因?
问答
分布式计算  ·  DataWorks  ·  MaxCompute
2023-02-07
1 2 3 4 5 6 7 8 9
...
20
跳转至:
DataWorks
2588 人关注 | 2956 讨论 | 204 内容
+ 订阅
  • 效率优先,DataWorks全链路数据治理年度发布
  • 阿里云DataWorks荣获DAMA中国数据治理优秀产品奖
  • DataWorks售前咨询
查看更多 >
阿里巴巴大数据计算
347441 人关注 | 716 讨论 | 962 内容
+ 订阅
  • 阿里云 MaxCompute 2022-12月刊
  • MaxCompute 新售卖规格解读与新版控制台操作详解
  • 阿里云 MaxCompute 2022-10月11月刊
查看更多 >
Dataphin智能数据建设与治理
77 人关注 | 292 讨论 | 65 内容
+ 订阅
  • 数据安全最佳实践(5):手动指定敏感数据【Dataphin V3.8】
  • Dataphin权限体系(5):自定义项目角色【Dataphin V3.8】
  • Dataphin权限体系(4):表级权限与字段级权限介绍【Dataphin V3.7】
查看更多 >
大数据
188374 人关注 | 29454 讨论 | 80793 内容
+ 订阅
  • Linux网络与数据封装
  • 猿创征文|Python迭代器、生成器、装饰器、函数闭包
  • 网络的几个问题
查看更多 >
开发与运维
5637 人关注 | 131540 讨论 | 303718 内容
+ 订阅
  • socket套接字
  • Linux网络与数据封装
  • C语言作用域与内存布局
查看更多 >