diff --git a/docs/chapter2/section3/0_test.md b/docs/chapter2/section3/0_test.md index 5e4b9a6c..a7411e11 100644 --- a/docs/chapter2/section3/0_test.md +++ b/docs/chapter2/section3/0_test.md @@ -3,88 +3,86 @@ テストを書く前に、テスト対象になる処理が必要です。今回は、「与えられた City のリストから国ごとの人口の和」を計算する処理を書いてみます。 ::: details ヒント -- 国ごとにデータを分けて持つには`map`を使えばいいでしょう -- 国単位で集計するので map の key は `CountryCode` を使うといいでしょう -- データが入っていない場合もあるので、条件分岐には気を付けてください +- 国ごとにデータを分けて持つには `std::collections::HashMap` を使えばいいでしょう +- 国単位で集計するので map の key は `country_code` を使うといいでしょう +- `country_code` が空文字列である City は、和を計算せず無視してください ::: ::: details 参考実装 -<<<@/chapter2/section3/src/calculate_population.go#calculate +<<<@/chapter2/section3/src/sum_population_by_country.rs#calculate ::: -そうしたら、このコードが期待した値を返すかテストを書いてみましょう。 +このメソッドが期待した値を返すかどうか、テストを書いて確認していきましょう。 -まず、`calculate_test.go`を作成します。 +メソッド 1 つなど、小さい単位でのテストは、ユニットテストと呼ばれます。 +ユニットテストは同じファイル内にテストを書くのが一般的です。 -::: tip -Go では、`_test`がファイル名の後ろについているファイルはテストファイルとして認識されます。 -::: - -続いて、`calculate_test.go`にテスト関数を実装していきます。 +同じファイル内の一番下にテストを書いていきます。 -```go -package main +```rs +// #[cfg(test)] 属性を追加したモジュールはテストモジュールとして扱われる // [!code ++] +#[cfg(test)] // [!code ++] +mod tests { // [!code ++] + use super::{sum_population_by_country, City}; // [!code ++] + use std::collections::HashMap; // [!code ++] -import "testing" -// Testで始まる関数はテスト関数として認識されます -// testingはGoのテストのための標準ライブラリです -func Test_calculatePopulation(t *testing.T) { - // ここにテストを書いていく -} + fn test_sum_population_by_country() { // [!code ++] + // ここにテストを追加する // [!code ++] + } // [!code ++] +} // [!code ++] ``` まずは、空のリストを渡したときに、空のマップが返ってくることをテストしてみましょう。 -```go -package main - -import "testing" - -func Test_calculatePopulation(t *testing.T) { - // ここにテストを書いていく - cities := []City{} - got := calculatePopulation(cities) - want := map[string]int{} - // 長さが0になっているかどうかを確認する - if len(got) != 0 { - t.Errorf("calculatePopulation(%v) = %v, want %v", cities, got, want) - } +```rs +// #[cfg(test)] 属性を追加したモジュールはテストモジュールとして扱われる +#[cfg(test)] +mod tests { + use super::{sum_population_by_country, City}; + use std::collections::HashMap; + + #[test] + fn test_sum_population_by_country() { + // ここにテストを追加する + let cities = vec![]; // [!code ++] + let result = sum_population_by_country(cities); // [!code ++] + assert!(result.is_empty()); // [!code ++] + } } - ``` -書き終わったら、関数の左上にある`run test`か、そのさらに左にある再生ボタンを押して、テストを実行してみましょう。 +書き終わったら、関数の左上またはモジュールの左上にある `run test` を押して、テストを実行してみましょう。 ![](./images/run_test.png) すると、VSCode の Output にテストの結果が表示されます。 -``` -=== RUN Test_calculatePopulation ---- PASS: Test_calculatePopulation (0.00s) -PASS -ok test 0.001s -``` -テストが正常に終了したことがわかりますね。 +![](./images/test_result.png) + +テストが正常に終了したことがわかります。 ## 様々なケースをテストしてみよう -次に、`calculatePopulation`のテストをもう少し充実させてみましょう。 +次に、 `sum_population_by_country` のテストをもう少し充実させてみましょう。 これから複数のテストを書くため、先ほどのテストの関数名を変更します。 -```go -func Test_calculatePopulation_empty(t *testing.T) { - // ここにテストを書いていく - cities := []City{} - got := calculatePopulation(cities) - want := map[string]int{} - // 長さが0になっているかどうかを確認する - if len(got) != 0 { - t.Errorf("calculatePopulation(%v) = %v, want %v", cities, got, want) - } +```rs +// #[cfg(test)] 属性を追加したモジュールはテストモジュールとして扱われる +#[cfg(test)] +mod tests { + use super::{sum_population_by_country, City}; + use std::collections::HashMap; + + #[test] + fn test_sum_population_by_country_empty() { // [!code warning] + // ここにテストを追加する + let cities = vec![]; + let result = sum_population_by_country(cities); + assert!(result.is_empty()); + } } ``` ### 課題 @@ -92,16 +90,22 @@ func Test_calculatePopulation_empty(t *testing.T) { - 1 つの国のみのデータが入っている場合 - 複数の国のデータが入っている場合 -- 空のデータ(`city.CountryCode.Valid = false`)が入っている場合 +- 空文字列の `country_code` が入っている場合 ::: details 答え #### 1 つの国のみのデータが入っている場合 -<<<@/chapter2/section3/src/calculate_population_test.go#single +<<<@/chapter2/section3/src/sum_population_by_country.rs#single #### 複数の国のデータが入っている場合 -<<<@/chapter2/section3/src/calculate_population_test.go#multiple +<<<@/chapter2/section3/src/sum_population_by_country.rs#multiple + +#### 空文字列の `country_code` が入っている場合 +<<<@/chapter2/section3/src/sum_population_by_country.rs#empty_country_code +::: + +実装が終わったら、モジュールの左上にある `run test` を押して、テストを実行してみましょう。 + +![](./images/test_result_all.png) -#### 空のデータ(`city.CountryCode.Valid = false`)が入っている場合 -<<<@/chapter2/section3/src/calculate_population_test.go#null -::: +モジュール内の全てのテストが成功したことを確認できます。 diff --git a/docs/chapter2/section3/images/run_test.png b/docs/chapter2/section3/images/run_test.png index 5c28349d..54aa3d14 100644 Binary files a/docs/chapter2/section3/images/run_test.png and b/docs/chapter2/section3/images/run_test.png differ diff --git a/docs/chapter2/section3/images/test_result.png b/docs/chapter2/section3/images/test_result.png new file mode 100644 index 00000000..3d55b37f Binary files /dev/null and b/docs/chapter2/section3/images/test_result.png differ diff --git a/docs/chapter2/section3/images/test_result_all.png b/docs/chapter2/section3/images/test_result_all.png new file mode 100644 index 00000000..02824e58 Binary files /dev/null and b/docs/chapter2/section3/images/test_result_all.png differ diff --git a/docs/chapter2/section3/src/calculate_population.go b/docs/chapter2/section3/src/calculate_population.go deleted file mode 100644 index 76fbb20d..00000000 --- a/docs/chapter2/section3/src/calculate_population.go +++ /dev/null @@ -1,30 +0,0 @@ -package main - -import ( - "database/sql" -) - -type City struct { - ID int `json:"id,omitempty" db:"ID"` - Name sql.NullString `json:"name,omitempty" db:"Name"` - CountryCode sql.NullString `json:"countryCode,omitempty" db:"CountryCode"` - District sql.NullString `json:"district,omitempty" db:"District"` - Population sql.NullInt64 `json:"population,omitempty" db:"Population"` -} - -// #region calculate -func calculatePopulation(cities []City) map[string]int64 { - result := make(map[string]int64) - for _, city := range cities { - if city.CountryCode.Valid { - // まだmapに存在しなかった場合、初期化する - if _, ok := result[city.CountryCode.String]; !ok { - result[city.CountryCode.String] = 0 - } - result[city.CountryCode.String] += city.Population.Int64 - } - } - return result -} - -// #endregion calculate diff --git a/docs/chapter2/section3/src/calculate_population_test.go b/docs/chapter2/section3/src/calculate_population_test.go deleted file mode 100644 index b5586605..00000000 --- a/docs/chapter2/section3/src/calculate_population_test.go +++ /dev/null @@ -1,153 +0,0 @@ -package main - -import ( - "database/sql" - "testing" -) - -func Test_calculatePopulation(t *testing.T) { - // ここにテストを書いていく - cities := []City{} - got := calculatePopulation(cities) - want := map[string]int64{} - // 長さが等しいかを確認する - if len(got) != 0 { - t.Errorf("calculatePopulation(%v) = %v, want %v", cities, got, want) - } -} - -// #region single -// 1 つの国のみのデータが入っている場合 -func Test_calculatePopulation_single(t *testing.T) { - cities := []City{ - { - CountryCode: sql.NullString{ - String: "JPN", - Valid: true, - }, - Population: sql.NullInt64{ - Int64: 100, - Valid: true, - }, - }, - { - CountryCode: sql.NullString{ - String: "JPN", - Valid: true, - }, - Population: sql.NullInt64{ - Int64: 200, - Valid: true, - }, - }, - } - got := calculatePopulation(cities) - want := map[string]int64{ - "JPN": 300, - } - // 長さが0になっているかどうかを確認する - if len(got) != len(want) { - t.Errorf("calculatePopulation(%v) = %v, want %v", cities, got, want) - } - // JPNの人口が100になっているかどうかを確認する - if got["JPN"] != want["JPN"] { - t.Errorf("calculatePopulation(%v) = %v, want %v", cities, got, want) - } -} - -// #endregion single - -// #region multiple -// 複数の国のデータが入っている場合 -func Test_calculatePopulation_multiple(t *testing.T) { - cities := []City{ - { - CountryCode: sql.NullString{ - String: "JPN", - Valid: true, - }, - Population: sql.NullInt64{ - Int64: 100, - Valid: true, - }, - }, - { - CountryCode: sql.NullString{ - String: "JPN", - Valid: true, - }, - Population: sql.NullInt64{ - Int64: 200, - Valid: true, - }, - }, - { - CountryCode: sql.NullString{ - String: "USA", - Valid: true, - }, - Population: sql.NullInt64{ - Int64: 300, - Valid: true, - }, - }, - } - got := calculatePopulation(cities) - want := map[string]int64{ - "JPN": 300, - "USA": 300, - } - // 長さが0になっているかどうかを確認する - if len(got) != len(want) { - t.Errorf("calculatePopulation(%v) = %v, want %v", cities, got, want) - } - for k, v := range got { - // 国ごとの人口が一致しているかどうかを確認する - if v != want[k] { - t.Errorf("calculatePopulation(%v) = %v, want %v", cities, got, want) - } - } -} - -// #endregion multiple - -// #region null -// 空のデータ(`city.CountryCode.Valid = false`)が入っている場合 -func Test_calculatePopulation_null(t *testing.T) { - cities := []City{ - { - CountryCode: sql.NullString{ - String: "", - Valid: false, - }, - Population: sql.NullInt64{ - Int64: 100, - Valid: true, - }, - }, - { - CountryCode: sql.NullString{ - String: "JPN", - Valid: true, - }, - Population: sql.NullInt64{ - Int64: 200, - Valid: true, - }, - }, - } - got := calculatePopulation(cities) - want := map[string]int64{ - "JPN": 200, - } - // 長さが1になっているかどうかを確認する - if len(got) != len(want) { - t.Errorf("calculatePopulation(%v) = %v, want %v", cities, got, want) - } - // JPNの人口が100になっているかどうかを確認する - if got["JPN"] != want["JPN"] { - t.Errorf("calculatePopulation(%v) = %v, want %v", cities, got, want) - } -} - -// #endregion null diff --git a/docs/chapter2/section3/src/sum_population_by_country.rs b/docs/chapter2/section3/src/sum_population_by_country.rs new file mode 100644 index 00000000..b7e05ee7 --- /dev/null +++ b/docs/chapter2/section3/src/sum_population_by_country.rs @@ -0,0 +1,135 @@ +#region calculate +use std::collections::HashMap; + +//与えられた City のリストから国ごとの人口の和を計算する +pub fn sum_population_by_country(cities: Vec) -> HashMap { + let mut map = HashMap::new(); + for city in cities { + if city.country_code.is_empty() { + continue; + } + let entry = map.entry(city.country_code).or_insert(0); + *entry += city.population; + } + map +} +#endregion calculate + +// #[cfg(test)] 属性を追加したモジュールはテストモジュールとして扱われる +#[cfg(test)] +mod tests { + use super::{sum_population_by_country, City}; + use std::collections::HashMap; + + #region empty + #[test] + fn test_sum_population_by_country_empty() { + // ここにテストを追加する + let cities = vec![]; + let result = sum_population_by_country(cities); + assert!(result.is_empty()); + } + #endregion empty + + #region single + #[test] + fn test_sum_population_by_country_single() { + let cities = vec![ + City { + id: Some(1), + name: "Tokyo".to_string(), + country_code: "JPN".to_string(), + district: "Tokyo".to_string(), + population: 100, + }, + City { + id: Some(2), + name: "Osaka".to_string(), + country_code: "JPN".to_string(), + district: "Osaka".to_string(), + population: 200, + }, + ]; + + let mut expected = HashMap::new(); + expected.insert("JPN".to_string(), 300); + + let result = sum_population_by_country(cities); + + assert_eq!(result, expected); + } + #endregion single + + #region multiple + #[test] + fn test_sum_population_by_country_multiple() { + let cities = vec![ + City { + id: Some(1), + name: "Tokyo".to_string(), + country_code: "JPN".to_string(), + district: "Tokyo".to_string(), + population: 100, + }, + City { + id: Some(2), + name: "Osaka".to_string(), + country_code: "JPN".to_string(), + district: "Osaka".to_string(), + population: 200, + }, + City { + id: Some(3), + name: "New York".to_string(), + country_code: "USA".to_string(), + district: "New York".to_string(), + population: 300, + }, + City { + id: Some(4), + name: "Los Angeles".to_string(), + country_code: "USA".to_string(), + district: "California".to_string(), + population: 400, + }, + ]; + + let mut expected = HashMap::new(); + expected.insert("JPN".to_string(), 300); + expected.insert("USA".to_string(), 700); + + let result = sum_population_by_country(cities); + + assert_eq!(result, expected); + } + #endregion multiple + + #region empty_country_code + #[test] + fn test_sum_population_by_country_empty_country_code() { + let cities = vec![ + City { + id: Some(1), + name: "Tokyo".to_string(), + country_code: "JPN".to_string(), + district: "Tokyo".to_string(), + population: 100, + }, + City { + id: Some(2), + name: "Osaka".to_string(), + country_code: "".to_string(), + district: "Osaka".to_string(), + population: 200, + }, + ]; + + let mut expected = HashMap::new(); + expected.insert("JPN".to_string(), 100); + + let result = sum_population_by_country(cities); + + assert_eq!(result, expected); + } + #endregion empty_country_code +}