Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
ラベル UnitTest の投稿を表示しています。 すべての投稿を表示
ラベル UnitTest の投稿を表示しています。 すべての投稿を表示

2008年9月13日土曜日

Pyscripter で UnitTest と logging - 問題のあった行へジャンプ

PyScripter で UnitTest を自動で生成」の続き。

 

次の 4 つの仕組みがあるおかげで、プログラムを書くことができると言っても過言ではない。

  1. assert 文 でメソッドの事前条件をチェック。
  2. UnitTest でメソッドの結果をチェック。
  3. logging でおかしな値をログに出力。
  4. デバッガで動作を追う。

一画面に納まる程度のスクリプトなら大丈夫だけれど、それ以上になると、もうダメなすぐに忘れる脳みそ。 (+_+) 上記の仕組みを作ってくれた人に感謝・感謝。 ^人^ それプラス軽くて使いやすい IDE、 Pyscripter があるから Python でスクリプトを書く気になれる。

ポカミスなんて当り前。バグなく一発で動かせることなんて滅多にない。だから、逆に何もなく動いたときには、「本当はどこか間違っているんじゃないか」 (@_@;) と疑ってしまう。そんなバグとお友達な自分は、Pyscripter ・ logging において次のような設定と使い方をしている。

 

行番号の表示

まずは、ソースコードにおいて行番号が表示されるようにする。エラーがでたときには、ファイル名と行番号が出力されるので、行番号が表示されていた方が使いやすい。また、ちょっとだけ現在編集している場所から離れるときに、行番号を見て覚えておけば、元の場所に戻りやすい。

方法は、ツールバーのボタンで、行番号の表示・非表示を切り替える。以下のボタンが表示されてないときは、View > Toolbars >Editor Toolbar をチェックする。

080913-002

 

または、次のように設定できる。

  1. メニューより 「Tools > Options > Editor Options
  2. Display タブ > Gutter の `Show line numbers' をチェックする。

 080913-001

 

UnitTest で問題が発生した場所へジャンプ

UnitTest を実行し、失敗またエラーがあった場合、ファイルのどの位置で発生したかについて、画面下部の Python Interpreter ペインに赤字で表示される。このとき、行番号が書かれている行でダブルクリックをすると、その位置へジャンプしてくれる。

080913-004

 

出力が一杯になって見にくくなったら、Python Interpreter ペインの中で、すべて選択 (Ctrl + A)、 Delete 。

 

ログの出力行へジャンプ

ログの出力の方法については、6.29.2 基本的な使い方を参照。 (cf. Google App Engine (Python) でログを出力 )

ログの設定を次のようにしておく。

    logging.basicConfig(level=logging.DEBUG,
        format='%(asctime)s %(levelname)s ' + \
               '"%(pathname)s", line %(lineno)d, %(message)s')

ログの出力フォーマットについては、6.29.6 Formatter オブジェクトを参照。

 

例えば次のログ出力、

logging.debug(u"こんにちは、バグです!")

の結果は、

2008-09-13 11:40:37,125 DEBUG "c:\develop\src\python\liquorchecksimple\liquortest.py", line 174, こんにちは、バグです!

Pyscripter の Python Interpreter ペインで、出力された行をクリックすると、その場所へジャンプしてくれる。上記のように書式を設定した理由は、UnitTest の出力を真似すれば Pyscripter がその行へジャンプしてくれるかと思って。

 

テンプレート
import logging



if __name__ == '__main__':
    logging.basicConfig(level=logging.DEBUG,
        format='%(asctime)s %(levelname)s ' + \
               '"%(pathname)s", line %(lineno)d, %(message)s')
    logging.getLogger().setLevel(logging.DEBUG)

2008年9月3日水曜日

Google App Engine でダイエット表の作成 (2)

追記(2008.9.6) : ダイエット表を作成するには → http://4diet.appspot.com/


Google App Engine でダイエット表の作成 (1)」の続き。

前回、Django のテンプレートタグを使って table 要素を生成するのを諦め、代わりに DietTable クラスで table 要素を生成するように変更した。しかし、実装しているうちにコードが複雑になり、実装を一時中断することに。 (+_+)

 

table 要素を生成することに専念した HtmlTable クラス

DietTable クラスが複雑になってしまった理由を考えてみると、

  1. ダイエットのための情報から「表」を作成するための「ルール」を管理している
  2. HTML の table 要素を生成する

という二つの異なることを担当しているからだと気がついた。ここで言う「ルール」というのは、例えば、作成するダイエット表において

  • 「土日の列は色を変える」
  • 「表の横線の一目盛は 0.1 kg 」
  • 「目標として設定できる体重には制限がある」

などのこと。これらは HTML の table 要素を生成することとは直接関係がない。関係がないので、HTMLを生成する責務を DietTable クラスから独立させ、 DietTable クラスはそのクラスを使うように変更してみた。 HTML の table 要素を生成するクラスなので HtmlTable と名付けた。これにより HtmlTable クラスは、保持する内容を HTMLとして書き出すことのみに専念し、 DietTable クラスはフォームに入力された Diet 情報から表として生成可能か、ルールに照らしてその妥当性を検証し、また、表のどこに何を書くかを決めることに専念できる。これにより役割分担がはっきりするようになり、誰に何を頼めばいいのか理解しやすくなった。

g5922

 

構造

HtmlTable クラスの構造は上図に示した通りである。クラスの内部は、シンプルに「表」の構造を表現。 HtmlTable クラスの直下にある Rows クラスは、表の各列を管理する Row クラスの集合を管理。そして、一つの Row オブジェクトは、表の列数だけ Cell を管理。 Cell クラスは、セルに設定された内容と、そこにおける複数のスタイルを管理するようにした。

HtmlTable クラスにおいては、

  • 表の大きさ (行・列数) を設定する
  • 特定のセルの内容とスタイルを設定する
  • 特定の行・列のスタイルを設定する
  • HTML の table 要素に変換する

の操作を持つ。

 

余談

当初は「列」とその集合を管理する Cols, Col クラスも作り、Cell へのリンクを持つようにしてた。しかし、結局必要がなかったので削除。 Cols, Col クラスの実装は、Rows, Row クラスとほぼ同じだった。もし、Cell に対して列方向からもリンクが必要であるならば、次元を表わす Dimention クラスを作成し、そのインスタンスとして「行・列」を表現するのがいいのかもしれない。

 

実装

Cell オブジェクトは、「内容やスタイル」が設定されたりする方が、何も設定されないものより圧倒的に少ない予定。だから、最初に HtmlTable オブジェクトを作成したときに、Cell オブジェクトを「行 × 列」の数だけ生成するのではなく、内容やスタイルが設定されたときに、はじめて Cell オブジェクトを生成するようにした。それは、Row オブジェクトについても同様にした。まぁ、どうせ表なんて大きくないのだから、最初から全て Cell オブジェクトを生成しておいてもいいような気もしたし、その方が実装がシンプルになったような気もするが、とりあえずよしとしておくか。 ^^;

 

ソースコード

以下にコードを示す。

HtmlTable.py

class HtmlTable:
    u""" HTML のtable 要素を作成するためのクラス

    構造: HtmlTable - Rows -* Row -* Cell
    """
    def __init__(self, row, col):
        self.rows = Rows()      # 行を管理するオブジェクト
        self.row = row          # 行数
        self.col = col          # 列数
        
    def setCell(self, row, col, content=""):
        u""" 指定された行・列番号のセルの内容を設定する

        セルを設定した場合、設定した Cell オブジェクトのみが存在する。
        """
        # 指定された行・列番号が生成したテーブルの範囲を超えてないことを確認する
        if self.row < row or self.col < col:
            raise OutOfRangeErr
        # セルを作成して、行方向からリンクする
        self._setLink(Cell(row, col, content))

    def _setLink(self, cell):
        u""" セルを行方向からリンクする """
        self.rows.setCellRow(cell)
        
    def addCssToCell(self, row, col, css):
        u""" セルに CSS を追加する"""
        # 指定された行・列番号のセルの存否を確認する
        c = self.findCell(row, col)
        if c: c.addCss(css)
        else:
            newCell = Cell(row, col, "")
            newCell.addCss(css)
            self._setLink(newCell)
    
    def addCssToCol(self, col, css):
        u""" 列に CSS を設定する """
        for i in range(0, self.row):
            self.addCssToCell(i, col, css)
            
    def addCssToRow(self, row, css):
        u""" 行に CSS を設定する """
        for i in range(0, self.col):
            self.addCssToCell(row, i, css)
        
    def findCell(self, row, col):
        u""" 行番号 row, 列番号 col のセルを取得する """
        return self.rows.findCell(row, col)
        
    def toHtml(self):
        u""" HTML に変換する """
        return "<table align='center'>\n" + \
                    self.rows.toHtml(self.row, self.col) +\
                "</table>"

class OutOfRangeErr(Exception):
    pass

class Rows:
    u""" 行の集合を管理するクラス """
    def __init__(self):
        self.rows = []      # 行のリスト
        
    def setCellRow(self, cell):
        u""" 指定されたセルを行に設定する """
        row = self.findRow(cell.row)
        if row:
            # 新しくセルが設定された場合、古いセルは上書き。
            row.setCell(cell)
        else:
            newRow = Row(cell.row)
            newRow.setCell(cell)
            self.add(newRow)

    def findRow(self, num):
        u""" 指定された番号の行が存在するか検索する """
        for row in self.rows:
            if row.num == num:
                return row
        return None

    def findCell(self, row, col):
        u""" セル(row,col) を取得する """
        r = self.findRow(row)
        if r: return r.findCell(col)
        else: return None

    def add(self, row):
        u""" 指定された行を追加する """
        if not self.rows:
            self.rows.append(row)
        else:
            for i,r in enumerate(self.rows):
                if r.num > row.num:
                    self.rows.insert(i-1, row)
                    return
            self.rows.append(row)
    
    def toHtml(self, row, col):
        u""" HTML に変換する

        HtmlTable#setCell() において設定されてないセルは、Cell オブジェクトが
        空である。その際、このオブジェクトが空の tr 要素を出力する責務がある。
        """
        result = ""
        for i in range(0,row):
            r = self.findRow(i)
            if r:
                result += r.toHtml(i, col)
            else:
                # 行の要素がない場合
                result += "\t<tr>\n\t\t" + "<td></td>"*col +"\n\t</tr>\n"
        return result

class Row:
    u""" 行を表わすクラス """
    def __init__(self, num):
        self.cells = []     # セルのリスト
        self.num = num      # 行番号

    def setCell(self, cell):
        u""" 列番号の順でセルのリストに追加する。 """
        for i,c in enumerate(self.cells):
            if c.col == cell.col:
                # 同じ列番号のセルが見つかった場合は上書きする。
                self.cells[i] = cell; return
            if c.col > cell.col:
                self.cells.insert(i-1, cell); return
        self.cells.append(cell)

    def findCell(self, col):
        u""" 指定された行番号のセルを検索する """
        for c in self.cells:
            if c.col == col: return c
        return None

    def toHtml(self, row, col):
        u""" HTML に変換する

        HtmlTable#setCell() において設定されてないセルは、Cell オブジェクトが
        空である。その際、このオブジェクトが空の td 要素を出力する責務がある。
        """
        result = "\t<tr>\n"
        for i in range(0,col):
            result += "\t\t"
            c = self.findCell(i)
            if c:
                result += c.toHtml()
            else:
                result += "<td></td>"
            result += "\n"
        result += "\t</tr>\n"
        return result

class Cell:
    u""" セルを表わすクラス """
    def __init__(self, row, col, content=""):
        self.row = row              # 行番号
        self.col = col              # 列番号
        self.content = content      # 内容
        self.csss = []              # CSS のリスト

    def addCss(self, css):
        u""" セルに CSS を追加 """
        self.csss.append(css)

    def toHtml(self):
        return "<td class='" + " ".join(str(x) for x in self.csss) + "'>" \
                    + str(self.content) + "</td>"

「セルの内容を削除する」など、今回必要のないメソッドは実装していない。

あ~、これでやっと各々のメソッドがシンプルになった。 ^^

 

ユニットテスト

ついでに ユニットテストもちょっとだけ書いておこう。 (cf. PyScripter で UnitTest を自動で生成)

import unittest
import HtmlTable

class TestHtmlTable(unittest.TestCase):

    def setUp(self): 
        #  2行3列のテーブルを作成
        self.tb = HtmlTable.HtmlTable(2,3)
        # セルの設定
        self.tb.setCell(0,0,"hoge")
        self.tb.setCell(0,1,"piyo")
        self.tb.setCell(1,0,"fuga")
        # セルにCSS を追加
        self.tb.addCssToCol(0, "sunday")
        self.tb.addCssToCol(2, "saturday")
        self.tb.addCssToCell(1,1,"sunday")
        # 行・列に CSS を追加
        self.tb.addCssToRow(1, "x_axis")
        self.tb.addCssToCol(1, "y_axis")

    def tearDown(self): 
        pass

    def testsetCell(self):
        tb = HtmlTable.HtmlTable(3,4)
        # 例外が投げられることを確認する
        self.assertRaises(HtmlTable.OutOfRangeErr, tb.setCell, 3, 5, "hoge")
        self.assertRaises(HtmlTable.OutOfRangeErr, tb.setCell, 5, 3, "hoge")
        self.assertRaises(HtmlTable.OutOfRangeErr, tb.setCell, 5, 5, "hoge")
        # 例外が投げられないことを確認する
        try:
            tb.setCell(3,3,"hoge")
        except OutOfRangeErr:
            fail("expected a OutOfRangeErr")
    
    def testtoHtml(self):
        self.assertEqual(self.tb.toHtml(), """\
<table align='center'>
 <tr>
  <td class='sunday'>hoge</td>
  <td class='y_axis'>piyo</td>
  <td class='saturday'></td>
 </tr>
 <tr>
  <td class='sunday x_axis'>fuga</td>
  <td class='sunday x_axis y_axis'></td>
  <td class='saturday x_axis'></td>
 </tr>
</table>\
""", self.tb.toHtml())

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

 

テストについて

久しぶりにちょっと本をひもといて、

私は、クラスがなすべきことをすべて調べてから、それらについて 1 つずつ、不具合を起こしそうな条件でテストするようにしています。プログラマによっては「すべての公開メソッドをテストする」よう勧めていますが、これとは違います。テストはリスク主導であるべきです。 (…)

テストをたくさん書こうとする余り、必要なテストを書き漏らしてしまうからです。(…)

大事なことは、一番怪しいと思う部分をテストすることです。

(リファクタリング , p97 より。太字は引用者による。)

なるほど。

あ、決してテストをあまりしてない言訳に使っているわけではありません…。 ^^;


関連記事