我们下面分几种常见场景,讲述下如何写好单元测试:
util 方法
测试调用第三方接口(SDK)
当我们想验证调用第三方接口(SDK)逻辑是否正确,一般会测试:
- 关键方法有无调用
- 参数有无正确
- 关键返回
- 第三方报错时我们的处理等
下面是如何验证 S3 上传逻辑:
import { config, S3 } from 'aws-sdk'
import { mocked } from 'ts-jest/utils'
jest.mock('aws-sdk')
describe('AwsS3FileStorage', () => {
let accessKey: string
let secret: string
let bucket: string
let fileName: string
let sut: AwsS3FileStorage
beforeAll(() => {
accessKey = 'any_access_key'
secret = 'any_secret'
bucket = 'any_bucket'
fileName = 'any_file_name'
})
beforeEach(() => {
sut = new AwsS3FileStorage(accessKey, secret, bucket)
})
it('should config aws credentials on creation', () => {
expect(sut).toBeDefined()
expect(config.update).toHaveBeenCalledWith({
credentials: {
accessKeyId: accessKey,
secretAccessKey: secret
}
})
// 深入到 sdk 的关键方法
expect(config.update).toHaveBeenCalledTimes(1)
})
describe('upload', () => {
let file: Buffer
let putObjectPromiseSpy: jest.Mock
let putObjectSpy: jest.Mock
beforeAll(() => {
file = Buffer.from('any_buffer')
// mock 关键方法的实现
putObjectPromiseSpy = jest.fn()
putObjectSpy = jest.fn().mockImplementation(() => ({ promise: putObjectPromiseSpy }))
mocked(S3).mockImplementation(jest.fn().mockImplementation(() => ({ putObject: putObjectSpy })))
})
it('should call putObject with correct input', async () => {
await sut.upload({ fileName, file })
// 测试关键方法有无被调用(正确的调用: 参数、次数)
expect(putObjectSpy).toHaveBeenCalledWith({
Bucket: bucket,
Key: fileName,
Body: file,
ACL: 'public-read'
})
expect(putObjectSpy).toHaveBeenCalledTimes(1)
expect(putObjectPromiseSpy).toHaveBeenCalledTimes(1)
})
// 测试报错异常情况的处理
it('should rethrow if putObject throws', async () => {
const error = new Error('upload_error')
putObjectPromiseSpy.mockRejectedValueOnce(error)
const promise = sut.upload({ fileName, file })
await expect(promise).rejects.toThrow(error)
})
})
})
测试登录
import { app } from '@/main/config/app'
import { UnauthorizedError } from '@/application/errors'
import { makeFakeDb } from '@/tests/infra/repos/postgres/mocks'
import { getConnection } from 'typeorm'
import request from 'supertest'
describe('Login Routes', () => {
describe('POST /login/facebook', () => {
let backup: IBackup
const loadUserSpy = jest.fn()
// 劫持一个类,mock 其中的一个方法 loadUser
jest.mock('@/infra/gateways/facebook-api', () => ({
FacebookApi: jest.fn().mockReturnValue({ loadUser: loadUserSpy })
}))
beforeEach(() => {
backup.restore()
})
it('should return 200 with AccessToken', async () => {
// 通过 mock 第三方接口有正确的返回,来 mock 登录成功的场景
loadUserSpy.mockResolvedValueOnce({ facebookId: 'any_id', name: 'any_name', email: 'any_email' })
const { status, body } = await request(app)
.post('/api/login/facebook')
// 这里的 token 并没卵用,只是呼应上面成功的场景
.send({ token: 'valid_token' })
expect(status).toBe(200)
expect(body.accessToken).toBeDefined()
})
it('should return 401 with UnauthorizedError', async () => {
const { status, body } = await request(app)
.post('/api/login/facebook')
// 因为没有 mock loadUser 的返回,所以这个登录必定失败(token 无用)
.send({ token: 'invalid_token' })
expect(status).toBe(401)
expect(body.error).toBe(new UnauthorizedError().message)
})
})
})
测试增/删/改逻辑
如果我们 nodejs 服务有维护数据库,必定有增删改,如何测试,又不污染数据源?
import { PgUser } from '@/infra/repos/postgres/entities'
import { PgConnection } from '@/infra/repos/postgres/helpers'
import { app } from '@/main/config/app'
import { env } from '@/main/config/env'
import { makeFakeDb } from '@/tests/infra/repos/postgres/mocks'
import { IBackup } from 'pg-mem'
import { Repository } from 'typeorm'
import { sign } from 'jsonwebtoken'
import request from 'supertest'
describe('User Routes', () => {
let backup: IBackup
let connection: PgConnection
let pgUserRepo: Repository<PgUser>
beforeAll(async () => {
connection = PgConnection.getInstance()
// 初始化一个假数据库(可使用一些 init 数据)
const db = await makeFakeDb([PgUser])
// 备份这个假数据库的初始状态
backup = db.backup()
pgUserRepo = connection.getRepository(PgUser)
})
afterAll(async () => {
await connection.disconnect()
})
beforeEach(() => {
// 在执行每次用例前都恢复下数据库备份
backup.restore()
})
describe('DELETE /users/picture', () => {
it('should return 403 if authorization header is not present', async () => {
const { status } = await request(app)
.delete('/api/users/picture')
expect(status).toBe(403)
})
it('should return 200 with valid data', async () => {
const { id } = await pgUserRepo.save({ email: 'any_email', name: 'any name' })
const authorization = sign({ key: id }, env.jwtSecret)
const { status, body } = await request(app)
.delete('/api/users/picture')
.set({ authorization })
expect(status).toBe(200)
expect(body).toEqual({ pictureUrl: undefined, initials: 'AN' })
})
})
describe('PUT /users/picture', () => {
const uploadSpy = jest.fn()
jest.mock('@/infra/gateways/aws-s3-file-storage', () => ({
AwsS3FileStorage: jest.fn().mockReturnValue({ upload: uploadSpy })
}))
it('should return 403 if authorization header is not present', async () => {
const { status } = await request(app)
.put('/api/users/picture')
expect(status).toBe(403)
})
it('should return 200 with valid data', async () => {
uploadSpy.mockResolvedValueOnce('any_url')
const { id } = await pgUserRepo.save({ email: 'any_email', name: 'any name' })
const authorization = sign({ key: id }, env.jwtSecret)
const { status, body } = await request(app)
.put('/api/users/picture')
.set({ authorization })
.attach('picture', Buffer.from('any_buffer'), { filename: 'any_name', contentType: 'image/png' })
expect(status).toBe(200)
expect(body).toEqual({ pictureUrl: 'any_url', initials: undefined })
})
})
})