单元测试,测试驱动开发

1. 简介 下图出自 Learning Curves (for different programming languages) 虽然文章调侃为主, 但也看出来作者对单元测试的态度。 对Python程序员来讲,随着经验的增长, 在掌握了单元测试之后, 个人的生产力得到一个突变(至于掌握装饰器,会自我感觉膨胀,但效率提升不明显)。 很多年前刚走出校门,还是C++年代。 有个前北电的Java大牛布道,给开发团队推荐cpp-unit,说单元测试 “可以显著提高单兵作战能力”。斗转星移,南征北战,北电的Eddie已失去联系,但他的布道显然成功了。我在不同的项目中实践过单元测试: cpp unit junit luaunit python unittest jest 虽然语言不同,但unittest思想都一样,都有setup,teardown,testcase,assert/expect,mock这些概念。 对单元测试“提高单兵作战能力”的说法,我“不能认同更多”。 2. 单元测试的好处 unittest的好处主要在两个方面。 2.1. 鼓励先设计接口 要测试驱动,就要先想怎么测,从而促进在很早期就从接口定义的角度考虑问题。也促进了模块的低耦合高内聚。 另外,还带来一个额外的好处,TDD也会沉淀出类似文档的测试用例。若干年后回顾一个软件时候,看看用例,基本也了解当时的思路了。 2.2. 跑一遍测试的成本低 测试用例的写法规范,测试框架支持方便的执行和反馈结果,这样跑一遍测试没有时间和精力的负担,随时跑。 经常跑测试,持续的集成,有问题也能早暴露。 接口稳定后,有测试用例做质量保障,做重构也方便。 3. 举个例子 假如在开发一个API服务器。 做单元测试从粗到细可以有几个不同的粒度: 从API层面,用http client模拟请求,然后assert返回的结果是否符合预期 从Handler层面,模拟http请求的header,params,body等,然后喂给Handler,看返回的结构是否符合预期 模块层面,如果Handler之下还有其它业务逻辑模块,可以针对模块接口做单元测试 以上 1, 已经可以看作系统测试(端到端测试)了。 2相对于1有一些额外的好处:一般情况下,对应一个请求的处理,除了Hanlder之外,还有一些中间件来做预处理。 对于1来说,必须把中间件的功能和Handler本身作为一个整体测试。 不够灵活。 2相对于1又有一些缺点,需要Mock。 根据所用framework不同,需要mock输入输出的数据结构。 好在一般情况下,mock都比较简单,有很多framework也有第三方做好的mock库。 例如,下面对JS express 的Handler测试: import { posthandler } from '../src/handlers/post'; import { getMockReq, getMockRes } from '@jest-mock/express'; // generate a mocked response and next function, with provided values const { res, next } = getMockRes({ }) test('check post handler returns token in JSON body', async () => { // generate a mock request with params const req = getMockReq({ params: { id: 'abc-def' }, headers:{authorization:'this is my token'} }) // provide the mock req, res, and next to assert await posthandler(req, res) expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ authorization: 'this is my token', }), ) }) 引入的第三方jest-mock/express,可以帮助来生成输入数据,检查输出数据。see? easy. ...

2022年3月6日 · 1 分钟 · 费小慢

一个简单的API Proxy

1. 起因 在配置Directus使用钉钉扫码登录时候,发现钉钉的免密登录(OAuth 2)和RFC规范不一致。 需要做协议转换后才能和Directus正常通信。 需求比较小众,没有现成的软件,只好自己动手了。 2. 主要功能 能作为API通信的中间人, 转发客户端和API服务器之间的通信, 记录LOG,方便分析协议; 作为中间人,能修改请求的内容, 修改响应的内容;可以做协议适配,转换。 APIPROXY is a RESTFUL API proxy, monitor and adaptor. Forward RESTFUL API to another host. It’s man in the middle who can monitor and modify the header and body of the API Request & Response. Good for protocol study and adaptation. Features API proxy: forward any incoming API to remote sever and return the response back to client. API monitor: you can get detailed log of the API req and res in the log file. API adpator: modify the request and response on the fly while forwarding, including parameters, body, http headers etc. API mock server: you can add your own API for testing purpose easily. 3. 目前状态 初步实现了对钉钉OAuth2协议的RFC6749兼容封装。 目前Directus已经可以通过它的翻译支持钉钉免密登录。 ...

2022年3月5日 · 1 分钟 · 费小慢

Typescript

Typescript学习笔记。From zero to hero。 1. Array,Tuple,Union,Enum // Basic Types let id: number = 5 //变量后面加类型,用冒号隔开 let company: string = 'Traversy Media' let isPublished: boolean = true let x: any = 'Hello' //any类型变量,可以放任何类型数据 let ids: number[] = [1, 2, 3, 4, 5] //不定长数组 区别传统静态语言的int a[4]; let arr: any[] = [1, true, 'Hello'] //any数组,可以混合各种值 // Tuple let person: [number, string, boolean] = [1, 'Brad', true] //元组:已知元素数量和类型的数组,各元素的类型不必相同。 // Tuple Array let employees: [number, string][] //每个元素是元组的数组 employee = [ [1, 'Brad'], [2, 'John'], [3, 'Jill'], ] // Union let pid: string | number //联合 /* 以下是C语言的联合,都是一个变量可以存几种不同类型数据 union data{ int n; char ch; double f; }; */ pid = '22' // Enum enum Direction1 { Up = 1, Down, Left, Right, } enum Direction2 { Up = 'Up', Down = 'Down', Left = 'Left', Right = 'Right', } 2. Map //定义 type MapType = { [id: string]: string; } //实例化 const map: MapType = {}; map['a'] = 'b'; map['c'] = 'd'; //删除 delete map['c']; //枚举 for (let i in map) { console.log(map[i]); } //得到包含所有key的数组 console.log(Object.keys(map)); //另外一种用Record的实现方式 const map: Record<string, string> = {}; map['a'] = 'b'; map['c'] = 'd'; 3. Object // Objects type User = { id: number name: string } const user: User = { id: 1, name: 'John', } // Type Assertion 类型的选择 let cid: any = 1 // let customerId = <number>cid 从any到 number 的选择 let customerId = cid as number 4. Function // Functions function addNum(x: number, y: number): number { return x + y } // Void function log(message: string | number): void { console.log(message) } 5. Interface, Class // Interfaces interface UserInterface { readonly id: number name: string age?: number } const user1: UserInterface = { id: 1, name: 'John', } interface MathFunc { (x: number, y: number): number } const add: MathFunc = (x: number, y: number): number => x + y const sub: MathFunc = (x: number, y: number): number => x - y interface PersonInterface { id: number name: string register(): string } // Classes class Person implements PersonInterface { id: number name: string constructor(id: number, name: string) { this.id = id this.name = name } register() { return `${this.name} is now registered` } } const brad = new Person(1, 'Brad Traversy') const mike = new Person(2, 'Mike Jordan') // Subclasses class Employee extends Person { position: string constructor(id: number, name: string, position: string) { super(id, name) this.position = position } } const emp = new Employee(3, 'Shawn', 'Developer') 6. Generics // Generics => C++的泛型 function getArray<T>(items: T[]): T[] { return new Array().concat(items) } let numArray = getArray<number>([1, 2, 3, 4]) let strArray = getArray<string>(['brad', 'John', 'Jill']) strArray.push(1) // Throws error 7. FAQ 7.1. type alias or interface https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#differences-between-type-aliases-and-interfaces ...

2022年3月4日 · 3 分钟 · 费小慢

Typescript中的模块

和python不同, Typescript/Javascript模块导入导出方式五花八门。 目前因为ES6已经标准化,推荐用import/export。 历史遗留的require/exports由于存在数目巨大的npm库,也还将长期共存。 1. ES6模块导入导出 1.1. 语法 三种export方式,两种import方式 export import export var; import {var} from module export {var}; import {var} from module export default var import var from module 另外可以用 import * as module_alias from module 导入模块所有export的变量到一个对象 module_alias 中。 1.2. 例子 1.2.1. testEs6Export.ts 'use strict' //导出变量 export const a = '100'; //导出方法 export const dogSay = function(){ console.log('wang wang'); } //导出方法第二种 function catSay(){ console.log('miao miao'); } export { catSay }; //export default导出 const m = 100; export default m; //export defult const m = 100;// 这里不能写这种格式。 1.2.2. index.ts import { dogSay, catSay } from './testEs6Export'; //导出了 export 方法 import m from './testEs6Export'; //导出了 export default import * as testModule from './testEs6Export'; //as 集合成对象导出 2. CommonJS 模块导入导出 2.1. 语法 模块中有module.export对象,里面存放的就是export的变量。 ...

2022年3月4日 · 1 分钟 · 费小慢