Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RankChartのutils.js内の処理のテストを追加 #868

Merged
merged 24 commits into from
Feb 6, 2025

Conversation

ryoppippi
Copy link
Member

昨日のリファクタリングが合ってるか確証が取れなかったのでテストを追加

  1. vitestの導入
  2. 昨日の変更を全てrevert
  3. テストを追加
  4. 変更のreapply
  5. 新しい関数に合わせてテストを修正&追加。出力が変わらないことを確認

@ryoppippi ryoppippi force-pushed the feature/raking-test branch from c71dc41 to db3af87 Compare February 5, 2025 15:48
@ryoppippi
Copy link
Member Author

ryoppippi commented Feb 5, 2025

お、vitestを導入するかどうかって話が出てたのか

#743 (comment)

まあ、Astroが推奨してるしいいのではという感想ですが、いかがでしょう。
実際ここ最近の風潮を見ているとvitest, node test, Bun testの3択ですが、AstroがViteベースなので素直にvitest使っておくのが無難だと思われますね
https://docs.astro.build/en/guides/testing/

Comment on lines 8 to 20
beforeEach(() => {
/** テストの結果が変わらないように、最初の50記事のみを取得 */
const PUBLISHED_FIRST_50_ARTICLES = getArticles({
isPublished: true,
}).slice(0, 50);
RANKING = utils.getRanking(PUBLISHED_FIRST_50_ARTICLES);
TOP_10_RANKING = utils.getTopNRanking(RANKING, 10);
Object.freeze(TOP_10_RANKING); // Ensure that the array is not mutated
});

it("getRanking", () => {
expect(RANKING).toMatchSnapshot(); // Snapshotが大きいので別途ファイルに保存
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このテストのやり方だと、今後ロジックへの変更が行われた時に、スナップショットを更新するだけになるので、こういったケースではスナップショットテストは向いていないと思います。
とりあえず getRanking の実装コードにあるコメントの動作を確認できる最小のデータとケースを作ってテストするのが良いと思いました。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

とりあえず getRanking だけテストして、他は別になくてもいいんじゃないかなという気がしますね。

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なるほど、snapshotじゃなくて

  • ちゃんとソートされているか
  • userのランクが適切かどうか
    みたいなものを個別にテストした方がいいってことですね

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ちなみに他の関数についてもsnapshotじゃない方がいいんでしょうか

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なるほど、とりあえずgetRankingのテスト書き直します

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

42b0dd4
ロジックを一つずつテストに落としてみました

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

他もsnapshot使うのは適切でないからごそっと消すべき?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getData と getOptions はたいしたロジックもないので snapshot でいいんじゃないかなという印象ですね。
ただ、describe ブロックは分けたほうがいいと思います。
特に beforeEach で初期化したデータをすべての関数のテストで利用しているのは、変化にとても弱いのでよくないです。

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なるほど、一つ一つ分けてロジック書きますか

@ryoppippi ryoppippi marked this pull request as draft February 5, 2025 16:13
`getRanking`にコメントとして結果の変数が満たしうる条件を残しているので、それらが満たされているかどうかをチェック
@ryoppippi ryoppippi force-pushed the feature/raking-test branch from 8321faa to 42b0dd4 Compare February 5, 2025 16:34
@ryoppippi ryoppippi marked this pull request as ready for review February 5, 2025 16:40
@ryoppippi ryoppippi requested a review from tomoya February 5, 2025 16:40
Comment on lines 24 to 51
/** ランクが1から始まっているか */
expect(ranking[0].rank).toBe(1);

/** 同じ記事数の場合は同じランクになるか.ソートされているか. */
Array.from({ length: 50 }).forEach((_, i) => {
const current = ranking[i];
const prev = ranking[i - 1];

/** どちらかが存在しない場合はスキップ */
if (!(current && prev)) {
return;
}

if (prev && prev.articleCount === current.articleCount) {
expect(current.rank).toBe(prev.rank);
} else {
expect(current.rank).toBeGreaterThan(prev.rank);
}
});

/** 記事数の合計が50であるか */
const totalArticleCount = ranking.reduce(
(acc, { articleCount }) => acc + articleCount,
0,
);

expect(totalArticleCount).toBe(50);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

同じ it ですべてテストせずに、 expect の数だけ it 分けたほうがテストケースの目的が単一になってよいです。

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

なるほど、getRanking単体でdescribeを作ってitで分けるべきですね

Comment on lines 28 to 42
Array.from({ length: 50 }).forEach((_, i) => {
const current = ranking[i];
const prev = ranking[i - 1];

/** どちらかが存在しない場合はスキップ */
if (!(current && prev)) {
return;
}

if (prev && prev.articleCount === current.articleCount) {
expect(current.rank).toBe(prev.rank);
} else {
expect(current.rank).toBeGreaterThan(prev.rank);
}
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

テストの中でループと条件分岐はバッドブラクティスです。

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この場合、どうすればいいんでしょう。
条件分岐なしにテストを書くのが難しそうなので気になりました

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

わかりました。具体的なもので作り直します

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ケースにそった input を用意して expect(getRanking(input)).toEqual(expected) のようにテストすれば十分だと思います。
この場合は、 同じ記事数の場合は同じランクになるソートされている の2種類のinputを用意すれば大丈夫なはずです。

@ryoppippi ryoppippi requested a review from tomoya February 5, 2025 17:55
@ryoppippi
Copy link
Member Author

@tomoya 色々直しました!

  • ダミーを使う
  • 結果をベタ書きする

Comment on lines 3 to 13
const ARTICLES = [
{ githubUser: "alice" },
{ githubUser: "alice" },
{ githubUser: "alice" },
{ githubUser: "bob" },
{ githubUser: "bob" },
{ githubUser: "bob" },
{ githubUser: "charlie" },
{ githubUser: "charlie" },
{ githubUser: "david" },
];
Copy link

@tomoya tomoya Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

だいぶいい感じになってきましたね。
それぞれのテストケースが何を目的としているのかわかるようになってきて、可読性も変更耐性も上がってきた感じがします。

次は、テストデータがこの ARTICLES ひとつですべてをまかなっているのを変えましょう。
いまだと、何か機能を追加したりするとき、テストデータを変更しなければ実現できない場合、このARTICLES の値を変更すると、その機能追加とは関係のないテストケースも失敗するようになるはずです(例:数が変わるといくつかのテストが落ちる)。

ですので、基本的にはそれぞれのテツトケースでテストデータを用意するようにしましょう。その際に、必要最小限のテストデータにすることで、データからテストの意図が読み取れるようになり、より可読性が向上します。

具体的には最初のテストのこのコードは、

    const ranking = utils.getRanking(ARTICLES);

    it("should start with rank 1", () => {
      expect(ranking[0].rank).toBe(1);
    });

こんな感じで、テストデータの順番から正しくランク順に並んでいることが示されればよい(数の重複は別のテストケースで担保するので、このテストではそういうデータを用いない)というわけです。

  it("should start with rank 1", () => {
    expect(utils.getRanking([
      { githubUser: "alice" },
      { githubUser: "bob" },
      { githubUser: "bob" },
      { githubUser: "bob" },
      { githubUser: "charlie" },
      { githubUser: "charlie" },
    ])).toEqual([
      {
        rank: 1,
        user: "bob",
        articleCount: 3,
      },
      {
        rank: 2,
        user: "charlie",
        articleCount: 2,
      },
      {
        rank: 3,
        user: "alice",
        articleCount: 1,
      }
    ]);
  });

考え方は最小構成と同じで、再現に必要なもの以外をすべて排除すること。

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

修正しました!レビューお願いします!

@ryoppippi ryoppippi requested a review from tomoya February 6, 2025 12:20
@tomoya
Copy link

tomoya commented Feb 6, 2025

これで各テスト同士の依存がなくなったので、次はテストの質を改善するフェーズですかね。

Comment on lines 5 to 15
it("should start with rank 1", () => {
const ranking = utils.getRanking([
{ githubUser: "alice" },
{ githubUser: "bob" },
{ githubUser: "bob" },
{ githubUser: "bob" },
{ githubUser: "charlie" },
{ githubUser: "charlie" },
]);
expect(ranking[0].rank).toBe(1);
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

このテストだと、配列の1件目が rank 1 にはなってるけど、user などの値はが何でも構わないというテストになるわけですが、それが本当に担保するべき仕様なのでしょうか?
ちなみに、そうであるならば、テストデータの配列は1件でも十分になります。

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ユーザーの値はなんでもいいですね。なので、一件でも十分だと思われます

Comment on lines 17 to 52
it("should have correct ranking", () => {
const ranking = utils.getRanking([
{ githubUser: "alice" },
{ githubUser: "alice" },
{ githubUser: "alice" },
{ githubUser: "bob" },
{ githubUser: "bob" },
{ githubUser: "bob" },
{ githubUser: "charlie" },
{ githubUser: "charlie" },
{ githubUser: "david" },
]);
expect(ranking[0]).toEqual({
rank: 1,
user: "alice",
articleCount: 3,
});
expect(ranking[1]).toEqual({
rank: 1,
user: "bob",
articleCount: 3,
});
expect(ranking[0].rank).toBe(ranking[1].rank);

expect(ranking[2]).toEqual({
rank: 3,
user: "charlie",
articleCount: 2,
});

expect(ranking[3]).toEqual({
rank: 4,
user: "david",
articleCount: 1,
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

correctとか、日本語だとよく「正しい」とか書いたりしますが、何が正しいのかが読んでも理解できないので、テストケースの説明としては不十分になります。

あと、1回の実行結果の expect を複数に分ける必要はなくて、配列ぜんぶを比較すればいいはずです。ぜんぶ比較しないのであれば、その理由が欲しいです。

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

そうですね、一つのexpectにするのが適切ですね

vim-jp#868 (comment)
- 説明に具体的な動作を追加
- 複数のexpectを一つにまとめ、配列全体で比較
@ryoppippi ryoppippi force-pushed the feature/raking-test branch from 1359a68 to 0d89882 Compare February 6, 2025 13:28
@ryoppippi
Copy link
Member Author

ryoppippi commented Feb 6, 2025

修正しました!

お願いします!

@@ -4,7 +4,7 @@ import type { Article, Ranking } from "./types";
/**
* ランキングを計算し、1つの配列にまとめる
*/
export function getRanking(articles: Article[]): Ranking[] {
export function getRanking(articles: Pick<Article, "githubUser">[]): Ranking[] {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

テストを書くと、必要なのは githubUserだけだとわかるので、余計な引数を要求しなくていいことがわかりましたね。

expect(ranking[0].rank).toBe(1);
});

it("should assign same rank for ties and skip next ranks appropriately", () => {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

同順位がない場合のテストケースもあった方がいいと思いました。
そうすれば、仮にどう順位のロジックに変更があったとしても、そうじゃない場合は変わっていないということが担保されるためです。

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ちなみに今の同順位の仕様としては、引数配列の登場順という仕様が暗黙としてあり、名前順に変えるなどの変更を加えようとした時に、2つのテストケースがいい感じに機能します。

Copy link
Member Author

@ryoppippi ryoppippi Feb 6, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ちなみに登場順に並んでいる仕様に関してはテストした方がいいんでしょうか
例えば

      const articles = [
        { githubUser: "alice" },
        { githubUser: "alice" },
        { githubUser: "bob" },
        { githubUser: "bob" },
      ];

      const articles = [
        { githubUser: "bob" },
        { githubUser: "bob" },
        { githubUser: "alice" },
        { githubUser: "alice" },
      ];

の2パターン作るとか

@ryoppippi ryoppippi requested a review from tomoya February 6, 2025 14:00
@ryoppippi
Copy link
Member Author

重複あるなしを分けてみました

Copy link

@tomoya tomoya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

これで良さそうですね!

@ryoppippi
Copy link
Member Author

ありがとうございます!
他の皆さんもマージして良さそうですか?

Copy link
Member

@thinca thinca left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

細かいことを言うとコミットログがだいぶとっ散らかってしまっているなと思いましたが、細けぇこたぁいいんだよ、というのもあるので Approve しておきます。

以下は私見なので別の意見もあるかも。

  • Revert はマージコミットに対してやればよい
  • レビューの指摘とその修正をいちいちコミットに残す必要はない。後から見てわかりやすいか、Revert しやすいかの単位になっているかの方が重要

@ryoppippi
Copy link
Member Author

マージコミットに対してrevertは本当にそうですね

指摘をコミットに残しておくと後から振り返りやすいと思ったのですがどうなんでしょう

もはや汚すぎてsquash mergeしたい

@ryoppippi
Copy link
Member Author

とりあえずsquash mergeします

@ryoppippi ryoppippi merged commit 8851758 into vim-jp:main Feb 6, 2025
2 checks passed
@ryoppippi ryoppippi deleted the feature/raking-test branch February 6, 2025 17:03
@ryoppippi ryoppippi restored the feature/raking-test branch February 6, 2025 17:03
@vim-jp vim-jp locked as resolved and limited conversation to collaborators Feb 8, 2025
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants