ふにゃるんv2

もとは、http://d.hatena.ne.jp/Wacky/

IronPythonで、C#で作ったDLLアセンブリモジュールの単体テストを行う

この間、C#でDLLアセンブリモジュールを作っていまして、チェック用に簡単な単体テストコードを作って動かしてました。


で、動かしていて「こんな機能欲しいなぁ」と思ったのが、いわゆる「assertマクロのように、エラーが発生した条件式を表示したい」なぁ、って事です。
例えば、↓こんな風に。

#include    <stdio.h>
#include    <assert.h>

int hoge(int v)
{
    return v;
}

int main(void)
{
    assert(hoge(1) == 0);
    
    return 0;
}

*** 実行例 ***
Assertion failed: hoge(1) == 0, file test.cpp, line 11

This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.

System.Diagnotics.StackFrame から引っ張り出せないかな?と思って、少し試してみたんですが、条件式というかコードの一部を取って来るのは、自分の力ではダメでした。エラー発生時のソースファイルや行などの情報は取り出せるんですけどね。*.pdbの情報から引っ張り出すしか無いのかなぁ?


他に、何かやり方が無いかなぁ?と つらつら考えていて、思いついたのが、以下のやり方。


Pythonは、色々単体テスト用のモジュールが揃っているんですよね。
だから、単体テストにはうってつけかな?と思った訳です。

早速試しましょう

早速、C#でDLLアセンブリモジュールを作ってみましょう。

// コンパイル: csc /debug+ /target:library csextend.cs
using System;
using System.Collections;
using CON = System.Console;

namespace Test {
    public class TestClass
    {
        public TestClass() {}
        
        public int int_test(int v){
            CON.WriteLine(v.GetType());
            return v;
        }
        
        public object object_test(object v){
            CON.WriteLine(v.GetType());
            return v;
        }
        
        public T generic_test<T>(T v) where T : struct {
            CON.WriteLine(v.GetType());
            return v;
        }
    };
    
    public struct TestData
    {
        public string   name;
        public int      value;
        
        public TestData(string _name, int _value){
            name = _name;
            value = _value;
        }
    };
};

上のソースプログラムは、Visual Studioを入れてなくても、.NET Framework 2.0 SDKを入れていれば、cscコマンドを呼び出すだけでコンパイルできます。
ここでは、仮に、csextend.dll というDLLアセンブリモジュールが出来たとします。


次、IronPythonから作ったアセンブリモジュールを動かす単体テストコードを記述してみます。
使用する単体テストモジュールは、CPythonに標準付属する unittest モジュールを使用します。

#!/usr/bin/env python
# coding: cp932
import sys

# CPythonの unittestモジュールを読み込んでます
sys.path.append(r"C:\Python24\Lib")
import unittest

# C#で作ったアセンブリモジュールを読み込んでいます(これがテスト対象)
import clr
clr.AddReferenceToFile("csextend.dll")
#import Test.TestClass
from Test import *
from System import *
from System.Collections.Generic import *

class DllTest(unittest.TestCase):
    """C#のDLL呼び出しテスト"""
    
    def setUp(self):
        #print dir(TestClass)
        pass
        
    def test1_success(self):
        """成功するテストコード"""
        o = TestClass()
        self.assertEqual(o.int_test(1), 1)
        
    def test1_error(self):
        """エラーになるテストコード"""
        o = TestClass()
        self.assertEqual(o.int_test(1), 2)
    
    def test_type(self):
        """IronPythonの値が、C#で どう読み取られるかのテスト"""
        o = TestClass()
        self.assertEqual(o.object_test(1), 1)
        self.assertEqual(o.object_test(1.23), 1.23)
        self.assertEqual(o.object_test("123"), "123")
        self.assertEqual(o.object_test(Convert.ToByte(10)), Convert.ToByte(10))
        
    def test_generic(self):
        """ジェネリックを使ったテスト"""
        o = TestClass()
        self.assertEqual(o.generic_test[Int16](1), 1)
    
    def test_generic2(self):
        """Listを使ったテスト"""
        ary = List[TestData]()
        ary.Add(TestData("hello", 1))
        ary.Add(TestData("hello2", 2))
        ary.Add(TestData("hello3", 3))
        
        for v in ary:
            print v.name, v.value
        
        self.assert_(ary[0].name == "hello")
        # インデックスをオーバーした配列へのアクセス時、例外が発生する事をチェック
        # lambdaを使っているのは、assertRaises内で、"ary[3].name"を実行させる為
        # (使い方事項に書かれています)
        self.assertRaises(ValueError, lambda: ary[3].name)

if __name__=="__main__":
    suite = unittest.makeSuite(DllTest)
    unittest.TextTestRunner(verbosity=2).run(suite)
    
    # ↓単に、こんな呼び出しでもOK
    #unittest.main()


実際に動かしてみましょう。

$ ipy test.py
エラーになるテストコード ... System.Int32
FAIL
成功するテストコード ... System.Int32
ok
ジェネリックを使ったテスト ... System.Int16
ok
Listを使ったテスト ... hello 1
hello2 2
hello3 3
ok
IronPythonの値が、C#で どう読み取られるかのテスト ... System.Int32
System.Double
System.String
System.Byte
ok

======================================================================
FAIL: エラーになるテストコード
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test.py", line 32, in test1_error
    self.assertEqual(o.int_test(1), 2)
AssertionError: 1 != 2

----------------------------------------------------------------------
Ran 5 tests in 0.297s

FAILED (failures=1)

おぉ、ちゃんとエラー行を表示してくれてますよ!

まとめ

という訳で、恒例のまとめ。

  • IronPythonで、アセンブリモジュールの単体テストは使い物になると思います。
  • 記述形式も、関数末尾に';(セミコロン)'が付くか付かないかの違い程度なので、そんなに迷わないでしょう。
  • IronPythonの定数は、C#では以下のように変換されます。
    (正確には、定数をSystem.Objectクラスに渡した際の挙動と言うべきですか)
IronPython C#
123 System.Int32
1.234 System.Double
"hello" System.String
  • C#に渡す際、特定の型に変更したければ、System.Convert.ToXXX メソッドを使えば良いでしょう。
  • ジェネリックのテストを行う際、IronPythonでは使用する型を明示しないといけません。
    上のサンプルのジェネリックテストを参照。


Pythonには、他にも単体テスト用のフレームワークがありますので、それを使ってみるというのも一興ですね。
個人的には、PyUnitXがIronPythonで動いたら面白いだろ〜な〜とか思ったりしてます。