ユニットテスト テスト駆動開発 継続的インテグレーション 【アジャイル開発】

この記事は次の書籍を参考にしています。

アジャイルサムライ(オーム社出版)

https://www.amazon.co.jp/アジャイルサムライ−達人開発者への道−-Jonathan-Rasmusson/dp/4274068560

アジャイルなソフトウェア開発手法:ユニットテスト

開発手法にはさまざまなものがあるが、その中でも「問答無用で実践するべきだ」と考えているアジャイルなソフトウェアエンジニアリングは4つある

これら4つのトピックには、それぞれ単独で一つの書籍を出せるだけの内容がある。

今回はユニットテストで確実に動くことがわかるソフトウェアのパーツを組み立てていく

ユニットテストの目的

例えば次のようなコードがあったとする。

このコードではトランプのシュミレーションを行うコードで、C#で書かれている。

public class Deck

{

    private readonly IList<Card> cards = new List<Card>();

    public Deck() {

        cards.Add(Card.TWO_OF_CLUBS);

        cards.Add(Card.THREE_OF_CLUBS);

        // .. remaining clubs

        cards.Add(Card.TWO_OF_DIAMONDS);

        cards.Add(Card.THREE_OF_DIAMONDS);

        // ... remaining diamonds

        cards.Add(Card.TWO_OF_SPADES);

        cards.Add(Card.THREE_OF_SPADES);

        // ... remaining spades

        cards.Add(Card.TWO_OF_HEARTS);

        cards.Add(Card.THREE_OF_HEARTS);

        // ... remaining diamonds

        // joker

        cards.Add(Card.JOKER); ///ここが今回のポイント

}

ある日品質改革チームから、このソフトについてのバグを受ける。

ブラックジャックのデッキにジョーカーは含んではいけない

この要件を聞き入れたあなたはコードからジョーカーを取り除いた。

だが後日、次のような連絡を受ける。

ブラックジャックのデッキにジョーカーが含まれているままだ

そんなはずはないとあなたは考えたが、コードには確かにジョーカーを加える処理が入っていた。

原因を調べると、今年入ったあなたが面倒を見ているインターン生が勝手にコードに追加してしまったのだ

このような事態を防ぐにはどのようにすればよかったのか?

結論から言うと、常に品質が担保されていることを保証するにはユニットテストが最も良い

ユニットテストを書くタイミング

あなたがバグを見つけた後は、すぐに直してはいけない。

このバグが再発するようなユニットテストを書く所からスタートするのが良い

なぜかというと、次の三つを担保できるからだ

  • バグの本質を理解している

  • 自信を持ってバグを修正したと言える

  • バグが再発したことを必ず検知できる

また、ユニットテスト

開発者がコードを変更するたびに書き直す

ユニットテストの書き方

ユニットテストはメソッドレベルの粒でかく。

小さいテストだ。

例えば以下のようなコードがユニットテストとなる。

[TestFixture]

public class DeckTest

{

    [Test]

    public void Verify_deck_contains_52_cards()

    {

        var deck = new Deck();

        Assert.AreEqual(52, deck.Count());

    }

この場面で担保しているのは

カードの枚数が52枚になる事

このように書いておくことで、ジョーカーは必ず含まれなくなる

また、その意図が他の開発者にも伝わるようになる。

ユニットテストの数

ユニットテストの数は、大抵のアジャイルのプロジェクトで何千の単位までは行かずとも、何百の単位まで膨れ上がる。

例えばlineは600種類以上のテストコードをKotlinで記述している。

https://logmi.jp/tech/articles/326703

テストケースはどうなっているのかというと、本当にいろいろなことをテストしなければいけません。

機能がいろいろあるというのもそうですが、ユーザーを作って、友だちを追加して、Botを友だち追加した時にメッセージを送ったとか、メッセージが来るとか、そういうチェックをして本当にBotがWebhookを受信できるかとか、

友だち追加のWebhookが届くからきちんと受信できているのかとかを確認して、メッセージをユーザーから受け取ったら、

Botはリプライトークンを受け取ってそれを返信するとか。そういうテストを全部書いていかなければいけません。



いろいろな機能があります。

プッシュ、リプライ、ブロードキャスト、特定のユーザーの属性、住んでいる国、年齢、性別など、

そういうものを絞って配信できる機能もあるので、そういうものもテストしなければなりません。



Webhookにも大量のタイプがあります。

動画、画像、音声をきちんと受信できるか。リッチメニューも全体に対してセットしたり、個人に対してセットしたりなど、

テストケースがたくさんあります。現在のテストケースは600個くらいあって、これらを管理しています。


危なっかしい箇所を全てテストする

「危なっかしいところを全てテスト」はエクストリーム・プログラミングマントラだ。

これは、システムを壊してしまう可能性が十分になるのなら、そこに対して自動化されたテストを書くべき、と言う愛初者としての心構えを表している。

テストコードを書くメリット

  • 素早いフィードバックが得られる

コードに変更を加えてユニットテストが失敗したら「どこかを壊してしまった」と言うことがすぐにわかる(リリース後に判明するなどはあり得ない)

ユニットテストが自動化されていれば、リリースのたびに作業を再テストしなくて良くなる。

簡単なテストの実行を自動化すれば、時間を大幅に節約できる。

そうやって浮かせた時間は、もっと複雑なテストのために使える。

  • デバック時間を大幅に削減できる

ユニットテストが失敗したら、どこを壊してしまったのかが正確にわかる。

問題が発生した箇所を特定するために、いちいちデバッガを起動して数千業ものステップ実行をしたり、ソースコードを検索したりする必要がなくなる。

  • 自信を持ってデプロイできる

自動化されたユニットテスト一式がソースを支えてくれると信じることができれば、安心してコードをリリースできる。

ユニットテストがあれば絶対確実と言うわけではないが、一連のテストのおかげで、もっと関心をもらうべき部分や、複雑な機能のテストに気持ちを向けられる。

テストが難しいコード

結論:テストが難しいコードは「ほぼほぼ設計に問題がある」と見て間違いない。

なぜなら単体テストが難しい場面では、ソースコード同士に激しい結合が存在すると言うことであるからだ。

モックフレームワークを使うことで、ランダムな結果を返すコードのモックテストを作ることもできる。

テスト条件の書き方

アジャイルのテスト条件は、基本的にはインデックスカードの裏に書く。

例えば、トランプのデッキを作成することを確認したい時は

  • 「トランプのカードが52枚存在すること」

  • 「それぞれの柄が13枚あること」

  • ジョーカーが含まれていないこと

と言うテスト条件が候補に上がる。

これらをコードに起こすと以下の通りだ。

[TestFixture]

public class DeckTest2

{

    [Test]

    public void Verify_deck_contains_52_cards()

    {

        var deck = new Deck();

        Assert.AreEqual(52, deck.Count());

    }

    [Test]

    public void Verify_deck_contains_thirteen_cards_for_each_suit()

    {

        var Deck = new Deck();

        Assert.AreEqual(13, Deck.NumberOfHearts());

        Assert.AreEqual(13, Deck.NumberOfClubs());

        Assert.AreEqual(13, Deck.NumberOfDiamonds());

        Assert.AreEqual(13, Deck.NumberOfSpades());

    }

    [Test]

    public void Verify_deck_contains_no_joker()

    {

        var Deck = new Deck();

        Assert.IsFalse(Deck.Contains(Card.JOKER));

    }

    [Test]

    public void Check_every_card_in_the_deck()

    {

        var Deck = new Deck();

        Assert.IsTrue(Deck.Contains(Card.TWO_OF_CLUBS));

        Assert.IsTrue(Deck.Contains(Card.TWO_OF_DIAMONDS));

        Assert.IsTrue(Deck.Contains(Card.TWO_OF_HEARTS));

        Assert.IsTrue(Deck.Contains(Card.TWO_OF_SPADES));

        Assert.IsTrue(Deck.Contains(Card.THREE_OF_CLUBS));

        Assert.IsTrue(Deck.Contains(Card.THREE_OF_DIAMONDS));

        Assert.IsTrue(Deck.Contains(Card.THREE_OF_HEARTS));

        Assert.IsTrue(Deck.Contains(Card.THREE_OF_SPADES));

        // the others

    }

ユニットテストまとめ

ユニットテストを書くことは、書くコードの量が2倍になることを意味する。

プログラミングが単なるタイピングであればその通りである。

しかしながら、変更を加えるたびに、システム全体のリグレッションテストを実行する手間を省くことができる。

今後何度も変更が発生することを想定すれば、コストの面でもユニットテストを書くことは理にかなっている

また、テストを書くことでコードの設計が向上すると言うメリットもある。

カバレッジについては100%は目指さなくても良い。

目安としては80~90%前後となるが、これはチームで決めるべき内容である。

テスト駆動開発とは何か?

コードを描こうとしたが、そもそもどこから手をつけたらいいのかすら検討がつかない、と言うときもあるかもしれない。

また、アルゴリズム自体は思い付いていても、いざコードに書き起こすときは納得のいく設計にならない。

そのような場合にこそ、テスト駆動開発は有用である。

テスト駆動開発はソフトウェア開発技法の一つだ。

ごく短いサイクルを回しながら、少しずつソフトウェアを設計していく。

テスト駆動開発の手順

https://service.shiftinc.jp/column/4654/

  1. 新しいコードを書く前に、まずは失敗するユニットテストを書いていく。

新しいコードの意図を先にテストで示しておく。

  1. テストが成功するようなコードを書く。実際の最終形態が思い描けるならば、最後まで書いてしまって良い。

逆にそこまでかけなくても、まずはテストに成功する程度の実装だけで構わない

  1. リファクタリング:実装を見直し、コードの負債がないようにしておく。重複や無駄を無くす。

このサイクルを、ユーザーストーリーを満たすまで繰り返していく

テスト駆動開発のポイント

  • 失敗するテストを一つ書き終わるまでは、新しいコードを一切書かない

ユーザーインターフェースはこの例外であるが、重要なのは本当に必要なコード以外は書いてはいけないと言う点である。

(基本的に、コードを書く行為は保守する対象が増える負債の増加と捉えた方が良い)

  • 危なっかしいい所を全てテストする

いかにも壊れてしまいそうな箇所や、特定の条件下で特殊な動きをする箇所は意図的にテストをする方が良いだろう。

  • テストで設計をする

テスト対象となるプロダクトコードがあたかも存在しているように書くのがポイントだ。

そのようにすることで、そのコードがなぜ存在するか、どのように使うのかが明確になる。

テスト駆動開発のサンプルコード

例えば、顧客情報を管理するjavaクラスを作りたいとする。

その場合のテストは「顧客のプロフィール情報を作成する」と言うコードを作ることで対応可能である。

[Test]

public void Create_Customer_Profile()

{

    // setup

    var manager = new CustomerProfileManager();

    // create a new customer profile

    var profile = new CustomerProfile("Scotty McLaren", "Hagis");

    // confirm it does not exist in the database

    Assert.IsFalse(manager.Exists(profile.Id));

    // add it

    int uniqueId = manager.Add(profile); // get id from database

    profile.Id = uniqueId;

    // confirm it's been added

    Assert.IsTrue(manager.Exists(uniqueId));

    // clean up

    manager.Remove(uniqueId);

}

ここまで書いたコードには設計書などは存在しない。

なぜならば、このテストコードそのものが設計書だからである

そしてあなたはこのコードが実際に動くように、目的としていた顧客完了クラスを作成する。

public class CustomerProfileManager

{

    public int Add(CustomerProfile profile){

        // pretend this code stored the profile

        // in the database, and returned a real id

        return 0;

    }

    public bool Exists(int id){

        // code to check if customer exists

    }

    public void Remove(int id){

        // code to remove a customer from the database

    }

}

実際にコードを動かすと、当然のことながらエラーが出る。

(内部の実装がまだまだ足りていないからだ)

そしてコードの一部分をさらに改善し、テストがOKを出すまで改善してゆく。

テストで複雑さを解消する

テストを描こうとすると、さまざまな複雑さと立ち会う必要がある。

例えば一つのメソッドを書くために、いくつ設計判断を下したかを見てみよう。

数えてみると、6箇所存在した。

設計を下したりトレードオフを考慮するのに一行だけで6箇所も存在している。

さらに忘れてはいけないのは、コードは通常100~1000行も存在するため、6の10000条以上の意思決定が発生することになる。

ところが、テスト駆動開発を導入することで一度に考えるべきスコープは一つの関数内部に治る

まずテストを描き、それが失敗することを確認してから実装するというTDDの原則に習うことで、日々のコーディングに対する複雑さに立ち向かうことができるのである。

多数の意思決定を行う中でも、一度に少ないコードを相手にすることテスト駆動開発で肝心である。

継続的インテグレーション

CIツールとは?

開発現場に入ると、よくCIツールだのCDツールだのが聞こえる。

これは何かといえば、継続的インテグレーションツール」のことだ

CIは「Continuous integration」の略で、以下の意味がある

継続的インテグレーションは、開発者が自分のコード変更を定期的にセントラルリポジトリにマージし、その後に自動化されたビルドとテストを実行する DevOps ソフトウェア開発の手法です。

継続的インテグレーションという用語が最もよく使われるのは、ソフトウェアのリリースプロセスのビルド段階または統合段階を指す場合で、オートメーションの要素 (CI やビルドサービスなど) と啓発の要素 (頻繁に統合する必要性を学習することなど) の両方が含まれます。

継続的インテグレーションの主な目的は、バグを早期に発見して対処すること、ソフトウェアの品質を高めること、そしてソフトウェアの更新を検証してリリースするためにかかる時間を短縮することです。

https://aws.amazon.com/jp/devops/continuous-integration/

アジャイル 継続的インテグレーションとは?

まとめると、早い段階でバグを退治できるソフトウェアのリリースツールのことである。

例えば、

  • 後一時間で今までの開発資材をビルドして、バイナリをサーバーに配置して、デモの準備を行わなければならないとき。

あるいは、

  • 何かしらのインシデントが発生したのでリグレッションを行わないといけないとき。

こんな場面で役に立つ。

なぜビルドツールが必要なのか?

例えばソフトウェアのデプロイでは以下のような失敗がありがちである。

  • ヒューマンエラー

  • タイプミス

  • バグ

  • 動作エラー

  • 設定ファイルの誤り

  • 開発環境

こうしたデプロイの失敗要因をできるだけなくす。

なくすことができなくともせめて把握することができるようにしたい。

リリースに備える文化

そもそもリリースを想定した開発ができていなければCIツールは意味をなさない。

エクストリームプログラミングにも「プロジェクトの本番は初日から始まっている」と言う格言がある。

プロジェクトのコードは全て本番環境に置かれてるかのように扱うんだ。

アジャイルは繰り返しのリリースを好む。

何よりも「開発している時間よりリリース可能な時間が多い」と言う事実がチームに自信を与える。

プロジェクトのハンドルも大胆に切ることができるだろう。

継続的インテグレーションツールのセットアップ

継続的インテグレーションツールの準備は具体的には次の4つが必要だ。

ソースコードリポジトリはGitのことだと思っていい(Subversionでもいいかもしれないが...)

アジャイル+CIツールの修正の手順

自分の作業の前に、portalポジ取りから最新のコードを取得する。

コードぼビルドしてから、綺麗な状態であることを確認して作業開始。

  • テストを変更する

本体の前にソースを変更するのは、目的を意識した改修を行い、変更箇所を最小限にするためだ。

  • 変更を加える

  • テストを実行する

自分の習性がコードベースを壊していないことを確認するために、テストを実行する。

テストは全て成功するはずだ。

  • 作業中に発生した更新差分を取得する

きちんと作業できたと確信を持てたら、もう一度リポジトリから最新版を取得する。

作業中に他のメンバーがリポジトリを更新しているかもしれないからだ。

  • 再度テストを実行する

他のメンバーの変更をマージした状態でもきちんと動くことを確認するために、もう一度テストを実行する。

  • チェックイン

全てうまく行った。

ビルドできたし、テストも全部パスした。

安心してチェックインしよう。

ビルドを大事にするときにするべきこと

チェックイン手順では、するべきこととするべきでないことがある。

  • 手元のソースが最新版か?

  • テストを全て実行する

  • 定期的にチェックインする

  • ビルドが壊れたら直す

すべきでないこと

  • ビルドを壊す

  • ビルドが壊れていないのにチェックインする

  • 失敗するテストをコメントアウトする

これらの行為は負債を積み重ねるだけで、「完了」とは呼べないものだ。

ビルドの自動化

ビルド自動化ユーティリティを活用すると、単純で何度も繰り返し実行するタスクを自動化できる。

ツールを使用すると、指定したゴールに到達するために、正しく、指定した順序で、各タスクを実行するプロセスを計算してくれる。

今時のプログラミング言語であれば、ビルドを自動化するフレームワークが存在するはずだ(JavaならAntが、RubyならRakeがある)

アジャイルにおける継続的インテグレーション

title:アジャイルにおける継続的インテグレーション

description:ビルド自動化ユーティリティを活用すると、単純で何度も繰り返し実行するタスクを自動化できる。今時のプログラミング言語であれば、ビルドを自動化するフレームワークが存在するはずだ(JavaならAntが、RubyならRakeがある)

img:https://eh-career.com/image/article_hub/40/41/140_01.jpg

category_script:True

page:https://minegishirei.hatenablog.com/entry/2024/04/28/170657