良い単位テストを書く初心者のための6つのチップス


オリジナルポストのために私のブログを訪問してください:6 Tips for Beginners to Write Good Unit Tests
TLR
ユニットテストは、アプリケーションの品質を保護する基本的なコンポーネントの一つです.それは、書くことにほとんど努力を要しませんが、コードの正しさを検証する期間で多くの値を生産します.
どのような単体テストについて話し、多くの記事があり、なぜそれが重要なユニットテストを書くために必要です.このブログ記事では、私はあなたがすでにそれらに気づいていると仮定するので、私はこれらについて話をしません.

簡単にテストするためにあなたの機能を短くする
私はプログラミングを始めた時を覚えています.私が働くことができる限り、私は満足した.しかし、実際には、長い長い手順を持つこのような関数は、関数がテストするのは難しい結果になる可能性があります.
ちょっと想像してください、条件チェックの何十もの機能と他の多くのブロックがあなたのコードをラザニアに変えるならば.あなたの関数から多くの可能な結果があることができます.この関数をテストするには、条件のすべての分岐をテストするために20または30単位のテストを記述する必要があります.それはちょうど超退屈な音!
// Codes
function superLongFunction() {
  if (conditionA) {
    // A bunch of operations
    // ...
    // ...
    if(conditionB) {
      // A bunch of operations
      // ...
      // ...
      return;
    }
    // A bunch of operations
    // ...
    // ...
  } else if (conditionC) {
    someList.forEach(item => {
      if (item.flag) {
        // A bunch operations
        // ...
        // ...
      }

      if(item.flag2) {
        // A bunch of operations
        // ...
        // ...
      }
    });
  }
  // A bunch of operations
  // ...
  // ...
}

// Tests
describe('superLongFunction' () => {
  it('should ... condition A', () => { /* ... */ })
  it('should ... condition A', () => { /* ... */ })
  it('should ... condition A', () => { /* ... */ })
  it('should ... condition A', () => { /* ... */ })
  it('should ... condition A', () => { /* ... */ })
  it('should ... condition A', () => { /* ... */ })
  it('should ... condition A', () => { /* ... */ })
  it('should ... condition A', () => { /* ... */ })
  it('should ... condition B', () => { /* ... */ })
  it('should ... condition B', () => { /* ... */ })
  it('should ... condition B', () => { /* ... */ })
  it('should ... condition B', () => { /* ... */ })
  it('should ... condition B', () => { /* ... */ })
  it('should ... condition C', () => { /* ... */ })
  it('should ... condition C', () => { /* ... */ })
  it('should ... condition C', () => { /* ... */ })
  it('should ... condition C', () => { /* ... */ })
  it('should ... condition C', () => { /* ... */ })
  it('should ... condition C', () => { /* ... */ })
  it('should ... condition Others', () => { /* ... */ })
  it('should ... condition Others', () => { /* ... */ })
  it('should ... condition Others', () => { /* ... */ })
  it('should ... condition Others', () => { /* ... */ })
});
さらに悪いことは、いくつかの論理を更新したり、将来的に機能を再評価する場合は、それはあなたのように多くの単体テストを更新するための真の災害になることができます!
どのように改善する必要がありますか?さて、それはちょうど小さな機能の複数に超巨大な機能を壊すことによって簡単です.このようにして、より小さなスコープのいくつかの小さなスコープに大きなスコープを有効にします.ユニットテストのすべてのセットだけでは、特定の機能に焦点を当てているので、他の関数の変更を気にする必要はありません!
// Codes
function shortFunction() {
  if (conditionA) {
    doA();
    checkB();
    doRestOfA();
  } else if (conditionC) {
    someList.forEach(item => {
      doC(item);
    });
  }
  doOthers();
}

function checkB() {
  if (conditionB) {
    doB();
  }
  doA();
}

function doC(item) {
  if (item.flag) {
    itemDo1();
  }

  if(item.flag2) {
    itemDo2();
  }
}

function doA() { /* A bunch of operations */ }
function doRestOfA() { /* A bunch of operations */ }
function doB() { /* A bunch of operations */ }
function doOthers() { /* A bunch of operations */ }
function itemDo1() { /* A bunch of operations */ }
function itemDo2() { /* A bunch of operations */ }

// Tests
describe('shortFunction' () => {
  it('should ...', () => { /* ... */ })
  it('should ...', () => { /* ... */ })
  it('should ...', () => { /* ... */ })
  it('should ...', () => { /* ... */ })
});

describe('doA', () => {
  it('should ...', () => { /* ... */ })
  it('should ...', () => { /* ... */ })
});

describe('doRestOfA', () => {
  it('should ...', () => { /* ... */ })
  it('should ...', () => { /* ... */ })
});

describe('doB', () => {
  it('should ...', () => { /* ... */ })
  it('should ...', () => { /* ... */ })
  it('should ...', () => { /* ... */ })
});

describe('doOthers', () => {
  it('should ...', () => { /* ... */ })
  it('should ...', () => { /* ... */ })
  it('should ...', () => { /* ... */ })
  it('should ...', () => { /* ... */ })
});

describe('itemDo1', () => {
  it('should ...', () => { /* ... */ })
  it('should ...', () => { /* ... */ })
});

describe('itemDo2', () => {
  it('should ...', () => { /* ... */ })
  it('should ...', () => { /* ... */ })
});

2 .悲しい道を忘れないでください
時々、我々は我々が我々がユーザーが我々がすると仮定する正確にすると信じているように、我々のアプリケーションについて楽観的である傾向があります.しかし、実際には、常にあなたのコードやユーザー(笑)から驚きがあります.
単体テストでは、幸せな経路を気にするだけでなく、悲しい道を考えるべきです.
では、幸せなパスと悲しいパスは何ですか?
それはちょうどコインの2面のようです.があるならばif , そうしたら、少なくとも2つのテストケースがあるでしょう.
// Codes
function check() {
  if (flag) {
    // do something
  } else {
    // do something
  }
}

// Tests
describe('check', () => {
  it('should ... when flag is true', () => { /** some test codes **/ })
  it('should ... when flag is false', () => { /** some test codes **/ })
});
または、関数が何らかのエラーを投げることが可能であれば、関数が正常に動作し、関数がエラーをスローしている場合に状況が発生します.
function haveATry() {
  try {
    // do something
  } catch {
    // handle error
  }
}

// Tests
describe('check', () => {
  it('should ...', () => { /** some test codes **/ })
  it('should ... when error is thrown', () => { /** some test codes **/ })
});
我々がテストを書いているとき、我々が常に幸せな経路と嘆かわしい経路の両方をテストすることについて思い出させるならば、予期しない状況と我々が優雅にそれらのケースを扱う方法を考慮することも強制されます.最終的に、我々は可能な限り堅牢なアプリケーションを構築することができます.

3 .テストはダムです
我々が開発をしているとき、スマートコードがおそらく我々のコード読みやすさ、柔軟性または拡張性を改善するかもしれないので、実装でスマートにしようとします.
しかし、テストに関しては、我々のテストの中で論理的な条件を書いていないという点で、代わりにダムになるべきです.
私はループのためにいくつかを見ました、そして、他のものがテストでブロックするならば
describe('some test suite', () => {
  it('should ...', () => {
    // Some testing codes...

    for (let i = 0; i < list.length; i++) {
      if (someCondition) {
        expect(someVariable).toBe(someValueA);
      } else if (someOtherCondition) {
        expect(someVariable).toBe(someValueB);
      } else {
        expect(someVariable).toBe(someValueC);
      }
    }

    // Some testing codes...
  });
});
さて、私たちがテストをしている理由の1つは、私たちが人間であり、論理、特に複雑な論理を書くとき、私たちは間違いをするからです.
そして今、テストでは、複雑な論理を書いています.そして、悲しいことは、我々が我々のテスト(lol)をテストするより多くのテストを持っていないということです.
したがって、あなたのテストをダムのままにし、テストで“スマート”コードを記述しないようにしてください.代わりに
describe('some test suite', () => {
  it('should ... when someCondition is true', () => {
    // Some testing codes...
    expect(someVariable).toBe(someValueA);
    // Some testing codes...
  });

  it('should ... when someOtherCondition is true', () => {
    // Some testing codes...
    expect(someVariable).toBe(someValueB);
    // Some testing codes...
  });

  it('should ... when both someCondition and someOtherCondition are false', () => {
    // Some testing codes...
    expect(someVariable).toBe(someVariable);
    // Some testing codes...
  });
});
または、データ駆動テストを試してみてください.

依存性のためのmock関数
近代的なアプリケーションを構築するときは、必然的に外部ライブラリやプラグインなどの依存関係を処理する必要があります.それから、あなた自身の機能の中で彼らの機能を呼びます、そして、あなたはそれをテストしなければなりません.
問題は、どのように我々はユニットテストでそれらに対処するつもりですか?
以下のコードを見てください.
// Codes
function greetings() {
  const today = dayjs();
  const hour = today.hour();

  if (hour >= 5 && hour < 12) {
    return 'morning';
  }

  if (hour >= 12 && hour < 18) {
    return 'afternoon';
  }

  if (hour >= 18 && hour < 22) {
    return 'evening';
  }

  return 'midnight';
}

// Tests
describe(() => {
  expect(greetings()).toBe('afternoon');
})
あなたは、そのようなテストが信頼できて、安定していると思いますか?あなたが午後3時にテストを実行するならば、あなたのテストはちょうど罰金です、そして、あなたは午後のお茶を楽しむことができます、しかし、午後7時にテストを走らせるならば、あなたのテストは壊れそうです、そして、あなたは残業(lol)を働かなければなりません.
したがって、DAVJSと呼ばれる外部ライブラリに依存するので、そのようなテストは安定していません.どうやって解決するの?
我々は、それが我々がテストしたい値を返すよう強制することによって、Dayjsの行動を模擬するつもりです.私たちはjest.fn() or sinon.stub() 使用しているテストフレームワークによって異なります.
// Tests 
jest.mock("dayjs");

describe("greetings", () => {
  const mockDayjsHour = jest.fn();

  beforeAll(() => {
    dayjs.mockImplementation(() => ({
      hour: mockDayjsHour,
    }));
  });

  afterEach(() => {
    jest.clearAllMocks();
  });

  it("should return morning when the time is 5:00", () => {
    mockDayjsHour.mockImplementation(() => 5);
    expect(greetings()).toBe("morning");
  });

  it("should return morning when the time is 12:00", () => {
    mockDayjsHour.mockImplementation(() => 12);
    expect(greetings()).toBe("afternoon");
  });

  it("should return morning when the time is 18:00", () => {
    mockDayjsHour.mockImplementation(() => 18);
    expect(greetings()).toBe("evening");
  });
});
コードスニペットから見ることができるように、各テストでは、我々はモックdayjs().hour() 異なる値を返すには、そのテストで保証できるように、返される時間は、実際の時刻によって変化しません.そして、ここで決定された時間を与えられた関数によって返される文字列をテストすることができます.

利用試験法
境界試験は入力を値の範囲とする関数をテストする非常に有用な手法である.テストの対象となる値の範囲は、前の例では、0から23までの範囲であり、範囲内で値をランダムに拾うのではなく、値を決定するために境界テストアプローチを使用することができます.
例えば、この機能から合計4つの可能な結果があります"morning" , "afternoon" , "evening" and "midnight" , どれも、その上限時間と下限の両方の上限を持つ.
挨拶
範囲
下限
上界
ミッドナイト
[ 0 - 5 ]
0
4

[ 5 - 12 ]
5
11
午後
[ 12 - 18 ]
12
17
夕方
[ 18 - 23 ]
18
21
ミッドナイト
[ 23 - 24 ]
22
23
この表から、私たちはそれを知ることができます"afternoon" は12と17です
  • 12と17の間の数字をテストする必要はありません"afternoon" 12と17の両方が通るならば、通過してください.
  • 12と17(<12または17)の外のどんな価値も、確かにありません"afternoon"
  • したがって、テストを次のように更新できます.
    jest.mock("dayjs");
    
    describe("greetings", () => {
      const mockDayjsHour = jest.fn();
    
      beforeAll(() => {
        dayjs.mockImplementation(() => ({
          hour: mockDayjsHour,
        }));
      });
    
      afterEach(() => {
        jest.clearAllMocks();
      });
    
      it("should return morning when the time is 5:00", () => {
        mockDayjsHour.mockImplementation(() => 5);
        expect(greetings()).toBe("morning");
      });
    
      it("should return morning when the time is 11:00", () => {
        mockDayjsHour.mockImplementation(() => 11);
        expect(greetings()).toBe("morning");
      });
    
      it("should return morning when the time is 12:00", () => {
        mockDayjsHour.mockImplementation(() => 12);
        expect(greetings()).toBe("afternoon");
      });
    
      it("should return morning when the time is 17:00", () => {
        mockDayjsHour.mockImplementation(() => 17);
        expect(greetings()).toBe("afternoon");
      });
    
      it("should return morning when the time is 18:00", () => {
        mockDayjsHour.mockImplementation(() => 18);
        expect(greetings()).toBe("evening");
      });
    
      it("should return morning when the time is 22:00", () => {
        mockDayjsHour.mockImplementation(() => 21);
        expect(greetings()).toBe("evening");
      });
    
      it("should return midnight when the time is 22:00", () => {
        mockDayjsHour.mockImplementation(() => 22);
        expect(greetings()).toBe("midnight");
      });
    
      it("should return midnight when the time is 23:00", () => {
        mockDayjsHour.mockImplementation(() => 23);
        expect(greetings()).toBe("midnight");
      });
    
      it("should return midnight when the time is 00:00", () => {
        mockDayjsHour.mockImplementation(() => 0);
        expect(greetings()).toBe("midnight");
      });
    
      it("should return midnight when the time is 4:00", () => {
        mockDayjsHour.mockImplementation(() => 4);
        expect(greetings()).toBe("midnight");
      });
    });
    

    6 .データ駆動テストの使用
    前の例では、この1つの特定の関数をテストするための冗長コードが多すぎます.最適化する方法はありますか?
    はい、あります.データ駆動テストを使用して、異なる結果を持つさまざまな条件をテストできます.つまり、テストのロジックが変更されず、変更されたものはテストデータと結果だけです.Jestでは、使用することができますit.each 目的を達成する機能.
    jest.mock("dayjs");
    
    describe("greetings", () => {
      const mockDayjsHour = jest.fn();
    
      beforeAll(() => {
        dayjs.mockImplementation(() => ({
          hour: mockDayjsHour,
        }));
      });
    
      afterEach(() => {
        jest.clearAllMocks();
      });
    
      it.each`
        hour  | greeting
        ${5}  | ${'morning'}
        ${11} | ${'morning'}
        ${12} | ${'afternoon'}
        ${17} | ${'afternoon'}
        ${18} | ${'evening'}
        ${21} | ${'evening'}
        ${22} | ${'midnight'}
        ${23} | ${'midnight'}
        ${0}  | ${'midnight'}
        ${4}  | ${'midnight'}
      `('should return $greeting when the time is $hour:00', ({hour, greeting}) => {
        mockDayjsHour.mockImplementation(() => hour);
        expect(greetings()).toBe(greeting);
      })
    });
    
    インit.each , 上のコードのような文字列リテラル、あるいはネストした配列としてテーブルを渡すことができますlike this . 条件と期待される結果を提供することによって、テストに論理の同じ部分を再利用することができます.また、ループの直接使用よりも読みやすいです.

    コードをデモ
    見ることができるthis Gist これらの単体テストのデモコードは.