惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

www.infosecurity-magazine.com
www.infosecurity-magazine.com
V
V2EX
博客园 - 【当耐特】
爱范儿
爱范儿
B
Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
MyScale Blog
MyScale Blog
月光博客
月光博客
博客园_首页
G
Google Developers Blog
Recent Announcements
Recent Announcements
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
D
Docker
aimingoo的专栏
aimingoo的专栏
博客园 - Franky
有赞技术团队
有赞技术团队
I
InfoQ
小众软件
小众软件
云风的 BLOG
云风的 BLOG
The GitHub Blog
The GitHub Blog
H
Help Net Security
The Register - Security
The Register - Security
U
Unit 42
P
Palo Alto Networks Blog
N
News and Events Feed by Topic
酷 壳 – CoolShell
酷 壳 – CoolShell
C
CERT Recently Published Vulnerability Notes
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Simon Willison's Weblog
Simon Willison's Weblog
Security Latest
Security Latest
Scott Helme
Scott Helme
罗磊的独立博客
B
Blog RSS Feed
C
Check Point Blog
H
Heimdal Security Blog
T
Threatpost
V
Visual Studio Blog
V
Vulnerabilities – Threatpost
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Project Zero
Project Zero
Security Archives - TechRepublic
Security Archives - TechRepublic
S
Security Affairs
S
Schneier on Security
Apple Machine Learning Research
Apple Machine Learning Research
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
N
Netflix TechBlog - Medium
Attack and Defense Labs
Attack and Defense Labs
L
Lohrmann on Cybersecurity
D
DataBreaches.Net

机核

游戏性能旗舰最强之选,一加 Ace 6 至尊版国补到手价2999元起 6元钱自己更换电动车刹车线 《生化危机9:安魂曲》编剧Haris Orkin专访 摸金游戏?音乐游戏!暗区新赛季这把能弹的琴有何来历? 好评国产武侠SRPG《息风谷战略》免费DLC现已推出 | 机核 GCORES 碎片 《生化危机:安魂曲》将于5月实装首个免费更新内容 | 机核 GCORES 新锐东方游戏,谱写世界新章! 沉浸式恋爱视觉小说游戏《心象演算》免费试玩版现已正式上线 | 机核 GCORES 互动影游《代号三国:龙起》上线!穿越三国与曹操并肩、与佳人同行、与权谋博弈! 《老头收集梦想生活》,游戏酒桌会6,录音笔VOL.689 | 机核 GCORES 破界·共生——《白日梦:无限世界》五大核心游戏特点解析 价格已到史低,锐龙5 9600X/锐龙7 9700X正适合抄底 时间循环之旅即刻启程!《归环》一周目测试今日开启 热门在线韩游变魂游,洛奇衍生作能否打破“花瓶”魔咒? LG UltraGear evo 全新高端显示器系列:当“5K”遇见“AI”,不止强大,更懂热爱 反套路三国互动影游《代号三国:龙起》今日上线! 愿望单登记人数突破10万!备受瞩目的“女儿养成游戏” 《梦幻魔法公主》今日于Steam平台上线!限时八折优惠中 《黑神话:悟空》全球音乐会2026巡演将于4月29日12时开票 | 机核 GCORES “Snowguelike”生存肉鸽挖矿新作《蛙穿雪境》公布发售日期,5月7日正式上线 | 机核 GCORES 重塑移动办公、AI创作新境!全新华硕灵耀Air系列、ProArt 骁龙版震撼首发,创芯未来 均分88:《Saros》媒体评分汇总 | 机核 GCORES 上海烛龙公布合作遗迹探险游戏《吉时已到》首支预告片 | 机核 GCORES 首个独立游戏《萝薇日记》已上线Steam! 烛龙新IP《吉时已到》首曝,打造国内首款中式合作遗迹探险游戏 喜加一:《暗黑破坏神Ⅳ》国服现已开启限时免费领取本体活动 | 机核 GCORES 新版《生化危机》电影定于9月18日上映,官方网站现已上线 | 机核 GCORES 《冲就完事模拟器2》“星球大战”联动DLC正式宣布 | 机核 GCORES SteamController将于5月4日发售,售价99美元 | 机核 GCORES 事已至此,内存这么用也算省钱了,“2+1”非对称双通道应用实测 505游戏母公司现已收购《明末:渊虚之羽》IP | 机核 GCORES 基石 手动杂谈12|格斗游戏也能讲好故事 | 机核 GCORES I Love You Mr Snowball 我的向日葵小姐 会比GTA6先发售吗?最硬核的生存游戏《DAYZ》要推新DLC了 周记02:在2026遇到新怪谈 什么硬件,能让游戏Loading界面快速消失? | 机核 GCORES GadioSpec《百年风云世界杯》免费试听集 | 机核 GCORES 百年风云世界杯Vol.1丨足球崛起 | 机核 GCORES 100年前,一群人提前替我们经历了AI恐惧 | 机核 GCORES 《呼啸山庄》2026 ——一辆当代艺术的大卡车冲撞了我的大脑 手游《天穗之咲稻姬:日之香巡灵传》宣布将于7月27日停服 | 机核 GCORES 集结梦之队,征战世界杯,《最佳球会ONLINE》上线Steam 山水绝景随心拼 休闲建造游戏《千里山河录》Steam商店页公开 巫师帽、法袍、魔杖,为什么它们是影视、游戏里的法师必备三件套? 《无鞘信使》-第一章 复古风自动战斗肉鸽《终结之终结》Steam商店页面现已上线 电脑里有一款不破不立的MMO,录音笔VOL.688 | 机核 GCORES 《生化危机》30周年纪念周边发售,这次是真的“保护伞” 可靠耐用+AI全能,惠普战66 2025锐龙版深度体验 《时之书:无尽终章》关卡“大航海时代”全球首次公开 经典名作《乌龙派出所》改编经营模拟游戏《乌龙派出所~阿两的商店街物语~》正式宣布支持简体中文 明日开冲,解锁反套路三国互动影游《代号三国:龙起》即将上线 独立游戏《这是我的宝藏!》已发售~ 降低难度不是唯一解,无压力死亡也是好体验 Netflix官宣新片《普通人》:讲述韩国现代史上的权力风暴 《生化危机:安魂曲》全球销量现已突破700万份 《绝地鸭卫》PC版5月15日正式发售 亡妻回忆录?女性向情感叙事游戏《S-mail》现已正式发售 全新酷黑风格,酷睿Ultra 200S PLUS的高性价比搭档!七彩虹BATTLE-AX B860M-PLUS S WIFI7 V20 超级黑刃主板测评 胖狗 索尼发布了港台地区PS5产品价格调整公告,将于5月1日起实施 二次元怪猎+性感美女!《碧蓝幻想:无尽黄昏》开启Beta公测 这款怪谈类型中式恐怖游戏居然更新了?! 《百日战纪 -最终防卫学园-》改编漫画将于今年冬季开启连载 《如被附身,请致电我们》:匈牙利黑色幽默恐怖小说 《竹屿山房杂部卷五》(译文) 大树 超越引擎 摄影分享丨四月 碎片杂记vol.72 当老式FPS与老式动画技术碰撞出新时代的火花—《神探杰克鼠》 想做独游,如何避免首个项目就褒姒? 在线多人动作游戏《OCTOPinbs》将于5月12日上线Steam! 《酒鬼女神的酒诡》确定将于2026年登陆Steam 《CRYMELIGHT》将于11月5日(周四)正式发售!4月25日开始预购! 钢铁国度MKI部落Evolution,蛮兵部分 【钢铁国度】部落-Primal MK I,蛮兵部分 这是一个高中生用ai跑出来的作品,我自称他为物理神话 钢铁国度MKI部落Metamorphosis,蛮兵部分上 钢铁国度MKI部落Metamorphosis,蛮兵部分下 INDIE Live Expo于4月25日举办:首发9款新作,超200款独立游戏亮相 暗黑卡牌策略新作《魔忌:穷鼠啮狸》发布全新中文试玩版和发售预告片,4月30日正式上线在即 战锤40K长篇小说:变节者・苦难主宰(三)(全书完) 战锤40K长篇小说:变节者・苦难主宰(二) 战锤40K长篇小说:变节者・苦难主宰(一) 英语语言学习丨短语专题1:短语的特点 唯一的EVE战争 【少前同人】【M200】战术人形会梦见音乐会吗 【昏迷3】即将发售,前作主角悉数到场!“恶灵”宋老师化身可操控角色 业内人士:游戏公司“十有八九”使用生成式AI,包括卡普空 欢庆一周年:《光与影:33号远征队》全新纪念艺术图、超值折扣与免费更新同步上线 才刚刚开始呢 【抽奖】《星际卡车司机》推出免费大型更新,四折平史低折扣进行中 四人合作FPS游戏《佣兵猎手》抢先体验重大更新 1 现已上线 Raw Fury新作《深馅地牢 Deep Dish Dungeon》将于今秋加入 XGP 塔防幸存者游戏《魔怪来袭》推出首个 DLC 《饿狼传说:群狼之城》1周年纪念!新DLC“沃尔夫冈·克劳萨”今日参战 猫狗相伴 欢乐闯关 双人合作平台跳跃游戏《猫狗同行》上线Steam商店页 《同行:月球逃脱》(Together: Moon Escape)上线将于明日上线Steam
墓碑上的软件工程:测试
韩大 · 2024-12-09 · via 机核

曾经我很不喜欢“软件测试”这个行当。因为曾经听某个老板这样宣扬:古代修城墙分两拨人,一拨人筑墙,一拨人检查,检查的人用长矛去戳城墙,如果戳的进去,筑墙的人砍头,戳不进去,检查的人砍头。——显然这个故事是瞎编的,但是这么拿来说,显然是想利用人性的弱点。事实上,有些公司就是把 BUG 数量指标加在开发人员和测试人员的头上,让这两拨人去斗争。如果仅仅把软件测试视为是“质量控制”的手段,那么显然是非常浅薄的。

现代的软件测试,已经是软件开发效率提升的重要手段。

每一个程序,如果写完之后仅仅是通过编译,而没有运行过,其实根本不算写完了程序。所以运行程序本身,就是软件开发的一部分。利用手工或者自动的方法,去运行程序,我们往往称为“软件测试”。——现代软件测试,是希望尽量多的减轻软件测试的人力负担,多让程序来做这个事情。软件工程的目的是提高软件的生产效率,软件测试技术正是达到这个目的的一个手段。

单元测试

很多年前,我曾经需要为一个新建的办公室安装20台PC兼容机。老板给了我20台PC的所有零件,包括主板、CPU、内存、显示卡等等,这些零件一共有三四个大纸皮箱。于是我立刻开始动手,一套配件开始一顿叮咣安装了3台PC,一接上点源,结果一台PC的显示器都不亮。主板、CPU、内存、显示卡任何一个配件有问题,都会导致显示器不亮——这一下该怎么办呢?总不能全部装好20台之后再一台台的去排除故障吧?(有一种维修专用卡,插在主板上,可以提示故障的配件,不过那个时候手上并没这种高档货)后来我就想了个办法:首先装一台正常的 PC,然后把所有的配件,都换上去测试一遍。很快我就把所有的内存、显卡、CPU都测试了一遍,果然里面有15%左右的配件是有故障的。后续就很好办了,拿那些好的配件去插上主板,如果有问题就是主板的了。这样大部分新装的PC都是能正常工作的,同时剩下一堆有故障的配件,就让老板去退货,换回来的零件,依然是先“测试一遍”,然后再组装。

其实,开发软件和上面安装电脑的例子有一定的类似,一个软件可能由很多个部分(模块)组成,如果每个部分能单独的进行测试,确保自身是工作正常的,那么整个软件运行起来,出故障的概率就会小很多。单元测试,指的就是单独对某个模块进行测试:

写一个程序,去调用测试目标软件的某个函数,然后得到返回值,用程序判断一下返回值是否符合预期,来宣告测试是否完成。

除了用来确保软件各部分的正确,单元测试还有其他的用途:从前我最开始做网络游戏的时候,其中有一个环节是很费时间的,那就是客户端和服务器端联调协议。那个时候 ProtocolBuffer 还没有诞生,协议的定义完全是靠文档和人力沟通。每个协议联调都需要两个程序员同时运行程序,然后反复手工运行程序、进行某些操作,才能一点点调试通过。后来,我们把客户端程序员和服务器端程序的座位搬到一起,这样沟通效率高了很多。再后来,我们让游戏客户端和服务器端的程序 IDE 都同时在一个电脑上打开,然后两边直接下断点调试,又进一步的提高了联调的效率。但是当我掌握了单元测试的技术之后,完全可以自己写一个程序来完成这种测试,我只要运行一个 mock 的客户端或者服务器,就能很准确的发现协议定义不一致的地方了。

我们用单元测试的程序,去确保另外一个程序运行正确的做法,对于某些特别的项目,还有额外的意义。我参加过一些“库”的编写,这些程序没有自己的 main 函数,完全是用于客户程序员调用的。当我写完一个“库”里面的功能之后,需要运行这个代码才能知道是否正确。在这种情况下,使用单元测试技术,不但能完成这个代码的测试运行,而且还可以把测试代码,作为库的“示例”代码进行积累。

对于有比较完整的测试代码的项目,在后续的工作中,我可以放心大胆的对各种细节进行重构。只要最后单元测试都通过,我就不必过于担心自己写的程序是否有问题。也不用太害怕让使用中的项目进行升级会不会出问题。甚至我丢失了全部的正式库代码,只要单元测试代码还在,我都可以很快的重新把这个库写出来。

单元测试的好处非常多:

  • 可以逐个部分验证程序是否正确,降低查找问题的难度

  • 可以用作程序之间调用的模拟程序,减少开发工作中的沟通难度

  • 可以作为复用代码(库)的示例程序,同时也是开发者对用户程序员的使用流程的一种设计过程

  • 可以用程序来描述需求,真实快速的确保程序是否完成了需求

  • 用来作为重构的保障机制,提高开发效率

因此,人们也开发了各种帮助我们编写单元测试的工具,这些工具一般包含了以下几个部分:

  1. 框架:一个可以独立运行的程序,通常是带 main() 函数的,这个程序可以由用户去填写一系列回调函数,用来对测试的整个流程进行填充,一般来说测试过程都需要有这些回调函数:开始测试、每个用例开始前、每个用例的调用、每个用例结束后、全体结束后。一般对于一系列预期的函数进行调用,称之为“用例”。 之所以需要这么多过程,是因为想要真实的运行一个函数,往往需要先准备好这个函数运行所需的内存环境,而且每个用例所需的环境是不一样的,所以结束一次用例之后,还需要清理环境。

  2. Mock: 对于各种数据库、网站数据源(如 Restful API)等外部依赖,提供本地内存中就可以使用的模拟。我们称之为 Mock 程序。这样单元测试才能在最简单的环境下快速运行。

  3. Assert: 把运行结果和期望值对比的函数——如果对比不同则显示对比的结果,并且宣告测试失败;如果全部结果对比成功就宣布一个用例测试通过。

JUnit

流程定义

JUnit 使用“注解”(annotation)来标注框架所需的回调函数,最简单的就是在你的方法上标注 @Test。除此以外,JUnit 还提供了很多其他注解,用以构建整个测试流程,譬如初始化测试环境、准备测试条件、清理测试结果环境等等,这些可以在手册上查到。

// 一个简单的单元测试程序 import org.junit.Test; import static org.junit.Assert.assertEquals; public class TestJunit { String message = "Hello World"; MessageUtil messageUtil = new MessageUtil(message); // 这个 MessageUtil 就是你要测试的对象 @Test public void testPrintMessage() { assertEquals(message,messageUtil.printMessage()); // 断言,报告测试成功或者失败 } }

然后你可以自己写一个简单的 main 入口启动测试,或者使用各种 IDE 来启动单元测试。所有标注了是单元测试的方法都会被调用,并且输出测试结果。

// 使用 main 函数自己启动 TestJunit 类的单元测试 import org.junit.runner.JUnitCore; import org.junit.runner.Result; import org.junit.runner.notification.Failure; public class TestRunner { public static void main(String[] args) { Result result = JUnitCore.runClasses(TestJunit.class); for (Failure failure : result.getFailures()) { System.out.println(failure.toString()); } System.out.println(result.wasSuccessful()); } }

如果测试失败,屏幕上会显示结果,并且说明哪个“用例”失败了,期望值和实际值是什么:

Hello World testPrintMessage(TestJunit): expected:<[New Wor]d> but was:<[Hello Worl]d> false

如果你是使用 IDE,单元测试的运行往往不需要去写 main 函数了,很多 IDE 都提供了直接运行你的用例,并且显示结果的友好界面。

断言

Junit 提供了挺多的断言函数,都是为了方便我们写最后的测试结果判断。

Assert.assertEquals("预期为1,相等啦", 1, 1); Assert.assertNotEquals("预期为1,不相等", 1, 2); Assert.assertTrue("真的", true); Assert.assertNull("空", someVar);

大部分的单元测试框架,都提供了这些断言函数,所以后面介绍就会略过这一部分咯。

Mock

对于编写单元测试来说,Mock 模拟程序运行环境,一直是一个难点。首先说说为什么需要模拟环境,而不是直接在真实环境下运行要测试的程序:假设你的程序是一个对 MySQL 数据进行操作的程序,如果要测试运行,是需要有一个能连接上的 MySQL 数据库服务器的。你当然可以在开发环境下安装一套开发用的 MySQL,确保测试运行的时候这个服务器是正常运行的。但是我们的单元测试有时候需要在 CI 持续集成系统上运行,那个环境未必安装了一套 MySQL 程序,如果要确保单元测试能运行,你就必须在每一个可能运行单元测试的地方都安装 MySQL,这个安装和维护的工作量会非常繁重;另外一个原因是,就算有了 MySQL 服务器,你还需要数据库里有合适的数据表和数据记录,当然你也可以在测试用例中的准备阶段进行删表重建、插入数据等等——但是这么一来,编写的代码量也很大了,而且如果有大量的用例需要运行,真实的数据库操作会让单元测试运行非常缓慢。如果依赖的仅仅是 MySQL 还好一点,但如果是一些非标准的程序,譬如某个 RESTful 的数据接口,你根本无法自己安装一套,那就只能用模拟 mock 了。

JUnit 本身并不提供 Mock 的功能,但是我们可以找到其他帮助软件,譬如 Mockito

@RunWith(MockitoJUnitRunner.class) public class UserServiceTest { @Mock private UserRepository userRepository; @InjectMocks private UserService userService; @Test public void testGetUserById () { // 安排 User mockUser = new User ( 1 , "John Doe" ); Mockito.when(userRepository.findById( 1 )).thenReturn(Optional.of(mockUser)); // 行动 User result = userService.getUserById( 1 ); // 断言 assertEquals( "John Doe" , result.getName()); } }

Mockito 提供 @Mock@InjectMocks 等注释,有助于简化模拟对象的创建及其向被测类的注入。

  • @Mock:此注释用于创建指定类的模拟实例。在上面的示例中,使用此注释模拟了 UserRepository。在我们的测试目标 userService 对象的代码中,是会调用 UserRepository 类来获取数据的,所以我们模拟了这个 UserRepository 的行为用于测试,而不是真正的去查数据库。

  • @InjectMocks:此注解告诉 Mockito 将模拟(如 UserRepository)注入到被测试的类(UserService)中,从而有效地自动连接模拟依赖关系。

一般的 Mock 框架,都会提供一种技术,去让某些已经写好的类(或者接口)产生固定的行为,譬如返回预定的结果值。这样被测试的代码,在运行时并不知道自己所依赖的类已经被替换。这样就避免了这些被依赖的类去连接真正的数据库或者其他不好在单元测试中运行的代码。

GTest

Google 的 C++ 单元测试框架 gtest,需要用户自己写个 main 函数:

#include "gtest/gtest.h" #include <iostream> #include <string> int add(int a, int b) { return a + b; } TEST(fun, add_a) { EXPECT_EQ(-3, add(-2,-1)); EXPECT_EQ(-2, add(1,-3)); } int main(int argc, char **argv){ ::testing::InitGoogleTest(&argc, argv); return RUN_ALL_TESTS(); return 0; }

这里的大写 TEST RUN_ALL_TESTS 都是 C++ 框架常用的“宏”(地精魔法),按照规矩写问题也不大就是了。

gtest 的 TEST() 定义的是一个用例,可以通过参数标注这个用例的目标、用例的名字,例子中 fun 就是用例目标,add_a 就是用例名字。EXPECT_EQ() 就是断言的宏函数了,和之前 JUnit 的 Asserts 类似,也是有一批各种判断断言。

gmock

c++ 单元测试也有 mock 工具,譬如 gmock,它可以模拟一个已经定义的类的行为,例子:

这个是被模拟的类 FooInterface.h

class FooInterface { public: virtual ~FooInterface() {} public: virtual std::string getArbitraryString() = 0; };

这个是模拟的类 MockFoo.h

#include <gmock/gmock.h> class MockFoo: public FooInterface { public: MOCK_METHOD0(getArbitraryString, std::string()); };

我们再次看到宏魔法 MOCK_METHOD0() ,用于定义需要模拟的方法。编写单元测试的时候可以直接定义模拟行为:

#include <cstdlib> #include <gmock/gmock.h> #include <gtest/gtest.h> #include <iostream> #include <string> #include "MockFoo.h" using namespace seamless; using namespace std; using ::testing::Return; int main(int argc, char** argv) { ::testing::InitGoogleMock(&argc, argv); string value = "Hello World!"; MockFoo mockFoo; EXPECT_CALL(mockFoo, getArbitraryString()).Times(1). WillOnce(Return(value)); string returnValue = mockFoo.getArbitraryString(); cout << "Returned Value: " << returnValue << endl; return EXIT_SUCCESS; }

这里通过 EXPECT_CALL(mockFoo, getArbitraryString()).Times(1).WillOnce(Return(value)) 表示模拟的类对象 mockFoo 的 getArbitraryString() 会返回 value 变量的值。这里的 Times(1) 表示模拟只被运行 1 次,WillOnce() 表示被运行这一次的时候会做出一次性的行为。实际上可以定义模拟方法多次调用应该做些什么事,返回什么值:

EXPECT_CALL(mockTurtle, getX()).Times(testing::AtLeast(5)). WillOnce(testing::Return(100)).WillOnce(testing::Return(150)). WillRepeatedly(testing::Return(200))

  • 调用mockTurtle的getX()方法

  • 这个方法会至少调用5次

  • 第一次被调用时返回100

  • 第2次被调用时返回150

  • 从第3次被调用开始每次都返回200

go test

go 语言充满了各种“暗喻”,包括单元测试也是:

  • 单元测试代码文件叫 xxx_test.go

  • 单元测试的用例函数叫 func TestXxx(t *testing.T)

go 可以不写断言,而是先自己用 if 判断情况,然后用单元测试函数的入参 (t *testing.T) 来宣告测试失败:

  • t.Error("error test") 单元测试失败,打印 error test,继续运行

  • t.Fatal("fatal test") 单元测试失败,打印 fatal test,程序退出

在有 xxx_test.go 的目录下,直接运行 go test 就可以运行所有单元测试文件里面的测试用例函数。

=== RUN TestSum --- PASS: TestSum (0.00s) === RUN TestAbs --- PASS: TestAbs (0.00s) PASS

运行结果会显示在命令行界面。go 的单元测试还会统计覆盖率,以及很多其他功能,可以通过手册进行探索。

现代大多数的语言,都会支持单元测试。说明单元测试已经是一个“标配”功能,是业界公认的一个重要事情。

冒烟测试

这一术语源自硬件行业。对一个硬件或硬件组件进行更改或修复后,直接给设备加电。如果没有冒烟,则该组件就通过了测试。微软公司在《微软项目求生法则》一书中提出的一种功能测试,目的是对一个新编译需要正式测试的软件版本,确认软件的基本功能是正常的,可以进行后续的测试工作。

“冒烟测试”这个名称,其实不是一个非常准确的说法,大概的意思,就是一个快速运行的测试过程,来确保软件的主要功能是可用的。在一些比较大型的项目中,测试团队在启动任何测试计划之前,都会先快速的执行一次“冒烟测试”,如果出现问题,就停止测试计划打回给开发团队,避免浪费测试时间。这种测试可以是自动化的,也可以是手工的。冒烟测试主要包含两个含义:

  • 主要测试主要功能的正常流程,那些次要功能,以及错误分支、异常输入一般都省略

  • 在尽量真实的环境下测试,保证用户角度的基本可用

我们都知道详细的测试能带来很多的好处,但是成本也相当的高,譬如需要大量的测试代码、测试环境、运行测试的时间等等。但如果我们只是改了一行代码,运行一次全面的测试就很不划算了,所以应该运行一次“冒烟测试”。我们修改软件的时候,常常会有意想不到的“副作用”,常常会造成明显的整程序挂了。冒烟测试可以保障我们的软件在最低程度上是可用的。

冒烟测试可以由一些单元测试组成,也可以和单元测试无关。但是通过自动化的手段,写一个程序来进行测试,显然是最好的做法,因为这可以放在 CI 持续集成的流水线中,当每次发布完成,都自动的运行一次。如果有严重的问题,就可以立刻被知道了。

我在某一个游戏项目里,使用了 gtest 框架写了一个对这个游戏服务器的冒烟测试:这个程序会启动游戏服务器,然后向这个服务器监听的网络端口发送网络包,模拟客户端的注册、登录、选人、匹配、开始游戏、结算这套基本流程。每当我开发完一个功能,都会首先运行这个测试,当然这个过程比单元测试要长,而且也对数据库和网络有依赖,不过确实帮我发现了很多程序的问题,在和游戏客户端联调之前,通过了这个冒烟测试后,问题会少很多。

压力测试/性能(bug)测试

  • 如果没有压力测试,就不要做性能优化。因为做了优化看不到效果,很有可能反而性能更差了。

  • 过早的性能优化,除了浪费开发时间、增加代码的复杂度以外,很可能随着开发进展和需求变化,这些措施反而让性能更差。

压力测试可以写代码来模拟很多请求。同时也可以通过录制真实用户的输入,然后并发执行来模拟。我曾经经历过一个网络游戏项目,曾经对用户的访问数据包,都旁路了一份进行存储。每个用户的每次登录,都建立一个文件,本次“会话”的所有之类都会记录在同一个文件里。我们把这些文件用程序同时打开,然后一起发送用户的指令,就非常真实的模拟了多人对游戏服务器的压力。

大部分语言,都有工具可以记录运行时的“火焰图”,这个图会根据程序调用函数的深度,生成火焰的高度,而函数运行的时间则生成火焰的宽度。这个图能非常直观的看到程序运行时间到底被什么函数使用了。

内存的使用,也有类似 pprof 的工具,可以展示程序运行过程中内存的分布情况

程序员都普遍很喜欢搞性能优化,因为这似乎能说明自己的技术水平。有一些大厂,也很喜欢拿这些来评判程序员的水平。但是我认为,大部分的性能优化,是通过时间-空间转换来进行优化的。除此之外的优化,往往需要在数学上使用更突破的方法,才能真正提高性能,而这并不是很常见的情况。当然也有专门利用新的硬件、操作系统功能来进行优化的,还有就是把功能设计成可以多个电脑并行处理。——这些都是技术水平的体现,但开发效率的提升往往更加急迫,而且开发效率的提升,也能提高性能优化的速度。

缺陷跟踪系统

通过测试,我们会发现软件的问题,这些问题如何被管理起来,是需要一些工具。最古老的工具就是一个 Excel 电子表格,高级一点的可以用一个云文档来记录。有些公司针对缺陷管理,开发了一些软件工具。我最早接触的是 JIRA,这个工具的界面有点奇怪,但是用来记录和跟踪 bug 还是足够的。之前也尝试拿它当项目管理工具,但是它没有“甘特图”,所以最后还是不够方便。另外我们还有一些,譬如 TAPD,Redmine。另外诸如 GitLab 也可以在作为源码服务器的同时,承担缺陷跟踪的功能。

测试驱动开发

测试驱动开发(英语:Test-driven development,缩写为TDD)是一种软件开发过程中的应用方法,由极限编程中倡导,以其倡导先写测试程序,然后编码实现其功能得名。测试驱动开发始于20世纪90年代。

关于测试驱动开发,曾经在概念上流行一时,但是实际上使用的项目却不多。原因有很多,其中主要的有:

  • 开发之前先写很多测试代码,不符合着急的老板们要看效果的诉求

  • 自动化测试对于有些项目,编写起来比较复杂,譬如 GUI 程序、游戏客户端程序。当然这些也有对应的框架,利用现在“大模型”AI 技术,这方面会更加方便。但是技术要求肯定比写单元测试要高。

不过,有自动化测试程序的项目,质量是明显比没有的项目,要高很多。随着测试用例的积累和维护,这种质量还能一直保持,这比起靠人工测试的责任心驱动,显然是好很多的。可能我们的项目,不是每个都适合使用测试驱动开发,但是用测试用例来描述需求,通过运行测试来检查需求是否完成,显然自动化程度远比人工验收来的快速和全面。

后续

前面说了大量的工具向的软件工程知识,下来想讲讲代码层面的软件工程知识,也是我最喜欢的部分。