缘起


前几天学习了 AWS Lambda,非常棒。首先,未来的开发者都会是云原生的,就是说,他们所有的开发工作都会在云上进行,而 AWS Lambda 是云计算中的翘楚;其次,对于开发者和开源作者来说,由于不以盈利为目的,因此免费的基础设施和产品对他们的可持续输出非常重要,而 AWS Lambda 的免费额度远远超出大多数开发者的起步需求。
image.png
以上是我学完了 AWS 基础课后的证书,这个课程的主讲人是一名解决方案架构师,讲得相当好,让你对 AWS Lambda 会有一个基本和全面的认识。

但是,对实际的开发工作可能没有太大帮助。你根据课程和 AWS Lambda 控制台的指引,可以很快搭建一个 Hello World 出来,但这只能算是 Playground,如果要真正进行工程实践,还需要本文这样的指南。AWS Lambda 支持的语言非常多,本文将对使用 NodeJs 进行 AWS Lambda 工程实战给出一个详尽的指导。除了 NodeJs,你可能还想有 C# 的 AWS Lambda 工程实战指南,Go 的 AWS Lambda 工程指南等等,如果哪天我学会了它们,再来分享。

一个现代化的实战工程,应该是方便测试的、可以持续自动化部署的。测试驱动开发是一个不错的开发实践,本文分享如何将它应用在 AWS Lambda 开发上。

首先,你需要学习一下 AWS Lambda 课程,链接在这里:https//www.aws.training/Details/eLearning?id=59558

工具链


开发语言采用 TypeScript,测试框架采用 Mocha,断言库采用 Chai。jest 非常优秀,但是运行速度比 Mocha 慢太多,因此这里没有采用。但是 jest 的使用者众多,必须得提一下,而且从 Mocha 切换到 jest 并不需要做太多修改。

Mocha 是一个在 NodeJs 上运行的 JavaScript 测试框架。Mocha 支持异步运行测试、测试覆盖报告,并且允许使用任何断言库。Chai 是一个 NodeJs 的 TDD/BDD 断言库,能够和任何 JavaScript 测试框架配合工作。

Mocha 使用钩子来组织其测试结构,具体地说有这些钩子:

  • describe() 用来将测试分成一个一个的测试组并描述当前测试分组
  • it() 用来描述测试用例
  • before() 在第一个 it() 或者 describe() 之前运行
  • beforeEach() 在每一个 it() 或者 describe() 之前运行
  • after() 在所有 it() 或者 describe() 之后运行
  • afterEach() 在每一个 it() 或者 describe() 之后运行

测试驱动开发简介


简称 TDD,它有很多好处,比如由于测试先行,所有让我们专注在需求上,并且保证只会发布能够恰好通过所有测试的代码。

TDD 的流程总体来说是一系列重复性的简单活动。首先,写一个注定失败的测试,因为这时还没有实现代码;然后,写能够通过测试的实现代码;一旦测试通过,就重构代码让它更简洁更易扩展。注意测试代码也是代码,所以测试代码本身也包括在重构的范围内。这个重复性的简单活动看起来是这样的:

image.png

示例需求


构建一个无服务器的 Lambda API,它接受一个电话号码作为输入,并且返回这个电话号码的国别 —— 中国大陆、美国、或者不是一个电话号码。

安装开发依赖


进入你的项目目录并且执行: shell npm install typescript -g npm install chai mocha ts-node @types/chai @types/mocha --save-dev

写第一个测试


让我们以熟悉的 Hello World 开始。创建一个新文件,命名为 HelloWorldService.spec.ts(这个 sepc.ts 的后缀表明这是一个测试文件)。在这个 HelloWorldService.spec.ts 的顶部,加载 mocha 和 chai,然后就开始使用 Mocha 的钩子函数(describe 和 it)来定义一个简单的测试,这个测试期待 HelloWorldService 返回一个 “HelloWorld”字符串。

typescript import {expect} from chai import mocha import {HelloWorldService} from ./HelloWorldService

describe(Hello World string function, () => { it(should return Hello World, () => { const result = HelloWorldService() const expectedResult = Hello World expect(result).to.equal(expectedResult) }) })


测试写好了,我们来运行它。为了方便起见,我们在 package.json 中创建一个 NPM 脚本,这个脚本调用 mocha,并将测试文件所在的目录作为参数传给它。由于我们的代码是使用 TypeScript 写的,所以还需要将 ts-node 注册进入 mocha 才行。一旦这个脚本创建好,我们就可以从命令行终端运行测试了(注意 .spec.ts 后缀,这是我们表明测试文件的约定): json ... scripts { ... test mocha -r ts-node/register src/*/.spec.ts ... } ...

尝试执行测试会报错,因为 HelloWorldService.ts 还不存在,让我做个快速修复,创建这个文件,并且写一行代码实现 HelloWorldService: typescript export const HelloWorldService = () =>

再次在命令行输入 npm test 运行测试,得到:
image.png
测试能够运行,但是结果告诉我们 HelloWorldService 没有返回期待的结果,让我们修复它,改写 HelloWorldService.ts 文件:

diff

  • export const HelloWorldService = () =>
  • export const HelloWorldService = () => Hello World

再次运行测试,得到:

image.png
测试通过,由于代码很简单,还不需要重构,所以第一个 TDD 周期就完成了!

测试电话号码服务算法


到目前位置,一个支持 TDD 和 TypeScript 的项目就搭建起来了,我们开始构建电话号码服务。为了简单起见,这个服务只用来判断一个电话号码是否是中国大陆电话号码。先写一个测试: typescript describe(PhoneNumberService.determinePhoneNumberTypeCNMOBILE, () => { it(should return CNMOBILE, () => { const expectedResult = PhoneNumberType.CNMOBILE;

const phoneNumbers = [+8617712345678, 17712345678]

phoneNumbers.forEach(phoneNumber => {
    const result = PhoneNumberService.determinPhoneNumberType(phoneNumber)
  
  expect(result).to.equal(expectedResult)
})

}) })

运行测试,失败。让我们来写第一个实现,能够通过这个测试用例: typescript import {PhoneNumberType} from ./enums/PhoneNumberType const CNMobilePhoneNumberRegex = /^(+86)1d{10}$/

export class PhoneNumberService { public static determinerPhoneNumberType(phoneNumber string) PhoneNumberType { return this.isCNMobile(phoneNumber) ? PhoneNumberType.CN_MOBILE PhoneNumberType.INVALID }

public static isCNMobile(phoneNumber string) boolean { return CNMobilePhoneNumberRegex.test(phoneNumber) } }

重新运行测试,通过!下一步是添加一个让测试失败的测试用例,然后改进代码让它通过。直到所有的测试用例都通过为止。这里略过不再赘述。

添加 AWS Lambda 事件处理器


首先添加 aws-lambda 依赖,这是 AWS Lambda 为 NodeJs 开发的 SDK。 shell npm install aws-lambda --save

然后新建一个文件,代码如下: typescript import {Context, APIGatewayProxyEvent} from aws-lambda import {PhoneNumberService} from ../services/PhoneNumberService;

module.exports.handler = async (apiGatewayEvent APIGatewayEvent, context Context) Promise => { if (!requestBody) { return new HttpResponse(HttpStatusCode.BADREQUEST); }

const res = PhoneNumberService.determinePhoneNumberType(requestBody.phoneNumber)

return new HttpResponse(HttpStatusCode.OK, res) }

配置 AWS Lambda 并本地运行验证

安装 AWS Lambda 提供的命令行工具 serverless: shell npm i -g serverless

配置: shell serverless config credentials --provider aws --key YOURACCESSKEY --secret YOURSECRETACCESS_KEY

再在项目中创建一个 serverless.yml 文件: yaml service phone-number-api

provider name aws runtime nodejs12.x

functions phonenumber handler index.handler name serverless-phone-number

在 package.json 文件里添加一个脚本命令: json { ... scripts { ... local tsc && serverless invoke local --function phonenumber --data +8617712345678 ... } ... }

执行 npm run local 验证。

配置部署脚本

在 package.json 中增加一个部署脚本: json { ... scripts { ... deploy tsc && serverless deploy ... } ... }

要将我们写好的 AWS Lambda 部署上线,只需要运行 npm run deploy 。Serverless 会帮我们处理所有事情,包括创建 CloudFormation Stack,上传构建制品到 S3,以及创建我们的 Lambda。
image.png
要持续开发 Lambda,只需要更改代码,确保测试通过。然后运行相同的 npm run deploy 脚本就能够将更新的改动部署上线。

验证部署后的服务


你可以用 Postman 发起一个 POST 请求到我们运行 npm run deploy 后得到的分配好的终端节点:
image.png
也可以配置一个命令行脚本到 package.json 文件中: json { ... scripts { ... remote serverless invoke --function phonenumber --data +8617712345678 ... } ... }

之后使用 npm run remote 即可。

总结


本文快速回顾了 TDD 的步骤,并给了一个将其应用于 AWS Lambda 开发的快速示例。欢迎参考该示例创建 AWS Lambda 的 TypeScript TDD 的开发项目模版。

彩蛋


AWS Lambda 在中国最大的竞争对手,应该是阿里云的函数计算。参考本文,做一些适当的修改,就可以写一个阿里云函数计算的 TypeScript TDD 开发项目模版,其实作者已经写了一个,欢迎自由取用:https//github.com/Jeff-Tian/ali-fc-typescript-skeleton

作者使用这个开发模版写了一个上传文件转码服务,用来将用户上传的 office 文档,转码成图片格式,并且添加水印,然后再在前端展示,既可以展示自己的原创内容,又能一定程度上防止抄袭。由于阿里云函数计算的免费额度也很大,所以运行至今一年多下来,给公司节省了很多钱。
image.png

因此鼓励你更多地使用函数计算,这是趋势。