29 views
B!0X

テストパフォーマンスの最適化

testing-performance-optimizations

こちらの文章は以下の記事を翻訳したものです。 https://github.com/HypothesisWorks/hypothesis/blob/master/HypothesisWorks.github.io/_posts/2016-04-29-testing-performance-optimizations.md

基本的なクラッシュバグをコードから除去した後、もっと興味深いテスト対象を見つける必要があります。

最もテストが容易なのは、すべての入力に対して正しい答えが既知であるコードです。

理論上、正しい答えが分かっている場合、そのコードを実行して検証することは可能です。しかし、それは検証しようとしている答えそのものであるため、あまり有用ではありません。

しかし、正しい答えを得る方法が複数存在する場合があります。そして、実際の運用で採用される方法が選ばれる理由は、異なる答えを提供するためではなく、同じ答えをより速く提供するためである可能性があります。

例えば、以下のような状況が考えられます。

  • 複雑かつ高速なバージョンのアルゴリズムと、遅く、単純なバージョンのアルゴリズムが存在する場合。
  • キャッシングレイヤーを導入しており、キャッシングを有効にした状態と無効にした状態、または異なるキャッシュのタイムアウト設定でコードを実行することができる場合。
  • スケーラビリティを向上させるために新しいデータベースバックエンドに移行していても、移行が完了するまでは古いバックエンドのコードが使われている場合。

他にも様々な可能性が考えられますが、これらが最も一般的なケースのようです。

いずれにしても、これはプロパティベーステストに適した素晴らしいユースケースを提供します。なぜなら、2つの関数が常に同じ答えを返すべきだとされているときに、それをテストできる機会だからです。単純に同じデータを使って両方の関数を呼び出し、その結果が一致することを確認すれば良いのです。

例えば、高度なアルゴリズムの場合を想定してみましょう。たとえば、マージソートを実装したとします。

def merge_sort(ls):
    if len(ls) <= 1:
        return ls
    else:
        k = len(ls) // 2
        return merge_sorted_lists(merge_sort(ls[:k]), merge_sort(ls[k:]))

def merge_sorted_lists(x, y):
    result = []
    i = 0
    j = 0
    while i < len(x) and j < len(y):
        if x[i] <= y[j]:
            result.append(x[i])
            i += 1
        else:
            result.append(y[j])
            j += 1
    return result

テストの対象として使用するためのリファレンス実装が必要ですので、バブルソートも実装してみましょう。

def bubble_sort(ls):
    ls = list(ls)
    needs_sorting = True
    while needs_sorting:
        needs_sorting = False
        for i in range(1, len(ls)):
            if ls[i - 1] > ls[i]:
                needs_sorting = True
                ls[i - 1], ls[i] = ls[i], ls[i - 1]
    return ls

これらのコードは同じ結果を返すはずですので、テストしてみましょう。

@given(lists(integers()))
def test_bubble_sorting_is_same_as_merge_sorting(ls):
    assert bubble_sort(ls) == merge_sort(ls)

しかし、エラーが発生しました。

@given(lists(integers()))
    def test_bubble_sorting_is_same_as_merge_sorting(ls):
>       assert bubble_sort(ls) == merge_sort(ls)
E       assert [0, 0] == [0]
E         Left contains more items, first extra item: 0
E         Use -v to get the full diff

foo.py:43: AssertionError
----- Hypothesis -----
Falsifying example: test_bubble_sorting_is_same_as_merge_sorting(ls=[0, 0])

問題が発生したのは、merge_sorted_lists 関数の実装に誤りがあったためです。片方のリストが終了した後にもう片方のリストに残っている要素を含める処理が不足していました。その結果、リストから要素が失われ、より単純な実装では発生しない問題が発生しました。

以下の方法で修正を行いました。その後、テストが成功するようになりました:

def merge_sorted_lists(x, y):
    result = []
    i = 0
    j = 0
    while i < len(x) and j < len(y):
        if x[i] <= y[j]:
            result.append(x[i])
            i += 1
        else:
            result.append(y[j])
            j += 1
    result.extend(x[i:])
    result.extend(y[j:])
    return result

このテクニックは特にHypothesisのルールベースドステートフルテスティングと非常にうまく組み合わせることができます。なぜなら、これを使用して複雑なAPIの異なる実装をテストできるからです。例えば、Hypothesisはこのプロパティをステートフルテスティングと組み合わせて、異なる実装が同一の動作を示すことを検証しています。

関連事例

  • データ分析支援

    事例:過去実績を提案に活用する分析の仕組み

    広告代理店との取り組みです。同社は過去実績の示唆を次の提案へ反映し、結果をどのように予測したか、その背景まで提案先のクライアントへ説明できるようになりました。KDOT は案ごとの見込みと判断材料を比較できる仕組みを業務アプリとして実装しました。さらに業務アプリの中核となる処理ロジックと機械学習モデルについて、特許手続きを担う弁理士が技術の新規性や工夫を検討するための資料を作成しました。その後この発明には特許が付与されました。

  • データ基盤構築 / 技術顧問(技術企業向け)

    事例:効果検証の分析処理基盤

    効果検証を提供する分析会社との取り組みです。同社は分析者の人数に比例してしか売上と利益を伸ばせない事業の形を、効果検証をプロダクトとして提供することで変革しようとしていました。手法とモデルは同社のまま、社内では開発できなかった、データと機械学習の重い推定を製品として動かす仕組みの開発を、開発支援会社の KDOT に任せました。推定を案件ごとの手作業ではなくシステムで実行できるようになったことで、同社は人数に比例しない形で売上と利益を伸ばすことを目指せるようになりました。

開発の相談先をお探しですか。

この記事に関わることについてご相談がある方はお問い合わせください。

KDOT についてお問い合わせ