Strapを開発している、あげです🙋♂️
最近、GitHub Copilot の業務利用が許可されたので、使って嬉しかった場面を紹介します。
このコードの問題点わかりますか?
期待する動作
- anyObject の arrayValue と objectValue はどちらも必須
- arrayValue は配列である
- arrayValue は空ではない
- objectValue はオブジェクトである
実装(TypeScript)
const isValidObject = (anyObject: any): anyObject is ValidObject => { if (!anyObject.arrayValue || !anyObject.objectValue) return false; if (typeof anyObject.objectValue !== "object") return false; if ( !Array.isArray(anyObject.arrayValue) && Array.isArray(anyObject.objectValue) ) { return false; } if (anyObject.arrayValue.length <= 0) return false; return true; };
ぱっと見は問題なさそうなコードです。 export していない内部実装なので、テストは書かないことにしました。
export する必要が生じたので、テストコードを書くことに
しかし、外部から呼び出す必要が発生したので、export するように変更しました。こうなってくるとテストコードが必要になります。でも正直、export しただけなのに数十行のテストコードを書かないといけないなんて、めんどくさいですよね?それにモチベーションが低いので、自分でも意識しないうちにテストコードが雑になったり、網羅性が低くなる気がします。そこで今回は、GitHub Copilot を使ってテストコードを自動生成してみました。
以下は、GitHub Copilot に作ってもらったものを、わかりやすいように少し修正したものです。(Jest のテストコードです)
describe("isValidObject", () => { const subject = () => { return isValidObject(anyObject); }; let anyObject = {} as ValidObject; describe("anyObject is inValid", () => { describe("anyObject has no arrayValue", () => { beforeEach(() => { anyObject = { objectValue: {}, } as any; }); it("should return false", () => { expect(subject()).toBe(false); }); }); describe("anyObject has no objectValue", () => { beforeEach(() => { anyObject = { arrayValue: [], } as any; }); it("should return false", () => { expect(subject()).toBe(false); }); }); describe("anyObject.arrayValue is not array", () => { beforeEach(() => { anyObject = { arrayValue: {}, objectValue: {}, } as any; }); it("should return false", () => { expect(subject()).toBe(false); }); }); describe("anyObject.arrayValue is empty array", () => { beforeEach(() => { anyObject = { arrayValue: [], objectValue: {}, } as any; }); it("should return false", () => { expect(subject()).toBe(false); }); }); describe("anyObject.objectValue is not object", () => { beforeEach(() => { anyObject = { arrayValue: [], objectValue: [], } as any; }); it("should return false", () => { expect(subject()).toBe(false); }); }); }); });
このテストを実行した結果、 anyObject.arrayValue is not array
が通らないことが判明しました。どうやら問題があったようです。「 arrayValue
が array
じゃない」と言っているので、以下のコードに問題がありそうです。
if ( !Array.isArray(anyObject.arrayValue) && Array.isArray(anyObject.objectValue) ) { return false; }
少し複雑なので、分解して考えてみましょう。
arrayValue に object が入っていて array ではないので、以下のコードでは true が返ってきます。ここは問題ないです。
!Array.isArray(anyObject.arrayValue);
objectValue には object が入っていて array ではないので、以下のコードでは false が返ってきます。ここも問題ないです。
Array.isArray(anyObject.objectValue);
問題点は &&
です。両方 true でないと false が返ってきます。正しくは以下のように ||
にすべきです。
!Array.isArray(anyObject.arrayValue) || Array.isArray(anyObject.objectValue);
ただし、直感的でないと感じたので、少々愚直ですが以下のようにリファクタしました。
const isValidObject = (anyObject: any): anyObject is ValidObject => { if (!object.arrayValue) return false; if (!object.objectValue) return false; if (!Array.isArray(object.arrayValue)) return false; if (typeof anyObject.objectValue !== "object") return false; if (Array.isArray(object.objectValue)) return false; if (object.arrayValue.length <= 0) return false; return true; };
これでテストが通るようになりました ✨
まとめ
結局のところ、バグを起こすのは人間です。テストを書こうにも、テスト仕様が間違ってたり網羅性が低かったりします。その原因を詰めていくと、バイアスが生じていたり、書くときのモチベーションなどが関わっていると思います。人のアウトプットの品質を保つにはモチベーションというのはとても大事でバカにできません。今回に関してはテストコードを書くモチベーションが低かったので、品質を落とす可能性がありました。そこで GitHub Copilot のような AI 支援ツールを使うと、品質にムラがなくなるというお話でした 🙋♂️ ただし、「人間が読みやすい」という意味の可読性は、結局人の思考で監修しないといけません。リファクタの前後で、どちらも正しいコードですが、どちらがチームにとって、あるいは自分にとって見やすいか判断する必要があります。そういった意味で、これからは人間と AI の人馬一体が求められるのかもしれません。
尚、自分はこのような記事を書くモチベーションが低いので、品質が低くなりがちです。なので、4 割くらいは GitHub Copilot 使って執筆を手伝ってもらいました 😉
おしまい