空飛ぶロボットのつくりかた

ロボットをつくるために必要な技術をまとめます。ロボットの未来についても考えたりします。

テストを学ぼう(3)!~テストダブル~

理想的なユニットテストでは、依存するすべてのシステムを利用して行う。しかし、依存する本物のオブジェクトを常に使用できるとは限らない。

こんな時、リファクタリングをしたり、仮のオブジェクトを用いてテストを行うことができる。

テスタビリティ

テスタビリティ:テストのしやすさ


テスト対象が複雑な場合、テストも複雑になりがち。

しかし、テスト対象が複雑でなくてもテストが複雑になったり、不安定になることがある…

例えば、以下のテストは不安定、というかうまく行かない↓

プロダクションコード

#!/usr/bin/python
# coding: UTF-8

import time
from datetime import datetime

class DateState:
    def __init__(self):
        self.date = datetime.today()

    def do_something(self):
        self.date = datetime.today()
        print "do something"
        return self.date


if __name__ == '__main__':
    date_state = DateState()
    print date_state.date    

    time.sleep(1)
    date_state.do_something()
    print date_state.date

テストコード

# -*- coding:utf-8 -*-                                                 
import unittest, date_example
from datetime import datetime


class TestDate(unittest.TestCase):
    # 前処理                                                           
    def setUp(self):
        pass

    # 後片付け                                                         
    def tearDowm(self):
        pass

    # 現在時刻のテスト                                                   
    def test_get_day(self):        
        date_state = date_example.DateState()
        date_state.do_something()
        self.assertEqual(date_state.date, datetime.today())


if __name__ == "__main__":
    unittest.main()

実行すると

do something
F
======================================================================
FAIL: test_get_day (test_date_example0.TestDate)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_date_example0.py", line 19, in test_get_day
    self.assertEqual(date_state.date,datetime.today())
AssertionError: datetime.datetime(2017, 5, 28, 13, 13, 44, 456115) != datetime.datetime(2017, 5, 28, 13, 13, 44, 456151)

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

そんな時はどうするの?

リファクタリング

リファクタリング:プログラムの外部的な振る舞いを変えずに内部構造を変更し、コードを整理するテクニック


例えば、

  1. メソッドの抽出
  2. クラスの抽出

などがある

Tips : リファクタリングを行う前にテストを書くこと

メソッドの抽出

方法:テストがむずかしいメソッドを抽出し、抽出したメソッドをオーバーライドしてテストする

プロダクションコード

#!/usr/bin/python
# coding: UTF-8

import time
from datetime import datetime

class DateState:
    def __init__(self):
        #self.date = datetime.today()
        self.date = self.new_date()

    def do_something(self):
        #self.date = datetime.today()
        self.date = self.new_date()
        print "do something"
        return self.date

    def new_date(self):
        return datetime.today()


if __name__ == '__main__':
    date_state = DateState()
    print date_state.date    

    time.sleep(1)
    date_state.do_something()
    print date_state.date

テストコード

# -*- coding:utf-8 -*-                                                 
import unittest, date_example
from datetime import datetime


class TestDate(unittest.TestCase):
    # 前処理                                                           
    def setUp(self):
        pass

    # 後片付け                                                         
    def tearDowm(self):
        pass

    # 現在時刻のテスト                                                   
    def test_get_day(self):
        current = datetime.today()
        
        class NewDateState(date_example.DateState):
            def new_date(self):
                return current

        date_state = NewDateState()
        date_state.do_something()
        self.assertEqual(date_state.date,current)


if __name__ == "__main__":
    unittest.main()

実行結果

do something
.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

このようにメソッドを抽出することで安定したテストが可能

実際、テスト対象はテスト対象クラスのサブクラスになっているが、不安定なテストよりこちらのほうが良いんじゃないでしょうか

委譲オブジェクトの抽出

方法:処理をオブジェクトとして抽出し委譲する

参考:

Python の関数はオブジェクト | すぐに忘れる脳みそのためのメモ

Pythonを書き始める前に見るべきTips - Qiita

Pyramidでzope.interfaceを使う — hirokiky's blog

テストダブル

テスト対象のクラスやメソッドは往々にして他のオブジェクト等に依存しており、依存する機能がユニットテストで扱いづらい時、依存するオブジェクトを代役で置き換える方法が有効。

この、代役は

  1. スタブ
  2. モック

などがあり、総称してテストダブルと呼ばれる。

スタブとは

依存するクラスやモジュールの代用として使用する仮のクラスやモジュールのこと。

↓こんな時にスタブを使う

  1. 依存オブジェクトが予測できない振る舞いをする
  2. 依存オブジェクトのクラスが存在しない
  3. 依存オブジェクトの実行コストが高く、簡単に利用できない
  4. 依存オブジェクトが実行環境に強く依存している
固定値を返すスタブ

スタブの例として、ランダムな整数(1〜100)を返す機能を考えてみる。

以下のように固定値を返すスタブを作ると、不確実性を排除できるため正しくテストすることができる。また、再利用できるスタブなどテストに応じて自由に作ることができる。

#!/usr/bin/python
# coding: UTF-8

import random

# 乱数生成クラス
class RandomNumberGenerator:
    def __init__(self):
        pass

    def nextInt(self):
        return random.randint(1,100)

# 固定値を返すスタブ
class RandomNumberGeneratorStub(RandomNumberGenerator):
    def nextInt(self):
        return 1

# 再利用可能な固定値を返すスタブ
class RandomNumberGeneratorFixedResultStub(RandomNumberGenerator):
    def __init__(self,num):
        self.num = num

    def nextInt(self):
        return self.num

# 乱数生成オブジェクトを持つクラス
class Randoms:
    def __init__(self):
        self.generator = RandomNumberGenerator()

    def choice(self,options):
        if len(options) is 0:
            return None
        idx = self.generator.nextInt() % len(options)
        return options[idx]


if __name__ == '__main__':
    options = []
    options.append("A")
    options.append("B")
    sut = Randoms()
    #sut.generator = RandomNumberGeneratorFixedResultStub(0)
    sut.generator = RandomNumberGeneratorFixedResultStub(1)
    print sut.choice(options)
例外を送出するスタブ

オブジェクトによっては例外の発生条件が非常に複雑であり、簡単にテストできない場合がある。そんな時はスタブオブジェクトで無条件に例外を送出するよう実装すれば簡単に テストできるよ。

#!/usr/bin/python
# coding: UTF-8

class SetUserInfo:
    def __init__(self):
        self.user_id = []
        pass

    def set(self,user_id):
        self.user_id = user_id
        if len(user_id) is 0:
            raise ValueError("error!")

class SetUserInfoStub(SetUserInfo):
    def set(self,user_id):
        raise ValueError("error!")


if __name__ == '__main__':
    #set_user_info = SetUserInfo()
    set_user_info = SetUserInfoStub()
    zero_list = [1,2]
    set_user_info.set(zero_list)