テストケース練習:シンプルな非純粋関数①
お題
あなたは天気予報アプリの開発者です。外部の天気予報APIから取得したデータを加工して、ユーザーに分かりやすい形式で提供する機能を実装します。
関数
code: ts
/**
* 指定された都市の天気予報を取得して整形する
* @param cityName 都市名(例: "Tokyo", "Osaka")
* @returns 整形された天気予報情報
* @throws APIエラー時は適切なエラーメッセージを投げる
*/
async function getWeatherForecast(cityName: string): Promise<WeatherInfo> {
// 実装してください
}
// 型定義
interface WeatherInfo {
city: string;
temperature: number; // 摂氏温度
condition: string; // 天気状態(晴れ、曇り、雨など)
humidity: number; // 湿度(%)
recommendation: string; // ユーザーへのおすすめメッセージ
}
// 外部APIのレスポンス型(モック対象)
interface ExternalWeatherResponse {
name: string;
main: {
temp: number; // ケルビン温度
humidity: number;
};
weather: Array<{
main: string; // "Clear", "Clouds", "Rain" など
}>;
}
エラー
code: txt
APIが404を返した場合 → "都市が見つかりません"
APIが500エラーの場合 → "天気情報の取得に失敗しました"
ネットワークエラー → "ネットワークエラーが発生しました"
テストケース作る前に...
hr.icon
純粋関数はテストしやすいし、安心感を持てる
引数によって結果を決めることができる。
引数と結果だけでテストケースを作れるので、抜け漏れ少なくテストケースを作れる(はず)
非純粋関数はテストしにくいし、不安が残る
引数によって結果を決めることができない。
結果を左右するのは内部の外部依存(API、DB、時刻など)
引数の同値クラス分析した程度では、テストケースを洗い出せない
「外部依存の振る舞いパターン」を考える必要がある
でもそれは内部実装の知識が必要になり、ブラックボックステストの理想が崩れる
さらに、引数と外部依存の結果は独立してないので、それも相まってテストケースを作りにくい。
というわけで、原理的に非純粋関数のテストは難しい
難しいので関数の設計を工夫すると良い。
純粋な部分を分離する:
変換ロジック、計算ロジックなど → 純粋関数
これは引数だけで結果を制御できる
テストケースの網羅性も明確
インプットが全て独立している
不純粋な部分は薄く保つ:
外部依存を呼ぶだけ → 不純粋なシェル
ロジックを持たせない
「明らかに正しい」レベルまで単純化
テスト戦略の使い分け:
純粋関数 → ユニットテスト(理想的、網羅的)
不純粋シェル → 統合テスト(最小限)
全体 → E2Eテスト(本物の依存で動作確認)
仕様をさらに整理する
hr.icon
tansformWeatherInfoという関数を作る。
こいつを純粋関数として扱ってテストするのが良い
変換map
table: 変換
name api info
--------- ----- ------
気温 ケルビン 摂氏
湿度 % 変更なし
天気 Clear | Clouds | Rain | Snow 晴れ | 曇り | 雨 | 雪
関数
hr.icon
code: ts
import { fetchWeatherFromApi, type ExternalWeatherApiResponse } from "./api";
import { type WeatherCondition, WeatherInfo } from "./model";
export async function getWeatherForecast(cityName: string): Promise<WeatherInfo> {
const weatherDataFromApi = await fetchWeatherFromApi(cityName)
return transformWeatherData(weatherDataFromApi);
}
export function transformWeatherData(external: ExternalWeatherApiResponse): WeatherInfo {
const celsius = external.main.temp - 273.15;
const conditionMap: { key: string: WeatherCondition } = { 'Clear': '晴れ',
'Clouds': '曇り',
'Rain': '雨',
'Snow': '雪',
};
return new WeatherInfo(
external.name,
celsius,
condition,
external.main.humidity
);
}