![[tdq-boss3.webp|cover-picture]] 崩壊寸前な[[JavaScript]]の世界からの脱出...。空の上の世界で最後に立ちはだかるBOSSを乗り越えて先の世界へ。今までの集大成で乗り越えましょう。 ## 事前準備 以下2つのファイルを作成してください。 ### libs.js ```js /** * HTTP GETリクエストを行う * @param {string} url * @param {Function} callback * @returns {void} * * @example * ```js * httpGet("https://reqres.in/api/users?page=2", function (error, data) { * if (error) { * console.error(error); * return; * } * console.log(data); * }) * ``` */ export function httpGet(url, callback) { fetch(url) .then(function (response) { if (!response.ok) { throw new Error("Request failed with status " + response.status); } return response.json(); }) .then(function (data) { callback(null, data); }) .catch(function (error) { callback(error, null); }); } ``` ### main.js ```js import { httpGet } from "./libs.js"; function fetchUsers(condition, callback) { httpGet("https://reqres.in/api/users", function (error, jsonRes) { if (error) { callback(error, null); return; } try { var users = jsonRes.data; var filteredUsers = []; for (var i = 0; i < users.length; i++) { var user = users[i]; if ( !condition.name || user.first_name.indexOf(condition.name) !== -1 || user.last_name.indexOf(condition.name) !== -1 ) { filteredUsers.push(user); } } callback(null, filteredUsers); } catch (e) { callback(e, null); } }); } if (import.meta.main) { fetchUsers({ name: "E" }, function (error, result) { if (error) { console.error(error); return; } console.log(result); }); } ``` ## Mission 1 #😁EASY `main.js` の `fetchUsers` が何をやっているかを説明してください。説明ではなく、[[JSDoc]]として記載してもらっても構いません。 %% 解答例 外部APIからユーザーデータを取得し、指定した名前でフィルタリングした結果をコールバック関数で返します。 ```js /** * 指定された条件に基づいてユーザーを取得しフィルタリングする * @param {Object} condition - フィルタリング条件 * @param {string} [condition.name] - ユーザーの名前(first_nameまたはlast_nameに含まれる文字列) * @param {Function} callback - コールバック関数 * @param {Error|null} callback.error - エラーオブジェクトまたはnull * @param {Array} callback.filteredUsers - 条件に一致するユーザーの配列 * @returns {void} */ ``` %% ## Mission 2 #🙂NORMAL `main.js` の以下コードについて ```js if (import.meta.main) { fetchUsers({ name: "E" }, function (error, result) { if (error) { console.error(error); return; } console.log(result); }); } ``` [[Promise (JavaScript)|Promise]]を使ったリファクタリングを行い、以下のコードを完成させてください。 ```js function fetchUsersPromise(condition) { // FIXME: fetchUsersを使った通信処理を行いPromiseを返すようにする } if (import.meta.main) { // FIXME: fetchUsersPromiseを使って async/await の実装にする } ``` %% 解答例 ```js function fetchUsersPromise(condition) { return new Promise((resolve, reject) => { fetchUsers(condition, (error, result) => { if (error) { reject(error); } else { resolve(result); } }); }); } if (import.meta.main) { try { const users = await fetchUsersPromise({ name: "E" }); console.log(users); } catch (error) { console.error(error); } } ``` %% > [!hint]- Hint 1 > - [[📗TDQ-023 古の非同期処理 コールバック関数]] > - [[📗TDQ-024 Promiseの基本 正常系]] > - [[📗TDQ-027 近代の非同期処理 async await]] ## Mission 3 #🙂NORMAL `main.js` をテストするコード `main_test.js` を作成し、`fetchUsersPromise` のテストを書いてください。 > [!attention] 注意 > - `https://reqres.in/api/users` のレスポンスを制御するために[[テストダブル]]を使う必要はありません (実際にリクエストしてOK) > - `https://reqres.in/api/users` のレスポンスは必ず決まった値を返し、その値を知っているという前提で問題ありません %% 解答例 ```js import { expect } from "jsr:@std/expect"; import { fetchUsersPromise } from "./main.js"; Deno.test("条件に合致するユーザーを正しくフィルタリングすること - E", async () => { const users = await fetchUsersPromise({ name: "E" }); expect(users.length).toBe(2); expect(users[0].first_name).toBe("Emma"); expect(users[1].first_name).toBe("Eve"); }); Deno.test("条件に合致するユーザーを正しくフィルタリングすること - G", async () => { const users = await fetchUsersPromise({ name: "G" }); expect(users.length).toBe(1); expect(users[0].first_name).toBe("George"); }); Deno.test("名前条件が指定されていない場合は全ユーザーを返すこと", async () => { const users = await fetchUsersPromise({}); expect(users.length).toBe(6); }); Deno.test("存在しない名前で検索した場合は空配列を返すこと", async () => { const users = await fetchUsersPromise({ name: "XYZ" }); expect(users.length).toBe(0); }); ``` %% > [!hint]- Hint 1 > テストコードでも [[async function (JavaScript)|async function]] と [[await (JavaScript)|await]] は使えます。 ## Mission 4 #🙂NORMAL `main.js` の `fetchUsers` 関数について、try/catch文をリファクタリングしてください。 ```js try { var users = jsonRes.data; var filteredUsers = []; for (var i = 0; i < users.length; i++) { var user = users[i]; if ( !condition.name || user.first_name.indexOf(condition.name) !== -1 || user.last_name.indexOf(condition.name) !== -1 ) { filteredUsers.push(user); } } callback(null, filteredUsers); } catch (e) { callback(e, null); } ``` %% 解答例 ポイントは複数ある。 - `var` が `const` や `let` に変わっていること - `for` が `filter` になっていること - `indexOf` が `includes` になっていること ```js try { const users = jsonRes.data; const filteredUsers = users.filter(user => !condition.name || user.first_name.includes(condition.name) || user.last_name.includes(condition.name) ); callback(null, filteredUsers); } catch (e) { callback(e, null); } ``` %% > [!hint]- Hint 1 > - [[📗TDQ-003 変数と宣言]] > - [[📗TDQ-015 配列の基礎と検索]] > - [[📗TDQ-018 配列の条件判定]] ## Mission 5 #😵HARD `main.js` の `fetchUsers` 関数について、`httpGet` ではなく `fetch` を使うように実装を変更してください。回答は **`main.js` のコードすべて** を記載してください。 > [!attention] 注意 > - `fetchUsers` のIFは変更して構いません。 %% 回答例 ```js import { httpGet } from "./libs.js"; /** * ユーザーデータを条件に基づいてフィルタリングして取得します * @param {Object} condition - フィルタリング条件 * @param {string} condition.name - ユーザー名による検索条件 * @returns {Promise<Array>} - フィルタリングされたユーザーデータのPromise */ export function fetchUsers(condition) { try { const response = await fetch("https://reqres.in/api/users"); if (!response.ok) { throw new Error("Request failed with status " + response.status); } const jsonRes = await response.json(); const users = jsonRes.data; const filteredUsers = users.filter(user => !condition.name || user.first_name.includes(condition.name) || user.last_name.includes(condition.name) ); return filteredUsers; } catch (error) { throw error; } } export function fetchUsersPromise(condition) { return fetchUsers(condition) } if (import.meta.main) { try { const users = await fetchUsersPromise({ name: "E" }); console.log(users); } catch (error) { console.error(error); } } ``` %% > [!hint]- Hint 1 > `fetchUsers` の戻り値は `Promise<any[]>` 型になります。 ## Mission 6 #😁EASY `main.js` から `fetchUsersPromise` を削除するようリファクタリングしてください。回答には **`main.js` と `main.test.js` の全コード** を記載してください。 %% 解答例 `main.js` ```js /** * ユーザーデータを条件に基づいてフィルタリングして取得します * @param {Object} condition - フィルタリング条件 * @param {string} condition.name - ユーザー名による検索条件 * @returns {Promise<Array>} - フィルタリングされたユーザーデータのPromise */ export async function fetchUsers(condition) { try { const response = await fetch("https://reqres.in/api/users"); if (!response.ok) { throw new Error(`HTTPエラー: ${response.status}`); } const jsonRes = await response.json(); const users = jsonRes.data; const filteredUsers = users.filter((user) => !condition.name || user.first_name.includes(condition.name) || user.last_name.includes(condition.name) ); return filteredUsers; } catch (error) { throw error; } } if (import.meta.main) { try { const users = await fetchUsers({ name: "E" }); console.log(users); } catch (error) { console.error(error); } } ``` `main.test.js` ```js import { expect } from "jsr:@std/expect"; import { fetchUsers } from "./main.js"; Deno.test("条件に合致するユーザーを正しくフィルタリングすること - E", async () => { const users = await fetchUsers({ name: "E" }); expect(users.length).toBe(2); expect(users[0].first_name).toBe("Emma"); expect(users[1].first_name).toBe("Eve"); }); Deno.test("条件に合致するユーザーを正しくフィルタリングすること - G", async () => { const users = await fetchUsers({ name: "G" }); expect(users.length).toBe(1); expect(users[0].first_name).toBe("George"); }); Deno.test("名前条件が指定されていない場合は全ユーザーを返すこと", async () => { const users = await fetchUsers({}); expect(users.length).toBe(6); }); Deno.test("存在しない名前で検索した場合は空配列を返すこと", async () => { const users = await fetchUsers({ name: "XYZ" }); expect(users.length).toBe(0); }); ``` %% ## Mission 7 #🙂NORMAL `main.js` を **[[#Mission 1]] 開始前の状態** に戻し、[[util.promisify]]を使って **`main.test.js` のテストが通るように** リファクタリングしてください。 > [!attention] 注意 > - `main.test.js` は**変更禁止**です %% 解答例 ```js import util from "node:util"; import { httpGet } from "./libs.js"; function _fetchUsers(condition, callback) { httpGet("https://reqres.in/api/users", function (error, jsonRes) { if (error) { callback(error, null); return; } try { var users = jsonRes.data; var filteredUsers = []; for (var i = 0; i < users.length; i++) { var user = users[i]; if ( !condition.name || user.first_name.indexOf(condition.name) !== -1 || user.last_name.indexOf(condition.name) !== -1 ) { filteredUsers.push(user); } } callback(null, filteredUsers); } catch (e) { callback(e, null); } }); } export const fetchUsers = util.promisify(_fetchUsers); if (import.meta.main) { fetchUsers({ name: "E" }, function (error, result) { if (error) { console.error(error); return; } console.log(result); }); } ``` %% > [!hint]- Hint 1 > https://docs.deno.com/api/node/util/~/promisify ## Mission 8 #😱NIGHTMARE [[#Mission 6]] 完了時点のコードに対し、リクエストが500エラーを返す場合に期待通り動作することを確認するテストコードを書いてください。 > [!attention] 注意 > - **`main.js` を変更してはいけません** %% 解答例 ```js import { expect } from "jsr:@std/expect"; import { assertRejects } from "jsr:@std/assert"; import { stub } from "jsr:@std/testing/mock"; import { fetchUsers } from "./main.js"; Deno.test("条件に合致するユーザーを正しくフィルタリングすること - E", async () => { const users = await fetchUsers({ name: "E" }); expect(users.length).toBe(2); expect(users[0].first_name).toBe("Emma"); expect(users[1].first_name).toBe("Eve"); }); Deno.test("条件に合致するユーザーを正しくフィルタリングすること - G", async () => { const users = await fetchUsers({ name: "G" }); expect(users.length).toBe(1); expect(users[0].first_name).toBe("George"); }); Deno.test("500エラーのときはHTTPエラーを送出する", async () => { const fetchUsersStub = stub( globalThis, "fetch", () => ({ ok: false, status: 500 }), ); try { // await忘れに注意 await assertRejects( async () => { await fetchUsers({ name: "XYZ" }); }, Error, "HTTPエラー: 500", ); } finally { // 忘れずにrestoreする fetchUsersStub.restore(); } }); Deno.test("名前条件が指定されていない場合は全ユーザーを返すこと", async () => { const users = await fetchUsers({}); expect(users.length).toBe(6); }); Deno.test("存在しない名前で検索した場合は空配列を返すこと", async () => { const users = await fetchUsers({ name: "XYZ" }); expect(users.length).toBe(0); }); ``` %% > [!hint]- Hint 1 > [[テストダブル]]を使って、テスト実行時のレスポンスを都合のよい値へと変更する必要があります。 > [!hint]- Hint 2 > https://jsr.io/@std/testing > [!hint]- Hint 3 > [[スタブ]]を使って、テストに必要な最低限のレスポンス操作をします。 > https://jsr.io/@std/testing/doc/mock#stubbing > [!hint]- Hint 4 > `stub` の第1引数は `globalThis` です。これは `fetch` が[[グローバルスコープ (JavaScript)|グローバルスコープ]]の関数であるためです。 > [!hint]- Hint 5 > 非同期関数に対して、エラーがthrowされることを確認するには [[@std.assert (Deno)|@std/asset]]パッケージの [[assertRejects (Deno)|assertRejects]] を使います。