今天的目標是通過昨天的測試,完成步驟3到步驟5
首先看第一筆測試,我們的目標是當使用者發出 http://localhost/api/v1/users/richard_01
請求時回應 200。
it('should reply 200 with a user object when username is richard_01', async () => {
//arrange
const username = 'richard_01'
const request = new Request(`http://localhost/api/v1/users/${username}`)
const params = { params: { username } }
//act
const response = await GET(request, params)
//assert
const status = response.status
const rawData = await response.json()
const data = S.parseSync(User.schema)(rawData)
expect(status).toBe(200)
expect(data.name).toBe(username)
})
參考以下程式碼,我們只要把昨天的 throw Error('Todo')
換掉就可以輕鬆通過
//src\app\api\v1\users\[username]\route.ts
interface Route {
params: { username: string }
}
export const GET = async (request: Request, route: Route): Promise<Response> => {
// throw Error('Todo') << remove
return NextResponse.json({ // add fake response to pass the test
_tag: 'Administrator',
name: 'richard_01',
})
}
不過一但加入第二筆測資,結果就又出錯了,當 username 不存在時,應該要回 404
而不是 200
it('should reply 404 when username is richard_x', async () => {
//arrange
const username = 'richard_x'
const request = new Request(`http://localhost/api/v1/users/${username}`)
const params = { params: { username } }
//act
const response = await GET(request, params)
//assert
expect(response.status).toBe(404)
})
因此我們得做一個判斷才行,這樣一來就能順利通過測試
export const GET = async (_: Request, route: Route) =>
route.params.username === 'richard_01'
? NextResponse.json({
_tag: 'Administrator',
name: 'richard_01',
})
: new NextResponse('', { status: 404 })
第三筆測試會比較困難一點,因為我們這次要在資料庫連不上的情況下回應 500 錯誤。
describe('in the case of database connecting failure', () => {
beforeAll(async () => {
vi.stubEnv('DB_URI', 'mongodb://localhost:54321')
})
it('should reply 500', async () => {
//arrange
const username = 'richard_01'
const request = new Request(`http://localhost/api/v1/users/${username}`)
const params = { params: { username } }
//act
const response = await GET(request, params)
//assert
expect(response.status).toBe(500)
})
})
因此我們接下來要真的和資料庫互動才能通過測試。
以下是通過測試並且初步重構過的程式碼,請參考註解說明。
程式碼請參考 D22/consumer-driven-contract
//src\app\api\v1\users\[username]\route.ts
export const GET = async (_: Request, route: Route) =>
pipe(
// 讀取環境變數,因為環境變數屬於不穩定的外部輸入,因此要利用 Schema 做驗證
// Env.of 的型別會是 (env:unknown) => Effect<never, EnvError, Env>
Env.of(process.env),
// 讀取 env 成功以後嘗試取得資料庫連線
// 如果已經有連線存在,會直接取用現有連線
Effect.flatMap(MongooseEx.connect),
// 等到確定資料庫連線完成,根據 username 找回 UserDocument
// findOneUser 的型別是
// (name: string) => () => Effect<never, DBError, UserDocument | null>
Effect.flatMap(findOneUser(route.params.username)),
// 如果結果是 null,把原本正常路線的 null 轉成錯誤路線的 NotFoundError
Effect.flatMap(validateFounded),
// 把存在資料庫的 UserDocument 物件,轉換成 User 物件
Effect.flatMap(UserDocument.toUser),
Effect.match({
// 錯誤的話對各種錯誤結果做 pattern matching,請參考下一個程式碼區塊
onFailure: matchErrors,
// 成功的話回應 User
onSuccess: NextResponse.json,
}),
Effect.runPromise
)
// src\app\api\common\matchErrors.ts
const notFound = (error: ModelError) =>
NextResponse.json(error, { status: 404 })
const internal = (error: ModelError) =>
NextResponse.json(error, { status: 500 })
export const matchErrors = (error: ModelError) =>
pipe(
M.value(error),
// 如果發生 DatabaseNotFoundError,
M.tag('DatabaseNotFoundError', notFound),
// 即使處理方式都相同,我也喜歡盡量把所有可能列出來,因為使用 orElse 容易造成疏忽
M.tag('EnvError', internal),
M.tag('DatabaseUnexpectedError', internal),
M.tag('MongooseExConnectError', internal),
M.tag('TransformError', internal),
M.exhaustive
)