0%

终于重新拾起笔记录生活了,先给自己点个赞2333🤪

这是来美国之后第一次记录生活,从八月到现在过的很充实精彩,简单总结下吧:

Read more »

哟伙计,被我标题党骗进来了吧👻👻

哎哎别走,虽然标题取得很有误导嫌疑,但是我可没撒谎,我真的在听MIT的课哦——就是那所QS全球大学排名连续11年霸榜No.1的麻省理工大学 (Massachusetts Institute of Technology),单项的话如计算机科学的CS Ranking也高居全球第三!

Massachusetts Institute of Technology

看了上期周刊的朋友肯定会说你可拉倒吧,你个东北大学生搁这装💩 好吧我招我招,我听的是由MIT发起的OpenCourseWare平台的在线课程,这是一个拥有超过2,500门课程的免费公开课平台,致力于免费分享最优秀的知识给全世界的学习和教育者!

Read more »

Hi 朋友们!下了一周雨的广东终于迎来一个晴朗的周末,舒服了😌

本期周刊打算分享我本人在2021年10月初开始起意,边工作边着手美国研究生的申请,12月中旬递交所有申请,最终在4月初收到梦校Offer的经历📚 目的是整理这段经历,跟大家坐下来分享一份属于打工人成功学术上岸的案例✅ 我的背景和经历也许并不普适,但如果其中有环节的某些信息能给到你帮助,那我会非常开心👾

那么,正题开始!🛫

Read more »

Hi,朋友们!我是布鲁斯基👾

距离上次发文章已经过去快两个月,真就大懒鸽了属于是🐦 在家里的这两个月,对我来说时间过得不算很快,因为这期间我感受、思考、经历了许多,数次有话想讲,又因为各种原因提笔放下(我承认懒惰占九成🤪

而今天,我终于又燃起动力写文章了!这次想聊下“个人周刊”,一种很适合个人”短平快“输出的内容形式

Read more »

在提交完最后一次整理项目REAMDE的merge request之后,我合上了Macbook——我的老战友,跟我的公仔们还有工卡放在一起,咔嚓——来了张合影。

没错,我的鹅厂生涯暂时要告一段落啦~


说起离职,我其实有过好几次冲动,每次的理由都不一样哈哈,然后又会听取朋友或前辈们的意见,冷静下来。但这一次,我做出了决定,如果要总结原因,我觉得是到“时候”了。

这里的“时候”,可以理解为三层意思:

  1. 是圆我的留学梦的好时候了
  2. 是时候重视生活与工作的平衡了
  3. 是时候想清楚我想成为什么样的人了

我想“行万里路,读万卷书”

去年9月的时候,朋友微信上突然跟我说:“美国Master今年申请不要求GRE哎!”

我查了下几所知名的大学,GRE确实是Optional了??这一消息把原本打算22年(工作满3年嘛)才开始准备留学的我,提前赶上了跑道。因为早就耳闻GRE挺麻烦的,免去了GRE相当于抄底申请呀哈哈。

我以为我英语还不错啊,结果第一次托福摸底考了个70分…于是在10月份的时候,我报了下线1对1托福班,开始了早六晚十二的边工作边考语言的生活(早上六点半爬起来背单词,中午饭后刷口语,午休刷阅读,晚上7点下班刷写作TUT)。还在墙上列了排期表,每过一天就叉掉,不断给自己正反馈。

经过了2个月的超自律的突击,我赶在11月底考出了99分(就差1分到100,真是气死人)…

嘛,看了下官网好像不是强制100分,那就这样吧,赶紧搞文书去,最终在名校DDL12月中旬前,申请了4所美国(CMU@MSCS, UCLA@MSCS, UCSD@MSCS, NEU@MSCS)、2所香港(HKU@MSC, CUHK@MSC)、1所新加坡(NTU@MSAI)。

出国留学这事儿,对我来说学历镀金可能只是个结果,我更在意的是出去看看、静心学习的过程。 我想趁还没有到被年龄、婚姻等束缚的年纪,满足我探索这个世界的好奇心。同时我相信大部分学生出来工作后,都会想念大学时大片的可支配时间吧!只可惜本科时玩心太重,没有把时间用好,现在带着目的重回校园,希望能不负韶华,学到真材实料。

至于为什么是美国,当然也是因为那边能看到、体验“全球化”的缩影,开阔视野,求同存异,广交人脉;也因为美国是计算机的发源地,去源头学习CS与工作,肯定会有不一样的收获吧。疫情和安全?在跟几个已经在美国的朋友聊过之后,其实做好个人防护和选相对安全的城市生活,还是有应对和避免的策略的。

所以现在有“行万里路,读万卷书”的机会,我当然愿意去争取!(保佑我三四月份能收到一份offer吧

我想要健康、能陪伴家人、完整的日常

2019年毕业体检,我就检查出有胆囊息肉(6x4mm),到了2021年7~12月分别测了两次,胆囊息肉从8mm长到10mm,长得太快了!医生说超过10mm就要切除胆囊了,对,是切除胆囊不是息肉,主要是怕胆囊病变。

胆囊息肉的成因就是油腻、饮酒、熬夜与压力。大学时比较浪,喝酒烧烤熬夜啥的;工作之后,我在熬夜和饮酒都有所克制,剩下的就是压力了。虽说我在的部门有双休(大约985的工作节奏),工作日能够按时完成业务就不错了,然而还有绩效和个人成长会一直萦绕心头,让我在休息时还会想要抓紧学技术。离谱的时候周末都会掐着表规划和学习,很多时候甚至会忽略家人和女朋友的感受,自己给自己压力太大,把自己整得焦虑不堪。

还有一件事让我压力倍增。去年8月,前leader拉我垫背二星。我上半年接了他甩来的联合项目锅,摊子蛮烂的说实话,人力少又服务多个业务部门,已经认真尽心尽力地在做,时间都耗上面了。结果因为换组该项目不是组里的目标了,他拿我没有技术输出来说事儿,硬给二星。一那个项目就是技术中台,提升了业务运营效率,到他那就不算技术输出;二是组内的技术项目我也有在参与和设计,他老早不关心代码了就选择忽视说没看到呗。真是受不了这侮辱,直接开怼+要走。虽然我后面还是冷静下来换组了,但对我后续的工作无形增加了很大的压力,毕竟拿个二星,年终少了不说,晋级要比正常人晚2个周期,哎。

槽吐完了,回到身体健康的话题来。我妈心软,看我压力大,常常劝我说“人生很长,做任何事都不用急这一会儿,但身体健康是关乎一辈子的”。大厂本来就注定是紧张快节奏的,加上自己的精神和身体状态都不太好,所以我决定Slow it down,让自己喘口气,调整身体和心态,再出发。

BTW,去年10月份突如其来的车祸夺走了我的外婆,也打碎了我美满的家庭。外婆勤快,烹饪家务样样拿手,有外婆在的家,是温馨而井井有条的,每次回家都能看见她慈祥开心的笑容,吃到她特意准备的饭菜。她的离去真的给我们打击很大,尤其是我的母亲。我想大学之后就没好好在家待过了,如果我22年拿到offer,8月就会前往异国,又要离家很久。我想多陪陪家人,外婆不在了,家里就由我来打理吧。

暂时告别看不见日落的互联网打工生涯,呼吸、生活才是人生的常态啊。

我想成为Web3.0的独立工作者、创业者

想起21年底游戏大神轻生的事件,有条知乎高赞回答用故事的方式尝试表现互联网人的承压工作现状:远大的理想与骨感的现实,超强的能力与过载的期望,百万年薪与高强度的工作,成功的事业与垮掉的身体,这些对立的矛盾真的刺痛到我的神经。

我到底想要什么,想成为怎样的人呢?在我已经过去2轮本命年的时候,我想我应该有一个大致的答案了。花了几个晚上,在画布上用脑图列出所有我能想到的方向,自问自答,最终没有得出具体的结论,但是承认了一个事实:“我只是一个普通人,我能选择从事感兴趣、有价值的领域就够了”。

Web3.0,这一2021年兴起的区块链概念,阐述了信息即资产的新一代互联网设想。依托于区块链与密码学技术,每个网络公民将对个人信息或生产的信息有着更强的自主权,同时享有收益的权利,这也就是信息即资产的意思。同时加密货币、NFT、DAO、DeFi、GameFi等生态正在吸引全世界的弄潮儿们加入,探索巨头垄断、红利逐渐枯竭的Web2.0时代的新解决方案。将制度交给数学与程序来实现,探索无人区,这对于技术人员来说,是最好的时代;对于非富二代官二代的年轻人来说,也是世界洗牌、阶级跃迁的好机会。

Web3.0的建设者来自全世界,自然不可能都在一处办公,所以成为独立工作者的想法就冒出来了。成为独立开发,意味着我可以选择方向、项目,意味着我可以把控work life balance;当然也意味着收入不稳定、需要超强自制力等等要求。但总体来说,它很吸引我,也天然适合程序员这个职业。

再仔细想想,其实独立工作不意味着单打独斗呀,我还可以找到志同道合的伙伴,一起合作作战,甚至日后创立属于我们的项目和事业。

另外对于加密货币的世界,无疑也是黑客们的黄金时代,网络安全和区块链安全将是hacker们激战的领域,这也是有意思的领域~

其实到了这里,我的视野和能力已经限制了我的想象,产生了很多不确定的不安全感。我肯定想的太简单了,日后的坑让未来的自己踩去吧哈哈哈。不过相比过那种能一眼望到头的生活,我更向往航行在颠簸的大海上去闯荡~

So,All in Web3, LFG!

最后

鹅厂真的就是一所社会大学,在这儿我不仅提高了技术、开阔了视野,还直面了来自企业、社会、生存等质问与挑战。一路磕磕绊绊,也幸有良师解惑、益友相伴,我真的很满意这段旅程!只是我给自己的规划到时间了,得提前下车啦。感恩在鹅厂和TME遇见的各位~

最后引用汪国真的《热爱生命》与大家共勉:

“我不去想是否能够成功。既然选择了远方,便只顾风雨兼程”

一年之计在于春,适合播种,做些新的尝试,开启人生 Round2

2022 年的春节在湖南的最后一天,迎来了一场大雪。真好,瑞雪兆丰年,既是结束,也是开始。

新的一年,我的关键词是 专注 。开始做减法,戒骄戒躁。思考自己想要的生活,大胆地做出选择,并且坚定地执行。

我想要的生活,应该是能够平衡工作与生活的独立工作者,从事有价值的事业,有时间阅读写作,日常有着些许爱好,陪伴家人与宠物,时不时出去旅游看看这个世界。 就足够了,无需大富大贵,强到没边,我只是个普通人,开始认同这一点的时候也长舒一口气。

2022,勇敢地提出辞职,同时有幸遇到 Web3.0 时代以及志同道合的伙伴们,投身 Web3.0 时代的建设,拥抱开源社区

2022 年度的目标写在这里,给自己这一年找到努力的方向:

学业

  • 复习计算机与数学基础,刷算法题,夯实学校与公司的面试要求,拿到美国大学的 offer
  • 提升英语,考出 GRE(320+)

职业

  • 与团队一起投入区块链与 Web3.0 的学习与建设
  • 学好称手兵器,编程语言以 Python、JS 为主,学习 Go, Solidity, C++。

生活

  • 规律生活,放慢节奏
  • 维持住胆囊息肉大小,实在要切除也要调养好身体
  • 坚持健身,增重至 70kg

其他

  • 培养阅读习惯,每月至少读 1 本书
  • 培养写作习惯,创立个人学习分享 IP,每周输出一篇文章或视频
  • 学习书法,每周坚持练习至少 1 次
  • 出一趟远门旅游,看看中国大好河山(新疆、西藏、甘肃选一)

回顾焦虑奔跑的 2021,我终于鼓起勇气去打破安稳的预期,追求自己未完成的出国学业梦,边工作边学托福,朝 6 晚 12 的日子想起来真的自律而热血。

然而自己的身体却开始报警,胆囊息肉已经接近临床判定要切除的大小,这让奔跑的我直接来了个急刹车。我陷入“担心工作没产出”、“不满没有个人生活”与“身体越来越多毛病”的三个焦虑构成的死循环里。

工作到底为了什么呢?思来想去,核心驱动力是赚钱,大厂平台和视野当然是很有诱惑力的驱动因素。但是我在大厂工作几年后有如下不适:

  • 业务需求与绩效压在头上,晚饭后周围同事们不愿过早下班的奇妙现象,企业微信周末也不敢不开。
  • 螺丝钉粒度的工作岗位,如果没有很确定这就是一辈子想做的领域,日子久了会感觉到束缚,要切换赛道也很有成本。
  • 工作日都是早起晚归,休息日也想着要多学些工作技能,看的书也都是技术相关的,其他方面思考与进步为零,除了工作没有其他生活的苍白感。
  • 下班后完全不想写代码,也没啥兴趣了解外面日新月异的技术浪潮。
  • 没有长假期,有钱却没时间去旅游、陪家人了。

Man, I don’t like this way. I want to live and breathe.

到底是我能力问题,还是国内互联网行业的快节奏陀飞轮,反正我找不到 work life balance 的平衡点。既然已经申请了 master,那 2022 年干脆就辞职,gap 一段时间休息一下,等 offer 的同时把失衡的身心和生活调整回来吧!

距离上一次写博客已经阔别两个多月,今天我又回来了。

这两个月发生了几件足以改变我的人生轨迹的事情,第一件是外婆受车祸离世,第二件是我工作之余冲托福一个半月冲到 99 分,第三件就是学校的申请。

10 月 29 日,一个再正常不过的周五 w 晚上,我跟同事刚吃完免费的稻香,准备买单回工位

本篇文章阅读时间:10min
读者预期的收获是:

  • 认识测试驱动开发
  • 非常简单开启你的 TDD 之旅
  • 可以编写自动化测试
  • 重构、重新设计旧的代码更加自信

引子

(压抑背景音乐渐入——)

旁白:为何深夜的办公室传来程序员的哀嚎?

为何说好的一刀 999,砍下去伤害为 0?

为何程序员好基友反目成仇,因代码调用出问题后甩锅大打出手?

当个程序员,好难!(捂着铮亮的脑门)

程序员甲:自从用了 TDD,测试驱动开发之后,每天下班早了,BUG 变少了,基友不吵了。

程序员乙丙丁:真的吗?有这么神奇吗?!(集体星星眼)

程序员甲:没错,让我来给你们安利吧!

(雪花屏)Beep——

在这里插入图片描述
Hi,我是 Bruski。开头的段子纯属瞎编,但其中描述的场景:代码不按预期执行、协作的接口不可靠等等,在我们日常工作中其实挺常见的。
原因可能千奇百怪,比如在犯困的午后工作,比如没想清楚就动手等等,而且在过程很糟糕的情况下,输出还没有自动化测试去保证,那线上在跑的程序很可能就是一颗不定时炸弹。

那有没有什么办法能最大程度避免以上情况呢?我会说,不妨试试极限编程(XP)中的优秀实践:测试驱动开发吧!
在这里插入图片描述

别问,先感受

那么到底什么是测试驱动开发呢?

别急,先来感受一道小题目,非常简单:FizzBuzz。

题目模板地址: git clone https://github.com/bruceeewong/tdd-kata.git -b kick-start

题目来源:极客学院-测试驱动开发实战营


FizzBuzz 是一个简单的猜数字游戏。
在这里插入图片描述
想象你是个小学 5 年级的学生,现在还有 5 分钟就要下课,数学老师带全班同学玩一个小游戏。他会用手指挨个指向每个学生,被指着的学生就要依次报数:

第一个被指着的学生说“1”,第二个被指着的学生说“2”,如果一个学生被指着的时候,应该报的数是 3 的倍数,那么他就不能说这个数,而是要说“Fizz”。5 的倍数也不能被说出来,而是要说“Buzz”。

于是游戏开始了,老师的手指向一个个同学,他们开心地喊

着:“1!”,“2!”,“Fizz!”,“4!”,“Buzz!”……

终于,老师指向了你,时间仿佛静止,你的嘴发干,你的掌心在出汗,你仔细计算,然后终于喊出“Fizz!”。运气不错,你躲过了一劫,游戏继续进行。

为了避免在自己这儿失败,我们想了一个作弊的法子:最好能提前把整个列表打印出来,这样就知道到我这儿的时候该说什么了。

题目要求

写一个程序,打印出从 1 到 100 的数字,将其中 3 的倍数替换成“Fizz”,5 的倍数替换成“Buzz”。既能被 3 整除、又能被 5 整除的数则替换成“FizzBuzz”。

要求:

  • 代码整洁,没有重复代码
  • 有单元测试,单元测试覆盖率 100%
  • 5 分钟内完成

题目解析

相信大家应该都能很快地实现题目的要求,不过,关于单元测试部分,大家写的是否轻松呢?接下来我想给大家展示下我的做题思路——用 TDD 的方式。

测试驱动开发的要义是:测试先行,没有失败的测试,就不允许实现。所以,在动手前我们需要想清楚题目要实现什么,即拆解需求。再回顾下题目要求:

打印出从 1 到 100 的数字,将其中 3 的倍数替换成“Fizz”,5 的倍数替换成“Buzz”。既能被 3 整除、又能被 5 整除的数则替换成“FizzBuzz”。

打印出 1 到 100 的数字?也许会有人开始构思程序:一个 for 循环,if-else 一下,再 console.log 一下。等等,输出打印到控制台的话,我们怎么写测试验证输出是否正确呢?所以不妨转换下思路,沿着函数的本质:input -> process -> output来思考,其实我们要做的是:

实现一个函数

输入: 1~100 的数字

处理:

  • 3 的倍数替换成”Fizz”

  • 5 的倍数替换成“Buzz”

  • 3 和 5 的公倍数(或者 15 的倍数)替换成“FizzBuzz”

  • 其他数字则转换为字符串

输出:字符串

将需求完全拆解后,对应的测试用例也就信手捻来了,就让我们从最最简单的测试开始,函数就叫 fizzbuzz 吧,接收参数 1,返回字符串“1”。(这种直白的语法就叫断言(Assertion),即把预期输出与实际输出作对比以验证程序是否正确运行)

1
2
3
4
5
6
7
8
// 以下语法为Jest.js的测试写法
const fizzbuzz = require("./fizzbuzz");

describe("fizzbuzz", () => {
test("测正常数字返回", () => {
expect(fizzbuzz(1)).toEqual("1");
});
});

执行jest命令运行测试,结果不出所料地报红了:fizzbuzz is not a function,毕竟我们此时连函数都没声明。

在这里插入图片描述

那我们赶紧定义函数:

1
2
3
4
function fizzbuzz(num) {
return "1";
}
module.exports = fizzbuzz;

有人会说,函数体返回常量,你在骗自己吗?别急,再执行一下 jest 命令运行测试:
在这里插入图片描述
Yes,测试通过,变为绿色!没错我是硬编码返回了,但这是 TDD 的第二个重要的要义:只写让测试恰好通过的代码。好吧我知道留着这样的代码,是不敢入睡的,那就再多加一条测试:

1
2
3
4
5
6
7
8
const fizzbuzz = require("./fizzbuzz");

describe("fizzbuzz", () => {
test("测正常数字返回", () => {
expect(fizzbuzz(1)).toEqual("1");
expect(fizzbuzz(98)).toEqual("98");
});
});

再运行测试,闭着眼睛都知道会失败,自动化的测试还非常贴心地帮我们指出了期待输出和实际输出的差异。
在这里插入图片描述
有了失败的测试,我们才开始动手写实现,实现也相当简单:

1
2
3
function fizzbuzz(num) {
return num.toString();
}

执行测试,OK,测试通过,结果又变回绿色。
在这里插入图片描述
这时候我们知道第一条需求已经被解决,无情划掉它:

  • 3 的倍数替换成”Fizz”
  • 5 的倍数替换成“Buzz”
  • 3 和 5 的公倍数(或者 15 的倍数)替换成“FizzBuzz”
  • 其他数字则转换为字符串

那就写下第二条测试用例:

1
2
3
test("测3的倍数返回", () => {
expect(fizzbuzz(3)).toEqual("Fizz");
});

执行测试,结果 3 原样返回,测试不通过:
在这里插入图片描述
又是一条失败的测试,快速实现它让它翻绿!

1
2
3
4
5
6
function fizzbuzz(num) {
if (num % 3 === 0) {
return "Fizz";
}
return num.toString();
}

再次运行测试:
在这里插入图片描述
那此时再加几条测试,结果肯定是正确的:

1
2
3
4
5
test("测3的倍数返回", () => {
expect(fizzbuzz(3)).toEqual("Fizz");
expect(fizzbuzz(6)).toEqual("Fizz");
expect(fizzbuzz(99)).toEqual("Fizz");
});

再划掉一项需求!

  • 3 的倍数替换成”Fizz”
  • 5 的倍数替换成“Buzz”
  • 3 和 5 的公倍数(或者 15 的倍数)替换成“FizzBuzz”
  • 其他数字则转换为字符串

接下来想必大家都知道了,复制一下 3 的测试用例,改成 5,然后执行测试,失败,然后复制一条 if 语句….等等!难道你忘了,Copy-Paste 是魔鬼吗?难道我是在教你成为一名 CV 工程师吗?好了,这里引出 TDD 又一条要义:消除所有重复。其实通过将运算抽象为函数,很容易就能消除重复的直白代码:

1
2
3
4
5
6
7
8
9
10
11
12
function fizzbuzz(num) {
function canDivideBy(num, divideNum) {
return num % divideNum === 0;
}
if (canDivideBy(num, 3)) {
return "Fizz";
}
if (canDivideBy(num, 5)) {
return "Buzz";
}
return num.toString();
}

运行测试,干净漂亮地通过了测试:
在这里插入图片描述
最后再补充一条 3 和 5 的公倍数测试用例,使用抽象好的函数实现,运行测试,测试通过后,那么整个需求就完成了。下面是完整的测试用例&实现&测试截图:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// fizzbuzz.test.js
const fizzbuzz = require("./fizzbuzz");

describe("fizzbuzz", () => {
test("测正常数字返回", () => {
expect(fizzbuzz(1)).toEqual("1");
expect(fizzbuzz(2)).toEqual("2");
expect(fizzbuzz(98)).toEqual("98");
});
test("测3的倍数返回", () => {
expect(fizzbuzz(3)).toEqual("Fizz");
expect(fizzbuzz(6)).toEqual("Fizz");
expect(fizzbuzz(99)).toEqual("Fizz");
});
test("测5的倍数返回", () => {
expect(fizzbuzz(5)).toEqual("Buzz");
expect(fizzbuzz(10)).toEqual("Buzz");
});
test("测3和5的公倍数返回", () => {
expect(fizzbuzz(15)).toEqual("FizzBuzz");
expect(fizzbuzz(45)).toEqual("FizzBuzz");
});
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// fizzbuzz.js 实现
function fizzbuzz(num) {
function canDivideBy(num, divideNum) {
return num % divideNum === 0;
}
if (canDivideBy(num, 15)) {
return "FizzBuzz";
}
if (canDivideBy(num, 3)) {
return "Fizz";
}
if (canDivideBy(num, 5)) {
return "Buzz";
}
return num.toString();
}

module.exports = fizzbuzz;

在这里插入图片描述
划掉所有需求,爽,可以下班了!

  • 3 的倍数替换成”Fizz”
  • 5 的倍数替换成“Buzz”
  • 3 和 5 的公倍数(或者 15 的倍数)替换成“FizzBuzz”
  • 其他数字则转换为字符串

最后,执行 Jest 命令jest --coverage生成测试覆盖率报告:
在这里插入图片描述
怎么样?100%的测试覆盖率,没有重复、多余的代码,漂亮地完成所有需求。如果你不放心,多加几条测试用例,多运行几遍测试命令,这就是测试驱动开发产出的有质量保证的代码。

有了自动化测试做保障,测试通过,我就敢说在我所预见的情况中,他会一直通过,除非,除非产品经理的需求又变了…
在这里插入图片描述
总结一下,在做 FizzBuzz 题目的过程中,用 TDD 的节奏开发流程如下图:执行测试覆盖率检查

如果大家有关注到图中的颜色,那么请大家跟我念一句 TDD 的咒语,念三遍:

Red / Green / Refactor

Red / Green / Refactor

Red / Green / Refactor
在这里插入图片描述
这就是贯穿测试驱动开发的整个流程的循环,也是 TDD 的节奏。其中:

  • Red表示测试不通过时,IDE 的状态条报红,此时我们有了失败的测试;
  • Green表示测试通过时,IDE 的状态条回到绿色状态,此时通过快速小步地开发,让测试恰好通过;
  • Refactor表示重构代码,消除所有重复的逻辑。

接下来,让我们跟随 Kent 大叔深入地琢磨下测试驱动开发吧!

深入测试驱动开发

到底什么是测试驱动开发(Test-driven Development)呢?

按照 Kent 大叔的原话:

TDD is an awareness of the gap between decision and feedback during programming, and techniques to control that gap.

粗浅地翻译过来意思是:

TDD 是一种编程意识,关注着程序设计与实现结果反馈之间的间隙;同时 TDD 也是用以填平该间隙的一系列技巧。

是的,概念总是过于晦涩与抽象。作者还提供了不同角度的定义来帮助理解:

  • 测试驱动开发是一种管理编程中的恐惧的方式。(Test-driven development is a way of managing fear during programming. )
  • 测试驱动开发是一种开发风格:我们通过自动化的测试来驱动开发(we drive development with automated tests, a style of development called Test-Driven Development)

你一定要记住,TDD 的终极目标是:

Clean code that works. (产出干净且可用的代码)

这是《测试驱动开发》序章的第一句话,也是我编程的座右铭。

TDD 开发模式

首先我们要搞清楚 3 个问题:

  1. 什么是测试?
  2. 测什么?
  3. 什么时候测试?

什么是测试

测试作为动词,是“去验证”的意思。测试作为名词,是对预期得出可接受或者不可接受判断的一个过程。

按 Kent 大叔的意思是:

测试是开发者的基石,也是将对程序运行结果从未知的恐惧转化为熟知的手段。

不喜欢写测试的程序员,通常将经历这样的消极循环:感到恐惧,因为没测试 -> 越恐惧,压力越大 -> 压力越大,越不会测试。

而善用测试的程序员呢?正向循环应该像这样:越感到恐惧,越执行测试 -> 越测试,恐惧越小 -> 压力越小,越愿意测试。

测什么

我们这里指的,是程序级别的单元测试(Program Level),主要关注逻辑与数据。

对于逻辑的测试,一般来说等同于需求,我们要对需求进行编程级的拆解,即要能拆解为可以动手编码的若干步骤,通过不断地写下你的期望与实际输出的测试语句(即断言),然后实现代码让其通过,从而一步步达成目的。

对于数据的测试,这里我也没有很多实践,有几点可以分享:

  1. 不要使用真实的数据(数据库数据、网络请求等)
  2. 按照预期的数据结构,构造直观的伪造数据来满足测试。

什么时候测试

按照测试驱动开发的节奏,每当:

  • 动手编程前,先写出一条会失败的测试
  • 重构前,保证测试通过

了解完前置概念后,又该怎么落笔我们的第一个测试用例?

Red Bar Patterns

Red Bar,顾名思义:因执行测试失败而显示红色的状态栏

要让测试失败,那首先要写下你的测试,我们上一节介绍了需求拆分,得到了一份 todo-list,那我们究竟该自顶向下实现(即从大问题的具体用例开始实现),还是自底向上实现(从小模块开始,再逐步聚合)呢?

其实这两者都不能很好描述 TDD 的实现过程,准确地说,实现顺序没有太大关系,因为 TDD 是基于 known-to-unknown 的模式进行,即用已知的来推出未知的。我们在拆分需求为一条条可编程验证的用例时,就是将未知的庞然大物拆解成不废力气就能达成的小目标,我们知道如果一步步实现了所有子测试,最终需求就能实现。

在 TDD 这里,万事开头难,但测试开头易。第一个测试应该写一条测什么都不做的操作的测试,这里看似没什么意义,但是它确实验证了:

  • 这个操作属于哪里?
  • 什么是正确的输入?
  • 什么是基于正确输入的正确输出?

Green Bar Patterns

Green Bar,顾名思义:因执行测试成功而显示绿色的状态栏

在 FizzBuzz 实现的过程中,我们用到了几种快速让测试通过的技巧,分别是:

Fake It (‘Til You Make It) 伪造数据

比如在 FizzBuzz 最开始的时候,为了让测试通过,直接在函数里返回常量。

为什么要写早晚要换掉的实现?原因有两点:

  • 心理暗示
    • 测试成功比测试失败好
  • 范围控制
    • 专注在解决当前测试上,避免过度设计
    • 保证当前代码始终可用

Triangulate 三角测量

  • 从不同角度测试代码,让伪造数据的代码失败,然后抽象、实现,让测试通过。例如我们前面用两条测试,宣告了硬编码返回”1”的代码实现的死亡。

Obvious Implementation 最简实现

  • 既然用例已经拆分成小步,一定可以快速实现,否则,反思步子是否迈大。

  • 写恰好实现的代码。

至此,结合 FizzBuzz 的解析,我们已经体验完测试驱动开发最核心的流程。

来总结下吧

再回到定义,测试驱动开发本质上是一种编程思考和实践的一种风格/方式,比起一开始的顶层设计,他更关注需求与实现之间的距离,要求程序员能拆解成若干可测试、可实现的步骤,然后借助自动化测试工具,按照一定的节奏Red / Green / Refactor、测试技巧和编程手法,从而产出干净的、可工作的代码。

  • 因为测试先行,倒逼我们必须思考清楚问题应该如何解决,避免了低效地走一步看一步的浑浑噩噩;
  • 因为测试先行,我知道做到什么程度算完成,并且自信地认为在我所预期的情况内,程序可以良好地工作。
  • 测试用例可以作为更棒的注释而存在,让协作的同事更清楚地知道函数的用途和用法。
  • 提交代码时,看着绿色的状态栏,心情愉悦,安心下班!

TDD 的挑战

TDD 更多的应用在程序级别的单元测试,这一块是开发人员完全自主掌控的部分。而在此之外的一些场景,TDD 也许就不那么合适,比如:

  • 对于 GUI 的测试(网页、App 级别的 UI 测试)
  • 对于依赖数据库的测试(通常我们使用 mock 对象测试)
  • 不要去测第三方的代码,那应该有他们的开发去保证(如框架等)
  • 不能测试编译器之类的东西。

写在最后

作为一名 Web 前端开发,在开发业务逻辑时,我都会有意识地使用 TDD 的方式来实现。(UI 方面的测试实践并不多,还要继续学习!)
TDD 测试驱动开发带给我的开发体验是:

  1. 享受可预测、尽在掌握的开发体验
    1. 当通过了所有测试、开发也就结束了
    2. 并且开发结束了,可预见的场景不会有太多 bug
  2. 给自己留一瓶后悔药
    1. 第一次的实现可以很烂,但只要有测试,再回过头来,只要测试是通过的,就可以放心地重构。
    2. 如果祖传代码没有测试,那就尝试找到程序输入输出的接缝处,给他补充测试,这样可以最大程度确保重构不会大刀阔斧地破坏原有逻辑。
  3. 保持代码年轻的秘诀?
    1. 如果是测试驱动出来的代码,将拥有输入输出清晰、幂等性特点。
    2. 每当添加新特性前,先思考清楚,先写测试,代码不会随乱涂乱改而腐败。
  4. 同事协作时之间更放心
    1. 你产出的代码值得信赖。
    2. 同事也用 TDD,看着测试用例就知道怎么用了,真香。

这篇文章只是展示 TDD 的基础玩法,想要深入了解测试驱动开发,去读下 Kent Beck 的 《Test-Driven Development By Example》,感受 Kent 大叔的幽默与智慧吧。
在这里插入图片描述
愿你的程序无 Bug,早点下班!
在这里插入图片描述

CSS 层叠样式表作为前端三剑客之一,通过各类选择器来解耦 HTML 结构与表现,让开发者拥有专注控制样式的能力,实现了关注点分离。通过层叠机制,为规则赋予不同的重要程度,让我们的样式代码能够灵活地继承与覆盖。

它就像精灵宝可梦里的百变怪 👾,拥有强大而奇妙的变化与适应能力,前端技术也因 CSS 的加入而变得漂亮与有趣 🤩

本文主要分享 CSS 技术中的一个切面概念:渐进增强,希望能够从原理+实例出发,给大家在设计、编写网页样式时带来一点启发 💡

渐进增强

随着 HTML 与 CSS 的发展,许多新特性陆续得到浏览器厂商们的支持,比如 HTML5 中的新增标签,CSS 中的 Flexbox,Grid、calc 等,给网页带来了很多新鲜而强大的能力。
但是由于时代推移、浏览器的厂商各自对规范实现的不一致,导致不同的浏览器以及版本存在新特性不兼容或者其他 Bug 问题,让开发者在新特性与兼容性之间放弃了尝试新特性的想法 🤪

但是!时代与技术一定是在进步的,而在新老交替的过渡期,我们可以采用渐进增强的策略,来平衡向后兼容性与最新的 HTML/CSS 特性。

什么是渐进增强 🧐

⛳️ 所谓渐进增强,即我们首先为最小公分母准备可用的内容,然后再为支持新特性的浏览器添加更多交互优化。

要实现渐进增强,意味着代码要分层,每一层增强代码都只会在相应特性被支持或被认为适当的情况下使用。听起来有点复杂,然而实际上 HTML 与 CSS 已经部分内置了这一策略。

HTML 的渐进增强策略

⛳️ 对于 HTML 而言,浏览器遇到未知元素或属性时并不会报错,也不会对页面产生什么影响。

假设表单中有输入邮件的控件:

1
<input type="text" name="field-email" />

我们就可以使用 HTML5 新增的 email 类型,可以拥有邮件格式检验以及移动设备中的特定格式软键盘的强化能力:

1
<input type="email" name="field-email" />

尚未实现这个新类型的浏览器就会想:”这是甚么玩意儿?不明白“🧐,然后给他退化成 type=”text”类型,就当无事发生。
这样,我们既渐进增强了页面,也不会对浏览器产生什么不好的影响。
类似的还有 HTML5 的文档声明,这样的语法也是向后兼容的。

CSS 的渐进增强策略

⛳️ 对于 CSS 来说,无法识别的属性/值都会被浏览器丢弃,所以只要提供合理的后备声明,使用新属性就不会带来不良后果。

例如,现代浏览器支持的颜色值 rgba 函数,我们可以这样定义红色:

1
2
3
4
.overlay {
background: #000;
background-color: rgba(0, 0, 0, 0.8);
}

对于旧的浏览器,它会丢 rgba 的声明,应用第一条规则;对于现代浏览器,第二条规则就会覆盖第一条,显示带透明度的红色。
那么即使不是所有浏览器都支持 rgba 函数,由于提供了合理后备,我们仍然可以大胆地使用它 😎

如何实现渐进增强 🧐

1. 厂商前缀

🛠 浏览器厂商也基于同样的原理为自家浏览器引入实验特性,并加上一串特殊前缀,这样其自家浏览器就能识别而其他浏览器忽略。
例如 transform 属性:

1
2
3
4
5
6
.thing {
-webkit-transform: translate(0, 10px);
-moz-transform: translate(0, 10px);
-ms-transform: translate(0, 10px);
transform: translate(0, 10px);
}

下面列举前缀对应的浏览器厂商:

-webkit-: 适用于 Webkit 内核的浏览器,包括 Blink 引擎的(基于 Webkit)

  • Safari
  • Chrome
  • Opera

-moz-: 基于 Mozilla 浏览器

  • Firefox

-ms-: 微软家

  • Internet Explorer

2. 条件规则与检测脚本

@supports块
⚙️ 如果希望通过检测浏览器是否支持某个 CSS 特性来提供样式,可通过条件规则检测:

1
2
3
@supports (display: grid) {
/* 编写网格布局的规则 */
}

3. JavaScript 库

⚙️ 通过 Modernizr 这个 JS 库可以检测各种特性的支持情况。
原理是 modernizr 会根据检测情况给 html 根元素添加相应特性的类名,如 flexbox,然后在编写 CSS 时带上前缀,即可安全的增强样式。

更多关于新特性的浏览器支持情况,可以到 http://caniuse.com 查阅 🔎

🌰 例子 1:在浮动之上应用 Flex

这里有一个使用 float 布局实现的卡片布局,我们来看看如何在不破坏原有代码的情况下,用 Flexbox 特性增强这个页面。

这个页面的使用了 float 来实现网格布局效果,主要原理是

  • 💡 使用 col 类名定义浮动组件,row 类名定义清除浮动的父组件
  • 💡 加上 row-quartet 定义子项列宽,实现网格布局效果
  • 核心代码如下:
1
2
3
4
5
6
<div class="row row-quartet">
<div class="col"><article></article></div>
<div class="col"><article></article></div>
<div class="col"><article></article></div>
<div class="col"><article></article></div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
// index.css/* 行组件 */
.row:after {
content: "";
display: block;
clear: both;
height: 0;
}
.row-quartet > * {
width: 25%;
} /* 列组件 */
.col {
float: left;
}

通过图片可以看到上述代码并未实现列等高效果,然而要实现浮动元素的列等高效果比较麻烦,详情方案参考:https://juejin.cn/post/6844904048278290440#heading-22

而使用 Flexbox 可以轻松实现,原理是 Flex 的等高机制:父容器定义为 Flexbox 容器之后,控制辅轴的属性 align-items 默认为 strech,即自动拉伸子项的高度以填满空间。

那么此时我们想通过 Flexbox 来实现列等高效果,又不破坏已实现的部分,该怎么做呢 🧐
⛳️ 答案是:引入检测机制,在检测块内编写新特性的规则。

上面也提到有两种方式:

  1. 使用 @supports 块检测,但由于此检测语法本身就比较新,所以对于旧浏览器不友好。
1
2
3
@supports (display: flex) {
/* 编写 flexbox 布局的规则 */
}
  1. 使用 Modernizr 库检测,然后使用带.flexbox 前缀的类名来编写增强的规则。

首先我们将 Modernizr 的 script 写在所有 link 的前面(保证检测脚本先加载)。

添加后,Modernizr 检测完毕后,我们的标签上就将支持的特性名字生成 classname,测试浏览器是当前最新的 Chrome 89, 所以可以看到 flexbox 赫然在列。

接下来就基于此类名前缀,编写增强代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// index.css​/_ flexbox 增强代码 _/
.flexbox .row {
display: flex;
}
.flexbox .col {
display: flex;
flex-direction: column;
}
.flexbox .col > _ {
flex: 1;
}
​/_ float 实现相关代码 _/.row:after {
content: "";
display: block;
clear: both;
height: 0;
}
.row-quartet > _ {
width: 25%;
}
.col {
float: left;
}

我们来看下效果,这里用到浏览器测试工具:browserstack ,选择在 Chrome 下打开页面,展示了等高效果,说明应用增强代码:

而在 IE 10 浏览器上就降级为无 flexbox 的效果:

查阅 CanIUse 网站,看到 IE10 只支持带 -ms- 前缀的 flexbox 属性,所以也解释了上述表现。

总结一下 在浮动之上应用 Flex 方案,有几个关键点:

  • 不理解 flex 关键字的浏览器会忽略它。
  • 给标签定义了 display: flex 之后,浏览器会忽略该标签定义的 float,clear 与 display:inline-block 等声明。

举一反三,其他场景我们也可以遵循首先写一个适合任何场景的布局,然后再通过 新特性检测 + 运用 Flexbox / Grid 等新特性来渐进增强我们的网页。

详情代码:CodePen:

🌰 例子 2:响应式图片

响应式 Web 设计出现以后,何时加载合适的图片是前端开发者要面对的问题。很多开发者不管设备、屏幕大小,一律使用相同的图片,这种做法导致小屏幕看大图浪费带宽与内存、大屏幕展示小图看不清楚等问题。而浏览器会对网页进行预处理,图片等资源会在浏览器构建完页面或运行 JavaScript 之前就开始下载,这意味着不可能仅凭脚本就完美解决图片响应式的问题。所以 HTML5 提出了响应式图片的解决方案。

响应式图片(Responsive Image)是指给 HTML5 规范给标签添加的 srcset、sizes 新属性,旨在解决不同的条件下告诉浏览器加载不同的图片。

  • srcset: 哪个是当前图片的可替换源文件,其宽度是多少像素?
  • sizes: 在各个断点中,图片的 CSS 宽度是多少?

Hi,img 标签的新属性

来看个例子 🌰:

1
2
3
4
5
6
7
8
9
<img
src="http://dummyimage.com/300x150"
srcset="
http://dummyimage.com/300x150 300w,
http://dummyimage.com/600x300 600w,
http://dummyimage.com/1200x600 1200w
"
sizes="(max-width: 600px) 300px, (max-width: 1200px) 600px, 1200px"
/>

分析一下:

  • srcset 的值是一组图片 URL+实际像素宽度(不是 CSS 像素),定义了一组资源后,还要告诉浏览器怎么使用这些图片。
  • sizes 通过开头可选的媒体查询条件 + 图片展示宽度值(CSS 单位,可为 px, em, vw)。

如果某条媒体查询条件为真,浏览器将取得条件后面定义的宽度,然后去匹配到最接近尺寸的图片,执行下载。最后,图片将按照定义的宽度进行展示。

在 Chrome 上运行的效果:



需要注意的点:

  • 在有图片缓存的情况下,浏览器可能会加载较大的图片。
  • 在网速慢或者电量低的情况下,浏览器会加载较小的图片。
  • 浏览器知道当前设备是不是高分辨率屏幕,从而决定是否自动加载大图。

通过 srcset,我们渐进增强了原有的图片组件,让不同宽度的屏幕加载更合适尺寸的图片。

进一步加强:picture 标签

MDN 介绍:picture 元素除了在多个不同分辨率的图片间切换,还有几个很重要的响应式图片的应用场景:

响应式图片在大小屏幕分别需要不同的裁切方式,根据浏览器的支持加载不同格式的图片。如 Google 的 Webp 格式会比 JPG 体积小 25%-34%,可以优化网页的性能。

这些问题的标准解决方案是:picture 元素,他作为 img 元素的容器,同时扩展了 srcset 和 sizes 的能力。举个例子:

1
2
3
4
5
6
<picture>
<source
type="image/webp"
srcset="https://www.gstatic.com/webp/gallery/4.sm.webp" />
<img src="https://www.gstatic.com/webp/gallery/4.sm.jpg" alt="tree"
/></picture>

picture 元素包含 source 标签与 img 标签,在支持 picture 与 webp 格式的浏览器下,就会加载 webp 格式的图片;否则忽略将 picture 与 source 元素,应用 img 标签中的 jpg 格式图片。以下是在 browerstack 的测试情况:图一在 IE11 上测试,IE11 不支持 webp 图片,所以加载 jpg。

Chrome85 支持 webp,加载 webp 图片

除此之外,picture 还可以添加 media 属性,结合媒体查询进一步控制浏览器选择图片的逻辑:

1
2
3
4
5
<picture>
<source media="(max-width: 799px)" srcset="elva-480w-close-portrait.jpg" />
<source media="(min-width: 800px)" srcset="elva-800w.jpg" />
<img src="elva-800w.jpg" alt="Chris standing up holding his daughter Elva"
/></picture>

对于开发者来说,我们在不破坏 img 原有能力的基础上,大大增加了操控图片的可能性,也得感谢 HTML 的渐进增强策略。

总结一下

在前端新标准、新技术百花齐放的时代,我们应该积极关注、拥抱新技术,用渐进、可降级的方式去为旧项目赋能焕新 ✅ 其实渐进增强,在我看来是一种严谨、巧妙的思维方式,感受其中的分层、容错、可扩展等设计理念,并尝试运用在日后的编程中吧 🤓

辅助工具

这里就列举几个辅助工具,有助于我们写出有良好兼容性的样式代码:

  • PostCSS 预处理器插件 - Autoprefixer, 根据 CanIUse 网站与你项目所支持的浏览器,自动为你的 CSS 代码添加相应前缀。
  • 静态分析及 Linter- Stylelint,能检查语法错误,也能检查选择符/声明中的有问题的规则。
  • 浏览器测试工具:Browserstack,是一款基于 Web 的实时浏览器测试工具,支持虚拟机测试全平台&浏览器版本,收费 💰
  • HTML/CSS/JS 兼容性查询平台:CanIUse

相关资源

  • 《精通 CSS 高级 Web 标准解决方案》(第三版)——埃米尔·比约克隆德
  • MDN - Responsive images