くらしのマーケット開発ブログ

「くらしのマーケット」を運営する、みんなのマーケット株式会社のテックブログ。積極採用中です。

コードをいじらずにPythonアプリケーションのメモリリークを検証する方法

こんにちは、バックエンドエンジニア・SREのカーキです。 最近くらしのマーケットのシステムで一部の Python アプリケーションにあったメモリリークを検証した時に学んだ検証方法について書きたいと思います。

メモリリークとは?

メモリリークはプログラムが確保したメモリを使用後に解放されず、プログラムのメモリ使用率がどんどん上がり続ける現象です。メモリリークがあると該当のプログラムがシステムのすべてのメモリを使い切って、システムがクラッシュする可能性があるので少し面倒なバグです。

リークの再現

弊社では現在Python 3.5.0を使っていますが、便宜のため以下のようにPython2系のdel関数の落とし穴を使ってメモリリークを再現します。

import time

class MyLeakyObject(object):
    def __init__(self, parent=None):
        self.parent = parent
        self.children = []
        self.value = 'x' * 100000

    def __del__(self):
        print("deleting")

def main():
    for i in range(2000):
        a = MyLeakyObject()
        a.children.append(MyLeakyObject(parent=a))
        time.sleep(3)

if __name__ == "__main__":
    main()

Python 3.4以降は上記__del__関数の落とし穴が解消されてるみたいです。

Python 2系でメモリリークを起こさないようにするには__del__関数使わない、または他のオブジェクトのレファレンスを持つ時にweakrefモジュールを使うのがおすすめです。

コードをいじらずにメモリリークを検証する方法

アプリケーションのコードにメモリプロファイラを入れて検証することが難しいケースもあると思って (例えばプロダクションでしか再現しないケースなど)、コードをいじらずにメモリリークの検証方法を調べたところ Pyrasiteという素晴らしいライブラリーを見つけました。Pyrasiteを使うと動いてるPythonのプロセスにコードを注入することができるので、Pyrasite + Pythonの好きなメモリプロファイラの組み合わせで簡単にメモリリークの検証ができます。

では、早速先程メモリリークを再現したコードで検証したいと思います。

1. Gdbをインストールする

Gdbunix系のシステムのデバッガーです。PyrasiteがGdbに依存してるのでインストールが必要です。インストール方法はOSによって違いますが、少し調べたら簡単にできるので割愛します。

2. Pyrasiteと好きなメモリプロファイラをインストールする

メモリ検証のツールとして今回Objgraphという便利なライブラリーを使います。

$ pip install pyrasite objgraph

3. Pyrasite shellを使ってプロファイラのコードを注入して検証する

まず、検証したいPythonプロセスのpidを調べます (以下の leak.py は上記のメモリリーク再現用のコードと同様のものです)

$ ps aux | grep leak.py
ec2-user 32497  0.0  0.0 132752  6224 pts/0    S+   04:46   0:00 /home/ec2-user/.pyenv/versions/2.7.10/bin/python leak.py
ec2-user 32541  0.0  0.0 119392   984 pts/4    S+   04:46   0:00 grep --color=auto leak.py

そして、pidを使ってpyrasiteのシェルに入ってプロファイラのコードを注入します。 今回はpyrasiteのシェルの中でobjgraphを使ってメモリリークの検証したいと思います。

$ pyrasite-shell 32497
Pyrasite Shell 2.0
Connected to '/home/ec2-user/.pyenv/versions/2.7.10/bin/python leak.py'
Python 2.7.10 (default, Nov 30 2020, 02:13:01)
[GCC 7.3.1 20180712 (Red Hat 7.3.1-9)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
(DistantInteractiveConsole)

>>> import objgraph
>>> objgraph.show_growth()
function                       1492     +1492
wrapper_descriptor             1050     +1050
builtin_function_or_method      724      +724
method_descriptor               590      +590
dict                            587      +587
tuple                           524      +524
weakref                         496      +496
list                            271      +271
getset_descriptor               222      +222
type                            214      +214

この段階でまだメモリリークしてることがはっきり見えませんが、数秒後にまた objgraph.show_growth()を実行してみると以下のような結果になります:

>>> objgraph.show_growth()
MyLeakyObject       16        +2
list               275        +2
dict               594        +2

メモリリークを再現したMyLeakyObjectクラスのオブジェクトがgcされずに増えてることがわかるかと思います。数秒後にまたチェックすると同じく増え続けます。

>>> objgraph.show_growth()
MyLeakyObject       18        +2
list               277        +2
dict               596        +2

....数秒後

>>> objgraph.show_growth()
MyLeakyObject       20        +2
list               279        +2
dict               598        +2

....数秒後

>>> objgraph.show_growth()
MyLeakyObject       24        +4
list               283        +4
dict               602        +4

これで明らかにMyLeakyObjectにどこかメモリリーク発生してることがわかるかと思います。

それでは、メモリリークを直してみましょう。__del__関数の落とし穴によってリークしていたので、__del__関数を削除するだけで直るはずです。

import time

class MyLeakyObject(object):
    def __init__(self, parent=None):
        self.parent = parent
        self.children = []
        self.value = 'x' * 100000 

    #def __del__(self):
    #    print("deleting")

def main():
    for i in range(2000):
        a = MyLeakyObject()
        a.children.append(MyLeakyObject(parent=a))
        time.sleep(3)

if __name__ == "__main__":
    main()

修正後に同じくpyrasite + objgraph で検証してみた結果は以下の通りです:

>>> import objgraph
>>> objgraph.show_growth()
function                       1491     +1491
wrapper_descriptor             1050     +1050
builtin_function_or_method      724      +724
method_descriptor               590      +590
dict                            577      +577
tuple                           524      +524
weakref                         496      +496
list                            261      +261
getset_descriptor               222      +222
type                            214      +214

>>> objgraph.show_growth()
wrapper_descriptor     1062       +12
getset_descriptor       226        +4
member_descriptor       214        +3
weakref                 499        +3
dict                    580        +3
method_descriptor       591        +1

>>> objgraph.show_growth()
>>> objgraph.show_growth()
>>> objgraph.show_growth()
>>> objgraph.show_growth()
>>> objgraph.show_growth()

ご覧の通り、objgraph.show_growth() を何回打ってもMyLeakyObject が上の方に上がって来なくなったことがわかるかと思います!

以上、コードをいじらずにPythonアプリケーションのメモリリークを検証する方法の紹介でした。 Pyrasiteobjgraph を使ってみて個人的にすごく便利だと思ったので、みなさんもぜひ機会があれば使ってみてください!

テストレベルとカバレッジとは!?

こんにちは、QAエンジニアのざきです。
冬といえば鍋ですね、好きな鍋は「もつ鍋」です。

f:id:curama-tech:20201119195257j:plain

はてさて
今日のブログは「テストレベル」と「カバレッジ」について解説します。
このブログを書くにあたり、英語を勉強している友人に聞いてみました。

Q1.テストレベルの意味って何だと思う?
A1.「どれだけちゃんとテストできているのかを示すレベル?」

Q2.カバレッジの意味って何だと思う?
A2.「え、知らない。聞いたことない。」

現場からは以上です。早速解説に入ります。

「テストレベル」とは
簡単に言うと、システム・ソフトウェアをテストするタイミングです。
※「テストフェーズ」や「テストステージ」とも呼びます。

開発手法により意味合いや取り扱いの範囲は変わりますが、代表的なテストレベルは以下の4つに分類されます。

f:id:curama-tech:20201119200921p:plain
●テストレベルの種類

単体テスト(UT)
 ユニットテスト[Unit Test]のテスト対象は、「コンポーネント」とも呼ばれるソフトウェア単体になります。
 テストの単位は、関数 / クラス / モジュール / コードなど、開発手法により異なりますが、ひとまとまりで動作する最小単位を指します。
 →くらマでは開発エンジニア自身で単体テストを行います。

結合テスト(IT)
 インテグレーションテスト[Integration Testing]のテスト対象は、2つ以上の「コンポーネント」の組み合わせです。
 また、「コンポーネント」と「サブシステム / データベース / 各種サービス」との組み合わせもテスト対象になります。
 結合テストの対象は、それぞれが単体テストを実施し欠陥が解消されている事が前提となります。
 →くらマでは結合テスト以降がQAチームの担当となります。

・総合テスト(ST)
 システムテスト[System Testing]のテスト対象は、全てのコンポーネントやサブシステムを統合した「システム全体」になります。
 全ての機能を統合した時に欠陥がないことを確認するため、実際の本番環境と同じ条件下、かつ、同様の使われ方を意識したテストを行います。
 もちろん、単体テストおよび結合テストを実施した上で欠陥が解消されている事が前提となります。

・受入/検収テスト(UAT, AT)
 (ユーザー)アクセプタンステスト[(User)Acceptance Test]のテスト対象は、総合テストを実施し検出した欠陥が解消された「システム全体」になります。
 受入/検収という名前の通り、大半は、開発の依頼元が納品前に行うテストとなり、実際に利用するユーザーがテストするケースも多いです。
 対象となるシステム・ソフトウェアが要求水準を満たしているかの検証試験や、利用者の意図通りに操作できるかの妥当性試験・ユーザビリティテストも行われます。

※上記は、あくまでも代表例として取り上げていますので、実際の開発現場や開発手法によって内容や取り扱いが変わる事があります。

テストレベルについての解説は以上です。そこまで難しい話ではないと思いますので覚えておいてもらえると嬉しいです ^^

カバレッジ」とは
テスト対象となるシステム・ソフトウェア全体のうち、どの程度のテストを実施した/実施しようとしているのかを割合(%)で表した「網羅率」を指します。
テストカバレッジ[Test Coverage]と呼ぶ事もありますが、大体は「カバレッジ」だけでも通じると個人的には感じています。

f:id:curama-tech:20201117213220p:plain
カバレッジの例

それぞれのカバレッジについて掘り下げると説明が長くなるので今回は省略しますが、計測する視点により様々なカバレッジが存在します。
テストを実施するにあたり、カバレッジ (網羅率)の測定/分析を行うことは、プロダクトの品質向上にとても大きな意味を持ちます。

以前も書きましたが、「全数テストを行う事は不可能」のテスト原則に基づき、計測視点別のカバレッジでテストを実施した/実施しようとしているのかを把握することで、テストの抜け・漏れの有無をチェックしやすくなります。
もしも過不足に気づいたら、そこから軌道修正する事でテストの妥当性を向上させる事もできます。

まとめ
①テストレベル(テストを行うタイミング)を理解しましょう。
無作為にテストするのではなく、テストレベルを把握して進めることで、後工程の出戻り(欠陥の検出・修正・再テスト、仕様変更等)を減らすことにも繋がります。

カバレッジ(網羅率:%)を意識しましょう。
開発されたシステム・ソフトウェアに係る全てをテストする事は不可能です。おさえるべき要点を掴み、カバレッジを読み取って、時には不要テストを切り捨てる覚悟を持ちましょう。

これからも、テストに関するネタを執筆していきます!
初歩的な内容にもなりますが、時に誰かの復習になったり、また時には誰かのテストに対する興味・関心を持っていただくキッカケになるかもしれません。
今回もお付き合いいただきありがとうございました!

くらマのオンライン決済を支えるデプロイ技術

こんにちは。 バックエンドエンジニア / SRE のまのめです。

くらしのマーケットではマイクロサービスアーキテクチャを一部採用しており、決済サービスも一つのマイクロサービスとして運用しています。
決済サービスは ECS で管理しており、コンテナは Fargate でコンピューティングしています。

f:id:curama-tech:20201110145722p:plain
構成の概略

ECS でのデプロイは、何も設定しないと Rolling update (徐々にリクエストを新しいインスタンスに流す) という戦略になります。
しかしデプロイ中に新旧のインスタンスが同時に存在すると、DB 構造やロジックの違いなどからデータが不整合を起こし、予期せぬ障害を招く恐れがあります。
そのため、新しいインスタンスに対してリクエストを一気に 100% 切り替える必要がありました。

さて、これを解決する方法は単純で、 Blue/Green デプロイができればリクエストを 100% 切り替える戦略が取れますね。
ECS でもこの戦略を取ることができ、インスタンスの切り替え方に様々な戦略を設定することが可能です。
この戦略は CodeDeploy で管理することができ、デフォルトで用意されている設定も数多くあります。
今回は、デフォルトで用意されている戦略の CodeDeployDefault.ECSAllAtOnce を利用して、Blue/Green デプロイを実装していきます。

実装

決済サービスをリリースする前から ECS を利用したサービスがいくつかあり、これらのデプロイは boto3 を利用しています。
なので、既存の仕組みに沿って実装していきます。
実はサービスとして CodeDeploy を使うのは初です。

注意点として、CodeDeploy による Blue/Green デプロイを実装するためには、Target Group を 2 つ用意する必要があります。
これは、古いインスタンスとは別の Target Group を新しいインスタンスに紐づけて healthcheck を行い、問題なければデプロイを実行して切り替える、という流れになるためです。

class CodeDeployManager:
    def __init__(self, cluster: str, service: str):
        session = Session(
            aws_access_key_id="YOUR_AWS_ACCESS_KEY_ID",
            aws_secret_access_key="YOUR_AWS_SECRET_ACCESS_KEY",
            region_name="ap-northeast-1")
        self.client = session.client("codedeploy")
        self.waiter = self.client.get_waiter('deployment_successful')
        self.application_name = "AppEcs-{0}-{1}".format(cluster, service)

        self.application = dict()
        self.application["applicationName"] = self.application_name
        self.application["computePlatform"] = "ECS"

        self.deployment_group = dict()

    def get_or_create_application(self):
        """
        :return response: dict
        """
        get_param = dict()
        get_param["applicationName"] = self.application_name

        try:
            self.application = self.client.get_application(**get_param)
        except (self.client.exceptions.ApplicationDoesNotExistException):
            self.application = self.client.create_application(**self.application)

    def get_or_create_deployment_group(self, **kwargs):
        """
        :param kwargs: dict

        require:
          - deploymentGroupName
          - serviceRoleArn
          - deploymentStyle
          - ecsServices
          - loadBalancerInfo
        """

        param = dict()
        param["applicationName"] = self.application_name
        param["deploymentGroupName"] = kwargs["deploymentGroupName"]

        try:
            self.deployment_group = self.client.get_deployment_group(**param)
        except (self.client.exceptions.DeploymentGroupDoesNotExistException):
            self.deployment_group = param
            self.deployment_group["applicationName"] = self.application_name
            self.deployment_group["deploymentGroupName"] = kwargs["deploymentGroupName"]
            self.deployment_group["serviceRoleArn"] = kwargs["serviceRoleArn"]
            self.deployment_group["deploymentStyle"] = kwargs["deploymentStyle"]
            self.deployment_group["blueGreenDeploymentConfiguration"] = {
                "terminateBlueInstancesOnDeploymentSuccess": {
                    "action": "TERMINATE",
                    "terminationWaitTimeInMinutes": 2
                },
                "deploymentReadyOption": {
                    "actionOnTimeout": "CONTINUE_DEPLOYMENT",
                    "waitTimeInMinutes": 0
                }
            }
            self.deployment_group["ecsServices"] = kwargs["ecsServices"]
            self.deployment_group["loadBalancerInfo"] = kwargs["loadBalancerInfo"]
            self.client.create_deployment_group(**self.deployment_group)

    def create_deployment(self, **kwargs):
        """
        :param kwargs: dict

        require:
        - deploymentGroupName
        - revision
        """
        param = dict()
        param["applicationName"] = self.application_name
        param["deploymentGroupName"] = kwargs["deploymentGroupName"]
        param["deploymentConfigName"] = "CodeDeployDefault.ECSAllAtOnce"
        param["revision"] = kwargs["revision"]

        deployment = self.client.create_deployment(**param)
        self.waiter.wait(deploymentId=deployment["deploymentId"])

今回の場合、要件として以下のように設定しています。

  • Blue/Green デプロイ時に、100% リクエストを切り替える
param["deploymentConfigName"] = "CodeDeployDefault.ECSAllAtOnce"
self.deployment_group["blueGreenDeploymentConfiguration"] = {
    "terminateBlueInstancesOnDeploymentSuccess": {
        "action": "TERMINATE",
        "terminationWaitTimeInMinutes": 2
    },
}
  • 全てのデプロイが完了するまで待機する (Slack 通知などのため)
deployment = self.client.create_deployment(**param)
self.waiter.wait(deploymentId=deployment["deploymentId"])

細かくは端折りますが、あとはこの CodeDeployManager を使って、デプロイの流れを作ります。

class DeployManager():
    def __init__(self):
        self.ecs = EcsManager()          # ECS の操作・設定
        self.params = ParameterManager() # デプロイの詳細設定

    def deploy(self):
        # 実装は書かないが、ここで ALB, Task, Role などの詳細設定を済ませておく

        self.code_deploy = CodeDeployManager(self.ecs.["cluster"], self.ecs["service"])
        self.code_deploy.get_or_create_application()
        self.code_deploy.get_or_create_deployment_group(**self.params.deployment_group)
        
        if service:
            # update service のときは、deployment を作ることで update できる
            self.code_deploy.create_deployment(**self.params.deployment)
        else:
            # create service のときは、deployment と deployment group を作るだけ
            self.ecs.create_service(**self.params.ecs)

デプロイを開始すると、CodeDeploy 上に Deployment が作成され、CodeDeploy による Update Service が始まります。

f:id:curama-tech:20201110145917p:plain
デプロイ中の様子

ECS の画面でも、状態が確認できます。

f:id:curama-tech:20201110145727p:plain
ECS での Deployment の表示

感想

思っていたよりも遥かに簡単に実装できました。
もちろん、より細かい要件を定めている場合は Deployment Group の設定などが複雑になったり、場合によっては Deployment config を自作するということもあるかと思います。
デフォルトで用意されている config だけでも十分な実装が可能ですので、 ECS でトラフィック管理をしっかりやりたい場合は CodeDeploy によるデプロイ戦略を作るのが重要だなと感じました。

今後も決済に関するアップデートは行われていきます。
アップデートをより安全にデプロイしていくために、デプロイ戦略もアップデートできたらいいなと思います!

緊急トラブルを解決するサービスをリリースしました!

f:id:curama-tech:20201106173728p:plain

最近寒くて布団から出るのが辛いPdMのハットリです。

今回は、緊急系サービスをリリースするにあたってどんな取り組みをしていたかを書いていきたいと思います。

緊急系サービスを一言で説明すると「24時間以内の作業を実現させるサービス」です。

詳しい説明は、PR担当の小田さんが書いた素晴らしいnoteがありますので、興味がある方はこちらをお読みください。

では緊急系チームについて書いていきます。

開発メンバー

  • カテゴリーマネージャー
  • PdM
  • デザイナー
  • エンジニア

開発手法

アジャイル開発

日々やっていたこと

デイリーMTG

毎日15時からチーム全員で集まりMTGをしていました。

進捗管理シートというもの作りそれを見ながら進捗確認を行っていました。

みんマではGitHubのissueベースで開発要件を整理しています。

3rdpartyプラグインのZenHubを使うことも考えたのですが、issue数や期限などが一覧で見にくいのとステータスの変更漏れなどがあるので、手間ですがスプレッドシートで管理するようになりました。

それを見ながら自分が担当しているissueの進捗共有と、困っていることがあればチームメンバーに相談して早期に解決するようにしていました。

毎日やると開発時間が減るのでは?と感じるかもしれませんが、15分〜30分程度の短時間で終わるのでそんなに影響ないのかなと思っています。


ウィークリーMTG

週次でチーム全員+CTOで集まりMTGをしていました。

デイリーMTGとは異なりissue単位での進捗共有ではなく、各自が今週やったことと来週やることを報告していました。


マンスリーKPT

月次でチーム全員で集まりKPTをしていました。

KPTとはプロダクトの進め方がどうだったのかを振り返り、「Keep」「Problem」「Try」の3項目を出し合い今後どう取り組んでいくかを決めます。

miroというツールを使い、各自KeepとProblemを出し合い全員でTryを考えました。

業務のKeepとProblemだけではなく、自然発生的にプライベートのKeepやProblemも出ていました。

ルールでプライベートのKeepやProblemも出してねって決めるのではなく、自然発生的に出てくるのがみんマっぽかったです。

月次で振り返ってみるとメンバーが同じようなKeepやProblemを持っていたことが分かりました。チーム全員が同じ認識を持っているのでTryに納得感があり来月からの行動に移しやすかったと思います。

業務のTryは改善策を真剣に考えて来月に生かす、プライベートのTryはちょっとふざけた改善策を出したりネタっぽくなっていたのもよかったです。

チーム発足後すぐにフルリモートになり、物理的に雑談する機会も減っていたのでKPT内で自然に雑談が生まれていたのもKPTのよかった点だなと思います。



うまくいったこと

リリース前にユーザーテストを行い最善の状態でリリースできたこと

プロダクトのリリース前に社員を対象にSUS評価を使ってユーザーテストを行っていました。

4回実施しリリース直前のユーザーテストでは、100点中82点を獲得しA評価のプロダクトになりました。

緊急系カテゴリの狙っていたポイント(会員登録から電話予約までスピーディーでスムーズ、わかりやすいUIなど)が評価されていたのが良かったです。

三者からの意見をもらうことで、プロダクトの方向性が間違ってないことが分かり自信にも繋がりました。

開発していると気がつけない部分や抜け漏れなど、他部署からのフィードバックはありがたかったです。


エンジニアとデザイナーがプロダクトの仕様理解度が高いこと

日々やっていたことの影響かもしれませんが、エンジニアとデザイナーのプロダクトの仕様理解度が高いなと感じました。

高い理由としては、エンジニアだからデザイナーだからという枠にとらわれず、自分が作っているプロダクトに興味を持っていたからだと思います。

その結果として実装時の要件漏れなどが少なかったです。

さらには「ユーザー的にはこうした方が使いやすくないですか?」など仕様の提案もしてくれました。

カテゴリマネージャーやPdMに仕様の最終的な決定権はあるのですが、少人数で考えると考慮漏れや抜け漏れが発生してしまいます。

エンジニアとデザイナーからの仕様の提案で考慮もれや抜け漏れが発生していたのに何度か気づくことができました。

どうせ仕様提案しても採用されないんでしょって投げやりになるのではなく、提案してくれたのは個人的にすごい嬉しかったです。



うまくいかなかったこと

issue内容やUIの更新ができていなかった

仕様やUIが変わった時に更新漏れがあり、issueの詳細やUIの内容が整理されておらず実装と解離がありました。

その結果、テストケース作成やテスト実施に影響があり想定以上にテストに時間がかかりました。

また、WebブラウザiOSアプリ、Androidアプリで実装に差異がある事もあり、都度仕様を確認する必要が発生しました。

アジャイル開発で開発を行っており、開発途中で仕様の変更・追加が多々あります。

その際にissue内容やUIの更新より先に実装を進めてしまったことに原因があると思っています。

開発スピードを求めた結果このようになってしまいました。

第二弾の緊急系カテゴリではその反省を生かし、仕様やUIが変わった際には開発に入る前にissueやUIの更新を行うようにしています。

リリースが1週間遅延してしまった

issue内容やUIの更新ができていなかったことが影響してリリースが1週間遅延してしまいました。

リリース後のKPTでチーム全員からリリースが遅延してしまって悔しいというProblemが出ました。

ほんとにあと少しのところで間に合わなかったです。

規模の大きい開発なので1週間は誤差でしょって考えもありますが、自分たちで決めたリリーススケジュールにあと少しのところで間に合わなかったのが悔しかったです。

今回の悔しさを晴らすために、第二弾の緊急系カテゴリはissueやUIの更新漏れをなくしスケジュール通りにリリースできるようにします。

最後に

緊急系サービスは生まれたばかりですが、今後飛躍的に成長するプロダクトだと思います!

一緒に開発したい方はぜひコーポレートサイトまでお気軽にご連絡ください!

ででんでんでん、デシジョンテーブル

ご無沙汰してます、QAエンジニアのざきです。
世の中の流れは早いもので、2020年も残り僅か。寒い地域は苦手なので宮崎から出られません。

はてさて

今回は数あるテスト設計技法の中からデシジョンテーブルについて書きます。

開発内容の規模が小さい場合、頭の中で仕様を巡らせテストケースを考える事は、そう難しくはありません。
しかしながら昨今のシステム開発では、開発の規模自体が大きく、1つの処理結果を得るためにも複数の条件や前提が存在する事がしばしばあります。
複数の条件が存在するだけでも頭が痛いのですが、その条件自体が複雑に絡み合い・入り乱れる事も多く、お手上げならぬ「思考停止」に陥ります。

QAとして、潜んでいる不具合を出来るだけ多く検出するために、正しい処理結果やユースケースを理解する事は避けて通れません。
「全数テストを行う事は不可能」というテスト原則もありますが、必要なテストケースをきちんと網羅できているのか、自分の中に不安が残る状態でテスト設計を行う事は避けたいです。

どうにか考え抜いたテストケースを並べて見た時に、「抜け・漏れがないか(網羅できているか)」「省略したテストは適切なのか(必要なものを省略していないか)」が瞬時に判断出来ない時、そんな時は深呼吸してデシジョンテーブルを作ってみましょう。

デシジョンテーブルとは

簡単に説明すると、入力データ・入力条件の組み合わせに対する処理・出力結果を表形式(テーブル構成)でまとめたものです。
日本工業規格『JIS X 0125 決定表』)で規格が定められていますが、実際の開発現場によって利用方法や作成方法は様々です。
この規格の中では「問題の記述において起こり得るすべての条件と、それに対して実行すべき動作とを組み合わせた表」と定義されています。

デシジョンテーブルの構成

デシジョンテーブルの構成は次の通りです。

f:id:curama-tech:20201026202410p:plain

ちょっとこれだけじゃ分かりにくいのでサンプルを1つ。
f:id:curama-tech:20201028124314p:plain 上の仕様に基づいてデシジョンテーブルを作成するとこうなります。

f:id:curama-tech:20201028124511p:plain
ドラッグストアの割引料金のデシジョンテーブル

デシジョンテーブルの解説

それでは、デシジョンテーブルの構成・要素について1つずつ解説します。

①条件の一覧(条件記述部 / condition stub)
 入力条件・入力データを列挙します。

f:id:curama-tech:20201028124821p:plain
①条件記述部

②結果の一覧(動作記述部 / action stub)
 条件に合わせて実行する動作や処理結果を列挙します。

f:id:curama-tech:20201028125035p:plain
②動作記述部

③条件の組合せ(条件指定部 / condition entry)
 条件の判定結果の組合せを指定します。判定結果は大半の場合、真か偽で表現します。
 表現方法として以下のものがありますが、どの組合せを使っても意味は同じです。
 [Y、N(Yes or No)][T、F(True or False)][1、0(真 or 偽)]

 組合せではなく、単独で真か偽を指定する場合は以下の表現方法を使用します。  [値、単語(この行に対応する条件が、記述された値や単語を満たすことを意味する)]
 [ -(この行に対応する条件が無関係であることを意味する)]

f:id:curama-tech:20201028125221p:plain
③条件指定部

④組合せに対応する結果(動作指定部 / action entry)
 条件の組合せに対応する動作結果を指定します。真か偽で表現する場合は条件指定部と同じ表現になります。
 [Y、N(Yes or No)][T、F(True or False)][1、0(真 or 偽)]

 その他の表現方法としては以下のものがあります。
 [X(eXecute):この列に指定された全条件の真偽値に合致する場合、この行に対応する動作や処理結果となる]
 [空白、-:この列に指定された全条件の真偽値に合致する場合、この行に対応する動作や処理結果の対象外となる]
 [値、単語:この列に指定された全条件の真偽値に合致する場合、この行に対応する動作や処理結果が記述された値や単語を満たす]

f:id:curama-tech:20201028125310p:plain
④動作指定部

⑤規則
 入力条件、入力データの組合せと、それに対応する動作や処理結果を組合せたものです。
 デシジョンテーブルテストを行う際は、この各列(規則)をテストケースとして扱います。

f:id:curama-tech:20201028125435p:plain
⑤規則

サンプルの内容は複雑な条件組合せはありませんが、頭の中で考えるよりは理解しやすいのではないでしょうか。
デシジョンテーブルを利用すると、テストケースの内容を明確に示せるメリットも感じられると思います。
(箇条書きのテストケースは数が多いと読むのに疲れますよね ^^;)

デメリット

良いことばかりじゃないもので、デシジョンテーブルにもデメリットがあります。
これまでの経験・実践から感じているものは次の内容です。

  • 条件を1つ増やすだけで組合せ数が跳ね上がる(収拾がつかない)

  • 条件自体に抜け・漏れがあると全く意味が無い(ゆるゆるなテスト設計)

  • 実行順序を考慮できない(個人差が出ます、本当に)

テストケースとして利用する際には、テストフェーズや対象システム・機能項目と照らし合わせながら判断しましょう。
やたらめったらデシジョンテーブルにしてしまうと、逆に、とっても複雑なテストを作り出してしまいますのでご注意ください。

まとめ

今回は「デシジョンテーブル」についてお届けしました。ネーミングからして難しそうな技法を使えば、最良・最善のテストが出来る、なんて事はありません。あくまでも目的(より良いテスト)の為の手段なので、使い方(あなた)次第です。

小難しい用語や、自分にはしっくりこないルールなど、テスト設計技法も本当に様々あります。 個人的に、知らないよりは知った上で、必要なタイミングで必要なものを使えればそれが最高だな!と思ってます(゚∀゚)

最後まで読んでいただいた皆様、お付き合いありがとうございました!